diff options
622 files changed, 17599 insertions, 3494 deletions
diff --git a/AconfigFlags.bp b/AconfigFlags.bp index f5bf437a738d..98b62b3e2046 100644 --- a/AconfigFlags.bp +++ b/AconfigFlags.bp @@ -26,6 +26,7 @@ aconfig_srcjars = [ ":android.content.flags-aconfig-java{.generated_srcjars}", ":android.content.pm.flags-aconfig-java{.generated_srcjars}", ":android.content.res.flags-aconfig-java{.generated_srcjars}", + ":android.crashrecovery.flags-aconfig-java{.generated_srcjars}", ":android.credentials.flags-aconfig-java{.generated_srcjars}", ":android.database.sqlite-aconfig-java{.generated_srcjars}", ":android.hardware.biometrics.flags-aconfig-java{.generated_srcjars}", @@ -64,7 +65,9 @@ aconfig_srcjars = [ ":com.android.input.flags-aconfig-java{.generated_srcjars}", ":com.android.internal.foldables.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.flags-aconfig-java{.generated_srcjars}", + ":com.android.net.thread.flags-aconfig-java{.generated_srcjars}", ":com.android.server.flags.services-aconfig-java{.generated_srcjars}", ":com.android.text.flags-aconfig-java{.generated_srcjars}", ":com.android.window.flags.window-aconfig-java{.generated_srcjars}", @@ -92,6 +95,7 @@ stubs_defaults { "android.companion.virtual.flags-aconfig", "android.content.pm.flags-aconfig", "android.content.res.flags-aconfig", + "android.crashrecovery.flags-aconfig", "android.credentials.flags-aconfig", "android.database.sqlite-aconfig", "android.hardware.biometrics.flags-aconfig", @@ -133,6 +137,7 @@ stubs_defaults { "com.android.input.flags-aconfig", "com.android.media.flags.bettertogether-aconfig", "com.android.net.flags-aconfig", + "com.android.net.thread.flags-aconfig", "com.android.server.flags.services-aconfig", "com.android.text.flags-aconfig", "com.android.window.flags.window-aconfig", @@ -536,6 +541,21 @@ java_aconfig_library { defaults: ["framework-minus-apex-aconfig-java-defaults"], } +// Media Editing +aconfig_declarations { + name: "com.android.media.flags.editing-aconfig", + package: "com.android.media.editing.flags", + srcs: [ + "media/java/android/media/flags/editing.aconfig", + ], +} + +java_aconfig_library { + name: "com.android.media.flags.editing-aconfig-java", + aconfig_declarations: "com.android.media.flags.editing-aconfig", + defaults: ["framework-minus-apex-aconfig-java-defaults"], +} + // Media TV aconfig_declarations { name: "android.media.tv.flags-aconfig", @@ -760,12 +780,25 @@ aconfig_declarations { srcs: ["core/java/android/net/flags.aconfig"], } +// Thread network +aconfig_declarations { + name: "com.android.net.thread.flags-aconfig", + package: "com.android.net.thread.flags", + srcs: ["core/java/android/net/thread/flags.aconfig"], +} + java_aconfig_library { name: "com.android.net.flags-aconfig-java", aconfig_declarations: "com.android.net.flags-aconfig", defaults: ["framework-minus-apex-aconfig-java-defaults"], } +java_aconfig_library { + name: "com.android.net.thread.flags-aconfig-java", + aconfig_declarations: "com.android.net.thread.flags-aconfig", + defaults: ["framework-minus-apex-aconfig-java-defaults"], +} + // Media aconfig_declarations { name: "android.media.playback.flags-aconfig", @@ -1047,3 +1080,16 @@ java_aconfig_library { aconfig_declarations: "android.adaptiveauth.flags-aconfig", defaults: ["framework-minus-apex-aconfig-java-defaults"], } + +// CrashRecovery Module +aconfig_declarations { + name: "android.crashrecovery.flags-aconfig", + package: "android.crashrecovery.flags", + srcs: ["packages/CrashRecovery/aconfig/flags.aconfig"], +} + +java_aconfig_library { + name: "android.crashrecovery.flags-aconfig-java", + aconfig_declarations: "android.crashrecovery.flags-aconfig", + defaults: ["framework-minus-apex-aconfig-java-defaults"], +}
\ No newline at end of file diff --git a/Android.bp b/Android.bp index 9c56733650cb..e12f74fcd7ca 100644 --- a/Android.bp +++ b/Android.bp @@ -95,6 +95,7 @@ filegroup { ":platform-compat-native-aidl", // AIDL sources from external directories + ":android.frameworks.location.altitude-V2-java-source", ":android.hardware.biometrics.common-V4-java-source", ":android.hardware.biometrics.fingerprint-V3-java-source", ":android.hardware.biometrics.face-V4-java-source", diff --git a/apct-tests/perftests/contentcapture/src/android/view/contentcapture/AbstractContentCapturePerfTestCase.java b/apct-tests/perftests/contentcapture/src/android/view/contentcapture/AbstractContentCapturePerfTestCase.java index 0ea2dafbb047..bc8470888a5a 100644 --- a/apct-tests/perftests/contentcapture/src/android/view/contentcapture/AbstractContentCapturePerfTestCase.java +++ b/apct-tests/perftests/contentcapture/src/android/view/contentcapture/AbstractContentCapturePerfTestCase.java @@ -248,6 +248,11 @@ public abstract class AbstractContentCapturePerfTestCase { return mServiceWatcher.waitOnCreate(); } + /** Wait for session paused. */ + public void waitForSessionPaused() throws InterruptedException { + mServiceWatcher.waitSessionPaused(); + } + @NonNull protected ActivityWatcher startWatcher() { return mActivitiesWatcher.watch(CustomTestActivity.class); diff --git a/apct-tests/perftests/contentcapture/src/android/view/contentcapture/LoginTest.java b/apct-tests/perftests/contentcapture/src/android/view/contentcapture/LoginTest.java index aa95dfdfdf16..44e8a67e72ef 100644 --- a/apct-tests/perftests/contentcapture/src/android/view/contentcapture/LoginTest.java +++ b/apct-tests/perftests/contentcapture/src/android/view/contentcapture/LoginTest.java @@ -80,6 +80,34 @@ public class LoginTest extends AbstractContentCapturePerfTestCase { testActivityLaunchTime(R.layout.test_container_activity, 500); } + @Test + public void testSendEventsLatency() throws Throwable { + enableService(); + + testSendEventLatency(R.layout.test_container_activity, 0); + } + + @Test + public void testSendEventsLatency_contains100Views() throws Throwable { + enableService(); + + testSendEventLatency(R.layout.test_container_activity, 100); + } + + @Test + public void testSendEventsLatency_contains300Views() throws Throwable { + enableService(); + + testSendEventLatency(R.layout.test_container_activity, 300); + } + + @Test + public void testSendEventsLatency_contains500Views() throws Throwable { + enableService(); + + testSendEventLatency(R.layout.test_container_activity, 500); + } + private void testActivityLaunchTime(int layoutId, int numViews) throws Throwable { final Object drawNotifier = new Object(); final Intent intent = getLaunchIntent(layoutId, numViews); @@ -111,6 +139,38 @@ public class LoginTest extends AbstractContentCapturePerfTestCase { } } + private void testSendEventLatency(int layoutId, int numViews) throws Throwable { + final Object drawNotifier = new Object(); + final Intent intent = getLaunchIntent(layoutId, numViews); + intent.putExtra(CustomTestActivity.INTENT_EXTRA_FINISH_ON_IDLE, true); + intent.putExtra(CustomTestActivity.INTENT_EXTRA_DRAW_CALLBACK, + new RemoteCallback(result -> { + synchronized (drawNotifier) { + drawNotifier.notifyAll(); + } + })); + final ActivityWatcher watcher = startWatcher(); + + final BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); + while (state.keepRunning()) { + mEntryActivity.startActivity(intent); + synchronized (drawNotifier) { + try { + drawNotifier.wait(GENERIC_TIMEOUT_MS); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + waitForSessionPaused(); + + // Ignore the time to finish the activity + state.pauseTiming(); + watcher.waitFor(DESTROYED); + sInstrumentation.waitForIdleSync(); + state.resumeTiming(); + } + } + @Test public void testOnVisibilityAggregated_visibleChanged() throws Throwable { enableService(); diff --git a/apct-tests/perftests/contentcapture/src/android/view/contentcapture/MyContentCaptureService.java b/apct-tests/perftests/contentcapture/src/android/view/contentcapture/MyContentCaptureService.java index ecc5112ab6dd..0b5345f5e2e1 100644 --- a/apct-tests/perftests/contentcapture/src/android/view/contentcapture/MyContentCaptureService.java +++ b/apct-tests/perftests/contentcapture/src/android/view/contentcapture/MyContentCaptureService.java @@ -114,6 +114,10 @@ public class MyContentCaptureService extends ContentCaptureService { public void onContentCaptureEvent(ContentCaptureSessionId sessionId, ContentCaptureEvent event) { Log.i(TAG, "onContentCaptureEventsRequest(session=" + sessionId + "): " + event); + if (sServiceWatcher != null + && event.getType() == ContentCaptureEvent.TYPE_SESSION_PAUSED) { + sServiceWatcher.mSessionPaused.countDown(); + } } @Override @@ -126,6 +130,7 @@ public class MyContentCaptureService extends ContentCaptureService { private static final long GENERIC_TIMEOUT_MS = 10_000; private final CountDownLatch mCreated = new CountDownLatch(1); private final CountDownLatch mDestroyed = new CountDownLatch(1); + private final CountDownLatch mSessionPaused = new CountDownLatch(1); private boolean mReadyToClear = true; private Pair<Set<String>, Set<ComponentName>> mAllowList; @@ -151,6 +156,11 @@ public class MyContentCaptureService extends ContentCaptureService { await(mDestroyed, "not destroyed"); } + /** Wait for session paused. */ + public void waitSessionPaused() throws InterruptedException { + await(mSessionPaused, "no Paused"); + } + /** * Allow just this package. */ diff --git a/apex/jobscheduler/framework/aconfig/job.aconfig b/apex/jobscheduler/framework/aconfig/job.aconfig index e73b434042af..788e82407926 100644 --- a/apex/jobscheduler/framework/aconfig/job.aconfig +++ b/apex/jobscheduler/framework/aconfig/job.aconfig @@ -13,3 +13,10 @@ flag { description: "Add APIs to let apps attach debug information to jobs" bug: "293491637" } + +flag { + name: "backup_jobs_exemption" + namespace: "backstage_power" + description: "Introduce a new RUN_BACKUP_JOBS permission and exemption logic allowing for longer running jobs for apps whose primary purpose is to backup or sync content." + bug: "318731461" +} 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 7a92cca74795..fc193d8147b5 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java +++ b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java @@ -1840,7 +1840,9 @@ public class JobSchedulerService extends com.android.server.SystemService /* isFlexConstraintSatisfied */ false, jobStatus.canApplyTransportAffinities(), jobStatus.getNumAppliedFlexibleConstraints(), - jobStatus.getNumDroppedFlexibleConstraints()); + jobStatus.getNumDroppedFlexibleConstraints(), + jobStatus.getFilteredTraceTag(), + jobStatus.getFilteredDebugTags()); // If the job is immediately ready to run, then we can just immediately // put it in the pending list and try to schedule it. This is especially @@ -2288,7 +2290,9 @@ public class JobSchedulerService extends com.android.server.SystemService cancelled.isConstraintSatisfied(JobStatus.CONSTRAINT_FLEXIBLE), cancelled.canApplyTransportAffinities(), cancelled.getNumAppliedFlexibleConstraints(), - cancelled.getNumDroppedFlexibleConstraints()); + cancelled.getNumDroppedFlexibleConstraints(), + cancelled.getFilteredTraceTag(), + cancelled.getFilteredDebugTags()); } // If this is a replacement, bring in the new version of the job if (incomingJob != null) { @@ -5448,6 +5452,9 @@ public class JobSchedulerService extends com.android.server.SystemService pw.print(Flags.FLAG_THROW_ON_UNSUPPORTED_BIAS_USAGE, Flags.throwOnUnsupportedBiasUsage()); pw.println(); + pw.print(android.app.job.Flags.FLAG_BACKUP_JOBS_EXEMPTION, + android.app.job.Flags.backupJobsExemption()); + pw.println(); pw.decreaseIndent(); 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 6f2393adfc7b..0cf6a7a8a4f6 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerShellCommand.java +++ b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerShellCommand.java @@ -356,6 +356,9 @@ public final class JobSchedulerShellCommand extends BasicShellCommandHandler { case com.android.server.job.Flags.FLAG_THROW_ON_UNSUPPORTED_BIAS_USAGE: pw.println(com.android.server.job.Flags.throwOnUnsupportedBiasUsage()); break; + case android.app.job.Flags.FLAG_BACKUP_JOBS_EXEMPTION: + pw.println(android.app.job.Flags.backupJobsExemption()); + break; default: pw.println("Unknown flag: " + flagName); break; diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java b/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java index fe55e2702916..8ab7d2fae49f 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java +++ b/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java @@ -541,7 +541,9 @@ public final class JobServiceContext implements ServiceConnection { job.isConstraintSatisfied(JobStatus.CONSTRAINT_FLEXIBLE), job.canApplyTransportAffinities(), job.getNumAppliedFlexibleConstraints(), - job.getNumDroppedFlexibleConstraints()); + job.getNumDroppedFlexibleConstraints(), + job.getFilteredTraceTag(), + job.getFilteredDebugTags()); sEnqueuedJwiAtJobStart.logSampleWithUid(job.getUid(), job.getWorkCount()); final String sourcePackage = job.getSourcePackageName(); if (Trace.isTagEnabled(Trace.TRACE_TAG_SYSTEM_SERVER)) { @@ -1630,7 +1632,9 @@ public final class JobServiceContext implements ServiceConnection { completedJob.isConstraintSatisfied(JobStatus.CONSTRAINT_FLEXIBLE), completedJob.canApplyTransportAffinities(), completedJob.getNumAppliedFlexibleConstraints(), - completedJob.getNumDroppedFlexibleConstraints()); + completedJob.getNumDroppedFlexibleConstraints(), + completedJob.getFilteredTraceTag(), + completedJob.getFilteredDebugTags()); if (Trace.isTagEnabled(Trace.TRACE_TAG_SYSTEM_SERVER)) { Trace.asyncTraceForTrackEnd(Trace.TRACE_TAG_SYSTEM_SERVER, "JobScheduler", getId()); diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/FlexibilityController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/FlexibilityController.java index 14cce19ef676..6883d18cd937 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/controllers/FlexibilityController.java +++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/FlexibilityController.java @@ -972,6 +972,20 @@ public final class FlexibilityController extends StateController { synchronized (mLock) { final long earliest = getLifeCycleBeginningElapsedLocked(js); final long latest = getLifeCycleEndElapsedLocked(js, nowElapsed, earliest); + if (latest <= earliest) { + // Something has gone horribly wrong. This has only occurred on incorrectly + // configured tests, but add a check here for safety. + Slog.wtf(TAG, "Got invalid latest when scheduling alarm." + + " Prefetch=" + js.getJob().isPrefetch()); + // Since things have gone wrong, the safest and most reliable thing to do is + // stop applying flex policy to the job. + mFlexibilityTracker.setNumDroppedFlexibleConstraints(js, + js.getNumAppliedFlexibleConstraints()); + mJobsToCheck.add(js); + mHandler.sendEmptyMessage(MSG_CHECK_JOBS); + return; + } + final long nextTimeElapsed = getNextConstraintDropTimeElapsedLocked(js, earliest, latest); diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java index d39863c85f33..a4df5d829281 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java +++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java @@ -48,6 +48,7 @@ import android.util.ArrayMap; import android.util.ArraySet; import android.util.IndentingPrintWriter; import android.util.Pair; +import android.util.Patterns; import android.util.Range; import android.util.Slog; import android.util.TimeUtils; @@ -76,6 +77,7 @@ import java.util.Collections; import java.util.Objects; import java.util.Random; import java.util.function.Predicate; +import java.util.regex.Pattern; /** * Uniquely identifies a job internally. @@ -203,6 +205,17 @@ public final class JobStatus { // TODO(b/129954980): ensure this doesn't spam statsd, especially at boot private static final boolean STATS_LOG_ENABLED = false; + /** + * Simple patterns to match some common forms of PII. This is not intended all-encompassing and + * any clients should aim to do additional filtering. + */ + private static final ArrayMap<Pattern, String> BASIC_PII_FILTERS = new ArrayMap<>(); + + static { + BASIC_PII_FILTERS.put(Patterns.EMAIL_ADDRESS, "[EMAIL]"); + BASIC_PII_FILTERS.put(Patterns.PHONE, "[PHONE]"); + } + // No override. public static final int OVERRIDE_NONE = 0; // Override to improve sorting order. Does not affect constraint evaluation. @@ -250,6 +263,18 @@ public final class JobStatus { private final long mLoggingJobId; /** + * List of tags from {@link JobInfo#getDebugTags()}, filtered using {@link #BASIC_PII_FILTERS}. + * Lazily loaded in {@link #getFilteredDebugTags()}. + */ + @Nullable + private String[] mFilteredDebugTags; + /** + * Trace tag from {@link JobInfo#getTraceTag()}, filtered using {@link #BASIC_PII_FILTERS}. + * Lazily loaded in {@link #getFilteredTraceTag()}. + */ + @Nullable + private String mFilteredTraceTag; + /** * Tag to identify the wakelock held for this job. Lazily loaded in * {@link #getWakelockTag()} since it's not typically needed until the job is about to run. */ @@ -1325,6 +1350,47 @@ public final class JobStatus { return batteryName; } + @VisibleForTesting + @NonNull + static String applyBasicPiiFilters(@NonNull String val) { + for (int i = BASIC_PII_FILTERS.size() - 1; i >= 0; --i) { + val = BASIC_PII_FILTERS.keyAt(i).matcher(val).replaceAll(BASIC_PII_FILTERS.valueAt(i)); + } + return val; + } + + /** + * List of tags from {@link JobInfo#getDebugTags()}, filtered using a basic set of PII filters. + */ + @NonNull + public String[] getFilteredDebugTags() { + if (mFilteredDebugTags != null) { + return mFilteredDebugTags; + } + final ArraySet<String> debugTags = job.getDebugTagsArraySet(); + mFilteredDebugTags = new String[debugTags.size()]; + for (int i = 0; i < mFilteredDebugTags.length; ++i) { + mFilteredDebugTags[i] = applyBasicPiiFilters(debugTags.valueAt(i)); + } + return mFilteredDebugTags; + } + + /** + * Trace tag from {@link JobInfo#getTraceTag()}, filtered using a basic set of PII filters. + */ + @Nullable + public String getFilteredTraceTag() { + if (mFilteredTraceTag != null) { + return mFilteredTraceTag; + } + final String rawTag = job.getTraceTag(); + if (rawTag == null) { + return null; + } + mFilteredTraceTag = applyBasicPiiFilters(rawTag); + return mFilteredTraceTag; + } + /** Return the String to be used as the tag for the wakelock held for this job. */ @NonNull public String getWakelockTag() { diff --git a/core/api/Android.bp b/core/api/Android.bp index 907916a125da..8d8a82b69b55 100644 --- a/core/api/Android.bp +++ b/core/api/Android.bp @@ -96,21 +96,3 @@ filegroup { name: "non-updatable-test-lint-baseline.txt", srcs: ["test-lint-baseline.txt"], } - -java_api_contribution { - name: "api-stubs-docs-non-updatable-public-stubs", - api_surface: "public", - api_file: "current.txt", - visibility: [ - "//build/orchestrator/apis", - ], -} - -java_api_contribution { - name: "frameworks-base-core-api-module-lib-stubs", - api_surface: "module-lib", - api_file: "module-lib-current.txt", - visibility: [ - "//build/orchestrator/apis", - ], -} diff --git a/core/api/current.txt b/core/api/current.txt index c92b4ffe60a6..66feebc6cc92 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -277,6 +277,7 @@ package android { field @FlaggedApi("android.companion.flags.device_presence") public static final String REQUEST_OBSERVE_DEVICE_UUID_PRESENCE = "android.permission.REQUEST_OBSERVE_DEVICE_UUID_PRESENCE"; field public static final String REQUEST_PASSWORD_COMPLEXITY = "android.permission.REQUEST_PASSWORD_COMPLEXITY"; field @Deprecated public static final String RESTART_PACKAGES = "android.permission.RESTART_PACKAGES"; + field @FlaggedApi("android.app.job.backup_jobs_exemption") public static final String RUN_BACKUP_JOBS = "android.permission.RUN_BACKUP_JOBS"; field public static final String RUN_USER_INITIATED_JOBS = "android.permission.RUN_USER_INITIATED_JOBS"; field public static final String SCHEDULE_EXACT_ALARM = "android.permission.SCHEDULE_EXACT_ALARM"; field public static final String SEND_RESPOND_VIA_MESSAGE = "android.permission.SEND_RESPOND_VIA_MESSAGE"; @@ -284,6 +285,7 @@ package android { field public static final String SET_ALARM = "com.android.alarm.permission.SET_ALARM"; field public static final String SET_ALWAYS_FINISH = "android.permission.SET_ALWAYS_FINISH"; field public static final String SET_ANIMATION_SCALE = "android.permission.SET_ANIMATION_SCALE"; + field @FlaggedApi("android.hardware.biometrics.custom_biometric_prompt") public static final String SET_BIOMETRIC_DIALOG_LOGO = "android.permission.SET_BIOMETRIC_DIALOG_LOGO"; field public static final String SET_DEBUG_APP = "android.permission.SET_DEBUG_APP"; field @Deprecated public static final String SET_PREFERRED_APPLICATIONS = "android.permission.SET_PREFERRED_APPLICATIONS"; field public static final String SET_PROCESS_LIMIT = "android.permission.SET_PROCESS_LIMIT"; @@ -442,6 +444,7 @@ package android { field public static final int alertDialogTheme = 16843529; // 0x1010309 field public static final int alignmentMode = 16843642; // 0x101037a field public static final int allContactsName = 16843468; // 0x10102cc + field @FlaggedApi("android.content.pm.relative_reference_intent_filters") public static final int allow; field public static final int allowAudioPlaybackCapture = 16844289; // 0x1010601 field public static final int allowBackup = 16843392; // 0x1010280 field public static final int allowClearUserData = 16842757; // 0x1010005 @@ -845,6 +848,7 @@ package android { field public static final int format24Hour = 16843723; // 0x10103cb field public static final int fraction = 16843992; // 0x10104d8 field public static final int fragment = 16843491; // 0x10102e3 + field @FlaggedApi("android.content.pm.relative_reference_intent_filters") public static final int fragmentAdvancedPattern; field public static final int fragmentAllowEnterTransitionOverlap = 16843976; // 0x10104c8 field public static final int fragmentAllowReturnTransitionOverlap = 16843977; // 0x10104c9 field public static final int fragmentCloseEnterAnimation = 16843495; // 0x10102e7 @@ -855,10 +859,13 @@ package android { field public static final int fragmentFadeExitAnimation = 16843498; // 0x10102ea field public static final int fragmentOpenEnterAnimation = 16843493; // 0x10102e5 field public static final int fragmentOpenExitAnimation = 16843494; // 0x10102e6 + field @FlaggedApi("android.content.pm.relative_reference_intent_filters") public static final int fragmentPattern; + field @FlaggedApi("android.content.pm.relative_reference_intent_filters") public static final int fragmentPrefix; field public static final int fragmentReenterTransition = 16843975; // 0x10104c7 field public static final int fragmentReturnTransition = 16843973; // 0x10104c5 field public static final int fragmentSharedElementEnterTransition = 16843972; // 0x10104c4 field public static final int fragmentSharedElementReturnTransition = 16843974; // 0x10104c6 + field @FlaggedApi("android.content.pm.relative_reference_intent_filters") public static final int fragmentSuffix; field public static final int freezesText = 16843116; // 0x101016c field public static final int fromAlpha = 16843210; // 0x10101ca field public static final int fromDegrees = 16843187; // 0x10101b3 @@ -1327,10 +1334,15 @@ package android { field public static final int propertyYName = 16843893; // 0x1010475 field public static final int protectionLevel = 16842761; // 0x1010009 field public static final int publicKey = 16843686; // 0x10103a6 + field @FlaggedApi("android.content.pm.relative_reference_intent_filters") public static final int query; field public static final int queryActionMsg = 16843227; // 0x10101db + field @FlaggedApi("android.content.pm.relative_reference_intent_filters") public static final int queryAdvancedPattern; field public static final int queryAfterZeroResults = 16843394; // 0x1010282 field public static final int queryBackground = 16843911; // 0x1010487 field public static final int queryHint = 16843608; // 0x1010358 + field @FlaggedApi("android.content.pm.relative_reference_intent_filters") public static final int queryPattern; + field @FlaggedApi("android.content.pm.relative_reference_intent_filters") public static final int queryPrefix; + field @FlaggedApi("android.content.pm.relative_reference_intent_filters") public static final int querySuffix; field public static final int quickContactBadgeStyleSmallWindowLarge = 16843443; // 0x10102b3 field public static final int quickContactBadgeStyleSmallWindowMedium = 16843442; // 0x10102b2 field public static final int quickContactBadgeStyleSmallWindowSmall = 16843441; // 0x10102b1 @@ -11385,10 +11397,12 @@ package android.content { method public final void addDataScheme(String); method public final void addDataSchemeSpecificPart(String, int); method public final void addDataType(String) throws android.content.IntentFilter.MalformedMimeTypeException; + method @FlaggedApi("android.content.pm.relative_reference_intent_filters") public final void addUriRelativeFilterGroup(@NonNull android.content.UriRelativeFilterGroup); method @NonNull public java.util.function.Predicate<android.content.Intent> asPredicate(); method @NonNull public java.util.function.Predicate<android.content.Intent> asPredicateWithTypeResolution(@NonNull android.content.ContentResolver); method public final java.util.Iterator<android.content.IntentFilter.AuthorityEntry> authoritiesIterator(); method public final java.util.Iterator<java.lang.String> categoriesIterator(); + method @FlaggedApi("android.content.pm.relative_reference_intent_filters") public final void clearUriRelativeFilterGroups(); method public final int countActions(); method public final int countCategories(); method public final int countDataAuthorities(); @@ -11396,6 +11410,7 @@ package android.content { method public final int countDataSchemeSpecificParts(); method public final int countDataSchemes(); method public final int countDataTypes(); + method @FlaggedApi("android.content.pm.relative_reference_intent_filters") public final int countUriRelativeFilterGroups(); method public static android.content.IntentFilter create(String, String); method public final int describeContents(); method public void dump(android.util.Printer, String); @@ -11407,6 +11422,7 @@ package android.content { method public final android.os.PatternMatcher getDataSchemeSpecificPart(int); method public final String getDataType(int); method public final int getPriority(); + method @FlaggedApi("android.content.pm.relative_reference_intent_filters") @NonNull public final android.content.UriRelativeFilterGroup getUriRelativeFilterGroup(int); method public final boolean hasAction(String); method public final boolean hasCategory(String); method public final boolean hasDataAuthority(android.net.Uri); @@ -11828,6 +11844,27 @@ package android.content { field public static final long INVALID_TIME = -9223372036854775808L; // 0x8000000000000000L } + @FlaggedApi("android.content.pm.relative_reference_intent_filters") public final class UriRelativeFilter { + ctor public UriRelativeFilter(int, int, @NonNull String); + method @NonNull public String getFilter(); + method public int getPatternType(); + method public int getUriPart(); + method public boolean matchData(@NonNull android.net.Uri); + field public static final int FRAGMENT = 2; // 0x2 + field public static final int PATH = 0; // 0x0 + field public static final int QUERY = 1; // 0x1 + } + + @FlaggedApi("android.content.pm.relative_reference_intent_filters") public final class UriRelativeFilterGroup { + ctor public UriRelativeFilterGroup(int); + method public void addUriRelativeFilter(@NonNull android.content.UriRelativeFilter); + method public int getAction(); + method @NonNull public java.util.Collection<android.content.UriRelativeFilter> getUriRelativeFilters(); + method public boolean matchData(@NonNull android.net.Uri); + field public static final int ACTION_ALLOW = 0; // 0x0 + field public static final int ACTION_BLOCK = 1; // 0x1 + } + } package android.content.om { @@ -18722,8 +18759,8 @@ package android.hardware.biometrics { method @Nullable public int getAllowedAuthenticators(); method @FlaggedApi("android.hardware.biometrics.custom_biometric_prompt") @Nullable public android.hardware.biometrics.PromptContentView getContentView(); method @Nullable public CharSequence getDescription(); - method @FlaggedApi("android.hardware.biometrics.custom_biometric_prompt") @Nullable @RequiresPermission("android.permission.MANAGE_BIOMETRIC_DIALOG") public android.graphics.Bitmap getLogoBitmap(); - method @FlaggedApi("android.hardware.biometrics.custom_biometric_prompt") @DrawableRes @RequiresPermission("android.permission.MANAGE_BIOMETRIC_DIALOG") public int getLogoRes(); + method @FlaggedApi("android.hardware.biometrics.custom_biometric_prompt") @Nullable @RequiresPermission(android.Manifest.permission.SET_BIOMETRIC_DIALOG_LOGO) public android.graphics.Bitmap getLogoBitmap(); + method @FlaggedApi("android.hardware.biometrics.custom_biometric_prompt") @DrawableRes @RequiresPermission(android.Manifest.permission.SET_BIOMETRIC_DIALOG_LOGO) public int getLogoRes(); method @Nullable public CharSequence getNegativeButtonText(); method @Nullable public CharSequence getSubtitle(); method @NonNull public CharSequence getTitle(); @@ -18773,8 +18810,8 @@ package android.hardware.biometrics { method @FlaggedApi("android.hardware.biometrics.custom_biometric_prompt") @NonNull public android.hardware.biometrics.BiometricPrompt.Builder setContentView(@NonNull android.hardware.biometrics.PromptContentView); method @NonNull public android.hardware.biometrics.BiometricPrompt.Builder setDescription(@NonNull CharSequence); method @Deprecated @NonNull public android.hardware.biometrics.BiometricPrompt.Builder setDeviceCredentialAllowed(boolean); - method @FlaggedApi("android.hardware.biometrics.custom_biometric_prompt") @NonNull @RequiresPermission("android.permission.MANAGE_BIOMETRIC_DIALOG") public android.hardware.biometrics.BiometricPrompt.Builder setLogo(@DrawableRes int); - method @FlaggedApi("android.hardware.biometrics.custom_biometric_prompt") @NonNull @RequiresPermission("android.permission.MANAGE_BIOMETRIC_DIALOG") public android.hardware.biometrics.BiometricPrompt.Builder setLogo(@NonNull android.graphics.Bitmap); + method @FlaggedApi("android.hardware.biometrics.custom_biometric_prompt") @NonNull @RequiresPermission(android.Manifest.permission.SET_BIOMETRIC_DIALOG_LOGO) public android.hardware.biometrics.BiometricPrompt.Builder setLogoBitmap(@NonNull android.graphics.Bitmap); + method @FlaggedApi("android.hardware.biometrics.custom_biometric_prompt") @NonNull @RequiresPermission(android.Manifest.permission.SET_BIOMETRIC_DIALOG_LOGO) public android.hardware.biometrics.BiometricPrompt.Builder setLogoRes(@DrawableRes int); method @NonNull public android.hardware.biometrics.BiometricPrompt.Builder setNegativeButton(@NonNull CharSequence, @NonNull java.util.concurrent.Executor, @NonNull android.content.DialogInterface.OnClickListener); method @NonNull public android.hardware.biometrics.BiometricPrompt.Builder setSubtitle(@NonNull CharSequence); method @NonNull public android.hardware.biometrics.BiometricPrompt.Builder setTitle(@NonNull CharSequence); @@ -18800,14 +18837,14 @@ package android.hardware.biometrics { } @FlaggedApi("android.hardware.biometrics.custom_biometric_prompt") public final class PromptContentItemBulletedText implements android.os.Parcelable android.hardware.biometrics.PromptContentItem { - ctor public PromptContentItemBulletedText(@NonNull CharSequence); + ctor public PromptContentItemBulletedText(@NonNull String); method public int describeContents(); method public void writeToParcel(@NonNull android.os.Parcel, int); field @NonNull public static final android.os.Parcelable.Creator<android.hardware.biometrics.PromptContentItemBulletedText> CREATOR; } @FlaggedApi("android.hardware.biometrics.custom_biometric_prompt") public final class PromptContentItemPlainText implements android.os.Parcelable android.hardware.biometrics.PromptContentItem { - ctor public PromptContentItemPlainText(@NonNull CharSequence); + ctor public PromptContentItemPlainText(@NonNull String); method public int describeContents(); method public void writeToParcel(@NonNull android.os.Parcel, int); field @NonNull public static final android.os.Parcelable.Creator<android.hardware.biometrics.PromptContentItemPlainText> CREATOR; @@ -18818,7 +18855,7 @@ package android.hardware.biometrics { @FlaggedApi("android.hardware.biometrics.custom_biometric_prompt") public final class PromptVerticalListContentView implements android.os.Parcelable android.hardware.biometrics.PromptContentView { method public int describeContents(); - method @Nullable public CharSequence getDescription(); + method @Nullable public String getDescription(); method @NonNull public java.util.List<android.hardware.biometrics.PromptContentItem> getListItems(); method public static int getMaxEachItemCharacterNumber(); method public static int getMaxItemCount(); @@ -18831,7 +18868,7 @@ package android.hardware.biometrics { method @NonNull public android.hardware.biometrics.PromptVerticalListContentView.Builder addListItem(@NonNull android.hardware.biometrics.PromptContentItem); method @NonNull public android.hardware.biometrics.PromptVerticalListContentView.Builder addListItem(@NonNull android.hardware.biometrics.PromptContentItem, int); method @NonNull public android.hardware.biometrics.PromptVerticalListContentView build(); - method @NonNull public android.hardware.biometrics.PromptVerticalListContentView.Builder setDescription(@NonNull CharSequence); + method @NonNull public android.hardware.biometrics.PromptVerticalListContentView.Builder setDescription(@NonNull String); } } @@ -24299,7 +24336,7 @@ package android.media { method @Nullable public android.media.MediaRouter2.RoutingController getController(@NonNull String); method @NonNull public java.util.List<android.media.MediaRouter2.RoutingController> getControllers(); method @NonNull public static android.media.MediaRouter2 getInstance(@NonNull android.content.Context); - method @FlaggedApi("com.android.media.flags.enable_cross_user_routing_in_media_router2") @NonNull @RequiresPermission(anyOf={android.Manifest.permission.MEDIA_CONTENT_CONTROL, android.Manifest.permission.MEDIA_ROUTING_CONTROL}) public static android.media.MediaRouter2 getInstance(@NonNull android.content.Context, @NonNull String, @NonNull android.os.UserHandle); + method @FlaggedApi("com.android.media.flags.enable_cross_user_routing_in_media_router2") @Nullable @RequiresPermission(anyOf={android.Manifest.permission.MEDIA_CONTENT_CONTROL, android.Manifest.permission.MEDIA_ROUTING_CONTROL}) public static android.media.MediaRouter2 getInstance(@NonNull android.content.Context, @NonNull String); method @FlaggedApi("com.android.media.flags.enable_rlp_callbacks_in_media_router2") @Nullable public android.media.RouteListingPreference getRouteListingPreference(); method @NonNull public java.util.List<android.media.MediaRoute2Info> getRoutes(); method @NonNull public android.media.MediaRouter2.RoutingController getSystemController(); @@ -25715,9 +25752,48 @@ package android.media.metrics { field public static final String KEY_STATSD_ATOM = "bundlesession-statsd-atom"; } + @FlaggedApi("com.android.media.editing.flags.add_media_metrics_editing") public final class EditingEndedEvent extends android.media.metrics.Event implements android.os.Parcelable { + method public int describeContents(); + method public int getErrorCode(); + method public int getFinalState(); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator<android.media.metrics.EditingEndedEvent> CREATOR; + field public static final int ERROR_CODE_AUDIO_PROCESSING_FAILED = 18; // 0x12 + field public static final int ERROR_CODE_DECODER_INIT_FAILED = 11; // 0xb + field public static final int ERROR_CODE_DECODING_FAILED = 12; // 0xc + field public static final int ERROR_CODE_DECODING_FORMAT_UNSUPPORTED = 13; // 0xd + field public static final int ERROR_CODE_ENCODER_INIT_FAILED = 14; // 0xe + field public static final int ERROR_CODE_ENCODING_FAILED = 15; // 0xf + field public static final int ERROR_CODE_ENCODING_FORMAT_UNSUPPORTED = 16; // 0x10 + field public static final int ERROR_CODE_FAILED_RUNTIME_CHECK = 2; // 0x2 + field public static final int ERROR_CODE_IO_BAD_HTTP_STATUS = 6; // 0x6 + field public static final int ERROR_CODE_IO_CLEARTEXT_NOT_PERMITTED = 9; // 0x9 + field public static final int ERROR_CODE_IO_FILE_NOT_FOUND = 7; // 0x7 + field public static final int ERROR_CODE_IO_NETWORK_CONNECTION_FAILED = 4; // 0x4 + field public static final int ERROR_CODE_IO_NETWORK_CONNECTION_TIMEOUT = 5; // 0x5 + field public static final int ERROR_CODE_IO_NO_PERMISSION = 8; // 0x8 + field public static final int ERROR_CODE_IO_READ_POSITION_OUT_OF_RANGE = 10; // 0xa + field public static final int ERROR_CODE_IO_UNSPECIFIED = 3; // 0x3 + field public static final int ERROR_CODE_MUXING_FAILED = 19; // 0x13 + field public static final int ERROR_CODE_NONE = 1; // 0x1 + field public static final int ERROR_CODE_VIDEO_FRAME_PROCESSING_FAILED = 17; // 0x11 + field public static final int FINAL_STATE_CANCELED = 2; // 0x2 + field public static final int FINAL_STATE_ERROR = 3; // 0x3 + field public static final int FINAL_STATE_SUCCEEDED = 1; // 0x1 + } + + @FlaggedApi("com.android.media.editing.flags.add_media_metrics_editing") public static final class EditingEndedEvent.Builder { + ctor public EditingEndedEvent.Builder(int); + method @NonNull public android.media.metrics.EditingEndedEvent build(); + method @NonNull public android.media.metrics.EditingEndedEvent.Builder setErrorCode(int); + method @NonNull public android.media.metrics.EditingEndedEvent.Builder setMetricsBundle(@NonNull android.os.Bundle); + method @NonNull public android.media.metrics.EditingEndedEvent.Builder setTimeSinceCreatedMillis(@IntRange(from=0xffffffff) long); + } + public final class EditingSession implements java.lang.AutoCloseable { method public void close(); method @NonNull public android.media.metrics.LogSessionId getSessionId(); + method @FlaggedApi("com.android.media.editing.flags.add_media_metrics_editing") public void reportEditingEndedEvent(@NonNull android.media.metrics.EditingEndedEvent); } public abstract class Event { @@ -33429,6 +33505,7 @@ package android.os { field public static final String DISALLOW_SHARING_ADMIN_CONFIGURED_WIFI = "no_sharing_admin_configured_wifi"; field public static final String DISALLOW_SMS = "no_sms"; field public static final String DISALLOW_SYSTEM_ERROR_DIALOGS = "no_system_error_dialogs"; + field @FlaggedApi("com.android.net.thread.flags.thread_user_restriction_enabled") public static final String DISALLOW_THREAD_NETWORK = "no_thread_network"; field public static final String DISALLOW_ULTRA_WIDEBAND_RADIO = "no_ultra_wideband_radio"; field public static final String DISALLOW_UNIFIED_PASSWORD = "no_unified_password"; field public static final String DISALLOW_UNINSTALL_APPS = "no_uninstall_apps"; @@ -40575,7 +40652,7 @@ package android.service.notification { method public int getPriorityCategoryReminders(); method public int getPriorityCategoryRepeatCallers(); method public int getPriorityCategorySystem(); - method @FlaggedApi("android.app.modes_api") public int getPriorityChannels(); + method @FlaggedApi("android.app.modes_api") public int getPriorityChannelsAllowed(); method public int getPriorityConversationSenders(); method public int getPriorityMessageSenders(); method public int getVisualEffectAmbient(); @@ -53625,13 +53702,13 @@ package android.view { method @Deprecated public android.view.Display getDefaultDisplay(); method @NonNull public default android.view.WindowMetrics getMaximumWindowMetrics(); method public default boolean isCrossWindowBlurEnabled(); - method @FlaggedApi("com.android.window.flags.surface_control_input_receiver") @NonNull public default android.os.IBinder registerBatchedSurfaceControlInputReceiver(int, @NonNull android.os.IBinder, @NonNull android.view.SurfaceControl, @NonNull android.view.Choreographer, @NonNull android.view.SurfaceControlInputReceiver); + method @FlaggedApi("com.android.window.flags.surface_control_input_receiver") public default void registerBatchedSurfaceControlInputReceiver(int, @NonNull android.os.IBinder, @NonNull android.view.SurfaceControl, @NonNull android.view.Choreographer, @NonNull android.view.SurfaceControlInputReceiver); method @FlaggedApi("com.android.window.flags.trusted_presentation_listener_for_window") public default void registerTrustedPresentationListener(@NonNull android.os.IBinder, @NonNull android.window.TrustedPresentationThresholds, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Boolean>); - method @FlaggedApi("com.android.window.flags.surface_control_input_receiver") @NonNull public default android.os.IBinder registerUnbatchedSurfaceControlInputReceiver(int, @NonNull android.os.IBinder, @NonNull android.view.SurfaceControl, @NonNull android.os.Looper, @NonNull android.view.SurfaceControlInputReceiver); + 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 public void removeViewImmediate(android.view.View); - method @FlaggedApi("com.android.window.flags.surface_control_input_receiver") public default void unregisterSurfaceControlInputReceiver(@NonNull android.os.IBinder); + 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"; diff --git a/core/api/system-current.txt b/core/api/system-current.txt index ff2279c68231..017de17b5416 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -129,6 +129,7 @@ package android { field public static final String DISABLE_SYSTEM_SOUND_EFFECTS = "android.permission.DISABLE_SYSTEM_SOUND_EFFECTS"; field public static final String DISPATCH_PROVISIONING_MESSAGE = "android.permission.DISPATCH_PROVISIONING_MESSAGE"; field public static final String DOMAIN_VERIFICATION_AGENT = "android.permission.DOMAIN_VERIFICATION_AGENT"; + field @FlaggedApi("android.content.pm.emergency_install_permission") public static final String EMERGENCY_INSTALL_PACKAGES = "android.permission.EMERGENCY_INSTALL_PACKAGES"; field public static final String ENTER_CAR_MODE_PRIORITIZED = "android.permission.ENTER_CAR_MODE_PRIORITIZED"; field public static final String EXEMPT_FROM_AUDIO_RECORD_RESTRICTIONS = "android.permission.EXEMPT_FROM_AUDIO_RECORD_RESTRICTIONS"; field public static final String FORCE_BACK = "android.permission.FORCE_BACK"; @@ -871,6 +872,10 @@ package android.app { field @NonNull public static final android.os.Parcelable.Creator<android.app.AppOpsManager.PackageOps> CREATOR; } + @FlaggedApi("android.app.bic_client") public final class BackgroundInstallControlManager { + method @FlaggedApi("android.app.bic_client") @NonNull @RequiresPermission(android.Manifest.permission.GET_BACKGROUND_INSTALLED_PACKAGES) public java.util.List<android.content.pm.PackageInfo> getBackgroundInstalledPackages(long); + } + public class BroadcastOptions { method public void clearRequireCompatChange(); method public int getPendingIntentBackgroundActivityStartMode(); @@ -3388,11 +3393,10 @@ package android.companion.virtual.camera { } @FlaggedApi("android.companion.virtual.flags.virtual_camera") public static final class VirtualCameraConfig.Builder { - ctor public VirtualCameraConfig.Builder(); + ctor public VirtualCameraConfig.Builder(@NonNull String); method @NonNull public android.companion.virtual.camera.VirtualCameraConfig.Builder addStreamConfig(@IntRange(from=1) int, @IntRange(from=1) int, int, @IntRange(from=1) int); method @NonNull public android.companion.virtual.camera.VirtualCameraConfig build(); method @NonNull public android.companion.virtual.camera.VirtualCameraConfig.Builder setLensFacing(int); - method @NonNull public android.companion.virtual.camera.VirtualCameraConfig.Builder setName(@NonNull String); method @NonNull public android.companion.virtual.camera.VirtualCameraConfig.Builder setSensorOrientation(int); method @NonNull public android.companion.virtual.camera.VirtualCameraConfig.Builder setVirtualCameraCallback(@NonNull java.util.concurrent.Executor, @NonNull android.companion.virtual.camera.VirtualCameraCallback); } @@ -4228,6 +4232,7 @@ package android.content.pm { public final class UserProperties implements android.os.Parcelable { method public int describeContents(); method public int getCrossProfileContentSharingStrategy(); + method @FlaggedApi("android.multiuser.support_hiding_profiles") @NonNull public int getProfileApiVisibility(); method public int getShowInQuietMode(); method public int getShowInSharingSurfaces(); method public boolean isCredentialShareableWithParent(); @@ -4237,6 +4242,9 @@ package android.content.pm { field public static final int CROSS_PROFILE_CONTENT_SHARING_DELEGATE_FROM_PARENT = 1; // 0x1 field public static final int CROSS_PROFILE_CONTENT_SHARING_NO_DELEGATION = 0; // 0x0 field public static final int CROSS_PROFILE_CONTENT_SHARING_UNKNOWN = -1; // 0xffffffff + field @FlaggedApi("android.multiuser.support_hiding_profiles") public static final int PROFILE_API_VISIBILITY_HIDDEN = 1; // 0x1 + field @FlaggedApi("android.multiuser.support_hiding_profiles") public static final int PROFILE_API_VISIBILITY_UNKNOWN = -1; // 0xffffffff + field @FlaggedApi("android.multiuser.support_hiding_profiles") public static final int PROFILE_API_VISIBILITY_VISIBLE = 0; // 0x0 field public static final int SHOW_IN_QUIET_MODE_DEFAULT = 2; // 0x2 field public static final int SHOW_IN_QUIET_MODE_HIDDEN = 1; // 0x1 field public static final int SHOW_IN_QUIET_MODE_PAUSED = 0; // 0x0 @@ -6886,7 +6894,6 @@ package android.media { public final class MediaRouter2 { method @NonNull public java.util.List<android.media.MediaRoute2Info> getAllRoutes(); method @Nullable public String getClientPackageName(); - method @Nullable @RequiresPermission(android.Manifest.permission.MEDIA_CONTENT_CONTROL) public static android.media.MediaRouter2 getInstance(@NonNull android.content.Context, @NonNull String); method @RequiresPermission(android.Manifest.permission.MEDIA_CONTENT_CONTROL) public void setRouteVolume(@NonNull android.media.MediaRoute2Info, int); method @RequiresPermission(android.Manifest.permission.MEDIA_CONTENT_CONTROL) public void startScan(); method @RequiresPermission(android.Manifest.permission.MEDIA_CONTENT_CONTROL) public void stopScan(); @@ -14774,6 +14781,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 @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); @@ -14952,6 +14960,21 @@ 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 { + method public long getLogcatCollectionStartTimeMillis(); + method public boolean isLogcatCollectionEnabled(); + 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 class TelephonyManager.ModemActivityInfoException extends java.lang.Exception { ctor public TelephonyManager.ModemActivityInfoException(int); method public int getErrorCode(); @@ -17159,7 +17182,7 @@ package android.telephony.satellite { method @FlaggedApi("com.android.internal.telephony.flags.carrier_enabled_satellite_flag") @NonNull @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public java.util.Set<java.lang.Integer> getSatelliteAttachRestrictionReasonsForCarrier(int); method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void pollPendingSatelliteDatagrams(@NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Integer>); method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void provisionSatelliteService(@NonNull String, @NonNull byte[], @Nullable android.os.CancellationSignal, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Integer>); - method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void registerForNtnSignalStrengthChanged(@NonNull java.util.concurrent.Executor, @NonNull android.telephony.satellite.NtnSignalStrengthCallback) throws android.telephony.satellite.SatelliteManager.SatelliteException; + method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void registerForNtnSignalStrengthChanged(@NonNull java.util.concurrent.Executor, @NonNull android.telephony.satellite.NtnSignalStrengthCallback); method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public int registerForSatelliteCapabilitiesChanged(@NonNull java.util.concurrent.Executor, @NonNull android.telephony.satellite.SatelliteCapabilitiesCallback); method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public int registerForSatelliteDatagram(@NonNull java.util.concurrent.Executor, @NonNull android.telephony.satellite.SatelliteDatagramCallback); method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public int registerForSatelliteModemStateChanged(@NonNull java.util.concurrent.Executor, @NonNull android.telephony.satellite.SatelliteModemStateCallback); @@ -17226,6 +17249,7 @@ package android.telephony.satellite { field @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public static final int SATELLITE_MODEM_STATE_UNKNOWN = -1; // 0xffffffff field @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public static final int SATELLITE_RESULT_ACCESS_BARRED = 16; // 0x10 field @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public static final int SATELLITE_RESULT_ERROR = 1; // 0x1 + field @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public static final int SATELLITE_RESULT_ILLEGAL_STATE = 23; // 0x17 field @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public static final int SATELLITE_RESULT_INVALID_ARGUMENTS = 8; // 0x8 field @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public static final int SATELLITE_RESULT_INVALID_MODEM_STATE = 7; // 0x7 field @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public static final int SATELLITE_RESULT_INVALID_TELEPHONY_STATE = 6; // 0x6 diff --git a/core/api/test-current.txt b/core/api/test-current.txt index b8b98a3cb3ba..7a3d320dddab 100644 --- a/core/api/test-current.txt +++ b/core/api/test-current.txt @@ -3646,6 +3646,7 @@ package android.view { public interface WindowManager extends android.view.ViewManager { method public default int getDisplayImePolicy(int); + method @FlaggedApi("com.android.window.flags.surface_control_input_receiver") @Nullable public default android.os.IBinder getSurfaceControlInputClientToken(@NonNull android.view.SurfaceControl); method public static boolean hasWindowExtensionsEnabled(); method public default void holdLock(android.os.IBinder, int); method public default boolean isGlobalKey(int); diff --git a/core/java/android/app/ActivityOptions.java b/core/java/android/app/ActivityOptions.java index 57fca74e7e59..4a566db3afb3 100644 --- a/core/java/android/app/ActivityOptions.java +++ b/core/java/android/app/ActivityOptions.java @@ -184,6 +184,12 @@ public class ActivityOptions extends ComponentOptions { public static final String KEY_ANIM_START_LISTENER = "android:activity.animStartListener"; /** + * Callback for when animation is aborted. + * @hide + */ + private static final String KEY_ANIM_ABORT_LISTENER = "android:activity.animAbortListener"; + + /** * Specific a theme for a splash screen window. * @hide */ @@ -459,6 +465,7 @@ public class ActivityOptions extends ComponentOptions { private int mHeight; private IRemoteCallback mAnimationStartedListener; private IRemoteCallback mAnimationFinishedListener; + private IRemoteCallback mAnimationAbortListener; private SceneTransitionInfo mSceneTransitionInfo; private PendingIntent mUsageTimeReport; private int mLaunchDisplayId = INVALID_DISPLAY; @@ -716,6 +723,14 @@ public class ActivityOptions extends ComponentOptions { } /** + * Callback for finding out when the given animation is finished + * @hide + */ + public void setOnAnimationFinishedListener(IRemoteCallback listener) { + mAnimationFinishedListener = listener; + } + + /** * Callback for finding out when the given animation has drawn its last frame. * @hide */ @@ -728,6 +743,14 @@ public class ActivityOptions extends ComponentOptions { } /** + * Callback for finding out when the given animation is aborted + * @hide + */ + public void setOnAnimationAbortListener(IRemoteCallback listener) { + mAnimationAbortListener = listener; + } + + /** * Create an ActivityOptions specifying an animation where the new * activity is scaled from a small originating area of the screen to * its final full representation. @@ -1327,6 +1350,8 @@ public class ActivityOptions extends ComponentOptions { KEY_PENDING_INTENT_CREATOR_BACKGROUND_ACTIVITY_START_MODE, MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED); mDisableStartingWindow = opts.getBoolean(KEY_DISABLE_STARTING_WINDOW); + mAnimationAbortListener = IRemoteCallback.Stub.asInterface( + opts.getBinder(KEY_ANIM_ABORT_LISTENER)); } /** @@ -1427,11 +1452,15 @@ public class ActivityOptions extends ComponentOptions { /** @hide */ public void abort() { - if (mAnimationStartedListener != null) { + sendResultIgnoreErrors(mAnimationStartedListener, null); + sendResultIgnoreErrors(mAnimationAbortListener, null); + } + + private void sendResultIgnoreErrors(IRemoteCallback callback, Bundle data) { + if (callback != null) { try { - mAnimationStartedListener.sendResult(null); - } catch (RemoteException e) { - } + callback.sendResult(data); + } catch (RemoteException e) { } } } @@ -2110,12 +2139,7 @@ public class ActivityOptions extends ComponentOptions { mCustomExitResId = otherOptions.mCustomExitResId; mCustomBackgroundColor = otherOptions.mCustomBackgroundColor; mThumbnail = null; - if (mAnimationStartedListener != null) { - try { - mAnimationStartedListener.sendResult(null); - } catch (RemoteException e) { - } - } + sendResultIgnoreErrors(mAnimationStartedListener, null); mAnimationStartedListener = otherOptions.mAnimationStartedListener; break; case ANIM_CUSTOM_IN_PLACE: @@ -2126,12 +2150,7 @@ public class ActivityOptions extends ComponentOptions { mStartY = otherOptions.mStartY; mWidth = otherOptions.mWidth; mHeight = otherOptions.mHeight; - if (mAnimationStartedListener != null) { - try { - mAnimationStartedListener.sendResult(null); - } catch (RemoteException e) { - } - } + sendResultIgnoreErrors(mAnimationStartedListener, null); mAnimationStartedListener = null; break; case ANIM_THUMBNAIL_SCALE_UP: @@ -2143,12 +2162,7 @@ public class ActivityOptions extends ComponentOptions { mStartY = otherOptions.mStartY; mWidth = otherOptions.mWidth; mHeight = otherOptions.mHeight; - if (mAnimationStartedListener != null) { - try { - mAnimationStartedListener.sendResult(null); - } catch (RemoteException e) { - } - } + sendResultIgnoreErrors(mAnimationStartedListener, null); mAnimationStartedListener = otherOptions.mAnimationStartedListener; break; case ANIM_SCENE_TRANSITION: @@ -2165,6 +2179,9 @@ public class ActivityOptions extends ComponentOptions { mRemoteAnimationAdapter = otherOptions.mRemoteAnimationAdapter; mLaunchIntoPipParams = otherOptions.mLaunchIntoPipParams; mIsEligibleForLegacyPermissionPrompt = otherOptions.mIsEligibleForLegacyPermissionPrompt; + + sendResultIgnoreErrors(mAnimationAbortListener, null); + mAnimationAbortListener = otherOptions.mAnimationAbortListener; } /** @@ -2363,6 +2380,8 @@ public class ActivityOptions extends ComponentOptions { if (mDisableStartingWindow) { b.putBoolean(KEY_DISABLE_STARTING_WINDOW, mDisableStartingWindow); } + b.putBinder(KEY_ANIM_ABORT_LISTENER, + mAnimationAbortListener != null ? mAnimationAbortListener.asBinder() : null); return b; } diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java index 1bdbd4c50634..5d2a26e13ee7 100644 --- a/core/java/android/app/ActivityThread.java +++ b/core/java/android/app/ActivityThread.java @@ -319,6 +319,10 @@ public final class ActivityThread extends ClientTransactionHandler public static final int SERVICE_DONE_EXECUTING_START = 1; /** Type for IActivityManager.serviceDoneExecuting: done stopping (destroying) service */ public static final int SERVICE_DONE_EXECUTING_STOP = 2; + /** Type for IActivityManager.serviceDoneExecuting: done with an onRebind call */ + public static final int SERVICE_DONE_EXECUTING_REBIND = 3; + /** Type for IActivityManager.serviceDoneExecuting: done with an onUnbind call */ + public static final int SERVICE_DONE_EXECUTING_UNBIND = 4; /** Use foreground GC policy (less pause time) and higher JIT weight. */ private static final int VM_PROCESS_STATE_JANK_PERCEPTIBLE = 0; @@ -4882,7 +4886,7 @@ public final class ActivityThread extends ClientTransactionHandler mServices.put(data.token, service); try { ActivityManager.getService().serviceDoneExecuting( - data.token, SERVICE_DONE_EXECUTING_ANON, 0, 0); + data.token, SERVICE_DONE_EXECUTING_ANON, 0, 0, null); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -4913,7 +4917,7 @@ public final class ActivityThread extends ClientTransactionHandler } else { s.onRebind(data.intent); ActivityManager.getService().serviceDoneExecuting( - data.token, SERVICE_DONE_EXECUTING_ANON, 0, 0); + data.token, SERVICE_DONE_EXECUTING_REBIND, 0, 0, data.intent); } } catch (RemoteException ex) { throw ex.rethrowFromSystemServer(); @@ -4943,7 +4947,7 @@ public final class ActivityThread extends ClientTransactionHandler data.token, data.intent, doRebind); } else { ActivityManager.getService().serviceDoneExecuting( - data.token, SERVICE_DONE_EXECUTING_ANON, 0, 0); + data.token, SERVICE_DONE_EXECUTING_UNBIND, 0, 0, data.intent); } } catch (RemoteException ex) { throw ex.rethrowFromSystemServer(); @@ -5057,7 +5061,7 @@ public final class ActivityThread extends ClientTransactionHandler try { ActivityManager.getService().serviceDoneExecuting( - data.token, SERVICE_DONE_EXECUTING_START, data.startId, res); + data.token, SERVICE_DONE_EXECUTING_START, data.startId, res, null); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -5089,7 +5093,7 @@ public final class ActivityThread extends ClientTransactionHandler try { ActivityManager.getService().serviceDoneExecuting( - token, SERVICE_DONE_EXECUTING_STOP, 0, 0); + token, SERVICE_DONE_EXECUTING_STOP, 0, 0, null); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } diff --git a/core/java/android/app/AppOpsManager.java b/core/java/android/app/AppOpsManager.java index ccd8456129eb..00c4b0f6515f 100644 --- a/core/java/android/app/AppOpsManager.java +++ b/core/java/android/app/AppOpsManager.java @@ -1548,9 +1548,16 @@ public class AppOpsManager { public static final int OP_READ_SYSTEM_GRAMMATICAL_GENDER = AppProtoEnums.APP_OP_READ_SYSTEM_GRAMMATICAL_GENDER; + /** + * Allows an app whose primary use case is to backup or sync content to run longer jobs. + * + * @hide + */ + public static final int OP_RUN_BACKUP_JOBS = AppProtoEnums.APP_OP_RUN_BACKUP_JOBS; + /** @hide */ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) - public static final int _NUM_OP = 144; + public static final int _NUM_OP = 145; /** * All app ops represented as strings. @@ -1700,6 +1707,7 @@ public class AppOpsManager { OPSTR_ENABLE_MOBILE_DATA_BY_USER, OPSTR_RESERVED_FOR_TESTING, OPSTR_RAPID_CLEAR_NOTIFICATIONS_BY_LISTENER, + OPSTR_RUN_BACKUP_JOBS, }) public @interface AppOpString {} @@ -2392,6 +2400,13 @@ public class AppOpsManager { public static final String OPSTR_READ_SYSTEM_GRAMMATICAL_GENDER = "android:read_system_grammatical_gender"; + /** + * Allows an app whose primary use case is to backup or sync content to run longer jobs. + * + * @hide + */ + public static final String OPSTR_RUN_BACKUP_JOBS = "android:run_backup_jobs"; + /** {@link #sAppOpsToNote} not initialized yet for this op */ private static final byte SHOULD_COLLECT_NOTE_OP_NOT_INITIALIZED = 0; /** Should not collect noting of this app-op in {@link #sAppOpsToNote} */ @@ -2504,6 +2519,7 @@ public class AppOpsManager { OP_RECEIVE_SANDBOXED_DETECTION_TRAINING_DATA, OP_MEDIA_ROUTING_CONTROL, OP_READ_SYSTEM_GRAMMATICAL_GENDER, + OP_RUN_BACKUP_JOBS, }; static final AppOpInfo[] sAppOpInfos = new AppOpInfo[]{ @@ -2961,6 +2977,8 @@ public class AppOpsManager { // will make it an app-op permission in the future. // .setPermission(Manifest.permission.READ_SYSTEM_GRAMMATICAL_GENDER) .build(), + new AppOpInfo.Builder(OP_RUN_BACKUP_JOBS, OPSTR_RUN_BACKUP_JOBS, "RUN_BACKUP_JOBS") + .setPermission(Manifest.permission.RUN_BACKUP_JOBS).build(), }; // The number of longs needed to form a full bitmask of app ops diff --git a/core/java/android/app/BackgroundInstallControlManager.java b/core/java/android/app/BackgroundInstallControlManager.java new file mode 100644 index 000000000000..664fcebcfc05 --- /dev/null +++ b/core/java/android/app/BackgroundInstallControlManager.java @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.app; + +import static android.Manifest.permission.GET_BACKGROUND_INSTALLED_PACKAGES; +import static android.annotation.SystemApi.Client.PRIVILEGED_APPS; + +import android.annotation.FlaggedApi; +import android.annotation.NonNull; +import android.annotation.RequiresPermission; +import android.annotation.SystemApi; +import android.annotation.SystemService; +import android.content.Context; +import android.content.pm.IBackgroundInstallControlService; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.os.RemoteException; +import android.os.ServiceManager; + +import java.util.List; + +/** + * BackgroundInstallControlManager client allows apps to query apps installed in background. + * + * <p>Any applications that was installed without an accompanying installer UI activity paired + * with recorded user interaction event is considered background installed. This is determined by + * analysis of user-activity logs. + * + * <p>Warning: BackgroundInstallControl should not be considered a definitive + * authority of identifying background installed applications. Consumers can use this as a + * supplementary signal, but must perform additional due diligence to confirm the install nature + * of the package. + * + * @hide + */ +@FlaggedApi(Flags.FLAG_BIC_CLIENT) +@SystemApi(client = PRIVILEGED_APPS) +@SystemService(Context.BACKGROUND_INSTALL_CONTROL_SERVICE) +public final class BackgroundInstallControlManager { + + private static final String TAG = "BackgroundInstallControlManager"; + private static IBackgroundInstallControlService sService; + private final Context mContext; + + BackgroundInstallControlManager(Context context) { + mContext = context; + } + + private static IBackgroundInstallControlService getService() { + if (sService == null) { + sService = + IBackgroundInstallControlService.Stub.asInterface( + ServiceManager.getService(Context.BACKGROUND_INSTALL_CONTROL_SERVICE)); + } + return sService; + } + + /** + * Returns a full list of {@link PackageInfo} of apps currently installed for the current user + * that are considered installed in the background. + * + * <p>Refer to top level doc {@link BackgroundInstallControlManager} for more details on + * background-installed applications. + * <p> + * + * @param flags - Flags will be used to call + * {@link PackageManager#getInstalledPackages(PackageInfoFlags)} to retrieve installed packages. + * @return A list of packages retrieved from {@link PackageManager} with non-background + * installed app filter applied. + * + * @hide + */ + @FlaggedApi(Flags.FLAG_BIC_CLIENT) + @SystemApi + @RequiresPermission(GET_BACKGROUND_INSTALLED_PACKAGES) + public @NonNull List<PackageInfo> getBackgroundInstalledPackages( + @PackageManager.PackageInfoFlagsBits long flags) { + List<PackageInfo> backgroundInstalledPackages; + try { + return getService() + .getBackgroundInstalledPackages(flags, mContext.getUserId()) + .getList(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + +} diff --git a/core/java/android/app/IActivityManager.aidl b/core/java/android/app/IActivityManager.aidl index 47403d26c907..b063d04655f5 100644 --- a/core/java/android/app/IActivityManager.aidl +++ b/core/java/android/app/IActivityManager.aidl @@ -294,7 +294,8 @@ interface IActivityManager { @UnsupportedAppUsage ParceledListSlice getRecentTasks(int maxNum, int flags, int userId); @UnsupportedAppUsage - oneway void serviceDoneExecuting(in IBinder token, int type, int startId, int res); + oneway void serviceDoneExecuting(in IBinder token, int type, int startId, int res, + in Intent intent); /** @deprecated Use {@link #getIntentSenderWithFeature} instead */ @UnsupportedAppUsage(maxTargetSdk=29, publicAlternatives="Use {@link PendingIntent#getIntentSender()} instead") IIntentSender getIntentSender(int type, in String packageName, in IBinder token, diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java index ed0cfbe3d9c3..a81ad3c429ea 100644 --- a/core/java/android/app/Notification.java +++ b/core/java/android/app/Notification.java @@ -5487,6 +5487,15 @@ public class Notification implements Parcelable return mColors; } + /** + * @param isHeader If the notification is a notification header + * @return An instance of mColors after resolving the palette + */ + private Colors getColors(boolean isHeader) { + mColors.resolvePalette(mContext, mN.color, !isHeader && mN.isColorized(), mInNightMode); + return mColors; + } + private void updateBackgroundColor(RemoteViews contentView, StandardTemplateParams p) { if (isBackgroundColorized(p)) { @@ -6618,6 +6627,23 @@ public class Notification implements Parcelable return getColors(p).getContrastColor(); } + /** + * Gets the foreground color of the small icon. If the notification is colorized, this + * is the primary text color, otherwise it's the contrast-adjusted app-provided color. + * @hide + */ + public @ColorInt int getSmallIconColor(boolean isHeader) { + return getColors(/* isHeader = */ isHeader).getContrastColor(); + } + + /** + * Gets the background color of the notification. + * @hide + */ + public @ColorInt int getBackgroundColor(boolean isHeader) { + return getColors(/* isHeader = */ isHeader).getBackgroundColor(); + } + /** @return the theme's accent color for colored UI elements. */ private @ColorInt int getPrimaryAccentColor(StandardTemplateParams p) { return getColors(p).getPrimaryAccentColor(); @@ -8532,6 +8558,8 @@ public class Notification implements Parcelable boolean isImportantConversation = mConversationType == CONVERSATION_TYPE_IMPORTANT; boolean isHeaderless = !isConversationLayout && isCollapsed; + //TODO (b/217799515): ensure mConversationTitle always returns the correct + // conversationTitle, probably set mConversationTitle = conversationTitle after this CharSequence conversationTitle = !TextUtils.isEmpty(super.mBigContentTitle) ? super.mBigContentTitle : mConversationTitle; diff --git a/core/java/android/app/OWNERS b/core/java/android/app/OWNERS index 0760d4db9169..3b5bba20a10b 100644 --- a/core/java/android/app/OWNERS +++ b/core/java/android/app/OWNERS @@ -90,8 +90,8 @@ per-file InstantAppResolveInfo.aidl = file:/services/core/java/com/android/serve per-file pinner-client.aconfig = file:/core/java/android/app/pinner/OWNERS # BackgroundInstallControlManager -per-file BackgroundInstallControlManager.java = file:/services/core/java/com/android/server/pm/OWNERS -per-file background_install_control_manager.aconfig = file:/services/core/java/com/android/server/pm/OWNERS +per-file BackgroundInstallControlManager.java = file:/services/core/java/com/android/server/pm/BACKGROUND_INSTALL_OWNERS +per-file background_install_control_manager.aconfig = file:/services/core/java/com/android/server/pm/BACKGROUND_INSTALL_OWNERS # ResourcesManager per-file ResourcesManager.java = file:RESOURCES_OWNERS diff --git a/core/java/android/companion/virtual/camera/VirtualCameraConfig.java b/core/java/android/companion/virtual/camera/VirtualCameraConfig.java index 350cf3d832d6..06a0f5c09e18 100644 --- a/core/java/android/companion/virtual/camera/VirtualCameraConfig.java +++ b/core/java/android/companion/virtual/camera/VirtualCameraConfig.java @@ -196,13 +196,12 @@ public final class VirtualCameraConfig implements Parcelable { * <li>At least one stream must be added with {@link #addStreamConfig(int, int, int, int)}. * <li>A callback must be set with {@link #setVirtualCameraCallback(Executor, * VirtualCameraCallback)} - * <li>A camera name must be set with {@link #setName(String)} * <li>A lens facing must be set with {@link #setLensFacing(int)} */ @FlaggedApi(Flags.FLAG_VIRTUAL_CAMERA) public static final class Builder { - private String mName; + private final String mName; private final ArraySet<VirtualCameraStreamConfig> mStreamConfigurations = new ArraySet<>(); private Executor mCallbackExecutor; private VirtualCameraCallback mCallback; @@ -210,12 +209,12 @@ public final class VirtualCameraConfig implements Parcelable { private int mLensFacing = LENS_FACING_UNKNOWN; /** - * Sets the name of the virtual camera instance. + * Creates a new instance of {@link Builder}. + * + * @param name The name of the {@link VirtualCamera}. */ - @NonNull - public Builder setName(@NonNull String name) { - mName = requireNonNull(name, "Display name cannot be null"); - return this; + public Builder(@NonNull String name) { + mName = requireNonNull(name, "Name cannot be null"); } /** diff --git a/core/java/android/content/IntentFilter.java b/core/java/android/content/IntentFilter.java index ad3acd713c6b..79af65a3a3e5 100644 --- a/core/java/android/content/IntentFilter.java +++ b/core/java/android/content/IntentFilter.java @@ -16,11 +16,13 @@ package android.content; +import android.annotation.FlaggedApi; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.SystemApi; import android.compat.annotation.UnsupportedAppUsage; +import android.content.pm.Flags; import android.net.Uri; import android.os.Build; import android.os.Bundle; @@ -175,6 +177,7 @@ public class IntentFilter implements Parcelable { private static final String ACTION_STR = "action"; private static final String AUTO_VERIFY_STR = "autoVerify"; private static final String EXTRAS_STR = "extras"; + private static final String URI_RELATIVE_FILTER_GROUP_STR = "uriRelativeFilterGroup"; private static final int[] EMPTY_INT_ARRAY = new int[0]; private static final long[] EMPTY_LONG_ARRAY = new long[0]; @@ -324,6 +327,7 @@ public class IntentFilter implements Parcelable { private ArrayList<PatternMatcher> mDataSchemeSpecificParts = null; private ArrayList<AuthorityEntry> mDataAuthorities = null; private ArrayList<PatternMatcher> mDataPaths = null; + private ArrayList<UriRelativeFilterGroup> mUriRelativeFilterGroups = null; private ArrayList<String> mStaticDataTypes = null; private ArrayList<String> mDataTypes = null; private ArrayList<String> mMimeGroups = null; @@ -520,6 +524,10 @@ public class IntentFilter implements Parcelable { if (o.mDataPaths != null) { mDataPaths = new ArrayList<PatternMatcher>(o.mDataPaths); } + if (o.mUriRelativeFilterGroups != null) { + mUriRelativeFilterGroups = + new ArrayList<UriRelativeFilterGroup>(o.mUriRelativeFilterGroups); + } if (o.mMimeGroups != null) { mMimeGroups = new ArrayList<String>(o.mMimeGroups); } @@ -1563,6 +1571,63 @@ public class IntentFilter implements Parcelable { } /** + * Add a new URI relative filter group to match against the Intent data. The + * intent filter must include one or more schemes (via {@link #addDataScheme}) + * <em>and</em> one or more authorities (via {@link #addDataAuthority}) for + * the group to be considered. + * + * <p>Groups will be matched in the order they were added and matching will only + * be done if no data paths match or if none are included. If both data paths and + * groups are not included, then only the scheme/authority must match.</p> + * + * @param group A {@link UriRelativeFilterGroup} to match the URI. + * + * @see UriRelativeFilterGroup + * @see #matchData + * @see #addDataScheme + * @see #addDataAuthority + */ + @FlaggedApi(Flags.FLAG_RELATIVE_REFERENCE_INTENT_FILTERS) + public final void addUriRelativeFilterGroup(@NonNull UriRelativeFilterGroup group) { + Objects.requireNonNull(group); + if (mUriRelativeFilterGroups == null) { + mUriRelativeFilterGroups = new ArrayList<>(); + } + mUriRelativeFilterGroups.add(group); + } + + /** + * Return the number of URI relative filter groups in the intent filter. + */ + @FlaggedApi(Flags.FLAG_RELATIVE_REFERENCE_INTENT_FILTERS) + public final int countUriRelativeFilterGroups() { + return mUriRelativeFilterGroups == null ? 0 : mUriRelativeFilterGroups.size(); + } + + /** + * Return a URI relative filter group in the intent filter. + * + * <p>Note: use of this method will result in a NullPointerException + * if no groups exists for this intent filter.</p> + * + * @param index index of the element to return + * @throws IndexOutOfBoundsException if index is out of range + */ + @FlaggedApi(Flags.FLAG_RELATIVE_REFERENCE_INTENT_FILTERS) + @NonNull + public final UriRelativeFilterGroup getUriRelativeFilterGroup(int index) { + return mUriRelativeFilterGroups.get(index); + } + + /** + * Removes all existing URI relative filter groups in the intent filter. + */ + @FlaggedApi(Flags.FLAG_RELATIVE_REFERENCE_INTENT_FILTERS) + public final void clearUriRelativeFilterGroups() { + mUriRelativeFilterGroups = null; + } + + /** * Match this intent filter against the given Intent data. This ignores * the data scheme -- unlike {@link #matchData}, the authority will match * regardless of whether there is a matching scheme. @@ -1677,12 +1742,24 @@ public class IntentFilter implements Parcelable { int authMatch = matchDataAuthority(data, wildcardSupported); if (authMatch >= 0) { final ArrayList<PatternMatcher> paths = mDataPaths; - if (paths == null) { - match = authMatch; - } else if (hasDataPath(data.getPath(), wildcardSupported)) { - match = MATCH_CATEGORY_PATH; + final ArrayList<UriRelativeFilterGroup> groups = mUriRelativeFilterGroups; + if (Flags.relativeReferenceIntentFilters()) { + if (paths == null && groups == null) { + match = authMatch; + } else if (hasDataPath(data.getPath(), wildcardSupported) + || matchRelRefGroups(data)) { + match = MATCH_CATEGORY_PATH; + } else { + return NO_MATCH_DATA; + } } else { - return NO_MATCH_DATA; + if (paths == null) { + match = authMatch; + } else if (hasDataPath(data.getPath(), wildcardSupported)) { + match = MATCH_CATEGORY_PATH; + } else { + return NO_MATCH_DATA; + } } } else { return NO_MATCH_DATA; @@ -1726,6 +1803,19 @@ public class IntentFilter implements Parcelable { return match + MATCH_ADJUSTMENT_NORMAL; } + private boolean matchRelRefGroups(Uri data) { + if (mUriRelativeFilterGroups == null) { + return false; + } + for (int i = 0; i < mUriRelativeFilterGroups.size(); i++) { + UriRelativeFilterGroup group = mUriRelativeFilterGroups.get(i); + if (group.matchData(data)) { + return group.getAction() == UriRelativeFilterGroup.ACTION_ALLOW; + } + } + return false; + } + /** * Add a new Intent category to match against. The semantics of * categories is the opposite of actions -- an Intent includes the @@ -2486,6 +2576,12 @@ public class IntentFilter implements Parcelable { } serializer.endTag(null, EXTRAS_STR); } + if (Flags.relativeReferenceIntentFilters()) { + N = countUriRelativeFilterGroups(); + for (int i = 0; i < N; i++) { + mUriRelativeFilterGroups.get(i).writeToXml(serializer); + } + } } /** @@ -2614,6 +2710,9 @@ public class IntentFilter implements Parcelable { } } else if (tagName.equals(EXTRAS_STR)) { mExtras = PersistableBundle.restoreFromXml(parser); + } else if (Flags.relativeReferenceIntentFilters() + && URI_RELATIVE_FILTER_GROUP_STR.equals(tagName)) { + addUriRelativeFilterGroup(new UriRelativeFilterGroup(parser)); } else { Log.w("IntentFilter", "Unknown tag parsing IntentFilter: " + tagName); } @@ -2680,6 +2779,12 @@ public class IntentFilter implements Parcelable { if (mExtras != null) { mExtras.dumpDebug(proto, IntentFilterProto.EXTRAS); } + if (Flags.relativeReferenceIntentFilters() && mUriRelativeFilterGroups != null) { + Iterator<UriRelativeFilterGroup> it = mUriRelativeFilterGroups.iterator(); + while (it.hasNext()) { + it.next().dumpDebug(proto, IntentFilterProto.URI_RELATIVE_FILTER_GROUPS); + } + } proto.end(token); } @@ -2744,6 +2849,15 @@ public class IntentFilter implements Parcelable { du.println(sb.toString()); } } + if (mUriRelativeFilterGroups != null) { + Iterator<UriRelativeFilterGroup> it = mUriRelativeFilterGroups.iterator(); + while (it.hasNext()) { + sb.setLength(0); + sb.append(prefix); sb.append("UriRelativeFilterGroup: \""); + sb.append(it.next()); sb.append("\""); + du.println(sb.toString()); + } + } if (mStaticDataTypes != null) { Iterator<String> it = mStaticDataTypes.iterator(); while (it.hasNext()) { @@ -2883,6 +2997,15 @@ public class IntentFilter implements Parcelable { } else { dest.writeInt(0); } + if (Flags.relativeReferenceIntentFilters() && mUriRelativeFilterGroups != null) { + final int N = mUriRelativeFilterGroups.size(); + dest.writeInt(N); + for (int i = 0; i < N; i++) { + mUriRelativeFilterGroups.get(i).writeToParcel(dest, flags); + } + } else { + dest.writeInt(0); + } } /** @@ -2989,6 +3112,13 @@ public class IntentFilter implements Parcelable { if (source.readInt() != 0) { mExtras = PersistableBundle.CREATOR.createFromParcel(source); } + N = source.readInt(); + if (Flags.relativeReferenceIntentFilters() && N > 0) { + mUriRelativeFilterGroups = new ArrayList<UriRelativeFilterGroup>(N); + for (int i = 0; i < N; i++) { + mUriRelativeFilterGroups.add(new UriRelativeFilterGroup(source)); + } + } } private boolean hasPartialTypes() { diff --git a/core/java/android/content/UriRelativeFilter.java b/core/java/android/content/UriRelativeFilter.java new file mode 100644 index 000000000000..9866cd0e992a --- /dev/null +++ b/core/java/android/content/UriRelativeFilter.java @@ -0,0 +1,260 @@ +/* + * 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; + +import android.annotation.FlaggedApi; +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.pm.Flags; +import android.net.Uri; +import android.os.Parcel; +import android.os.PatternMatcher; +import android.util.proto.ProtoOutputStream; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; +import org.xmlpull.v1.XmlSerializer; + +import java.io.IOException; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * A filter for matching Intent URI Data as part of a + * {@link UriRelativeFilterGroup}. A single filter can only be + * matched against either a URI path, query or fragment + */ +@FlaggedApi(Flags.FLAG_RELATIVE_REFERENCE_INTENT_FILTERS) +public final class UriRelativeFilter { + private static final String FILTER_STR = "filter"; + private static final String PART_STR = "part"; + private static final String PATTERN_STR = "pattern"; + static final String URI_RELATIVE_FILTER_STR = "uriRelativeFilter"; + + /** + * Value to indicate that the filter is to be applied to a URI path. + */ + public static final int PATH = 0; + /** + * Value to indicate that the filter is to be applied to a URI query. + */ + public static final int QUERY = 1; + /** + * Value to indicate that the filter is to be applied to a URI fragment. + */ + public static final int FRAGMENT = 2; + + /** @hide */ + @IntDef(value = { + PATH, + QUERY, + FRAGMENT + }) + @Retention(RetentionPolicy.SOURCE) + public @interface UriPart {} + + private final @UriPart int mUriPart; + private final @PatternMatcher.PatternType int mPatternType; + private final String mFilter; + + /** + * Creates a new UriRelativeFilter. + * + * @param uriPart The URI part this filter operates on. Can be either a + * {@link UriRelativeFilter#PATH}, {@link UriRelativeFilter#QUERY}, + * or {@link UriRelativeFilter#FRAGMENT}. + * @param patternType The pattern type of the filter. Can be either a + * {@link PatternMatcher#PATTERN_LITERAL}, + * {@link PatternMatcher#PATTERN_PREFIX}, +* {@link PatternMatcher#PATTERN_SUFFIX}, + * {@link PatternMatcher#PATTERN_SIMPLE_GLOB}, + * or {@link PatternMatcher#PATTERN_ADVANCED_GLOB}. + * @param filter A literal or pattern string depedning on patterType + * used to match a uriPart . + */ + public UriRelativeFilter( + @UriPart int uriPart, + @PatternMatcher.PatternType int patternType, + @NonNull String filter) { + mUriPart = uriPart; + com.android.internal.util.AnnotationValidations.validate( + UriPart.class, null, mUriPart); + mPatternType = patternType; + com.android.internal.util.AnnotationValidations.validate( + PatternMatcher.PatternType.class, null, mPatternType); + mFilter = filter; + com.android.internal.util.AnnotationValidations.validate( + NonNull.class, null, mFilter); + } + + /** + * The URI part this filter operates on. + */ + public @UriPart int getUriPart() { + return mUriPart; + } + + /** + * The pattern type of the filter. + */ + public @PatternMatcher.PatternType int getPatternType() { + return mPatternType; + } + + /** + * The string used to filter the URI. + */ + public @NonNull String getFilter() { + return mFilter; + } + + /** + * Match this URI filter against an Intent's data. QUERY filters can + * match against any key value pair in the query string. PATH and + * FRAGMENT filters must match the entire string. + * + * @param data The full data string to match against, as supplied in + * Intent.data. + * + * @return true if there is a match. + */ + public boolean matchData(@NonNull Uri data) { + PatternMatcher pe = new PatternMatcher(mFilter, mPatternType); + switch (getUriPart()) { + case PATH: + return pe.match(data.getPath()); + case QUERY: + return matchQuery(pe, data.getQuery()); + case FRAGMENT: + return pe.match(data.getFragment()); + default: + return false; + } + } + + private boolean matchQuery(PatternMatcher pe, String query) { + if (query != null) { + String[] params = query.split("&"); + if (params.length == 1) { + params = query.split(";"); + } + for (int i = 0; i < params.length; i++) { + if (pe.match(params[i])) return true; + } + } + return false; + } + + /** @hide */ + public void dumpDebug(ProtoOutputStream proto, long fieldId) { + long token = proto.start(fieldId); + proto.write(UriRelativeFilterProto.URI_PART, mUriPart); + proto.write(UriRelativeFilterProto.PATTERN_TYPE, mPatternType); + proto.write(UriRelativeFilterProto.FILTER, mFilter); + proto.end(token); + } + + /** @hide */ + public void writeToXml(XmlSerializer serializer) throws IOException { + serializer.startTag(null, URI_RELATIVE_FILTER_STR); + serializer.attribute(null, PATTERN_STR, Integer.toString(mPatternType)); + serializer.attribute(null, PART_STR, Integer.toString(mUriPart)); + serializer.attribute(null, FILTER_STR, mFilter); + serializer.endTag(null, URI_RELATIVE_FILTER_STR); + } + + private String uriPartToString() { + switch (mUriPart) { + case PATH: + return "PATH"; + case QUERY: + return "QUERY"; + case FRAGMENT: + return "FRAGMENT"; + default: + return "UNKNOWN"; + } + } + + private String patternTypeToString() { + switch (mPatternType) { + case PatternMatcher.PATTERN_LITERAL: + return "LITERAL"; + case PatternMatcher.PATTERN_PREFIX: + return "PREFIX"; + case PatternMatcher.PATTERN_SIMPLE_GLOB: + return "GLOB"; + case PatternMatcher.PATTERN_ADVANCED_GLOB: + return "ADVANCED_GLOB"; + case PatternMatcher.PATTERN_SUFFIX: + return "SUFFIX"; + default: + return "UNKNOWN"; + } + } + + @Override + public String toString() { + return "UriRelativeFilter { " + + "uriPart = " + uriPartToString() + ", " + + "patternType = " + patternTypeToString() + ", " + + "filter = " + mFilter + + " }"; + } + + @Override + public boolean equals(@Nullable Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + @SuppressWarnings("unchecked") + UriRelativeFilter that = (UriRelativeFilter) o; + return mUriPart == that.mUriPart + && mPatternType == that.mPatternType + && java.util.Objects.equals(mFilter, that.mFilter); + } + + @Override + public int hashCode() { + int _hash = 1; + _hash = 31 * _hash + mUriPart; + _hash = 31 * _hash + mPatternType; + _hash = 31 * _hash + java.util.Objects.hashCode(mFilter); + return _hash; + } + + /** @hide */ + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeInt(mUriPart); + dest.writeInt(mPatternType); + dest.writeString(mFilter); + } + + /** @hide */ + UriRelativeFilter(@NonNull android.os.Parcel in) { + mUriPart = in.readInt(); + mPatternType = in.readInt(); + mFilter = in.readString(); + } + + /** @hide */ + public UriRelativeFilter(XmlPullParser parser) throws XmlPullParserException, IOException { + mUriPart = Integer.parseInt(parser.getAttributeValue(null, PART_STR)); + mPatternType = Integer.parseInt(parser.getAttributeValue(null, PATTERN_STR)); + mFilter = parser.getAttributeValue(null, FILTER_STR); + } +} diff --git a/core/java/android/content/UriRelativeFilterGroup.java b/core/java/android/content/UriRelativeFilterGroup.java new file mode 100644 index 000000000000..72c396a73ec8 --- /dev/null +++ b/core/java/android/content/UriRelativeFilterGroup.java @@ -0,0 +1,216 @@ +/* + * 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; + +import android.annotation.FlaggedApi; +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.content.pm.Flags; +import android.net.Uri; +import android.os.Parcel; +import android.util.ArraySet; +import android.util.Log; +import android.util.proto.ProtoOutputStream; + +import com.android.internal.util.CollectionUtils; +import com.android.internal.util.XmlUtils; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; +import org.xmlpull.v1.XmlSerializer; + +import java.io.IOException; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.Collection; +import java.util.Collections; +import java.util.Iterator; +import java.util.Objects; + +/** + * An intent data matching group based on a URI's relative reference which + * includes the path, query and fragment. The group is only considered as + * matching if <em>all</em> UriRelativeFilters in the group match. Each + * UriRelativeFilter defines a matching rule for a URI path, query or fragment. + * A group must contain one or more UriRelativeFilters to match but does not need to + * contain UriRelativeFilters for all existing parts of a URI to match. + * + * <p>For example, given a URI that contains path, query and fragment parts, + * a group containing only a path filter will match the URI if the path + * filter matches the URI path. If the group contains a path and query + * filter, then the group will only match if both path and query filters + * match. If a URI contains only a path with no query or fragment then a + * group can only match if it contains only a matching path filter. If the + * group also contained additional query or fragment filters then it will + * not match.</p> + */ +@FlaggedApi(Flags.FLAG_RELATIVE_REFERENCE_INTENT_FILTERS) +public final class UriRelativeFilterGroup { + private static final String ALLOW_STR = "allow"; + private static final String URI_RELATIVE_FILTER_GROUP_STR = "uriRelativeFilterGroup"; + + /** + * Value to indicate that the group match is allowed. + */ + public static final int ACTION_ALLOW = 0; + /** + * Value to indicate that the group match is blocked. + */ + public static final int ACTION_BLOCK = 1; + + /** @hide */ + @IntDef(value = { + ACTION_ALLOW, + ACTION_BLOCK + }) + @Retention(RetentionPolicy.SOURCE) + public @interface Action {} + + private final @Action int mAction; + private final ArraySet<UriRelativeFilter> mUriRelativeFilters = new ArraySet<>(); + + /** + * New UriRelativeFilterGroup that matches a Intent data. + * + * @param action Whether this matching group should be allowed or disallowed. + */ + public UriRelativeFilterGroup(@Action int action) { + mAction = action; + } + + /** @hide */ + public UriRelativeFilterGroup(XmlPullParser parser) throws XmlPullParserException, IOException { + mAction = Integer.parseInt(parser.getAttributeValue(null, ALLOW_STR)); + + int outerDepth = parser.getDepth(); + int type; + while ((type = parser.next()) != XmlPullParser.END_DOCUMENT + && (type != XmlPullParser.END_TAG + || parser.getDepth() > outerDepth)) { + if (type == XmlPullParser.END_TAG + || type == XmlPullParser.TEXT) { + continue; + } + + String tagName = parser.getName(); + if (tagName.equals(UriRelativeFilter.URI_RELATIVE_FILTER_STR)) { + addUriRelativeFilter(new UriRelativeFilter(parser)); + } else { + Log.w("IntentFilter", "Unknown tag parsing IntentFilter: " + tagName); + } + XmlUtils.skipCurrentTag(parser); + } + } + + /** + * Return {@link UriRelativeFilterGroup#ACTION_ALLOW} if a URI is allowed when matched + * and {@link UriRelativeFilterGroup#ACTION_BLOCK} if a URI is blacked when matched. + */ + public @Action int getAction() { + return mAction; + } + + /** + * Add a filter to the group. + */ + public void addUriRelativeFilter(@NonNull UriRelativeFilter uriRelativeFilter) { + Objects.requireNonNull(uriRelativeFilter); + if (!CollectionUtils.contains(mUriRelativeFilters, uriRelativeFilter)) { + mUriRelativeFilters.add(uriRelativeFilter); + } + } + + /** + * Returns a unmodifiable view of the UriRelativeFilters list in this group. + */ + @NonNull + public Collection<UriRelativeFilter> getUriRelativeFilters() { + return Collections.unmodifiableCollection(mUriRelativeFilters); + } + + /** + * Match all URI filter in this group against {@link Intent#getData()}. + * + * @param data The full data string to match against, as supplied in + * Intent.data. + * @return true if all filters match. + */ + public boolean matchData(@NonNull Uri data) { + if (mUriRelativeFilters.size() == 0) { + return false; + } + for (UriRelativeFilter filter : mUriRelativeFilters) { + if (!filter.matchData(data)) { + return false; + } + } + return true; + } + + /** @hide */ + public void dumpDebug(ProtoOutputStream proto, long fieldId) { + long token = proto.start(fieldId); + proto.write(UriRelativeFilterGroupProto.ACTION, mAction); + Iterator<UriRelativeFilter> it = mUriRelativeFilters.iterator(); + while (it.hasNext()) { + it.next().dumpDebug(proto, UriRelativeFilterGroupProto.URI_RELATIVE_FILTERS); + } + proto.end(token); + } + + /** @hide */ + public void writeToXml(XmlSerializer serializer) throws IOException { + serializer.startTag(null, URI_RELATIVE_FILTER_GROUP_STR); + serializer.attribute(null, ALLOW_STR, Integer.toString(mAction)); + Iterator<UriRelativeFilter> it = mUriRelativeFilters.iterator(); + while (it.hasNext()) { + UriRelativeFilter filter = it.next(); + filter.writeToXml(serializer); + } + serializer.endTag(null, URI_RELATIVE_FILTER_GROUP_STR); + } + + @Override + public String toString() { + return "UriRelativeFilterGroup { allow = " + mAction + + ", uri_filters = " + mUriRelativeFilters + ", }"; + } + + /** @hide */ + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeInt(mAction); + final int n = mUriRelativeFilters.size(); + if (n > 0) { + dest.writeInt(n); + Iterator<UriRelativeFilter> it = mUriRelativeFilters.iterator(); + while (it.hasNext()) { + it.next().writeToParcel(dest, flags); + } + } else { + dest.writeInt(0); + } + } + + /** @hide */ + UriRelativeFilterGroup(@NonNull Parcel src) { + mAction = src.readInt(); + final int n = src.readInt(); + for (int i = 0; i < n; i++) { + mUriRelativeFilters.add(new UriRelativeFilter(src)); + } + } +} diff --git a/core/java/android/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java index 4724e866f094..8744eaee4341 100644 --- a/core/java/android/content/pm/PackageManager.java +++ b/core/java/android/content/pm/PackageManager.java @@ -128,6 +128,7 @@ import java.util.function.Function; * <a href="/training/basics/intents/package-visibility">manage package visibility</a>. * </p> */ +@android.ravenwood.annotation.RavenwoodKeepPartialClass public abstract class PackageManager { private static final String TAG = "PackageManager"; @@ -5492,6 +5493,7 @@ public abstract class PackageManager { * application info. * @hide */ + @android.ravenwood.annotation.RavenwoodKeepWholeClass public static class Flags { final long mValue; protected Flags(long value) { @@ -5506,6 +5508,7 @@ public abstract class PackageManager { * Specific flags used for retrieving package info. Example: * {@code PackageManager.getPackageInfo(packageName, PackageInfoFlags.of(0)} */ + @android.ravenwood.annotation.RavenwoodKeepWholeClass public final static class PackageInfoFlags extends Flags { private PackageInfoFlags(@PackageInfoFlagsBits long value) { super(value); @@ -5519,6 +5522,7 @@ public abstract class PackageManager { /** * Specific flags used for retrieving application info. */ + @android.ravenwood.annotation.RavenwoodKeepWholeClass public final static class ApplicationInfoFlags extends Flags { private ApplicationInfoFlags(@ApplicationInfoFlagsBits long value) { super(value); @@ -5532,6 +5536,7 @@ public abstract class PackageManager { /** * Specific flags used for retrieving component info. */ + @android.ravenwood.annotation.RavenwoodKeepWholeClass public final static class ComponentInfoFlags extends Flags { private ComponentInfoFlags(@ComponentInfoFlagsBits long value) { super(value); @@ -5545,6 +5550,7 @@ public abstract class PackageManager { /** * Specific flags used for retrieving resolve info. */ + @android.ravenwood.annotation.RavenwoodKeepWholeClass public final static class ResolveInfoFlags extends Flags { private ResolveInfoFlags(@ResolveInfoFlagsBits long value) { super(value); diff --git a/core/java/android/content/pm/UserInfo.java b/core/java/android/content/pm/UserInfo.java index 8fd78bd4276c..3e9f260566bd 100644 --- a/core/java/android/content/pm/UserInfo.java +++ b/core/java/android/content/pm/UserInfo.java @@ -52,6 +52,7 @@ import java.lang.annotation.RetentionPolicy; * @hide */ @TestApi +@android.ravenwood.annotation.RavenwoodKeepWholeClass public class UserInfo implements Parcelable { /** @@ -438,6 +439,7 @@ public class UserInfo implements Parcelable { /** * @return true if this user can be switched to. **/ + @android.ravenwood.annotation.RavenwoodThrow public boolean supportsSwitchTo() { if (partial || !isEnabled()) { // Don't support switching to disabled or partial users, which includes users with @@ -455,6 +457,7 @@ public class UserInfo implements Parcelable { * @return true if user is of type {@link UserManager#USER_TYPE_SYSTEM_HEADLESS} and * {@link com.android.internal.R.bool.config_canSwitchToHeadlessSystemUser} is true. */ + @android.ravenwood.annotation.RavenwoodThrow private boolean canSwitchToHeadlessSystemUser() { return UserManager.USER_TYPE_SYSTEM_HEADLESS.equals(userType) && Resources.getSystem() .getBoolean(com.android.internal.R.bool.config_canSwitchToHeadlessSystemUser); @@ -465,6 +468,7 @@ public class UserInfo implements Parcelable { * @deprecated Use {@link UserInfo#supportsSwitchTo} instead. */ @Deprecated + @android.ravenwood.annotation.RavenwoodThrow public boolean supportsSwitchToByUser() { return supportsSwitchTo(); } diff --git a/core/java/android/content/pm/UserProperties.java b/core/java/android/content/pm/UserProperties.java index 269c6c27821c..1d0e2db78bcb 100644 --- a/core/java/android/content/pm/UserProperties.java +++ b/core/java/android/content/pm/UserProperties.java @@ -16,6 +16,9 @@ package android.content.pm; +import static android.multiuser.Flags.FLAG_SUPPORT_HIDING_PROFILES; + +import android.annotation.FlaggedApi; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; @@ -73,7 +76,7 @@ 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"; /** Index values of each property (to indicate whether they are present in this object). */ @IntDef(prefix = "INDEX_", value = { INDEX_SHOW_IN_LAUNCHER, @@ -93,6 +96,7 @@ 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 }) @Retention(RetentionPolicy.SOURCE) private @interface PropertyIndex { @@ -114,6 +118,7 @@ public final class UserProperties implements Parcelable { private static final int INDEX_SHOW_IN_SHARING_SURFACES = 14; 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; /** A bit set, mapping each PropertyIndex to whether it is present (1) or absent (0). */ private long mPropertiesPresent = 0; @@ -450,6 +455,41 @@ public final class UserProperties implements Parcelable { @SuppressLint("UnflaggedApi") // b/306636213 public static final int CROSS_PROFILE_CONTENT_SHARING_DELEGATE_FROM_PARENT = 1; + /** + * Possible values for the profile visibility in public API surfaces. This indicates whether or + * not the information linked to the profile (userId, package names) should not be returned in + * API surfaces if a user is marked as hidden. + * + * @hide + */ + @Retention(RetentionPolicy.SOURCE) + @IntDef(prefix = "PROFILE_API_VISIBILITY_", + value = { + PROFILE_API_VISIBILITY_UNKNOWN, + PROFILE_API_VISIBILITY_VISIBLE, + PROFILE_API_VISIBILITY_HIDDEN, + } + ) + public @interface ProfileApiVisibility { + } + /* + * The api visibility value for this profile user is undefined or unknown. + */ + @FlaggedApi(FLAG_SUPPORT_HIDING_PROFILES) + public static final int PROFILE_API_VISIBILITY_UNKNOWN = -1; + + /** + * Indicates that information about this profile user should be shown in API surfaces. + */ + @FlaggedApi(FLAG_SUPPORT_HIDING_PROFILES) + public static final int PROFILE_API_VISIBILITY_VISIBLE = 0; + + /** + * Indicates that information about this profile should be not be visible in API surfaces. + */ + @FlaggedApi(FLAG_SUPPORT_HIDING_PROFILES) + public static final int PROFILE_API_VISIBILITY_HIDDEN = 1; + /** * Creates a UserProperties (intended for the SystemServer) that stores a reference to the given @@ -510,6 +550,9 @@ public final class UserProperties implements Parcelable { setShowInQuietMode(orig.getShowInQuietMode()); setShowInSharingSurfaces(orig.getShowInSharingSurfaces()); setCrossProfileContentSharingStrategy(orig.getCrossProfileContentSharingStrategy()); + if (android.multiuser.Flags.supportHidingProfiles()) { + setProfileApiVisibility(orig.getProfileApiVisibility()); + } } /** @@ -951,9 +994,31 @@ public final class UserProperties implements Parcelable { } private @CrossProfileContentSharingStrategy int mCrossProfileContentSharingStrategy; + /** + * Returns the visibility of the profile user in API surfaces. Any information linked to the + * profile (userId, package names) should be hidden API surfaces if a user is marked as hidden. + */ + @NonNull + @FlaggedApi(FLAG_SUPPORT_HIDING_PROFILES) + public @ProfileApiVisibility int getProfileApiVisibility() { + if (isPresent(INDEX_PROFILE_API_VISIBILITY)) return mProfileApiVisibility; + if (mDefaultProperties != null) return mDefaultProperties.mProfileApiVisibility; + throw new SecurityException("You don't have permission to query profileApiVisibility"); + } + /** @hide */ + @NonNull + @FlaggedApi(FLAG_SUPPORT_HIDING_PROFILES) + public void setProfileApiVisibility(@ProfileApiVisibility int profileApiVisibility) { + this.mProfileApiVisibility = profileApiVisibility; + setPresent(INDEX_PROFILE_API_VISIBILITY); + } + private @ProfileApiVisibility int mProfileApiVisibility; @Override public String toString() { + String profileApiVisibility = + android.multiuser.Flags.supportHidingProfiles() ? ", mProfileApiVisibility=" + + getProfileApiVisibility() : ""; // Please print in increasing order of PropertyIndex. return "UserProperties{" + "mPropertiesPresent=" + Long.toBinaryString(mPropertiesPresent) @@ -977,6 +1042,7 @@ public final class UserProperties implements Parcelable { + ", mDeleteAppWithParent=" + getDeleteAppWithParent() + ", mAlwaysVisible=" + getAlwaysVisible() + ", mCrossProfileContentSharingStrategy=" + getCrossProfileContentSharingStrategy() + + profileApiVisibility + "}"; } @@ -1010,6 +1076,9 @@ public final class UserProperties implements Parcelable { pw.println(prefix + " mAlwaysVisible=" + getAlwaysVisible()); pw.println(prefix + " mCrossProfileContentSharingStrategy=" + getCrossProfileContentSharingStrategy()); + if (android.multiuser.Flags.supportHidingProfiles()) { + pw.println(prefix + " mProfileApiVisibility=" + getProfileApiVisibility()); + } } /** @@ -1093,6 +1162,12 @@ public final class UserProperties implements Parcelable { break; case ATTR_CROSS_PROFILE_CONTENT_SHARING_STRATEGY: setCrossProfileContentSharingStrategy(parser.getAttributeInt(i)); + break; + case ATTR_PROFILE_API_VISIBILITY: + if (android.multiuser.Flags.supportHidingProfiles()) { + setProfileApiVisibility(parser.getAttributeInt(i)); + } + break; default: Slog.w(LOG_TAG, "Skipping unknown property " + attributeName); } @@ -1175,6 +1250,12 @@ public final class UserProperties implements Parcelable { serializer.attributeInt(null, ATTR_CROSS_PROFILE_CONTENT_SHARING_STRATEGY, mCrossProfileContentSharingStrategy); } + if (isPresent(INDEX_PROFILE_API_VISIBILITY)) { + if (android.multiuser.Flags.supportHidingProfiles()) { + serializer.attributeInt(null, ATTR_PROFILE_API_VISIBILITY, + mProfileApiVisibility); + } + } } // For use only with an object that has already had any permission-lacking fields stripped out. @@ -1198,6 +1279,7 @@ public final class UserProperties implements Parcelable { dest.writeBoolean(mDeleteAppWithParent); dest.writeBoolean(mAlwaysVisible); dest.writeInt(mCrossProfileContentSharingStrategy); + dest.writeInt(mProfileApiVisibility); } /** @@ -1225,6 +1307,7 @@ public final class UserProperties implements Parcelable { mDeleteAppWithParent = source.readBoolean(); mAlwaysVisible = source.readBoolean(); mCrossProfileContentSharingStrategy = source.readInt(); + mProfileApiVisibility = source.readInt(); } @Override @@ -1274,6 +1357,7 @@ public final class UserProperties implements Parcelable { private boolean mAlwaysVisible = false; private @CrossProfileContentSharingStrategy int mCrossProfileContentSharingStrategy = CROSS_PROFILE_CONTENT_SHARING_NO_DELEGATION; + private @ProfileApiVisibility int mProfileApiVisibility = 0; /** * @hide @@ -1428,6 +1512,17 @@ public final class UserProperties implements Parcelable { return this; } + /** + * Sets the value for {@link #mProfileApiVisibility} + * @hide + */ + @NonNull + @FlaggedApi(FLAG_SUPPORT_HIDING_PROFILES) + public Builder setProfileApiVisibility(@ProfileApiVisibility int profileApiVisibility){ + mProfileApiVisibility = profileApiVisibility; + return this; + } + /** Builds a UserProperties object with *all* values populated. * @hide */ @@ -1452,7 +1547,8 @@ public final class UserProperties implements Parcelable { mAllowStoppingUserWithDelayedLocking, mDeleteAppWithParent, mAlwaysVisible, - mCrossProfileContentSharingStrategy); + mCrossProfileContentSharingStrategy, + mProfileApiVisibility); } } // end Builder @@ -1473,7 +1569,8 @@ public final class UserProperties implements Parcelable { boolean allowStoppingUserWithDelayedLocking, boolean deleteAppWithParent, boolean alwaysVisible, - @CrossProfileContentSharingStrategy int crossProfileContentSharingStrategy) { + @CrossProfileContentSharingStrategy int crossProfileContentSharingStrategy, + @ProfileApiVisibility int profileApiVisibility) { mDefaultProperties = null; setShowInLauncher(showInLauncher); setStartWithParent(startWithParent); @@ -1493,5 +1590,8 @@ public final class UserProperties implements Parcelable { setDeleteAppWithParent(deleteAppWithParent); setAlwaysVisible(alwaysVisible); setCrossProfileContentSharingStrategy(crossProfileContentSharingStrategy); + if (android.multiuser.Flags.supportHidingProfiles()) { + setProfileApiVisibility(profileApiVisibility); + } } } diff --git a/core/java/android/content/pm/flags.aconfig b/core/java/android/content/pm/flags.aconfig index 903875bef1fc..caff4576c9c3 100644 --- a/core/java/android/content/pm/flags.aconfig +++ b/core/java/android/content/pm/flags.aconfig @@ -110,6 +110,14 @@ flag { } flag { + name: "relative_reference_intent_filters" + namespace: "package_manager_service" + description: "Feature flag to enable relative reference intent filters" + bug: "307556883" + is_fixed_read_only: true +} + +flag { name: "fix_duplicated_flags" namespace: "package_manager_service" description: "Feature flag to fix duplicated PackageManager flag values" @@ -161,3 +169,17 @@ flag { bug: "321258605" is_fixed_read_only: true } + +flag { + name: "allow_sdk_sandbox_query_intent_activities" + namespace: "package_manager_service" + description: "Feature flag to allow the sandbox SDK to query intent activities of the client app." + bug: "295842134" +} + +flag { + name: "emergency_install_permission" + namespace: "permissions" + description: "Feature flag to enable permission EMERGENCY_INSTALL_PACKAGES" + bug: "321080601" +} diff --git a/core/java/android/content/pm/multiuser.aconfig b/core/java/android/content/pm/multiuser.aconfig index 9644d8095a4d..efb8607f75f7 100644 --- a/core/java/android/content/pm/multiuser.aconfig +++ b/core/java/android/content/pm/multiuser.aconfig @@ -85,6 +85,7 @@ flag { description: "Enable auto-locking private space on device restarts" bug: "296993385" } + flag { name: "enable_system_user_only_for_services_and_providers" namespace: "multiuser" @@ -92,3 +93,18 @@ flag { bug: "302354856" is_fixed_read_only: true } + +flag { + name: "allow_private_profile_apis" + namespace: "profile_experiences" + description: "Enable only the API changes to support private space" + bug: "299069460" +} + +flag { + name: "support_hiding_profiles" + namespace: "profile_experiences" + description: "Allow the use of a hide_profile property to hide some profiles behind a permission" + bug: "316362775" + is_fixed_read_only: true +} diff --git a/core/java/android/content/res/FontScaleConverterFactory.java b/core/java/android/content/res/FontScaleConverterFactory.java index cbe4c62d7069..625d7cb66900 100644 --- a/core/java/android/content/res/FontScaleConverterFactory.java +++ b/core/java/android/content/res/FontScaleConverterFactory.java @@ -58,6 +58,16 @@ public class FontScaleConverterFactory { synchronized (LOOKUP_TABLES_WRITE_LOCK) { putInto( sLookupTables, + /* scaleKey= */ 1.1f, + new FontScaleConverterImpl( + /* fromSp= */ + new float[] { 8f, 10f, 12f, 14f, 18f, 20f, 24f, 30f, 100}, + /* toDp= */ + new float[] { 8.8f, 11f, 13.2f, 15.6f, 19.2f, 21.2f, 24.8f, 30f, 100}) + ); + + putInto( + sLookupTables, /* scaleKey= */ 1.15f, new FontScaleConverterImpl( /* fromSp= */ diff --git a/core/java/android/credentials/ui/AuthenticationEntry.java b/core/java/android/credentials/ui/AuthenticationEntry.java index b1a382c8ea96..9bd0871b3d3b 100644 --- a/core/java/android/credentials/ui/AuthenticationEntry.java +++ b/core/java/android/credentials/ui/AuthenticationEntry.java @@ -34,15 +34,24 @@ import java.lang.annotation.RetentionPolicy; /** * 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. + * * @hide */ @TestApi public final class AuthenticationEntry implements Parcelable { - @NonNull private final String mKey; - @NonNull private final String mSubkey; - @NonNull private final @Status int mStatus; - @Nullable private Intent mFrameworkExtrasIntent; - @NonNull private final Slice mSlice; + @NonNull + private final String mKey; + @NonNull + private final String mSubkey; + @NonNull + private final @Status int mStatus; + @Nullable + private Intent mFrameworkExtrasIntent; + @NonNull + private final Slice mSlice; /** @hide **/ @IntDef(prefix = {"STATUS_"}, value = { @@ -51,15 +60,21 @@ public final class AuthenticationEntry implements Parcelable { STATUS_UNLOCKED_BUT_EMPTY_MOST_RECENT, }) @Retention(RetentionPolicy.SOURCE) - public @interface Status {} + public @interface Status { + } /** This entry is still locked, as initially supplied by the provider. */ 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 was unlocked but didn't contain any credential. Meanwhile, "less recent" means + * there is another such entry that was unlocked more recently. + */ 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 should be at most one authentication entry with this status. */ + /** + * 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. + */ public static final int STATUS_UNLOCKED_BUT_EMPTY_MOST_RECENT = 2; private AuthenticationEntry(@NonNull Parcel in) { @@ -74,9 +89,11 @@ public final class AuthenticationEntry implements Parcelable { AnnotationValidations.validate(NonNull.class, null, mSlice); } - /** Constructor to be used for an entry that does not require further activities + /** + * Constructor to be used for an entry that does not require further activities * to be invoked when selected. */ + // TODO(b/322065508): remove this constructor. public AuthenticationEntry(@NonNull String key, @NonNull String subkey, @NonNull Slice slice, @Status int status) { mKey = key; @@ -95,9 +112,9 @@ public final class AuthenticationEntry implements Parcelable { } /** - * Returns the identifier of this entry that's unique within the context of the CredentialManager - * request. - */ + * Returns the identifier of this entry that's unique within the context of the + * CredentialManager request. + */ @NonNull public String getKey() { return mKey; @@ -111,23 +128,23 @@ public final class AuthenticationEntry implements Parcelable { return mSubkey; } - /** - * Returns the Slice to be rendered. - */ + /** Returns the Slice to be rendered. */ @NonNull public Slice getSlice() { return mSlice; } - /** - * Returns the entry status. - */ + /** Returns the entry status, depending on which the entry will be rendered differently. */ @NonNull @Status public int getStatus() { return mStatus; } + /** + * Returns the framework intent to be filled in when launching this entry's provider + * PendingIntent. + */ @Nullable @SuppressLint("IntentBuilderName") // Not building a new intent. public Intent getFrameworkExtrasIntent() { diff --git a/core/java/android/credentials/ui/BaseDialogResult.java b/core/java/android/credentials/ui/BaseDialogResult.java index e8cf5abd5239..e985a4666d31 100644 --- a/core/java/android/credentials/ui/BaseDialogResult.java +++ b/core/java/android/credentials/ui/BaseDialogResult.java @@ -24,8 +24,6 @@ import android.os.IBinder; import android.os.Parcel; import android.os.Parcelable; -import com.android.internal.util.AnnotationValidations; - import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -46,7 +44,7 @@ public class BaseDialogResult implements Parcelable { /** * Used for the UX to construct the {@code resultData Bundle} to send via the {@code - * ResultReceiver}. + * ResultReceiver}. */ public static void addToBundle(@NonNull BaseDialogResult result, @NonNull Bundle bundle) { bundle.putParcelable(EXTRA_BASE_RESULT, result); @@ -66,13 +64,14 @@ public class BaseDialogResult implements Parcelable { RESULT_CODE_DATA_PARSING_FAILURE, }) @Retention(RetentionPolicy.SOURCE) - public @interface ResultCode {} + public @interface ResultCode { + } /** User intentionally canceled the dialog. */ public static final int RESULT_CODE_DIALOG_USER_CANCELED = 0; /** - * The user has consented to switching to a new default provider. The provider info is in the - * {@code resultData}. + * The UI was stopped since the user has chosen to navigate to the Settings UI to reconfigure + * their providers. */ public static final int RESULT_CODE_CANCELED_AND_LAUNCHED_SETTINGS = 1; /** @@ -86,6 +85,7 @@ public class BaseDialogResult implements Parcelable { public static final int RESULT_CODE_DATA_PARSING_FAILURE = 3; @Nullable + @Deprecated private final IBinder mRequestToken; public BaseDialogResult(@Nullable IBinder requestToken) { @@ -94,6 +94,7 @@ public class BaseDialogResult implements Parcelable { /** Returns the unique identifier for the request that launched the operation. */ @Nullable + @Deprecated public IBinder getRequestToken() { return mRequestToken; } @@ -115,14 +116,14 @@ public class BaseDialogResult implements Parcelable { public static final @NonNull Creator<BaseDialogResult> CREATOR = new Creator<BaseDialogResult>() { - @Override - public BaseDialogResult createFromParcel(@NonNull Parcel in) { - return new BaseDialogResult(in); - } - - @Override - public BaseDialogResult[] newArray(int size) { - return new BaseDialogResult[size]; - } - }; + @Override + public BaseDialogResult createFromParcel(@NonNull Parcel in) { + return new BaseDialogResult(in); + } + + @Override + public BaseDialogResult[] newArray(int size) { + return new BaseDialogResult[size]; + } + }; } diff --git a/core/java/android/credentials/ui/CancelUiRequest.java b/core/java/android/credentials/ui/CancelUiRequest.java index d4c249e58c8a..712424ced41b 100644 --- a/core/java/android/credentials/ui/CancelUiRequest.java +++ b/core/java/android/credentials/ui/CancelUiRequest.java @@ -24,7 +24,7 @@ import android.os.Parcelable; import com.android.internal.util.AnnotationValidations; /** - * A request to cancel any ongoing UI matching this request. + * A request to cancel the ongoing UI matching the identifier token in this request. * * @hide */ @@ -33,9 +33,12 @@ 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.ui.extra.EXTRA_CANCEL_UI_REQUEST"; + @NonNull + public static final String EXTRA_CANCEL_UI_REQUEST = + "android.credentials.ui.extra.CANCEL_UI_REQUEST"; @NonNull private final IBinder mToken; @@ -51,6 +54,10 @@ public final class CancelUiRequest implements Parcelable { 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; @@ -64,6 +71,7 @@ public final class CancelUiRequest implements Parcelable { return mShouldShowCancellationUi; } + /** Constructs a {@link CancelUiRequest}. */ public CancelUiRequest(@NonNull IBinder token, boolean shouldShowCancellationUi, @NonNull String appPackageName) { mToken = token; @@ -91,7 +99,8 @@ public final class CancelUiRequest implements Parcelable { return 0; } - @NonNull public static final Creator<CancelUiRequest> CREATOR = new Creator<>() { + @NonNull + public static final Creator<CancelUiRequest> CREATOR = new Creator<>() { @Override public CancelUiRequest createFromParcel(@NonNull Parcel in) { return new CancelUiRequest(in); diff --git a/core/java/android/credentials/ui/Constants.java b/core/java/android/credentials/ui/Constants.java index 37f850bc46c5..68f28e74dad0 100644 --- a/core/java/android/credentials/ui/Constants.java +++ b/core/java/android/credentials/ui/Constants.java @@ -36,7 +36,5 @@ public class Constants { public static final String EXTRA_REQ_FOR_ALL_OPTIONS = "android.credentials.ui.extra.REQ_FOR_ALL_OPTIONS"; - /** The intent action for when the enabled Credential Manager providers has been updated. */ - public static final String CREDMAN_ENABLED_PROVIDERS_UPDATED = - "android.credentials.ui.action.CREDMAN_ENABLED_PROVIDERS_UPDATED"; + private Constants() {} } diff --git a/core/java/android/credentials/ui/CreateCredentialProviderData.java b/core/java/android/credentials/ui/CreateCredentialProviderData.java index 2508d8eb20ab..d7a4f5bdbfca 100644 --- a/core/java/android/credentials/ui/CreateCredentialProviderData.java +++ b/core/java/android/credentials/ui/CreateCredentialProviderData.java @@ -47,6 +47,17 @@ public final class CreateCredentialProviderData extends ProviderData implements mRemoteEntry = remoteEntry; } + /** + * Converts the instance to a {@link CreateCredentialProviderInfo}. + * + * @hide + */ + @NonNull + public CreateCredentialProviderInfo toCreateCredentialProviderInfo() { + return new CreateCredentialProviderInfo( + getProviderFlattenedComponentName(), mSaveEntries, mRemoteEntry); + } + @NonNull public List<Entry> getSaveEntries() { return mSaveEntries; diff --git a/core/java/android/credentials/ui/CreateCredentialProviderInfo.java b/core/java/android/credentials/ui/CreateCredentialProviderInfo.java new file mode 100644 index 000000000000..41ca852c2351 --- /dev/null +++ b/core/java/android/credentials/ui/CreateCredentialProviderInfo.java @@ -0,0 +1,113 @@ +/* + * 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.ui; + +import android.annotation.NonNull; +import android.annotation.Nullable; + +import com.android.internal.util.Preconditions; + +import java.util.ArrayList; +import java.util.List; + +/** + * Information pertaining to a specific provider during the given create-credential flow. + * + * This includes provider metadata and its credential creation options for display purposes. + * + * @hide + */ +public final class CreateCredentialProviderInfo { + + @NonNull + private final String mProviderName; + + @NonNull + private final List<Entry> mSaveEntries; + @Nullable + private final Entry mRemoteEntry; + + CreateCredentialProviderInfo( + @NonNull String providerName, @NonNull List<Entry> saveEntries, + @Nullable Entry remoteEntry) { + mProviderName = Preconditions.checkStringNotEmpty(providerName); + mSaveEntries = new ArrayList<>(saveEntries); + mRemoteEntry = remoteEntry; + } + + /** Returns the fully-qualified provider (component or package) name. */ + @NonNull + public String getProviderName() { + return mProviderName; + } + + /** Returns all the options this provider has, to which the credential can be saved. */ + @NonNull + public List<Entry> getSaveEntries() { + return mSaveEntries; + } + + /** + * 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. + */ + @Nullable + public Entry getRemoteEntry() { + return mRemoteEntry; + } + + /** + * Builder for {@link CreateCredentialProviderInfo}. + * + * @hide + */ + public static final class Builder { + @NonNull + private String mProviderName; + @NonNull + private List<Entry> mSaveEntries = new ArrayList<>(); + @Nullable + private Entry mRemoteEntry = null; + + /** Constructor with required properties. */ + public Builder(@NonNull String providerName) { + mProviderName = Preconditions.checkStringNotEmpty(providerName); + } + + /** Sets the list of options for credential saving to be displayed to the user. */ + @NonNull + public Builder setSaveEntries(@NonNull List<Entry> credentialEntries) { + mSaveEntries = credentialEntries; + return this; + } + + /** Sets the remote entry of the provider. */ + @NonNull + public Builder setRemoteEntry(@Nullable Entry remoteEntry) { + mRemoteEntry = remoteEntry; + return this; + } + + /** Builds a {@link CreateCredentialProviderInfo}. */ + @NonNull + public CreateCredentialProviderInfo build() { + return new CreateCredentialProviderInfo(mProviderName, mSaveEntries, mRemoteEntry); + } + } +} diff --git a/core/java/android/credentials/ui/DisabledProviderData.java b/core/java/android/credentials/ui/DisabledProviderData.java index c266fd56acef..8bccdc9a199f 100644 --- a/core/java/android/credentials/ui/DisabledProviderData.java +++ b/core/java/android/credentials/ui/DisabledProviderData.java @@ -34,6 +34,16 @@ public final class DisabledProviderData extends ProviderData implements Parcelab super(providerFlattenedComponentName); } + /** + * Converts the instance to a {@link DisabledProviderInfo}. + * + * @hide + */ + @NonNull + public DisabledProviderInfo toDisabledProviderInfo() { + return new DisabledProviderInfo(getProviderFlattenedComponentName()); + } + private DisabledProviderData(@NonNull Parcel in) { super(in); } diff --git a/core/java/android/credentials/ui/DisabledProviderInfo.java b/core/java/android/credentials/ui/DisabledProviderInfo.java new file mode 100644 index 000000000000..7ce63681cf1c --- /dev/null +++ b/core/java/android/credentials/ui/DisabledProviderInfo.java @@ -0,0 +1,50 @@ +/* + * 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.ui; + +import android.annotation.NonNull; + +import com.android.internal.util.Preconditions; + +/** + * 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. + * + * @hide + */ +public final class DisabledProviderInfo { + + @NonNull + private final String mProviderName; + + /** + * Constructs a {@link DisabledProviderInfo}. + * + * @throws IllegalArgumentException if {@code providerName} is empty + */ + public DisabledProviderInfo( + @NonNull String providerName) { + mProviderName = Preconditions.checkStringNotEmpty(providerName); + } + + /** Returns the fully-qualified provider (component or package) name. */ + @NonNull + public String getProviderName() { + return mProviderName; + } +} diff --git a/core/java/android/credentials/ui/Entry.java b/core/java/android/credentials/ui/Entry.java index 55f2a3eb490b..84694471ce70 100644 --- a/core/java/android/credentials/ui/Entry.java +++ b/core/java/android/credentials/ui/Entry.java @@ -35,10 +35,14 @@ import com.android.internal.util.AnnotationValidations; */ @TestApi public final class Entry implements Parcelable { - @NonNull private final String mKey; - @NonNull private final String mSubkey; - @Nullable private PendingIntent mPendingIntent; - @Nullable private Intent mFrameworkExtrasIntent; + @NonNull + private final String mKey; + @NonNull + private final String mSubkey; + @Nullable + private PendingIntent mPendingIntent; + @Nullable + private Intent mFrameworkExtrasIntent; @NonNull private final Slice mSlice; @@ -58,16 +62,19 @@ public final class Entry implements Parcelable { mFrameworkExtrasIntent = in.readTypedObject(Intent.CREATOR); } - /** Constructor to be used for an entry that does not require further activities + /** + * 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 + /** + * Constructor to be used for an entry that requires a pending intent to be invoked * when clicked. */ public Entry(@NonNull String key, @NonNull String subkey, @NonNull Slice slice, @@ -77,9 +84,12 @@ public final class Entry implements Parcelable { } /** - * Returns the identifier of this entry that's unique within the context of the CredentialManager - * request. - */ + * Returns the identifier of this entry that's unique within the context of the + * CredentialManager + * request. + * + * Generally used when sending the user selection result back to the system service. + */ @NonNull public String getKey() { return mKey; @@ -87,25 +97,33 @@ public final class Entry implements Parcelable { /** * Returns the sub-identifier of this entry that's unique within the context of the {@code key}. + * + * Generally used when sending the user selection result back to the system service. */ @NonNull public String getSubkey() { return mSubkey; } - /** - * Returns the Slice to be rendered. - */ + /** Returns the Slice to be rendered. */ @NonNull public Slice getSlice() { return mSlice; } + /** + * 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. + */ @Nullable @SuppressLint("IntentBuilderName") // Not building a new intent. public Intent getFrameworkExtrasIntent() { @@ -126,7 +144,7 @@ public final class Entry implements Parcelable { return 0; } - public static final @NonNull Creator<Entry> CREATOR = new Creator<Entry>() { + public static final @NonNull Creator<Entry> CREATOR = new Creator<>() { @Override public Entry createFromParcel(@NonNull Parcel in) { return new Entry(in); diff --git a/core/java/android/credentials/ui/FailureDialogResult.java b/core/java/android/credentials/ui/FailureDialogResult.java new file mode 100644 index 000000000000..abd5a92415d8 --- /dev/null +++ b/core/java/android/credentials/ui/FailureDialogResult.java @@ -0,0 +1,97 @@ +/* + * 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.ui; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.os.Bundle; +import android.os.IBinder; +import android.os.Parcel; +import android.os.Parcelable; + +/** + * Result data when the selector UI has encountered a failure. + * + * @hide + */ +public final class FailureDialogResult extends BaseDialogResult implements Parcelable { + /** Parses and returns a UserSelectionDialogResult from the given resultData. */ + @Nullable + public static FailureDialogResult fromResultData(@NonNull Bundle resultData) { + return resultData.getParcelable( + EXTRA_FAILURE_RESULT, FailureDialogResult.class); + } + + /** + * Used for the UX to construct the {@code resultData Bundle} to send via the {@code + * ResultReceiver}. + */ + public static void addToBundle( + @NonNull FailureDialogResult result, @NonNull Bundle bundle) { + bundle.putParcelable(EXTRA_FAILURE_RESULT, result); + } + + /** + * The intent extra key for the {@code UserSelectionDialogResult} object when the credential + * selector activity finishes. + */ + private static final String EXTRA_FAILURE_RESULT = + "android.credentials.ui.extra.FAILURE_RESULT"; + + @Nullable + private final String mErrorMessage; + + public FailureDialogResult(@Nullable IBinder requestToken, @Nullable String errorMessage) { + super(requestToken); + mErrorMessage = errorMessage; + } + + /** Returns provider package name whose entry was selected by the user. */ + @Nullable + public String getErrorMessage() { + return mErrorMessage; + } + + protected FailureDialogResult(@NonNull Parcel in) { + super(in); + mErrorMessage = in.readString8(); + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + super.writeToParcel(dest, flags); + dest.writeString8(mErrorMessage); + } + + @Override + public int describeContents() { + return 0; + } + + public static final @NonNull Creator<FailureDialogResult> CREATOR = + new Creator<>() { + @Override + public FailureDialogResult createFromParcel(@NonNull Parcel in) { + return new FailureDialogResult(in); + } + + @Override + public FailureDialogResult[] newArray(int size) { + return new FailureDialogResult[size]; + } + }; +} diff --git a/core/java/android/credentials/ui/FailureResult.java b/core/java/android/credentials/ui/FailureResult.java new file mode 100644 index 000000000000..ec584170fba2 --- /dev/null +++ b/core/java/android/credentials/ui/FailureResult.java @@ -0,0 +1,96 @@ +/* + * 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.ui; + +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.Nullable; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Failure or cancellation result encountered during a UI flow. + * + * @hide + */ +public final class FailureResult implements UiResult { + @Nullable + private final String mErrorMessage; + @NonNull + private final int mErrorCode; + + /** @hide **/ + @IntDef(prefix = {"ERROR_CODE_"}, value = { + ERROR_CODE_DIALOG_CANCELED_BY_USER, + ERROR_CODE_CANCELED_AND_LAUNCHED_SETTINGS, + ERROR_CODE_UI_FAILURE, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface ErrorCode { + } + + /** + * The UI was stopped due to a failure, e.g. because it failed to parse the incoming data, + * or it encountered an irrecoverable internal issue. + */ + public static final int ERROR_CODE_UI_FAILURE = 0; + /** The user intentionally canceled the dialog. */ + public static final int ERROR_CODE_DIALOG_CANCELED_BY_USER = 1; + /** + * The UI was stopped since the user has chosen to navigate to the Settings UI to reconfigure + * their providers. + */ + public static final int ERROR_CODE_CANCELED_AND_LAUNCHED_SETTINGS = 2; + + /** + * Constructs a {@link FailureResult}. + * + * @throws IllegalArgumentException if {@code providerId} is empty + */ + public FailureResult(@ErrorCode int errorCode, @Nullable String errorMessage) { + mErrorCode = errorCode; + mErrorMessage = errorMessage; + } + + /** Returns the error code. */ + @ErrorCode + public int getErrorCode() { + return mErrorCode; + } + + /** Returns the error message. */ + @Nullable + public String getErrorMessage() { + return mErrorMessage; + } + + FailureDialogResult toFailureDialogResult() { + return new FailureDialogResult(/*requestToken=*/null, mErrorMessage); + } + + int errorCodeToResultCode() { + switch (mErrorCode) { + case ERROR_CODE_DIALOG_CANCELED_BY_USER: + return BaseDialogResult.RESULT_CODE_DIALOG_USER_CANCELED; + case ERROR_CODE_CANCELED_AND_LAUNCHED_SETTINGS: + return BaseDialogResult.RESULT_CODE_CANCELED_AND_LAUNCHED_SETTINGS; + default: + return BaseDialogResult.RESULT_CODE_DATA_PARSING_FAILURE; + } + } +} diff --git a/core/java/android/credentials/ui/GetCredentialProviderData.java b/core/java/android/credentials/ui/GetCredentialProviderData.java index 181475c7ce5a..481419b4f732 100644 --- a/core/java/android/credentials/ui/GetCredentialProviderData.java +++ b/core/java/android/credentials/ui/GetCredentialProviderData.java @@ -55,6 +55,17 @@ public final class GetCredentialProviderData extends ProviderData implements Par mRemoteEntry = remoteEntry; } + /** + * Converts the instance to a {@link GetCredentialProviderInfo}. + * + * @hide + */ + @NonNull + public GetCredentialProviderInfo toGetCredentialProviderInfo() { + return new GetCredentialProviderInfo(getProviderFlattenedComponentName(), + mCredentialEntries, mActionChips, mAuthenticationEntries, mRemoteEntry); + } + @NonNull public List<Entry> getCredentialEntries() { return mCredentialEntries; @@ -83,12 +94,12 @@ public final class GetCredentialProviderData extends ProviderData implements Par mCredentialEntries = credentialEntries; AnnotationValidations.validate(NonNull.class, null, mCredentialEntries); - List<Entry> actionChips = new ArrayList<>(); + List<Entry> actionChips = new ArrayList<>(); in.readTypedList(actionChips, Entry.CREATOR); mActionChips = actionChips; AnnotationValidations.validate(NonNull.class, null, mActionChips); - List<AuthenticationEntry> authenticationEntries = new ArrayList<>(); + List<AuthenticationEntry> authenticationEntries = new ArrayList<>(); in.readTypedList(authenticationEntries, AuthenticationEntry.CREATOR); mAuthenticationEntries = authenticationEntries; AnnotationValidations.validate(NonNull.class, null, mAuthenticationEntries); @@ -113,16 +124,16 @@ public final class GetCredentialProviderData extends ProviderData implements Par public static final @NonNull Creator<GetCredentialProviderData> CREATOR = new Creator<GetCredentialProviderData>() { - @Override - public GetCredentialProviderData createFromParcel(@NonNull Parcel in) { - return new GetCredentialProviderData(in); - } + @Override + public GetCredentialProviderData createFromParcel(@NonNull Parcel in) { + return new GetCredentialProviderData(in); + } - @Override - public GetCredentialProviderData[] newArray(int size) { - return new GetCredentialProviderData[size]; - } - }; + @Override + public GetCredentialProviderData[] newArray(int size) { + return new GetCredentialProviderData[size]; + } + }; /** * Builder for {@link GetCredentialProviderData}. @@ -131,11 +142,16 @@ public final class GetCredentialProviderData extends ProviderData implements Par */ @TestApi public static final class Builder { - @NonNull private String mProviderFlattenedComponentName; - @NonNull private List<Entry> mCredentialEntries = new ArrayList<>(); - @NonNull private List<Entry> mActionChips = new ArrayList<>(); - @NonNull private List<AuthenticationEntry> mAuthenticationEntries = new ArrayList<>(); - @Nullable private Entry mRemoteEntry = null; + @NonNull + private String mProviderFlattenedComponentName; + @NonNull + private List<Entry> mCredentialEntries = new ArrayList<>(); + @NonNull + private List<Entry> mActionChips = new ArrayList<>(); + @NonNull + private List<AuthenticationEntry> mAuthenticationEntries = new ArrayList<>(); + @Nullable + private Entry mRemoteEntry = null; /** Constructor with required properties. */ public Builder(@NonNull String providerFlattenedComponentName) { diff --git a/core/java/android/credentials/ui/GetCredentialProviderInfo.java b/core/java/android/credentials/ui/GetCredentialProviderInfo.java new file mode 100644 index 000000000000..bac71472acd1 --- /dev/null +++ b/core/java/android/credentials/ui/GetCredentialProviderInfo.java @@ -0,0 +1,168 @@ +/* + * 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.ui; + +import android.annotation.NonNull; +import android.annotation.Nullable; + +import com.android.internal.util.Preconditions; + +import java.util.ArrayList; +import java.util.List; + +/** + * Information pertaining to a specific provider during the given create-credential flow. + * + * This includes provider metadata and its credential creation options for display purposes. + * + * @hide + */ +public final class GetCredentialProviderInfo { + + @NonNull + private final String mProviderName; + + @NonNull + private final List<Entry> mCredentialEntries; + @NonNull + private final List<Entry> mActionChips; + @NonNull + private final List<AuthenticationEntry> mAuthenticationEntries; + @Nullable + private final Entry mRemoteEntry; + + GetCredentialProviderInfo( + @NonNull String providerName, @NonNull List<Entry> credentialEntries, + @NonNull List<Entry> actionChips, + @NonNull List<AuthenticationEntry> authenticationEntries, + @Nullable Entry remoteEntry) { + mProviderName = Preconditions.checkStringNotEmpty(providerName); + mCredentialEntries = new ArrayList<>(credentialEntries); + mActionChips = new ArrayList<>(actionChips); + mAuthenticationEntries = new ArrayList<>(authenticationEntries); + mRemoteEntry = remoteEntry; + } + + /** Returns the fully-qualified provider (component or package) name. */ + @NonNull + public String getProviderName() { + return mProviderName; + } + + /** Returns the display information for all the candidate credentials this provider has. */ + @NonNull + public List<Entry> getCredentialEntries() { + return mCredentialEntries; + } + + /** + * Returns a list of actions defined by the provider that intent into the provider's app for + * specific user actions, each of which should eventually lead to an actual credential. + */ + @NonNull + public List<Entry> getActionChips() { + return mActionChips; + } + + /** + * Returns a list of authentication actions that each intents into a provider authentication + * activity. + * + * When the authentication activity succeeds, the provider will return a list of actual + * credential candidates to render. However, the UI should not attempt to parse the result + * itself, but rather send the result back to the system service, which will then process the + * new candidates and relaunch the UI with updated display data. + */ + @NonNull + public List<AuthenticationEntry> getAuthenticationEntries() { + return mAuthenticationEntries; + } + + /** + * 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. + */ + @Nullable + public Entry getRemoteEntry() { + return mRemoteEntry; + } + + /** + * Builder for {@link GetCredentialProviderInfo}. + * + * @hide + */ + public static final class Builder { + @NonNull + private String mProviderName; + @NonNull + private List<Entry> mCredentialEntries = new ArrayList<>(); + @NonNull + private List<Entry> mActionChips = new ArrayList<>(); + @NonNull + private List<AuthenticationEntry> mAuthenticationEntries = new ArrayList<>(); + @Nullable + private Entry mRemoteEntry = null; + + /** + * Constructs a {@link GetCredentialProviderInfo.Builder}. + * + * @throws IllegalArgumentException if {@code providerName} is null or empty + */ + public Builder(@NonNull String providerName) { + mProviderName = Preconditions.checkStringNotEmpty(providerName); + } + + /** Sets the list of credential candidates to be displayed to the user. */ + @NonNull + public Builder setCredentialEntries(@NonNull List<Entry> credentialEntries) { + mCredentialEntries = credentialEntries; + return this; + } + + /** Sets the list of action chips to be displayed to the user. */ + @NonNull + public Builder setActionChips(@NonNull List<Entry> actionChips) { + mActionChips = actionChips; + return this; + } + + /** Sets the authentication entry to be displayed to the user. */ + @NonNull + public Builder setAuthenticationEntries( + @NonNull List<AuthenticationEntry> authenticationEntry) { + mAuthenticationEntries = authenticationEntry; + return this; + } + + /** Sets the remote entry to be displayed to the user. */ + @NonNull + public Builder setRemoteEntry(@Nullable Entry remoteEntry) { + mRemoteEntry = remoteEntry; + return this; + } + + /** Builds a {@link GetCredentialProviderInfo}. */ + @NonNull + public GetCredentialProviderInfo build() { + return new GetCredentialProviderInfo(mProviderName, + mCredentialEntries, mActionChips, mAuthenticationEntries, mRemoteEntry); + } + } +} diff --git a/core/java/android/credentials/ui/IntentFactory.java b/core/java/android/credentials/ui/IntentFactory.java index 49321d514128..5e1e0efe39c4 100644 --- a/core/java/android/credentials/ui/IntentFactory.java +++ b/core/java/android/credentials/ui/IntentFactory.java @@ -113,25 +113,6 @@ public class IntentFactory { } /** - * Notify the UI that providers have been enabled/disabled. - * - * @hide - */ - @NonNull - public static Intent createProviderUpdateIntent() { - Intent intent = new Intent(); - ComponentName componentName = - ComponentName.unflattenFromString( - Resources.getSystem() - .getString( - com.android.internal.R.string - .config_credentialManagerReceiverComponent)); - intent.setComponent(componentName); - intent.setAction(Constants.CREDMAN_ENABLED_PROVIDERS_UPDATED); - return intent; - } - - /** * Convert an instance of a "locally-defined" ResultReceiver to an instance of {@link * android.os.ResultReceiver} itself, which the receiving process will be able to unmarshall. */ diff --git a/core/java/android/credentials/ui/IntentHelper.java b/core/java/android/credentials/ui/IntentHelper.java new file mode 100644 index 000000000000..c5f34c1440a7 --- /dev/null +++ b/core/java/android/credentials/ui/IntentHelper.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 android.credentials.ui; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SuppressLint; +import android.content.Intent; +import android.os.ResultReceiver; + +import java.util.List; + +/** + * Utilities for parsing the intent data used to launch the UI activity. + * + * @hide + */ +public final class IntentHelper { + /** + * Attempts to extract a {@link CancelUiRequest} 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); + } + + /** + * Attempts to extract a {@link RequestInfo} from the given intent; returns null + * if not found. + */ + @Nullable + public static RequestInfo extractRequestInfo(@NonNull Intent intent) { + return intent.getParcelableExtra(RequestInfo.EXTRA_REQUEST_INFO, + RequestInfo.class); + } + + /** + * Attempts to extract the list of {@link GetCredentialProviderInfo} from the given intent; + * returns null 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( + @NonNull Intent intent) { + List<GetCredentialProviderData> providerList = intent.getParcelableArrayListExtra( + ProviderData.EXTRA_ENABLED_PROVIDER_DATA_LIST, + GetCredentialProviderData.class); + return providerList == null ? null : providerList.stream().map( + GetCredentialProviderData::toGetCredentialProviderInfo).toList(); + } + + /** + * Attempts to extract the list of {@link CreateCredentialProviderInfo} from the given intent; + * returns null 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) { + List<CreateCredentialProviderData> providerList = intent.getParcelableArrayListExtra( + ProviderData.EXTRA_ENABLED_PROVIDER_DATA_LIST, + CreateCredentialProviderData.class); + return providerList == null ? null : providerList.stream().map( + CreateCredentialProviderData::toCreateCredentialProviderInfo).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. + */ + @Nullable + public static ResultReceiver extractResultReceiver(@NonNull Intent intent) { + return intent.getParcelableExtra(Constants.EXTRA_RESULT_RECEIVER, + ResultReceiver.class); + } + + private IntentHelper() { + } +} diff --git a/core/java/android/credentials/ui/ProviderDialogResult.java b/core/java/android/credentials/ui/ProviderDialogResult.java deleted file mode 100644 index 53f1864f7bc6..000000000000 --- a/core/java/android/credentials/ui/ProviderDialogResult.java +++ /dev/null @@ -1,100 +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.ui; - -import android.annotation.NonNull; -import android.annotation.Nullable; -import android.os.Bundle; -import android.os.IBinder; -import android.os.Parcel; -import android.os.Parcelable; - -import com.android.internal.util.AnnotationValidations; - -/** - * Result data matching {@link BaseDialogResult#RESULT_CODE_PROVIDER_ENABLED}, or {@link - * BaseDialogResult#RESULT_CODE_DEFAULT_PROVIDER_CHANGED}. - * - * @hide - */ -public final class ProviderDialogResult extends BaseDialogResult implements Parcelable { - /** Parses and returns a ProviderDialogResult from the given resultData. */ - @Nullable - public static ProviderDialogResult fromResultData(@NonNull Bundle resultData) { - return resultData.getParcelable(EXTRA_PROVIDER_RESULT, ProviderDialogResult.class); - } - - /** - * Used for the UX to construct the {@code resultData Bundle} to send via the {@code - * ResultReceiver}. - */ - public static void addToBundle( - @NonNull ProviderDialogResult result, @NonNull Bundle bundle) { - bundle.putParcelable(EXTRA_PROVIDER_RESULT, result); - } - - /** - * The intent extra key for the {@code ProviderDialogResult} object when the credential - * selector activity finishes. - */ - private static final String EXTRA_PROVIDER_RESULT = - "android.credentials.ui.extra.PROVIDER_RESULT"; - - @NonNull - private final String mProviderId; - - public ProviderDialogResult(@NonNull IBinder requestToken, @NonNull String providerId) { - super(requestToken); - mProviderId = providerId; - } - - @NonNull - public String getProviderId() { - return mProviderId; - } - - protected ProviderDialogResult(@NonNull Parcel in) { - super(in); - String providerId = in.readString8(); - mProviderId = providerId; - AnnotationValidations.validate(NonNull.class, null, mProviderId); - } - - @Override - public void writeToParcel(@NonNull Parcel dest, int flags) { - super.writeToParcel(dest, flags); - dest.writeString8(mProviderId); - } - - @Override - public int describeContents() { - return 0; - } - - public static final @NonNull Creator<ProviderDialogResult> CREATOR = - new Creator<ProviderDialogResult>() { - @Override - public ProviderDialogResult createFromParcel(@NonNull Parcel in) { - return new ProviderDialogResult(in); - } - - @Override - public ProviderDialogResult[] newArray(int size) { - return new ProviderDialogResult[size]; - } - }; -} diff --git a/core/java/android/credentials/ui/ProviderPendingIntentResponse.java b/core/java/android/credentials/ui/ProviderPendingIntentResponse.java index 47936c48f927..11cc21f9d2db 100644 --- a/core/java/android/credentials/ui/ProviderPendingIntentResponse.java +++ b/core/java/android/credentials/ui/ProviderPendingIntentResponse.java @@ -16,15 +16,20 @@ package android.credentials.ui; +import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.SuppressLint; import android.content.Intent; import android.os.Parcel; import android.os.Parcelable; -import androidx.annotation.NonNull; - /** - * Response from a provider's pending intent + * Result of launching a provider's PendingIntent associated with an {@link Entry} after it is + * selected by the user. + * + * The provider sets the credential creation / retrieval result through + * {@link android.app.Activity#setResult(int, Intent)}, which is then directly propagated back + * through this data structure. * * @hide */ @@ -33,20 +38,21 @@ public final class ProviderPendingIntentResponse implements Parcelable { @Nullable private final Intent mResultData; + /** Constructs a {@link ProviderPendingIntentResponse}. */ public ProviderPendingIntentResponse(int resultCode, @Nullable Intent resultData) { mResultCode = resultCode; mResultData = resultData; } - protected ProviderPendingIntentResponse(Parcel in) { + private ProviderPendingIntentResponse(@NonNull Parcel in) { mResultCode = in.readInt(); mResultData = in.readTypedObject(Intent.CREATOR); } - public static final Creator<ProviderPendingIntentResponse> CREATOR = - new Creator<ProviderPendingIntentResponse>() { + public static final @NonNull Creator<ProviderPendingIntentResponse> CREATOR = + new Creator<>() { @Override - public ProviderPendingIntentResponse createFromParcel(Parcel in) { + public ProviderPendingIntentResponse createFromParcel(@NonNull Parcel in) { return new ProviderPendingIntentResponse(in); } @@ -67,13 +73,15 @@ public final class ProviderPendingIntentResponse implements Parcelable { dest.writeTypedObject(mResultData, flags); } - /** Returns the result code associated with this pending intent activity result. */ + /** Returns the result code associated with this provider PendingIntent activity result. */ public int getResultCode() { return mResultCode; } - /** Returns the result data associated with this pending intent activity result. */ - @NonNull public Intent getResultData() { + /** Returns the result data associated with this provider PendingIntent activity result. */ + @SuppressLint("IntentBuilderName") // Not building a new intent. + @NonNull + public Intent getResultData() { return mResultData; } } diff --git a/core/java/android/credentials/ui/RequestInfo.java b/core/java/android/credentials/ui/RequestInfo.java index 4fedc8353bf7..f65158444e48 100644 --- a/core/java/android/credentials/ui/RequestInfo.java +++ b/core/java/android/credentials/ui/RequestInfo.java @@ -48,17 +48,24 @@ public final class RequestInfo implements Parcelable { @NonNull public static final String EXTRA_REQUEST_INFO = "android.credentials.ui.extra.REQUEST_INFO"; - /** Type value for any request that does not require UI. */ + /** + * Type value for any request that does not require UI. + */ @NonNull public static final String TYPE_UNDEFINED = "android.credentials.ui.TYPE_UNDEFINED"; - /** Type value for a getCredential request. */ + /** + * Type value for a getCredential request. + */ @NonNull public static final String TYPE_GET = "android.credentials.ui.TYPE_GET"; - /** Type value for a getCredential request that utilizes the credential registry. + /** + * Type value for a getCredential request that utilizes the credential registry. * * @hide - **/ + */ @NonNull public static final String TYPE_GET_VIA_REGISTRY = "android.credentials.ui.TYPE_GET_VIA_REGISTRY"; - /** Type value for a createCredential request. */ + /** + * Type value for a createCredential request. + */ @NonNull public static final String TYPE_CREATE = "android.credentials.ui.TYPE_CREATE"; /** @hide */ diff --git a/core/java/android/credentials/ui/ResultHelper.java b/core/java/android/credentials/ui/ResultHelper.java new file mode 100644 index 000000000000..7b9d5e87d666 --- /dev/null +++ b/core/java/android/credentials/ui/ResultHelper.java @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.credentials.ui; + +import android.annotation.NonNull; +import android.content.Intent; +import android.os.Bundle; +import android.os.ResultReceiver; + +/** + * Utilities for sending the UI results back to the system service. + * + * @hide + */ +public final class ResultHelper { + /** + * Sends the {@code failureResult} that caused the UI to stop back to the CredentialManager + * service. + * + * The {code resultReceiver} for a UI flow can be extracted from the UI launch intent via + * {@link IntentHelper#extractResultReceiver(Intent)}. + */ + public static void sendFailureResult(@NonNull ResultReceiver resultReceiver, + @NonNull FailureResult failureResult) { + FailureDialogResult result = failureResult.toFailureDialogResult(); + Bundle resultData = new Bundle(); + FailureDialogResult.addToBundle(result, resultData); + resultReceiver.send(failureResult.errorCodeToResultCode(), + resultData); + } + + /** + * Sends the completed {@code userSelectionResult} back to the CredentialManager service. + * + * The {code resultReceiver} for a UI flow can be extracted from the UI launch intent via + * {@link IntentHelper#extractResultReceiver(Intent)}. + */ + public static void sendUserSelectionResult(@NonNull ResultReceiver resultReceiver, + @NonNull UserSelectionResult userSelectionResult) { + UserSelectionDialogResult result = userSelectionResult.toUserSelectionDialogResult(); + Bundle resultData = new Bundle(); + UserSelectionDialogResult.addToBundle(result, resultData); + resultReceiver.send(BaseDialogResult.RESULT_CODE_DIALOG_COMPLETE_WITH_SELECTION, + resultData); + } + + private ResultHelper() {} +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/domain/interactor/ComponentsInteractorTest.kt b/core/java/android/credentials/ui/UiResult.java index dbff63f355c8..692584d1a561 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/domain/interactor/ComponentsInteractorTest.kt +++ b/core/java/android/credentials/ui/UiResult.java @@ -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,9 +14,11 @@ * limitations under the License. */ -package com.android.systemui.volume.panel.domain.interactor +package android.credentials.ui; -class ComponentsInteractorTest { - - // TODO(b/318080198) Write tests -} +/** + * Base class for different types of ui results. + * + * @hide + */ +public interface UiResult {} diff --git a/core/java/android/credentials/ui/UserSelectionResult.java b/core/java/android/credentials/ui/UserSelectionResult.java new file mode 100644 index 000000000000..431dc631f3f6 --- /dev/null +++ b/core/java/android/credentials/ui/UserSelectionResult.java @@ -0,0 +1,84 @@ +/* + * 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.ui; + +import android.annotation.NonNull; +import android.annotation.Nullable; + +import com.android.internal.util.Preconditions; + +/** + * Result sent back from the UI after the user chose an option and completed the following + * transaction launched through the provider PendingIntent associated with that option. + * + * @hide + */ +public final class UserSelectionResult implements UiResult { + @NonNull + private final String mProviderId; + @NonNull + private final String mEntryKey; + @NonNull + private final String mEntrySubkey; + @Nullable + private ProviderPendingIntentResponse mProviderPendingIntentResponse; + + /** + * Constructs a {@link UserSelectionResult}. + * + * @throws IllegalArgumentException if {@code providerId} is empty + */ + + public UserSelectionResult(@NonNull String providerId, + @NonNull String entryKey, @NonNull String entrySubkey, + @Nullable ProviderPendingIntentResponse providerPendingIntentResponse) { + mProviderId = Preconditions.checkStringNotEmpty(providerId); + mEntryKey = Preconditions.checkNotNull(entryKey); + mEntrySubkey = Preconditions.checkNotNull(entrySubkey); + mProviderPendingIntentResponse = providerPendingIntentResponse; + } + + /** Returns provider package name whose entry was selected by the user. */ + @NonNull + public String getProviderId() { + return mProviderId; + } + + /** Returns the key of the visual entry that the user selected. */ + @NonNull + public String getEntryKey() { + return mEntryKey; + } + + /** Returns the subkey of the visual entry that the user selected. */ + @NonNull + public String getEntrySubkey() { + return mEntrySubkey; + } + + /** Returns the pending intent response from the provider. */ + @Nullable + public ProviderPendingIntentResponse getPendingIntentProviderResponse() { + return mProviderPendingIntentResponse; + } + + @NonNull + UserSelectionDialogResult toUserSelectionDialogResult() { + return new UserSelectionDialogResult(/*requestToken=*/null, mProviderId, mEntryKey, + mEntrySubkey, mProviderPendingIntentResponse); + } +} diff --git a/core/java/android/hardware/HardwareBuffer.java b/core/java/android/hardware/HardwareBuffer.java index 0047b7d69282..ce0f9f598897 100644 --- a/core/java/android/hardware/HardwareBuffer.java +++ b/core/java/android/hardware/HardwareBuffer.java @@ -115,14 +115,16 @@ public final class HardwareBuffer implements Parcelable, AutoCloseable { @FlaggedApi(com.android.graphics.hwui.flags.Flags.FLAG_REQUESTED_FORMATS_V) public static final int R_8 = 0x38; /** - * Format: 16 bits red. Bits should be represented in unsigned integer, instead of the - * implicit unsigned normalized. + * Format: 16 bits red. When sampled on the GPU this is represented as an + * unsigned integer instead of implicit unsigned normalize. + * For more information see https://www.khronos.org/opengl/wiki/Normalized_Integer */ @FlaggedApi(com.android.graphics.hwui.flags.Flags.FLAG_REQUESTED_FORMATS_V) public static final int R_16 = 0x39; /** - * Format: 16 bits each red, green. Bits should be represented in unsigned integer, - * instead of the implicit unsigned normalized. + * Format: 16 bits each red, green. When sampled on the GPU this is represented + * as an unsigned integer instead of implicit unsigned normalize. + * For more information see https://www.khronos.org/opengl/wiki/Normalized_Integer */ @FlaggedApi(com.android.graphics.hwui.flags.Flags.FLAG_REQUESTED_FORMATS_V) public static final int RG_1616 = 0x3a; diff --git a/core/java/android/hardware/biometrics/BiometricPrompt.java b/core/java/android/hardware/biometrics/BiometricPrompt.java index c0424dbeb813..bdaf9d789960 100644 --- a/core/java/android/hardware/biometrics/BiometricPrompt.java +++ b/core/java/android/hardware/biometrics/BiometricPrompt.java @@ -16,7 +16,7 @@ package android.hardware.biometrics; -import static android.Manifest.permission.MANAGE_BIOMETRIC_DIALOG; +import static android.Manifest.permission.SET_BIOMETRIC_DIALOG_LOGO; import static android.Manifest.permission.TEST_BIOMETRIC; import static android.Manifest.permission.USE_BIOMETRIC; import static android.Manifest.permission.USE_BIOMETRIC_INTERNAL; @@ -174,9 +174,9 @@ public class BiometricPrompt implements BiometricAuthenticator, BiometricConstan * @return This builder. */ @FlaggedApi(FLAG_CUSTOM_BIOMETRIC_PROMPT) - @RequiresPermission(MANAGE_BIOMETRIC_DIALOG) + @RequiresPermission(SET_BIOMETRIC_DIALOG_LOGO) @NonNull - public BiometricPrompt.Builder setLogo(@DrawableRes int logoRes) { + public BiometricPrompt.Builder setLogoRes(@DrawableRes int logoRes) { mPromptInfo.setLogoRes(logoRes); return this; } @@ -193,9 +193,9 @@ public class BiometricPrompt implements BiometricAuthenticator, BiometricConstan * @return This builder. */ @FlaggedApi(FLAG_CUSTOM_BIOMETRIC_PROMPT) - @RequiresPermission(MANAGE_BIOMETRIC_DIALOG) + @RequiresPermission(SET_BIOMETRIC_DIALOG_LOGO) @NonNull - public BiometricPrompt.Builder setLogo(@NonNull Bitmap logoBitmap) { + public BiometricPrompt.Builder setLogoBitmap(@NonNull Bitmap logoBitmap) { mPromptInfo.setLogoBitmap(logoBitmap); return this; } @@ -719,25 +719,25 @@ public class BiometricPrompt implements BiometricAuthenticator, BiometricConstan /** * Gets the drawable resource of the logo for the prompt, as set by - * {@link Builder#setLogo(int)}. Currently for system applications use only. + * {@link Builder#setLogoRes(int)}. Currently for system applications use only. * * @return The drawable resource of the logo, or -1 if the prompt has no logo resource set. */ @FlaggedApi(FLAG_CUSTOM_BIOMETRIC_PROMPT) - @RequiresPermission(MANAGE_BIOMETRIC_DIALOG) + @RequiresPermission(SET_BIOMETRIC_DIALOG_LOGO) @DrawableRes public int getLogoRes() { return mPromptInfo.getLogoRes(); } /** - * Gets the logo bitmap for the prompt, as set by {@link Builder#setLogo(Bitmap)}. Currently for - * system applications use only. + * Gets the logo bitmap for the prompt, as set by {@link Builder#setLogoBitmap(Bitmap)}. + * Currently for system applications use only. * * @return The logo bitmap of the prompt, or null if the prompt has no logo bitmap set. */ @FlaggedApi(FLAG_CUSTOM_BIOMETRIC_PROMPT) - @RequiresPermission(MANAGE_BIOMETRIC_DIALOG) + @RequiresPermission(SET_BIOMETRIC_DIALOG_LOGO) @Nullable public Bitmap getLogoBitmap() { return mPromptInfo.getLogoBitmap(); diff --git a/core/java/android/hardware/biometrics/PromptContentItemBulletedText.java b/core/java/android/hardware/biometrics/PromptContentItemBulletedText.java index c5e5a8076747..25e5cca485d2 100644 --- a/core/java/android/hardware/biometrics/PromptContentItemBulletedText.java +++ b/core/java/android/hardware/biometrics/PromptContentItemBulletedText.java @@ -28,14 +28,14 @@ import android.os.Parcelable; */ @FlaggedApi(FLAG_CUSTOM_BIOMETRIC_PROMPT) public final class PromptContentItemBulletedText implements PromptContentItemParcelable { - private final CharSequence mText; + private final String mText; /** * A list item with bulleted text shown on {@link PromptVerticalListContentView}. * * @param text The text of this list item. */ - public PromptContentItemBulletedText(@NonNull CharSequence text) { + public PromptContentItemBulletedText(@NonNull String text) { mText = text; } @@ -43,7 +43,7 @@ public final class PromptContentItemBulletedText implements PromptContentItemPar * @hide */ @NonNull - public CharSequence getText() { + public String getText() { return mText; } @@ -60,7 +60,7 @@ public final class PromptContentItemBulletedText implements PromptContentItemPar */ @Override public void writeToParcel(@NonNull Parcel dest, int flags) { - dest.writeCharSequence(mText); + dest.writeString(mText); } /** @@ -70,7 +70,7 @@ public final class PromptContentItemBulletedText implements PromptContentItemPar public static final Creator<PromptContentItemBulletedText> CREATOR = new Creator<>() { @Override public PromptContentItemBulletedText createFromParcel(Parcel in) { - return new PromptContentItemBulletedText(in.readCharSequence()); + return new PromptContentItemBulletedText(in.readString()); } @Override diff --git a/core/java/android/hardware/biometrics/PromptContentItemPlainText.java b/core/java/android/hardware/biometrics/PromptContentItemPlainText.java index 6434c5975c12..7919256f9c6d 100644 --- a/core/java/android/hardware/biometrics/PromptContentItemPlainText.java +++ b/core/java/android/hardware/biometrics/PromptContentItemPlainText.java @@ -28,14 +28,14 @@ import android.os.Parcelable; */ @FlaggedApi(FLAG_CUSTOM_BIOMETRIC_PROMPT) public final class PromptContentItemPlainText implements PromptContentItemParcelable { - private final CharSequence mText; + private final String mText; /** * A list item with plain text shown on {@link PromptVerticalListContentView}. * * @param text The text of this list item. */ - public PromptContentItemPlainText(@NonNull CharSequence text) { + public PromptContentItemPlainText(@NonNull String text) { mText = text; } @@ -43,7 +43,7 @@ public final class PromptContentItemPlainText implements PromptContentItemParcel * @hide */ @NonNull - public CharSequence getText() { + public String getText() { return mText; } @@ -60,7 +60,7 @@ public final class PromptContentItemPlainText implements PromptContentItemParcel */ @Override public void writeToParcel(@NonNull Parcel dest, int flags) { - dest.writeCharSequence(mText); + dest.writeString(mText); } /** @@ -70,7 +70,7 @@ public final class PromptContentItemPlainText implements PromptContentItemParcel public static final Creator<PromptContentItemPlainText> CREATOR = new Creator<>() { @Override public PromptContentItemPlainText createFromParcel(Parcel in) { - return new PromptContentItemPlainText(in.readCharSequence()); + return new PromptContentItemPlainText(in.readString()); } @Override diff --git a/core/java/android/hardware/biometrics/PromptInfo.java b/core/java/android/hardware/biometrics/PromptInfo.java index d788b37c781d..0f9cadc52608 100644 --- a/core/java/android/hardware/biometrics/PromptInfo.java +++ b/core/java/android/hardware/biometrics/PromptInfo.java @@ -166,9 +166,9 @@ public class PromptInfo implements Parcelable { } /** - * Returns whether MANAGE_BIOMETRIC_DIALOG is contained. + * Returns whether SET_BIOMETRIC_DIALOG_LOGO is contained. */ - public boolean containsManageBioApiConfigurations() { + public boolean containsSetLogoApiConfigurations() { if (mLogoRes != -1) { return true; } else if (mLogoBitmap != null) { diff --git a/core/java/android/hardware/biometrics/PromptVerticalListContentView.java b/core/java/android/hardware/biometrics/PromptVerticalListContentView.java index f3e62907d845..38d32dc73ccb 100644 --- a/core/java/android/hardware/biometrics/PromptVerticalListContentView.java +++ b/core/java/android/hardware/biometrics/PromptVerticalListContentView.java @@ -52,11 +52,11 @@ public final class PromptVerticalListContentView implements PromptContentViewPar private static final int MAX_ITEM_NUMBER = 20; private static final int MAX_EACH_ITEM_CHARACTER_NUMBER = 640; private final List<PromptContentItemParcelable> mContentList; - private final CharSequence mDescription; + private final String mDescription; private PromptVerticalListContentView( @NonNull List<PromptContentItemParcelable> contentList, - @NonNull CharSequence description) { + @NonNull String description) { mContentList = contentList; mDescription = description; } @@ -65,7 +65,7 @@ public final class PromptVerticalListContentView implements PromptContentViewPar mContentList = in.readArrayList( PromptContentItemParcelable.class.getClassLoader(), PromptContentItemParcelable.class); - mDescription = in.readCharSequence(); + mDescription = in.readString(); } /** @@ -84,12 +84,12 @@ public final class PromptVerticalListContentView implements PromptContentViewPar /** * Gets the description for the content view, as set by - * {@link PromptVerticalListContentView.Builder#setDescription(CharSequence)}. + * {@link PromptVerticalListContentView.Builder#setDescription(String)}. * * @return The description for the content view, or null if the content view has no description. */ @Nullable - public CharSequence getDescription() { + public String getDescription() { return mDescription; } @@ -118,7 +118,7 @@ public final class PromptVerticalListContentView implements PromptContentViewPar @Override public void writeToParcel(@androidx.annotation.NonNull Parcel dest, int flags) { dest.writeList(mContentList); - dest.writeCharSequence(mDescription); + dest.writeString(mDescription); } /** @@ -143,7 +143,7 @@ public final class PromptVerticalListContentView implements PromptContentViewPar */ public static final class Builder { private final List<PromptContentItemParcelable> mContentList = new ArrayList<>(); - private CharSequence mDescription; + private String mDescription; /** * Optional: Sets a description that will be shown on the content view. @@ -152,7 +152,7 @@ public final class PromptVerticalListContentView implements PromptContentViewPar * @return This builder. */ @NonNull - public Builder setDescription(@NonNull CharSequence description) { + public Builder setDescription(@NonNull String description) { mDescription = description; return this; } diff --git a/core/java/android/hardware/input/InputSettings.java b/core/java/android/hardware/input/InputSettings.java index d93953231eaf..fdbd3197fb79 100644 --- a/core/java/android/hardware/input/InputSettings.java +++ b/core/java/android/hardware/input/InputSettings.java @@ -17,6 +17,7 @@ package android.hardware.input; import static com.android.hardware.input.Flags.keyboardA11yBounceKeysFlag; +import static com.android.hardware.input.Flags.keyboardA11ySlowKeysFlag; import static com.android.hardware.input.Flags.keyboardA11yStickyKeysFlag; import static com.android.input.flags.Flags.enableInputFilterRustImpl; @@ -68,6 +69,12 @@ public class InputSettings { */ public static final int MAX_ACCESSIBILITY_BOUNCE_KEYS_THRESHOLD_MILLIS = 5000; + /** + * The maximum allowed Accessibility slow keys threshold. + * @hide + */ + public static final int MAX_ACCESSIBILITY_SLOW_KEYS_THRESHOLD_MILLIS = 5000; + private InputSettings() { } @@ -419,6 +426,86 @@ public class InputSettings { } /** + * Whether Accessibility slow keys feature flags is enabled. + * + * <p> + * 'Slow keys' is an accessibility feature to aid users who have physical disabilities, that + * allows the user to specify the duration for which one must press-and-hold a key before the + * system accepts the keypress. + * </p> + * + * @hide + */ + public static boolean isAccessibilitySlowKeysFeatureFlagEnabled() { + return keyboardA11ySlowKeysFlag() && enableInputFilterRustImpl(); + } + + /** + * Whether Accessibility slow keys is enabled. + * + * <p> + * 'Slow keys' is an accessibility feature to aid users who have physical disabilities, that + * allows the user to specify the duration for which one must press-and-hold a key before the + * system accepts the keypress. + * </p> + * + * @hide + */ + public static boolean isAccessibilitySlowKeysEnabled(@NonNull Context context) { + return getAccessibilitySlowKeysThreshold(context) != 0; + } + + /** + * Get Accessibility slow keys threshold duration in milliseconds. + * + * <p> + * 'Slow keys' is an accessibility feature to aid users who have physical disabilities, that + * allows the user to specify the duration for which one must press-and-hold a key before the + * system accepts the keypress. + * </p> + * + * @hide + */ + public static int getAccessibilitySlowKeysThreshold(@NonNull Context context) { + if (!isAccessibilitySlowKeysFeatureFlagEnabled()) { + return 0; + } + return Settings.Secure.getIntForUser(context.getContentResolver(), + Settings.Secure.ACCESSIBILITY_SLOW_KEYS, 0, UserHandle.USER_CURRENT); + } + + /** + * Set Accessibility slow keys threshold duration in milliseconds. + * @param thresholdTimeMillis time duration for which a key should be pressed to be registered + * in the system. The threshold must be between 0 and + * {@link MAX_ACCESSIBILITY_SLOW_KEYS_THRESHOLD_MILLIS} + * + * <p> + * 'Slow keys' is an accessibility feature to aid users who have physical disabilities, that + * allows the user to specify the duration for which one must press-and-hold a key before the + * system accepts the keypress. + * </p> + * + * @hide + */ + @RequiresPermission(Manifest.permission.WRITE_SETTINGS) + public static void setAccessibilitySlowKeysThreshold(@NonNull Context context, + int thresholdTimeMillis) { + if (!isAccessibilitySlowKeysFeatureFlagEnabled()) { + return; + } + if (thresholdTimeMillis < 0 + || thresholdTimeMillis > MAX_ACCESSIBILITY_SLOW_KEYS_THRESHOLD_MILLIS) { + throw new IllegalArgumentException( + "Provided Slow keys threshold should be in range [0, " + + MAX_ACCESSIBILITY_SLOW_KEYS_THRESHOLD_MILLIS + "]"); + } + Settings.Secure.putIntForUser(context.getContentResolver(), + Settings.Secure.ACCESSIBILITY_SLOW_KEYS, thresholdTimeMillis, + UserHandle.USER_CURRENT); + } + + /** * Whether Accessibility sticky keys feature is enabled. * * <p> diff --git a/core/java/android/hardware/input/input_framework.aconfig b/core/java/android/hardware/input/input_framework.aconfig index 362fe78b14b8..0ed6569afd2a 100644 --- a/core/java/android/hardware/input/input_framework.aconfig +++ b/core/java/android/hardware/input/input_framework.aconfig @@ -29,4 +29,11 @@ flag { name: "pointer_coords_is_resampled_api" description: "Makes MotionEvent.PointerCoords#isResampled() a public API" bug: "298197511" +} + +flag { + namespace: "input_native" + name: "keyboard_a11y_slow_keys_flag" + description: "Controls if the slow keys accessibility feature for physical keyboard is available to the user" + bug: "294546335" }
\ No newline at end of file diff --git a/core/java/android/net/thread/OWNERS b/core/java/android/net/thread/OWNERS new file mode 100644 index 000000000000..55c307b5eb62 --- /dev/null +++ b/core/java/android/net/thread/OWNERS @@ -0,0 +1,3 @@ +# Bug component: 1203089 + +include platform/packages/modules/ThreadNetwork:/OWNERS diff --git a/core/java/android/net/thread/flags.aconfig b/core/java/android/net/thread/flags.aconfig new file mode 100644 index 000000000000..6e72f8ebd8d1 --- /dev/null +++ b/core/java/android/net/thread/flags.aconfig @@ -0,0 +1,8 @@ +package: "com.android.net.thread.flags" + +flag { + name: "thread_user_restriction_enabled" + namespace: "thread_network" + description: "Controls whether user restriction on thread networks is enabled" + bug: "307679182" +} diff --git a/core/java/android/os/PatternMatcher.java b/core/java/android/os/PatternMatcher.java index b5425b43fe7e..79a2c59d6a03 100644 --- a/core/java/android/os/PatternMatcher.java +++ b/core/java/android/os/PatternMatcher.java @@ -16,9 +16,12 @@ package android.os; +import android.annotation.IntDef; import android.util.Log; import android.util.proto.ProtoOutputStream; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.util.Arrays; /** @@ -68,6 +71,17 @@ public class PatternMatcher implements Parcelable { */ public static final int PATTERN_SUFFIX = 4; + /** @hide */ + @IntDef(value = { + PATTERN_LITERAL, + PATTERN_PREFIX, + PATTERN_SIMPLE_GLOB, + PATTERN_ADVANCED_GLOB, + PATTERN_SUFFIX, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface PatternType {} + // token types for advanced matching private static final int TOKEN_TYPE_LITERAL = 0; private static final int TOKEN_TYPE_ANY = 1; diff --git a/core/java/android/os/PersistableBundle.java b/core/java/android/os/PersistableBundle.java index 02704f5b346b..236194d16ad8 100644 --- a/core/java/android/os/PersistableBundle.java +++ b/core/java/android/os/PersistableBundle.java @@ -294,6 +294,43 @@ public final class PersistableBundle extends BaseBundle implements Cloneable, Pa XmlUtils.writeMapXml(mMap, out, this); } + /** + * Checks whether all keys and values are within the given character limit. + * Note: Maximum character limit of String that can be saved to XML as part of bundle is 65535. + * Otherwise IOException is thrown. + * @param limit length of String keys and values in the PersistableBundle, including nested + * PersistableBundles to check against. + * + * @hide + */ + public boolean isBundleContentsWithinLengthLimit(int limit) { + unparcel(); + if (mMap == null) { + return true; + } + for (int i = 0; i < mMap.size(); i++) { + if (mMap.keyAt(i) != null && mMap.keyAt(i).length() > limit) { + return false; + } + final Object value = mMap.valueAt(i); + if (value instanceof String && ((String) value).length() > limit) { + return false; + } else if (value instanceof String[]) { + String[] stringArray = (String[]) value; + for (int j = 0; j < stringArray.length; j++) { + if (stringArray[j] != null + && stringArray[j].length() > limit) { + return false; + } + } + } else if (value instanceof PersistableBundle + && !((PersistableBundle) value).isBundleContentsWithinLengthLimit(limit)) { + return false; + } + } + return true; + } + /** @hide */ static class MyReadMapCallback implements XmlUtils.ReadMapCallback { @Override diff --git a/core/java/android/os/Process.java b/core/java/android/os/Process.java index dd0436cbb2f2..1f3a1620a9f2 100644 --- a/core/java/android/os/Process.java +++ b/core/java/android/os/Process.java @@ -1589,7 +1589,15 @@ public class Process { @UnsupportedAppUsage public static final native long getPss(int pid); - /** @hide */ + /** + * Gets the total Rss value for a given process, in bytes. + * + * @param pid the process to the Rss for + * @return an ordered array containing multiple values, they are: + * [total_rss, file, anon, swap, shmem]. + * or NULL if the value cannot be determined + * @hide + */ public static final native long[] getRss(int pid); /** diff --git a/core/java/android/os/UserManager.java b/core/java/android/os/UserManager.java index c280d132ae38..d6df8d940904 100644 --- a/core/java/android/os/UserManager.java +++ b/core/java/android/os/UserManager.java @@ -88,6 +88,7 @@ import java.util.Set; * See {@link DevicePolicyManager#ACTION_PROVISION_MANAGED_PROFILE} for more on managed profiles. */ @SystemService(Context.USER_SERVICE) +@android.ravenwood.annotation.RavenwoodKeepPartialClass public class UserManager { private static final String TAG = "UserManager"; @@ -106,6 +107,21 @@ public class UserManager { /** Whether the device is in headless system user mode; null until cached. */ private static Boolean sIsHeadlessSystemUser = null; + /** Maximum length of username. + * @hide + */ + public static final int MAX_USER_NAME_LENGTH = 100; + + /** Maximum length of user property String value. + * @hide + */ + public static final int MAX_ACCOUNT_STRING_LENGTH = 500; + + /** Maximum length of account options String values. + * @hide + */ + public static final int MAX_ACCOUNT_OPTIONS_LENGTH = 1000; + /** * User type representing a {@link UserHandle#USER_SYSTEM system} user that is a human user. * This type of user cannot be created; it can only pre-exist on first boot. @@ -1882,6 +1898,30 @@ public class UserManager { "no_near_field_communication_radio"; /** + * This user restriction specifies if Thread network is disallowed on the device. If Thread + * network is disallowed it cannot be turned on via Settings. + * + * <p>This restriction can only be set by a device owner or a profile owner of an + * organization-owned managed profile on the parent profile. + * In both cases, the restriction applies globally on the device and will turn off the + * Thread network radio if it's currently on and prevent the radio from being turned + * on in the future. + * + * <p> <a href="https://www.threadgroup.org">Thread</a> is a low-power and low-latency wireless + * mesh networking protocol built on IPv6. + * + * <p>Default is <code>false</code>. + * + * <p>Key for user restrictions. + * <p>Type: Boolean + * @see DevicePolicyManager#addUserRestriction(ComponentName, String) + * @see DevicePolicyManager#clearUserRestriction(ComponentName, String) + * @see #getUserRestrictions() + */ + @FlaggedApi("com.android.net.thread.flags.thread_user_restriction_enabled") + public static final String DISALLOW_THREAD_NETWORK = "no_thread_network"; + + /** * List of key values that can be passed into the various user restriction related methods * in {@link UserManager} & {@link DevicePolicyManager}. * Note: This is slightly different from the real set of user restrictions listed in {@link @@ -1967,6 +2007,7 @@ public class UserManager { DISALLOW_ULTRA_WIDEBAND_RADIO, DISALLOW_GRANT_ADMIN, DISALLOW_NEAR_FIELD_COMMUNICATION_RADIO, + DISALLOW_THREAD_NETWORK, }) @Retention(RetentionPolicy.SOURCE) public @interface UserRestrictionKey {} @@ -2906,6 +2947,7 @@ public class UserManager { * {@link UserManager#USER_TYPE_PROFILE_MANAGED managed profile}. * @hide */ + @android.ravenwood.annotation.RavenwoodKeep public static boolean isUserTypeManagedProfile(@Nullable String userType) { return USER_TYPE_PROFILE_MANAGED.equals(userType); } @@ -2914,6 +2956,7 @@ public class UserManager { * Returns whether the user type is a {@link UserManager#USER_TYPE_FULL_GUEST guest user}. * @hide */ + @android.ravenwood.annotation.RavenwoodKeep public static boolean isUserTypeGuest(@Nullable String userType) { return USER_TYPE_FULL_GUEST.equals(userType); } @@ -2923,6 +2966,7 @@ public class UserManager { * {@link UserManager#USER_TYPE_FULL_RESTRICTED restricted user}. * @hide */ + @android.ravenwood.annotation.RavenwoodKeep public static boolean isUserTypeRestricted(@Nullable String userType) { return USER_TYPE_FULL_RESTRICTED.equals(userType); } @@ -2931,6 +2975,7 @@ public class UserManager { * Returns whether the user type is a {@link UserManager#USER_TYPE_FULL_DEMO demo user}. * @hide */ + @android.ravenwood.annotation.RavenwoodKeep public static boolean isUserTypeDemo(@Nullable String userType) { return USER_TYPE_FULL_DEMO.equals(userType); } @@ -2939,6 +2984,7 @@ public class UserManager { * Returns whether the user type is a {@link UserManager#USER_TYPE_PROFILE_CLONE clone user}. * @hide */ + @android.ravenwood.annotation.RavenwoodKeep public static boolean isUserTypeCloneProfile(@Nullable String userType) { return USER_TYPE_PROFILE_CLONE.equals(userType); } @@ -2948,6 +2994,7 @@ public class UserManager { * {@link UserManager#USER_TYPE_PROFILE_COMMUNAL communal profile}. * @hide */ + @android.ravenwood.annotation.RavenwoodKeep public static boolean isUserTypeCommunalProfile(@Nullable String userType) { return USER_TYPE_PROFILE_COMMUNAL.equals(userType); } @@ -2958,6 +3005,7 @@ public class UserManager { * * @hide */ + @android.ravenwood.annotation.RavenwoodKeep public static boolean isUserTypePrivateProfile(@Nullable String userType) { return USER_TYPE_PROFILE_PRIVATE.equals(userType); } @@ -4423,15 +4471,15 @@ public class UserManager { * This API should only be called if the current user is an {@link #isAdminUser() admin} user, * as otherwise the returned intent will not be able to create a user. * - * @param userName Optional name to assign to the user. + * @param userName Optional name to assign to the user. Character limit is 100. * @param accountName Optional account name that will be used by the setup wizard to initialize - * the user. + * the user. Character limit is 500. * @param accountType Optional account type for the account to be created. This is required - * if the account name is specified. + * if the account name is specified. Character limit is 500. * @param accountOptions Optional bundle of data to be passed in during account creation in the * new user via {@link AccountManager#addAccount(String, String, String[], * Bundle, android.app.Activity, android.accounts.AccountManagerCallback, - * Handler)}. + * Handler)}. Character limit is 1000. * @return An Intent that can be launched from an Activity. * @see #USER_CREATION_FAILED_NOT_PERMITTED * @see #USER_CREATION_FAILED_NO_MORE_USERS diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index ecd6f22607b6..11edcafecdee 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -444,6 +444,18 @@ public final class Settings { "android.settings.ACCESSIBILITY_DETAILS_SETTINGS"; /** + * Activity Action: Show settings to allow configuration of an accessibility + * shortcut belonging to an accessibility feature or features. + * <p> + * Input: ":settings:show_fragment_args" must contain "targets" denoting the services to edit. + * <p> + * Output: Nothing. + * @hide + **/ + public static final String ACTION_ACCESSIBILITY_SHORTCUT_SETTINGS = + "android.settings.ACCESSIBILITY_SHORTCUT_SETTINGS"; + + /** * Activity Action: Show settings to allow configuration of accessibility color and motion. * <p> * In some cases, a matching Activity may not exist, so ensure you @@ -4513,10 +4525,11 @@ public final class Settings { /** @hide */ public static void adjustConfigurationForUser(ContentResolver cr, Configuration outConfig, int userHandle, boolean updateSettingsIfEmpty) { + final float defaultFontScale = getDefaultFontScale(cr, userHandle); outConfig.fontScale = Settings.System.getFloatForUser( - cr, FONT_SCALE, DEFAULT_FONT_SCALE, userHandle); + cr, FONT_SCALE, defaultFontScale, userHandle); if (outConfig.fontScale < 0) { - outConfig.fontScale = DEFAULT_FONT_SCALE; + outConfig.fontScale = defaultFontScale; } outConfig.fontWeightAdjustment = Settings.Secure.getIntForUser( cr, Settings.Secure.FONT_WEIGHT_ADJUSTMENT, DEFAULT_FONT_WEIGHT, userHandle); @@ -4541,6 +4554,12 @@ public final class Settings { } } + private static float getDefaultFontScale(ContentResolver cr, int userHandle) { + return com.android.window.flags.Flags.configurableFontScaleDefault() + ? Settings.System.getFloatForUser(cr, DEFAULT_DEVICE_FONT_SCALE, + DEFAULT_FONT_SCALE, userHandle) : DEFAULT_FONT_SCALE; + } + /** * @hide Erase the fields in the Configuration that should be applied * by the settings. @@ -4907,6 +4926,15 @@ public final class Settings { public static final String FONT_SCALE = "font_scale"; /** + * Default scaling factor for fonts for the specific device, float. + * The value is read from the {@link R.dimen.def_device_font_scale} + * configuration property. + * + * @hide + */ + public static final String DEFAULT_DEVICE_FONT_SCALE = "device_font_scale"; + + /** * The serialized system locale value. * * Do not use this value directory. @@ -6245,6 +6273,7 @@ public final class Settings { PRIVATE_SETTINGS.add(CAMERA_FLASH_NOTIFICATION); PRIVATE_SETTINGS.add(SCREEN_FLASH_NOTIFICATION); PRIVATE_SETTINGS.add(SCREEN_FLASH_NOTIFICATION_COLOR); + PRIVATE_SETTINGS.add(DEFAULT_DEVICE_FONT_SCALE); } /** @@ -7881,6 +7910,17 @@ public final class Settings { public static final String ACCESSIBILITY_BOUNCE_KEYS = "accessibility_bounce_keys"; /** + * Whether to enable slow keys for Physical Keyboard accessibility. + * + * If set to non-zero value, any key press on physical keyboard needs to be pressed and + * held for the provided threshold duration (in milliseconds) to be registered in the + * system. + * + * @hide + */ + public static final String ACCESSIBILITY_SLOW_KEYS = "accessibility_slow_keys"; + + /** * Whether to enable sticky keys for Physical Keyboard accessibility. * * This is a boolean value that determines if Sticky keys feature is enabled. @@ -12276,6 +12316,8 @@ public final class Settings { CLONE_TO_MANAGED_PROFILE.add(LOCATION_MODE); CLONE_TO_MANAGED_PROFILE.add(SHOW_IME_WITH_HARD_KEYBOARD); CLONE_TO_MANAGED_PROFILE.add(ACCESSIBILITY_BOUNCE_KEYS); + CLONE_TO_MANAGED_PROFILE.add(ACCESSIBILITY_SLOW_KEYS); + CLONE_TO_MANAGED_PROFILE.add(ACCESSIBILITY_STICKY_KEYS); CLONE_TO_MANAGED_PROFILE.add(NOTIFICATION_BUBBLES); CLONE_TO_MANAGED_PROFILE.add(NOTIFICATION_HISTORY_ENABLED); } diff --git a/core/java/android/service/autofill/AutofillService.java b/core/java/android/service/autofill/AutofillService.java index 5ad2502d1546..298bdb881e9f 100644 --- a/core/java/android/service/autofill/AutofillService.java +++ b/core/java/android/service/autofill/AutofillService.java @@ -622,6 +622,15 @@ public abstract class AutofillService extends Service { new FillCallback(callback, request.getId()))); } + @Override + public void onConvertCredentialRequest( + @NonNull ConvertCredentialRequest convertCredentialRequest, + @NonNull IConvertCredentialCallback convertCredentialCallback) { + mHandler.sendMessage(obtainMessage( + AutofillService::onConvertCredentialRequest, + AutofillService.this, convertCredentialRequest, + new ConvertCredentialCallback(convertCredentialCallback))); + } @Override public void onFillCredentialRequest(FillRequest request, IFillCallback callback, @@ -707,7 +716,19 @@ public abstract class AutofillService extends Service { */ public void onFillCredentialRequest(@NonNull FillRequest request, @NonNull CancellationSignal cancellationSignal, @NonNull FillCallback callback, - IAutoFillManagerClient autofillClientCallback) {} + @NonNull IAutoFillManagerClient autofillClientCallback) {} + + /** + * Called by the Android system to convert a credential manager response to a dataset + * + * @param convertCredentialRequest the request that has the original credential manager response + * @param convertCredentialCallback callback used to notify the result of the request. + * + * @hide + */ + public void onConvertCredentialRequest( + @NonNull ConvertCredentialRequest convertCredentialRequest, + @NonNull ConvertCredentialCallback convertCredentialCallback){} /** * Called when the user requests the service to save the contents of a screen. diff --git a/core/java/android/service/autofill/ConvertCredentialCallback.java b/core/java/android/service/autofill/ConvertCredentialCallback.java new file mode 100644 index 000000000000..a39f01115338 --- /dev/null +++ b/core/java/android/service/autofill/ConvertCredentialCallback.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.service.autofill; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.os.RemoteException; + +/** + * <p><code>ConvertCredentialCallback</code> handles convertCredentialResponse from Autofill + * Service. + * + * @hide + */ +public final class ConvertCredentialCallback { + + private static final String TAG = "ConvertCredentialCallback"; + + private final IConvertCredentialCallback mCallback; + + /** @hide */ + public ConvertCredentialCallback(IConvertCredentialCallback callback) { + mCallback = callback; + } + + /** + * Notifies the Android System that a convertCredentialRequest was fulfilled by the service. + * + * @param convertCredentialResponse the result + */ + public void onSuccess(@NonNull ConvertCredentialResponse convertCredentialResponse) { + try { + mCallback.onSuccess(convertCredentialResponse); + } catch (RemoteException e) { + e.rethrowAsRuntimeException(); + } + } + + /** + * Notifies the Android System that a convert credential request has failed + * + * @param message the error message + */ + public void onFailure(@Nullable CharSequence message) { + try { + mCallback.onFailure(message); + } catch (RemoteException e) { + e.rethrowAsRuntimeException(); + } + } +} diff --git a/core/java/android/service/autofill/ConvertCredentialRequest.aidl b/core/java/android/service/autofill/ConvertCredentialRequest.aidl new file mode 100644 index 000000000000..79681e2dbe84 --- /dev/null +++ b/core/java/android/service/autofill/ConvertCredentialRequest.aidl @@ -0,0 +1,19 @@ +/** + * Copyright (c) 2024, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.service.autofill; + +parcelable ConvertCredentialRequest;
\ No newline at end of file diff --git a/core/java/android/service/autofill/ConvertCredentialRequest.java b/core/java/android/service/autofill/ConvertCredentialRequest.java new file mode 100644 index 000000000000..d2d7556f40b7 --- /dev/null +++ b/core/java/android/service/autofill/ConvertCredentialRequest.java @@ -0,0 +1,158 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.service.autofill; + +import android.annotation.NonNull; +import android.credentials.GetCredentialResponse; +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; + +import com.android.internal.util.DataClass; + + +/** + * This class represents a request to an autofill service to convert the credential manager response + * to a dataset. + * + * @hide + */ +@DataClass( + genToString = true, + genHiddenConstructor = true, + genHiddenConstDefs = true) +public final class ConvertCredentialRequest implements Parcelable { + private final @NonNull GetCredentialResponse mGetCredentialResponse; + private final @NonNull Bundle mClientState; + + + + // Code below generated by codegen v1.0.23. + // + // DO NOT MODIFY! + // CHECKSTYLE:OFF Generated code + // + // To regenerate run: + // $ codegen $ANDROID_BUILD_TOP/frameworks/base/core/java/android/service/autofill/ConvertCredentialRequest.java + // + // To exclude the generated code from IntelliJ auto-formatting enable (one-time): + // Settings > Editor > Code Style > Formatter Control + //@formatter:off + + + /** + * Creates a new ConvertCredentialRequest. + * + * @hide + */ + @DataClass.Generated.Member + public ConvertCredentialRequest( + @NonNull GetCredentialResponse getCredentialResponse, + @NonNull Bundle clientState) { + this.mGetCredentialResponse = getCredentialResponse; + com.android.internal.util.AnnotationValidations.validate( + NonNull.class, null, mGetCredentialResponse); + this.mClientState = clientState; + com.android.internal.util.AnnotationValidations.validate( + NonNull.class, null, mClientState); + + // onConstructed(); // You can define this method to get a callback + } + + @DataClass.Generated.Member + public @NonNull GetCredentialResponse getGetCredentialResponse() { + return mGetCredentialResponse; + } + + @DataClass.Generated.Member + public @NonNull Bundle getClientState() { + return mClientState; + } + + @Override + @DataClass.Generated.Member + public String toString() { + // You can override field toString logic by defining methods like: + // String fieldNameToString() { ... } + + return "ConvertCredentialRequest { " + + "getCredentialResponse = " + mGetCredentialResponse + ", " + + "clientState = " + mClientState + + " }"; + } + + @Override + @DataClass.Generated.Member + public void writeToParcel(@NonNull Parcel dest, int flags) { + // You can override field parcelling by defining methods like: + // void parcelFieldName(Parcel dest, int flags) { ... } + + dest.writeTypedObject(mGetCredentialResponse, flags); + dest.writeBundle(mClientState); + } + + @Override + @DataClass.Generated.Member + public int describeContents() { return 0; } + + /** @hide */ + @SuppressWarnings({"unchecked", "RedundantCast"}) + @DataClass.Generated.Member + /* package-private */ ConvertCredentialRequest(@NonNull Parcel in) { + // You can override field unparcelling by defining methods like: + // static FieldType unparcelFieldName(Parcel in) { ... } + + GetCredentialResponse getCredentialResponse = (GetCredentialResponse) in.readTypedObject(GetCredentialResponse.CREATOR); + Bundle clientState = in.readBundle(); + + this.mGetCredentialResponse = getCredentialResponse; + com.android.internal.util.AnnotationValidations.validate( + NonNull.class, null, mGetCredentialResponse); + this.mClientState = clientState; + com.android.internal.util.AnnotationValidations.validate( + NonNull.class, null, mClientState); + + // onConstructed(); // You can define this method to get a callback + } + + @DataClass.Generated.Member + public static final @NonNull Parcelable.Creator<ConvertCredentialRequest> CREATOR + = new Parcelable.Creator<ConvertCredentialRequest>() { + @Override + public ConvertCredentialRequest[] newArray(int size) { + return new ConvertCredentialRequest[size]; + } + + @Override + public ConvertCredentialRequest createFromParcel(@NonNull Parcel in) { + return new ConvertCredentialRequest(in); + } + }; + + @DataClass.Generated( + time = 1706132305002L, + codegenVersion = "1.0.23", + sourceFile = "frameworks/base/core/java/android/service/autofill/ConvertCredentialRequest.java", + inputSignatures = "private final @android.annotation.NonNull android.credentials.GetCredentialResponse mGetCredentialResponse\nprivate final @android.annotation.NonNull android.os.Bundle mClientState\nclass ConvertCredentialRequest extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genToString=true, genHiddenConstructor=true, genHiddenConstDefs=true)") + @Deprecated + private void __metadata() {} + + + //@formatter:on + // End of generated code + +} diff --git a/core/java/android/service/autofill/ConvertCredentialResponse.aidl b/core/java/android/service/autofill/ConvertCredentialResponse.aidl new file mode 100644 index 000000000000..98ac6f67c521 --- /dev/null +++ b/core/java/android/service/autofill/ConvertCredentialResponse.aidl @@ -0,0 +1,19 @@ +/** + * Copyright (c) 2024, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.service.autofill; + +parcelable ConvertCredentialResponse;
\ No newline at end of file diff --git a/core/java/android/service/autofill/ConvertCredentialResponse.java b/core/java/android/service/autofill/ConvertCredentialResponse.java new file mode 100644 index 000000000000..5da4f63f31f9 --- /dev/null +++ b/core/java/android/service/autofill/ConvertCredentialResponse.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 android.service.autofill; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; + +import com.android.internal.util.DataClass; + +/** + * Response for a {@Link ConvertCredentialRequest} + * + * @hide + */ +@DataClass( + genToString = true, + genHiddenConstructor = true, + genHiddenConstDefs = true) +public final class ConvertCredentialResponse implements Parcelable { + private final @NonNull Dataset mDataset; + private final @Nullable Bundle mClientState; + + + + + // Code below generated by codegen v1.0.23. + // + // DO NOT MODIFY! + // CHECKSTYLE:OFF Generated code + // + // To regenerate run: + // $ codegen $ANDROID_BUILD_TOP/frameworks/base/core/java/android/service/autofill/ConvertCredentialResponse.java + // + // To exclude the generated code from IntelliJ auto-formatting enable (one-time): + // Settings > Editor > Code Style > Formatter Control + //@formatter:off + + + /** + * Creates a new ConvertCredentialResponse. + * + * @hide + */ + @DataClass.Generated.Member + public ConvertCredentialResponse( + @NonNull Dataset dataset, + @Nullable Bundle clientState) { + this.mDataset = dataset; + com.android.internal.util.AnnotationValidations.validate( + NonNull.class, null, mDataset); + this.mClientState = clientState; + + // onConstructed(); // You can define this method to get a callback + } + + @DataClass.Generated.Member + public @NonNull Dataset getDataset() { + return mDataset; + } + + @DataClass.Generated.Member + public @Nullable Bundle getClientState() { + return mClientState; + } + + @Override + @DataClass.Generated.Member + public String toString() { + // You can override field toString logic by defining methods like: + // String fieldNameToString() { ... } + + return "ConvertCredentialResponse { " + + "dataset = " + mDataset + ", " + + "clientState = " + mClientState + + " }"; + } + + @Override + @DataClass.Generated.Member + public void writeToParcel(@NonNull Parcel dest, int flags) { + // You can override field parcelling by defining methods like: + // void parcelFieldName(Parcel dest, int flags) { ... } + + byte flg = 0; + if (mClientState != null) flg |= 0x2; + dest.writeByte(flg); + dest.writeTypedObject(mDataset, flags); + if (mClientState != null) dest.writeBundle(mClientState); + } + + @Override + @DataClass.Generated.Member + public int describeContents() { return 0; } + + /** @hide */ + @SuppressWarnings({"unchecked", "RedundantCast"}) + @DataClass.Generated.Member + /* package-private */ ConvertCredentialResponse(@NonNull Parcel in) { + // You can override field unparcelling by defining methods like: + // static FieldType unparcelFieldName(Parcel in) { ... } + + byte flg = in.readByte(); + Dataset dataset = (Dataset) in.readTypedObject(Dataset.CREATOR); + Bundle clientState = (flg & 0x2) == 0 ? null : in.readBundle(); + + this.mDataset = dataset; + com.android.internal.util.AnnotationValidations.validate( + NonNull.class, null, mDataset); + this.mClientState = clientState; + + // onConstructed(); // You can define this method to get a callback + } + + @DataClass.Generated.Member + public static final @NonNull Parcelable.Creator<ConvertCredentialResponse> CREATOR + = new Parcelable.Creator<ConvertCredentialResponse>() { + @Override + public ConvertCredentialResponse[] newArray(int size) { + return new ConvertCredentialResponse[size]; + } + + @Override + public ConvertCredentialResponse createFromParcel(@NonNull Parcel in) { + return new ConvertCredentialResponse(in); + } + }; + + @DataClass.Generated( + time = 1706132669373L, + codegenVersion = "1.0.23", + sourceFile = "frameworks/base/core/java/android/service/autofill/ConvertCredentialResponse.java", + inputSignatures = "private final @android.annotation.NonNull android.service.autofill.Dataset mDataset\nprivate final @android.annotation.Nullable android.os.Bundle mClientState\nclass ConvertCredentialResponse extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genToString=true, genHiddenConstructor=true, genHiddenConstDefs=true)") + @Deprecated + private void __metadata() {} + + + //@formatter:on + // End of generated code + +} diff --git a/core/java/android/service/autofill/FillEventHistory.java b/core/java/android/service/autofill/FillEventHistory.java index 5d58120ef5bb..98dda1031eff 100644 --- a/core/java/android/service/autofill/FillEventHistory.java +++ b/core/java/android/service/autofill/FillEventHistory.java @@ -309,12 +309,19 @@ public final class FillEventHistory implements Parcelable { /** The autofill suggestion is shown as a dialog presentation. */ public static final int UI_TYPE_DIALOG = 3; + /** + * The autofill suggestion is shown os a credman bottom sheet + * @hide + */ + public static final int UI_TYPE_CREDMAN_BOTTOM_SHEET = 4; + /** @hide */ @IntDef(prefix = { "UI_TYPE_" }, value = { UI_TYPE_UNKNOWN, UI_TYPE_MENU, UI_TYPE_INLINE, - UI_TYPE_DIALOG + UI_TYPE_DIALOG, + UI_TYPE_CREDMAN_BOTTOM_SHEET }) @Retention(RetentionPolicy.SOURCE) public @interface UiType {} @@ -755,6 +762,8 @@ public final class FillEventHistory implements Parcelable { return "UI_TYPE_INLINE"; case UI_TYPE_DIALOG: return "UI_TYPE_FILL_DIALOG"; + case UI_TYPE_CREDMAN_BOTTOM_SHEET: + return "UI_TYPE_CREDMAN_BOTTOM_SHEET"; default: return "UI_TYPE_UNKNOWN"; } diff --git a/core/java/android/service/autofill/FillResponse.java b/core/java/android/service/autofill/FillResponse.java index 7ea74d375ffb..09ec933880d4 100644 --- a/core/java/android/service/autofill/FillResponse.java +++ b/core/java/android/service/autofill/FillResponse.java @@ -28,6 +28,7 @@ import android.annotation.StringRes; import android.annotation.SuppressLint; import android.annotation.TestApi; import android.app.Activity; +import android.app.PendingIntent; import android.content.Intent; import android.content.IntentSender; import android.content.pm.ParceledListSlice; @@ -116,6 +117,7 @@ public final class FillResponse implements Parcelable { private final boolean mShowFillDialogIcon; private final boolean mShowSaveDialogIcon; private final @Nullable FieldClassification[] mDetectedFieldTypes; + private final @Nullable PendingIntent mDialogPendingIntent; /** * Creates a shollow copy of the provided FillResponse. @@ -150,7 +152,8 @@ public final class FillResponse implements Parcelable { r.mServiceDisplayNameResourceId, r.mShowFillDialogIcon, r.mShowSaveDialogIcon, - r.mDetectedFieldTypes); + r.mDetectedFieldTypes, + r.mDialogPendingIntent); } private FillResponse(ParceledListSlice<Dataset> datasets, SaveInfo saveInfo, Bundle clientState, @@ -163,7 +166,7 @@ public final class FillResponse implements Parcelable { int[] cancelIds, boolean supportsInlineSuggestions, int iconResourceId, int serviceDisplayNameResourceId, boolean showFillDialogIcon, boolean showSaveDialogIcon, - FieldClassification[] detectedFieldTypes) { + FieldClassification[] detectedFieldTypes, PendingIntent dialogPendingIntent) { mDatasets = datasets; mSaveInfo = saveInfo; mClientState = clientState; @@ -190,6 +193,7 @@ public final class FillResponse implements Parcelable { mShowFillDialogIcon = showFillDialogIcon; mShowSaveDialogIcon = showSaveDialogIcon; mDetectedFieldTypes = detectedFieldTypes; + mDialogPendingIntent = dialogPendingIntent; } private FillResponse(@NonNull Builder builder) { @@ -219,6 +223,7 @@ public final class FillResponse implements Parcelable { mShowFillDialogIcon = builder.mShowFillDialogIcon; mShowSaveDialogIcon = builder.mShowSaveDialogIcon; mDetectedFieldTypes = builder.mDetectedFieldTypes; + mDialogPendingIntent = builder.mDialogPendingIntent; } /** @hide */ @@ -399,6 +404,7 @@ public final class FillResponse implements Parcelable { private boolean mShowFillDialogIcon = true; private boolean mShowSaveDialogIcon = true; private FieldClassification[] mDetectedFieldTypes; + private PendingIntent mDialogPendingIntent; /** * Adds a new {@link FieldClassification} to this response, to @@ -1079,6 +1085,24 @@ public final class FillResponse implements Parcelable { } /** + * Sets credential dialog pending intent. Framework will use the intent to launch the + * selector UI. A replacement for previous fill bottom sheet. + * + * @throws IllegalStateException if {@link #build()} was already called. + * @throws NullPointerException if {@code pendingIntent} is {@code null}. + * + * @hide + */ + @NonNull + public Builder setDialogPendingIntent(@NonNull PendingIntent pendingIntent) { + throwIfDestroyed(); + Preconditions.checkNotNull(pendingIntent, + "can't pass a null object to setDialogPendingIntent"); + mDialogPendingIntent = pendingIntent; + return this; + } + + /** * Builds a new {@link FillResponse} instance. * * @throws IllegalStateException if any of the following conditions occur: @@ -1187,6 +1211,9 @@ public final class FillResponse implements Parcelable { if (mAuthentication != null) { builder.append(", hasAuthentication"); } + if (mDialogPendingIntent != null) { + builder.append(", hasDialogPendingIntent"); + } if (mAuthenticationIds != null) { builder.append(", authenticationIds=").append(Arrays.toString(mAuthenticationIds)); } @@ -1232,6 +1259,7 @@ public final class FillResponse implements Parcelable { parcel.writeParcelable(mInlineTooltipPresentation, flags); parcel.writeParcelable(mDialogPresentation, flags); parcel.writeParcelable(mDialogHeader, flags); + parcel.writeParcelable(mDialogPendingIntent, flags); parcel.writeParcelableArray(mFillDialogTriggerIds, flags); parcel.writeParcelable(mHeader, flags); parcel.writeParcelable(mFooter, flags); @@ -1282,6 +1310,11 @@ public final class FillResponse implements Parcelable { if (dialogHeader != null) { builder.setDialogHeader(dialogHeader); } + final PendingIntent dialogPendingIntent = parcel.readParcelable(null, + PendingIntent.class); + if (dialogPendingIntent != null) { + builder.setDialogPendingIntent(dialogPendingIntent); + } final AutofillId[] triggerIds = parcel.readParcelableArray(null, AutofillId.class); if (triggerIds != null) { builder.setFillDialogTriggerIds(triggerIds); diff --git a/core/java/android/service/autofill/IAutoFillService.aidl b/core/java/android/service/autofill/IAutoFillService.aidl index 03ead3266521..2c2feae7aeea 100644 --- a/core/java/android/service/autofill/IAutoFillService.aidl +++ b/core/java/android/service/autofill/IAutoFillService.aidl @@ -16,6 +16,8 @@ package android.service.autofill; +import android.service.autofill.ConvertCredentialRequest; +import android.service.autofill.IConvertCredentialCallback; import android.service.autofill.FillRequest; import android.service.autofill.IFillCallback; import android.service.autofill.ISaveCallback; @@ -32,7 +34,8 @@ oneway interface IAutoFillService { void onConnectedStateChanged(boolean connected); void onFillRequest(in FillRequest request, in IFillCallback callback); void onFillCredentialRequest(in FillRequest request, in IFillCallback callback, - in IAutoFillManagerClient client); + in IAutoFillManagerClient client); void onSaveRequest(in SaveRequest request, in ISaveCallback callback); void onSavedPasswordCountRequest(in IResultReceiver receiver); + void onConvertCredentialRequest(in ConvertCredentialRequest convertCredentialRequest, in IConvertCredentialCallback convertCredentialCallback); } diff --git a/core/java/android/service/autofill/IConvertCredentialCallback.aidl b/core/java/android/service/autofill/IConvertCredentialCallback.aidl new file mode 100644 index 000000000000..9dfc29429876 --- /dev/null +++ b/core/java/android/service/autofill/IConvertCredentialCallback.aidl @@ -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 android.service.autofill; + +import android.os.ICancellationSignal; + +import android.service.autofill.ConvertCredentialResponse; + +/** + * Interface to receive the result of a convert credential request + * + * @hide + */ +oneway interface IConvertCredentialCallback { + void onSuccess(in ConvertCredentialResponse convertCredentialResponse); + void onFailure(CharSequence message); +} diff --git a/core/java/android/service/notification/ZenModeConfig.java b/core/java/android/service/notification/ZenModeConfig.java index 9895551a8672..9d19ef6bdc64 100644 --- a/core/java/android/service/notification/ZenModeConfig.java +++ b/core/java/android/service/notification/ZenModeConfig.java @@ -1053,7 +1053,7 @@ public class ZenModeConfig implements Parcelable { out); if (Flags.modesApi()) { - writeZenPolicyState(ALLOW_ATT_CHANNELS, policy.getPriorityChannels(), out); + writeZenPolicyState(ALLOW_ATT_CHANNELS, policy.getPriorityChannelsAllowed(), out); } } @@ -1381,7 +1381,7 @@ public class ZenModeConfig implements Parcelable { int state = defaultPolicy.state; if (Flags.modesApi()) { state = Policy.policyState(defaultPolicy.hasPriorityChannels(), - ZenPolicy.stateToBoolean(zenPolicy.getPriorityChannels(), + ZenPolicy.stateToBoolean(zenPolicy.getPriorityChannelsAllowed(), DEFAULT_ALLOW_PRIORITY_CHANNELS)); } diff --git a/core/java/android/service/notification/ZenPolicy.java b/core/java/android/service/notification/ZenPolicy.java index d8318a6bee7c..786d768bc55b 100644 --- a/core/java/android/service/notification/ZenPolicy.java +++ b/core/java/android/service/notification/ZenPolicy.java @@ -570,7 +570,7 @@ public final class ZenPolicy implements Parcelable { * with {@link NotificationChannel#canBypassDnd()} will be intercepted. */ @FlaggedApi(Flags.FLAG_MODES_API) - public @State int getPriorityChannels() { + public @State int getPriorityChannelsAllowed() { switch (mAllowChannels) { case CHANNEL_POLICY_PRIORITY: return STATE_ALLOW; @@ -1529,7 +1529,7 @@ public final class ZenPolicy implements Parcelable { proto.write(DNDPolicyProto.ALLOW_CONVERSATIONS_FROM, getPriorityConversationSenders()); if (Flags.modesApi()) { - proto.write(DNDPolicyProto.ALLOW_CHANNELS, getPriorityChannels()); + proto.write(DNDPolicyProto.ALLOW_CHANNELS, getPriorityChannelsAllowed()); } proto.flush(); diff --git a/core/java/android/util/Singleton.java b/core/java/android/util/Singleton.java index 92646b47cef2..d27bef9e9adc 100644 --- a/core/java/android/util/Singleton.java +++ b/core/java/android/util/Singleton.java @@ -25,6 +25,7 @@ import android.compat.annotation.UnsupportedAppUsage; * * @hide */ +@android.ravenwood.annotation.RavenwoodKeepWholeClass public abstract class Singleton<T> { @UnsupportedAppUsage diff --git a/core/java/android/view/AttachedSurfaceControl.java b/core/java/android/view/AttachedSurfaceControl.java index f28574ecb3b2..27c509a8605f 100644 --- a/core/java/android/view/AttachedSurfaceControl.java +++ b/core/java/android/view/AttachedSurfaceControl.java @@ -197,9 +197,28 @@ public interface AttachedSurfaceControl { * Transfer the currently in progress touch gesture from the host to the requested * {@link SurfaceControlViewHost.SurfacePackage}. This requires that the * SurfaceControlViewHost was created with the current host's inputToken. + * <p> + * When the touch is transferred, the window currently receiving touch gets an ACTION_CANCEL + * and does not receive any further input events for this gesture. + * <p> + * The transferred-to window receives an ACTION_DOWN event and then the remainder of the + * input events for this gesture. It does not receive any of the previous events of this gesture + * that the originating window received. + * <p> + * The "transferTouch" API only works for the current gesture. When a new gesture arrives, + * input dispatcher will do a new round of hit testing. So, if the "host" window is still the + * first thing that's being touched, then it will receive the new gesture again. It will + * again be up to the host to transfer this new gesture to the embedded. + * <p> + * Once the transferred-to window receives the gesture, it can choose to give up this gesture + * and send it to another window that it's linked to (it can't be an arbitrary window for + * security reasons) using the same transferTouch API. Only the window currently receiving + * touch is allowed to transfer the gesture. * * @param surfacePackage The SurfacePackage to transfer the gesture to. * @return Whether the touch stream was transferred. + * @see SurfaceControlViewHost#transferTouchGestureToHost() for the reverse to transfer touch + * gesture from the embedded to the host. */ @FlaggedApi(Flags.FLAG_TRANSFER_GESTURE_TO_EMBEDDED) default boolean transferHostTouchGestureToEmbedded( diff --git a/core/java/android/view/Display.java b/core/java/android/view/Display.java index 1908c64ce42d..fbadef3d19ef 100644 --- a/core/java/android/view/Display.java +++ b/core/java/android/view/Display.java @@ -2079,6 +2079,7 @@ public final class Display { * * @see Display#getSupportedModes() */ + @android.ravenwood.annotation.RavenwoodKeepWholeClass public static final class Mode implements Parcelable { /** * @hide @@ -2467,6 +2468,7 @@ public final class Display { * <p>You can get an instance for a given {@link Display} object with * {@link Display#getHdrCapabilities getHdrCapabilities()}. */ + @android.ravenwood.annotation.RavenwoodKeepWholeClass public static final class HdrCapabilities implements Parcelable { /** * Invalid luminance value. diff --git a/core/java/android/view/DisplayInfo.java b/core/java/android/view/DisplayInfo.java index 981911ec8880..5654bc159568 100644 --- a/core/java/android/view/DisplayInfo.java +++ b/core/java/android/view/DisplayInfo.java @@ -51,6 +51,7 @@ import java.util.Objects; * Describes the characteristics of a particular logical display. * @hide */ +@android.ravenwood.annotation.RavenwoodKeepWholeClass public final class DisplayInfo implements Parcelable { /** * The surface flinger layer stack associated with this logical display. diff --git a/core/java/android/view/IWindowSession.aidl b/core/java/android/view/IWindowSession.aidl index 1d81be17f580..d2c25cde35d6 100644 --- a/core/java/android/view/IWindowSession.aidl +++ b/core/java/android/view/IWindowSession.aidl @@ -235,7 +235,7 @@ interface IWindowSession { */ oneway void setWallpaperDisplayOffset(IBinder windowToken, int x, int y); - Bundle sendWallpaperCommand(IBinder window, String action, int x, int y, + oneway void sendWallpaperCommand(IBinder window, String action, int x, int y, int z, in Bundle extras, boolean sync); @UnsupportedAppUsage diff --git a/core/java/android/view/InsetsSource.java b/core/java/android/view/InsetsSource.java index 86ab21348e4f..bc33d5e2f6b1 100644 --- a/core/java/android/view/InsetsSource.java +++ b/core/java/android/view/InsetsSource.java @@ -17,6 +17,7 @@ package android.view; import static android.view.InsetsSourceProto.FRAME; +import static android.view.InsetsSourceProto.TYPE; import static android.view.InsetsSourceProto.TYPE_NUMBER; import static android.view.InsetsSourceProto.VISIBLE; import static android.view.InsetsSourceProto.VISIBLE_FRAME; @@ -442,6 +443,10 @@ public class InsetsSource implements Parcelable { */ public void dumpDebug(ProtoOutputStream proto, long fieldId) { final long token = proto.start(fieldId); + if (!android.os.Flags.androidOsBuildVanillaIceCream()) { + // Deprecated since V. + proto.write(TYPE, WindowInsets.Type.toString(mType)); + } mFrame.dumpDebug(proto, FRAME); if (mVisibleFrame != null) { mVisibleFrame.dumpDebug(proto, VISIBLE_FRAME); diff --git a/core/java/android/view/Surface.java b/core/java/android/view/Surface.java index ba7874eb2d21..6c6e8b247886 100644 --- a/core/java/android/view/Surface.java +++ b/core/java/android/view/Surface.java @@ -1073,8 +1073,7 @@ public class Surface implements Parcelable { if (error == -EINVAL) { throw new IllegalArgumentException("Invalid argument to Surface.setFrameRate()"); } else if (error != 0) { - throw new RuntimeException("Failed to set frame rate on Surface. Native error: " - + error); + Log.e(TAG, "Failed to set frame rate on Surface. Native error: " + error); } } } @@ -1256,13 +1255,13 @@ public class Surface implements Parcelable { } private static void registerNativeMemoryUsage() { - if (Flags.enableSurfaceNativeAllocRegistration()) { + if (Flags.enableSurfaceNativeAllocRegistrationRo()) { VMRuntime.getRuntime().registerNativeAllocation(SURFACE_NATIVE_ALLOCATION_SIZE_BYTES); } } private static void freeNativeMemoryUsage() { - if (Flags.enableSurfaceNativeAllocRegistration()) { + if (Flags.enableSurfaceNativeAllocRegistrationRo()) { VMRuntime.getRuntime().registerNativeFree(SURFACE_NATIVE_ALLOCATION_SIZE_BYTES); } } diff --git a/core/java/android/view/WindowManager.java b/core/java/android/view/WindowManager.java index 42355bb17c2d..427d053f754e 100644 --- a/core/java/android/view/WindowManager.java +++ b/core/java/android/view/WindowManager.java @@ -6022,8 +6022,8 @@ public interface WindowManager extends ViewManager { * This is different from * {@link #registerUnbatchedSurfaceControlInputReceiver(int, IBinder, SurfaceControl, Looper, * SurfaceControlInputReceiver)} in that the input events are received batched. The caller must - * invoke {@link #unregisterSurfaceControlInputReceiver(IBinder)} to clean up the resources when - * no longer needing to use the {@link SurfaceControlInputReceiver} + * invoke {@link #unregisterSurfaceControlInputReceiver(SurfaceControl)} to clean up the + * resources when no longer needing to use the {@link SurfaceControlInputReceiver} * * @param displayId The display that the SurfaceControl will be placed on. Input will * only work @@ -6035,14 +6035,9 @@ public interface WindowManager extends ViewManager { * @param choreographer The Choreographer used for batching. This should match the rendering * Choreographer. * @param receiver The SurfaceControlInputReceiver that will receive the input events - * @return an {@link IBinder} token that is used to unregister the input receiver via - * {@link #unregisterSurfaceControlInputReceiver(IBinder)}. - * @see #registerUnbatchedSurfaceControlInputReceiver(int, IBinder, SurfaceControl, Looper, - * SurfaceControlInputReceiver) */ @FlaggedApi(Flags.FLAG_SURFACE_CONTROL_INPUT_RECEIVER) - @NonNull - default IBinder registerBatchedSurfaceControlInputReceiver(int displayId, + default void registerBatchedSurfaceControlInputReceiver(int displayId, @NonNull IBinder hostToken, @NonNull SurfaceControl surfaceControl, @NonNull Choreographer choreographer, @NonNull SurfaceControlInputReceiver receiver) { throw new UnsupportedOperationException( @@ -6054,8 +6049,8 @@ public interface WindowManager extends ViewManager { * receive every input event. This is different than calling @link * #registerBatchedSurfaceControlInputReceiver(int, IBinder, SurfaceControl, Choreographer, * SurfaceControlInputReceiver)} in that the input events are received unbatched. The caller - * must invoke {@link #unregisterSurfaceControlInputReceiver(IBinder)} to clean up the resources - * when no longer needing to use the {@link SurfaceControlInputReceiver} + * must invoke {@link #unregisterSurfaceControlInputReceiver(SurfaceControl)} to clean up the + * resources when no longer needing to use the {@link SurfaceControlInputReceiver} * * @param displayId The display that the SurfaceControl will be placed on. Input will only * work if SurfaceControl is on that display and that display was @@ -6066,14 +6061,9 @@ public interface WindowManager extends ViewManager { * @param surfaceControl The SurfaceControl to register the InputChannel for * @param looper The looper to use when invoking callbacks. * @param receiver The SurfaceControlInputReceiver that will receive the input events - * @return an {@link IBinder} token that is used to unregister the input receiver via - * {@link #unregisterSurfaceControlInputReceiver(IBinder)}. - * @see #registerBatchedSurfaceControlInputReceiver(int, IBinder, SurfaceControl, Choreographer, - * SurfaceControlInputReceiver) **/ @FlaggedApi(Flags.FLAG_SURFACE_CONTROL_INPUT_RECEIVER) - @NonNull - default IBinder registerUnbatchedSurfaceControlInputReceiver(int displayId, + default void registerUnbatchedSurfaceControlInputReceiver(int displayId, @NonNull IBinder hostToken, @NonNull SurfaceControl surfaceControl, @NonNull Looper looper, @NonNull SurfaceControlInputReceiver receiver) { throw new UnsupportedOperationException( @@ -6091,17 +6081,32 @@ public interface WindowManager extends ViewManager { * {@link #registerUnbatchedSurfaceControlInputReceiver(int, IBinder, SurfaceControl, Looper, * SurfaceControlInputReceiver)} * - * @param token The token that was returned via - * {@link #registerBatchedSurfaceControlInputReceiver(int, IBinder, - * SurfaceControl, - * Choreographer, SurfaceControlInputReceiver)} or - * {@link #registerUnbatchedSurfaceControlInputReceiver(int, IBinder, - * SurfaceControl, - * Looper, SurfaceControlInputReceiver)} + * @param surfaceControl The SurfaceControl to remove and unregister the input channel for. */ @FlaggedApi(Flags.FLAG_SURFACE_CONTROL_INPUT_RECEIVER) - default void unregisterSurfaceControlInputReceiver(@NonNull IBinder token) { + default void unregisterSurfaceControlInputReceiver(@NonNull SurfaceControl surfaceControl) { throw new UnsupportedOperationException( "unregisterSurfaceControlInputReceiver is not implemented"); } + + /** + * Returns the input client token for the {@link SurfaceControl}. This will only return non null + * if the SurfaceControl was registered for input via + * { #registerBatchedSurfaceControlInputReceiver(int, IBinder, SurfaceControl, Choreographer, + * SurfaceControlInputReceiver)} or + * {@link #registerUnbatchedSurfaceControlInputReceiver(int, IBinder, SurfaceControl, Looper, + * SurfaceControlInputReceiver)}. + * <p> + * This is helpful for testing to ensure the test waits for the layer to be registered with + * SurfaceFlinger and Input before proceeding with the test. + * + * @hide + */ + @FlaggedApi(Flags.FLAG_SURFACE_CONTROL_INPUT_RECEIVER) + @TestApi + @Nullable + default IBinder getSurfaceControlInputClientToken(@NonNull SurfaceControl surfaceControl) { + throw new UnsupportedOperationException( + "getSurfaceControlInputClientToken is not implemented"); + } } diff --git a/core/java/android/view/WindowManagerGlobal.java b/core/java/android/view/WindowManagerGlobal.java index 8d40f9a4f7b1..c49fce5b558f 100644 --- a/core/java/android/view/WindowManagerGlobal.java +++ b/core/java/android/view/WindowManagerGlobal.java @@ -38,10 +38,12 @@ import android.util.ArrayMap; import android.util.ArraySet; import android.util.Log; import android.util.Pair; +import android.util.SparseArray; import android.view.inputmethod.InputMethodManager; import android.window.ITrustedPresentationListener; import android.window.TrustedPresentationThresholds; +import com.android.internal.annotations.GuardedBy; import com.android.internal.util.FastPrintWriter; import java.io.FileDescriptor; @@ -50,7 +52,6 @@ import java.io.PrintWriter; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.WeakHashMap; -import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Executor; import java.util.function.Consumer; import java.util.function.IntConsumer; @@ -156,8 +157,9 @@ public final class WindowManagerGlobal { private final TrustedPresentationListener mTrustedPresentationListener = new TrustedPresentationListener(); - private final ConcurrentHashMap<IBinder, InputEventReceiver> mSurfaceControlInputReceivers = - new ConcurrentHashMap<>(); + @GuardedBy("mSurfaceControlInputReceivers") + private final SparseArray<SurfaceControlInputReceiverInfo> + mSurfaceControlInputReceivers = new SparseArray<>(); private WindowManagerGlobal() { } @@ -816,7 +818,7 @@ public final class WindowManagerGlobal { mTrustedPresentationListener.removeListener(listener); } - IBinder registerBatchedSurfaceControlInputReceiver(int displayId, + void registerBatchedSurfaceControlInputReceiver(int displayId, @NonNull IBinder hostToken, @NonNull SurfaceControl surfaceControl, @NonNull Choreographer choreographer, @NonNull SurfaceControlInputReceiver receiver) { IBinder clientToken = new Binder(); @@ -830,19 +832,21 @@ public final class WindowManagerGlobal { e.rethrowAsRuntimeException(); } - mSurfaceControlInputReceivers.put(clientToken, - new BatchedInputEventReceiver(inputChannel, choreographer.getLooper(), - choreographer) { - @Override - public void onInputEvent(InputEvent event) { - boolean handled = receiver.onInputEvent(event); - finishInputEvent(event, handled); - } - }); - return clientToken; + synchronized (mSurfaceControlInputReceivers) { + mSurfaceControlInputReceivers.put(surfaceControl.getLayerId(), + new SurfaceControlInputReceiverInfo(clientToken, + new BatchedInputEventReceiver(inputChannel, choreographer.getLooper(), + choreographer) { + @Override + public void onInputEvent(InputEvent event) { + boolean handled = receiver.onInputEvent(event); + finishInputEvent(event, handled); + } + })); + } } - IBinder registerUnbatchedSurfaceControlInputReceiver( + void registerUnbatchedSurfaceControlInputReceiver( int displayId, @NonNull IBinder hostToken, @NonNull SurfaceControl surfaceControl, @NonNull Looper looper, @NonNull SurfaceControlInputReceiver receiver) { IBinder clientToken = new Binder(); @@ -856,32 +860,53 @@ public final class WindowManagerGlobal { e.rethrowAsRuntimeException(); } - mSurfaceControlInputReceivers.put(clientToken, - new InputEventReceiver(inputChannel, looper) { - @Override - public void onInputEvent(InputEvent event) { - boolean handled = receiver.onInputEvent(event); - finishInputEvent(event, handled); - } - }); - - return clientToken; + synchronized (mSurfaceControlInputReceivers) { + mSurfaceControlInputReceivers.put(surfaceControl.getLayerId(), + new SurfaceControlInputReceiverInfo(clientToken, + new InputEventReceiver(inputChannel, looper) { + @Override + public void onInputEvent(InputEvent event) { + boolean handled = receiver.onInputEvent(event); + finishInputEvent(event, handled); + } + })); + } } - void unregisterSurfaceControlInputReceiver(IBinder token) { - InputEventReceiver inputEventReceiver = mSurfaceControlInputReceivers.get(token); - if (inputEventReceiver == null) { - Log.w(TAG, "No registered input event receiver with token: " + token); + void unregisterSurfaceControlInputReceiver(SurfaceControl surfaceControl) { + SurfaceControlInputReceiverInfo surfaceControlInputReceiverInfo; + synchronized (mSurfaceControlInputReceivers) { + surfaceControlInputReceiverInfo = mSurfaceControlInputReceivers.removeReturnOld( + surfaceControl.getLayerId()); + } + + if (surfaceControlInputReceiverInfo == null) { + Log.w(TAG, "No registered input event receiver with sc: " + surfaceControl); return; } try { - WindowManagerGlobal.getWindowSession().remove(token); + WindowManagerGlobal.getWindowSession().remove( + surfaceControlInputReceiverInfo.mClientToken); } catch (RemoteException e) { Log.e(TAG, "Failed to remove input channel", e); e.rethrowAsRuntimeException(); } - inputEventReceiver.dispose(); + surfaceControlInputReceiverInfo.mInputEventReceiver.dispose(); + } + + IBinder getSurfaceControlInputClientToken(SurfaceControl surfaceControl) { + SurfaceControlInputReceiverInfo surfaceControlInputReceiverInfo; + synchronized (mSurfaceControlInputReceivers) { + surfaceControlInputReceiverInfo = mSurfaceControlInputReceivers.get( + surfaceControl.getLayerId()); + } + + if (surfaceControlInputReceiverInfo == null) { + Log.w(TAG, "No registered input event receiver with sc: " + surfaceControl); + return null; + } + return surfaceControlInputReceiverInfo.mClientToken; } private final class TrustedPresentationListener extends @@ -976,6 +1001,17 @@ public final class WindowManagerGlobal { throw e.rethrowFromSystemServer(); } } + + private static class SurfaceControlInputReceiverInfo { + final IBinder mClientToken; + final InputEventReceiver mInputEventReceiver; + + private SurfaceControlInputReceiverInfo(IBinder clientToken, + InputEventReceiver inputEventReceiver) { + mClientToken = clientToken; + mInputEventReceiver = inputEventReceiver; + } + } } final class WindowLeaked extends AndroidRuntimeException { diff --git a/core/java/android/view/WindowManagerImpl.java b/core/java/android/view/WindowManagerImpl.java index aaf5fcc6f095..41d181c1b10c 100644 --- a/core/java/android/view/WindowManagerImpl.java +++ b/core/java/android/view/WindowManagerImpl.java @@ -523,26 +523,30 @@ public final class WindowManagerImpl implements WindowManager { mGlobal.unregisterTrustedPresentationListener(listener); } - @NonNull @Override - public IBinder registerBatchedSurfaceControlInputReceiver(int displayId, + public void registerBatchedSurfaceControlInputReceiver(int displayId, @NonNull IBinder hostToken, @NonNull SurfaceControl surfaceControl, @NonNull Choreographer choreographer, @NonNull SurfaceControlInputReceiver receiver) { - return mGlobal.registerBatchedSurfaceControlInputReceiver(displayId, hostToken, + mGlobal.registerBatchedSurfaceControlInputReceiver(displayId, hostToken, surfaceControl, choreographer, receiver); } - @NonNull @Override - public IBinder registerUnbatchedSurfaceControlInputReceiver( + public void registerUnbatchedSurfaceControlInputReceiver( int displayId, @NonNull IBinder hostToken, @NonNull SurfaceControl surfaceControl, @NonNull Looper looper, @NonNull SurfaceControlInputReceiver receiver) { - return mGlobal.registerUnbatchedSurfaceControlInputReceiver(displayId, hostToken, + mGlobal.registerUnbatchedSurfaceControlInputReceiver(displayId, hostToken, surfaceControl, looper, receiver); } @Override - public void unregisterSurfaceControlInputReceiver(@NonNull IBinder token) { - mGlobal.unregisterSurfaceControlInputReceiver(token); + public void unregisterSurfaceControlInputReceiver(@NonNull SurfaceControl surfaceControl) { + mGlobal.unregisterSurfaceControlInputReceiver(surfaceControl); + } + + @Override + @Nullable + public IBinder getSurfaceControlInputClientToken(@NonNull SurfaceControl surfaceControl) { + return mGlobal.getSurfaceControlInputClientToken(surfaceControl); } } diff --git a/core/java/android/view/WindowlessWindowManager.java b/core/java/android/view/WindowlessWindowManager.java index b95e4595d6b9..c4d18c6a8da5 100644 --- a/core/java/android/view/WindowlessWindowManager.java +++ b/core/java/android/view/WindowlessWindowManager.java @@ -534,9 +534,8 @@ public class WindowlessWindowManager implements IWindowSession { } @Override - public android.os.Bundle sendWallpaperCommand(android.os.IBinder window, + public void sendWallpaperCommand(android.os.IBinder window, java.lang.String action, int x, int y, int z, android.os.Bundle extras, boolean sync) { - return null; } @Override diff --git a/core/java/android/view/accessibility/AccessibilityManager.java b/core/java/android/view/accessibility/AccessibilityManager.java index 49d2ceb8fecf..7782fd7e6c2a 100644 --- a/core/java/android/view/accessibility/AccessibilityManager.java +++ b/core/java/android/view/accessibility/AccessibilityManager.java @@ -66,6 +66,7 @@ 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; @@ -155,22 +156,6 @@ 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; @@ -184,32 +169,6 @@ 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 */ @@ -1785,7 +1744,8 @@ public final class AccessibilityManager { @TestApi @RequiresPermission(Manifest.permission.MANAGE_ACCESSIBILITY) @NonNull - public List<String> getAccessibilityShortcutTargets(@ShortcutType int shortcutType) { + public List<String> getAccessibilityShortcutTargets( + @ShortcutConstants.UserShortcutType int shortcutType) { final IAccessibilityManager service; synchronized (mLock) { service = getServiceLocked(); diff --git a/core/java/android/view/autofill/AutofillManager.java b/core/java/android/view/autofill/AutofillManager.java index dbeffc89fa09..559ccfea72c2 100644 --- a/core/java/android/view/autofill/AutofillManager.java +++ b/core/java/android/view/autofill/AutofillManager.java @@ -298,6 +298,15 @@ public final class AutofillManager { "android.view.autofill.extra.AUGMENTED_AUTOFILL_CLIENT"; /** + * Internal extra used to pass the fill request id in client state of + * {@link ConvertCredentialResponse} + * + * @hide + */ + public static final String EXTRA_AUTOFILL_REQUEST_ID = + "android.view.autofill.extra.AUTOFILL_REQUEST_ID"; + + /** * Autofill Hint to indicate that it can match any field. * * @hide diff --git a/core/java/android/view/autofill/OWNERS b/core/java/android/view/autofill/OWNERS index 37c6f5bf3425..898947adcd1b 100644 --- a/core/java/android/view/autofill/OWNERS +++ b/core/java/android/view/autofill/OWNERS @@ -4,6 +4,7 @@ simranjit@google.com haoranzhang@google.com skxu@google.com yunicorn@google.com +reemabajwa@google.com # Bug component: 543785 = per-file *Augmented* per-file *Augmented* = wangqi@google.com diff --git a/core/java/android/window/ScreenCapture.java b/core/java/android/window/ScreenCapture.java index befb0023ebe6..544642811a39 100644 --- a/core/java/android/window/ScreenCapture.java +++ b/core/java/android/window/ScreenCapture.java @@ -30,6 +30,8 @@ import android.os.Parcelable; import android.util.Log; import android.view.SurfaceControl; +import com.android.window.flags.Flags; + import libcore.util.NativeAllocationRegistry; import java.util.concurrent.CountDownLatch; @@ -48,7 +50,7 @@ public class ScreenCapture { private static native int nativeCaptureDisplay(DisplayCaptureArgs captureArgs, long captureListener); private static native int nativeCaptureLayers(LayerCaptureArgs captureArgs, - long captureListener); + long captureListener, boolean sync); private static native long nativeCreateScreenCaptureListener( ObjIntConsumer<ScreenshotHardwareBuffer> consumer); private static native void nativeWriteListenerToParcel(long nativeObject, Parcel out); @@ -134,7 +136,8 @@ public class ScreenCapture { */ public static ScreenshotHardwareBuffer captureLayers(LayerCaptureArgs captureArgs) { SynchronousScreenCaptureListener syncScreenCapture = createSyncCaptureListener(); - int status = captureLayers(captureArgs, syncScreenCapture); + int status = nativeCaptureLayers(captureArgs, syncScreenCapture.mNativeObject, + Flags.syncScreenCapture()); if (status != 0) { return null; } @@ -171,7 +174,7 @@ public class ScreenCapture { */ public static int captureLayers(@NonNull LayerCaptureArgs captureArgs, @NonNull ScreenCaptureListener captureListener) { - return nativeCaptureLayers(captureArgs, captureListener.mNativeObject); + return nativeCaptureLayers(captureArgs, captureListener.mNativeObject, false /* sync */); } /** @@ -674,7 +677,7 @@ public class ScreenCapture { * This listener can only be used for a single call to capture content call. */ public static class ScreenCaptureListener implements Parcelable { - private final long mNativeObject; + final long mNativeObject; private static final NativeAllocationRegistry sRegistry = NativeAllocationRegistry.createMalloced( ScreenCaptureListener.class.getClassLoader(), getNativeListenerFinalizer()); diff --git a/core/java/android/window/WindowOnBackInvokedDispatcher.java b/core/java/android/window/WindowOnBackInvokedDispatcher.java index 86804c6117c7..65075aea7b27 100644 --- a/core/java/android/window/WindowOnBackInvokedDispatcher.java +++ b/core/java/android/window/WindowOnBackInvokedDispatcher.java @@ -226,9 +226,6 @@ 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(); } @@ -442,8 +439,7 @@ public class WindowOnBackInvokedDispatcher implements OnBackInvokedDispatcher { return WindowOnBackInvokedDispatcher .isOnBackInvokedCallbackEnabled(activityInfo, applicationInfo, - () -> originalContext.obtainStyledAttributes( - new int[] {android.R.attr.windowSwipeToDismiss}), true); + () -> originalContext); } @Override @@ -501,7 +497,7 @@ public class WindowOnBackInvokedDispatcher implements OnBackInvokedDispatcher { */ public static boolean isOnBackInvokedCallbackEnabled(@Nullable ActivityInfo activityInfo, @NonNull ApplicationInfo applicationInfo, - @NonNull Supplier<TypedArray> windowAttrSupplier, boolean recycleTypedArray) { + @NonNull Supplier<Context> contextSupplier) { // new back is enabled if the feature flag is enabled AND the app does not explicitly // request legacy back. if (!ENABLE_PREDICTIVE_BACK) { @@ -547,15 +543,15 @@ public class WindowOnBackInvokedDispatcher implements OnBackInvokedDispatcher { // setTrigger(true) // Use the original context to resolve the styled attribute so that they stay // true to the window. - TypedArray windowAttr = windowAttrSupplier.get(); + final Context context = contextSupplier.get(); boolean windowSwipeToDismiss = true; - if (windowAttr != null) { - if (windowAttr.getIndexCount() > 0) { - windowSwipeToDismiss = windowAttr.getBoolean(0, true); - } - if (recycleTypedArray) { - windowAttr.recycle(); + if (context != null) { + final TypedArray array = context.obtainStyledAttributes( + new int[]{android.R.attr.windowSwipeToDismiss}); + if (array.getIndexCount() > 0) { + windowSwipeToDismiss = array.getBoolean(0, true); } + array.recycle(); } if (DEBUG) { 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 edfbea4f51a4..1de77f6d29e7 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 @@ -58,3 +58,11 @@ flag { bug: "319808237" is_fixed_read_only: true } + +flag { + name: "camera_compat_for_freeform" + namespace: "large_screen_experiences_app_compat" + description: "Whether to apply Camera Compat treatment to fixed-orientation apps in freeform windowing mode" + bug: "314952133" + is_fixed_read_only: true +} diff --git a/core/java/android/window/flags/window_surfaces.aconfig b/core/java/android/window/flags/window_surfaces.aconfig index 751c1a8dd0ff..069affb4c06c 100644 --- a/core/java/android/window/flags/window_surfaces.aconfig +++ b/core/java/android/window/flags/window_surfaces.aconfig @@ -88,3 +88,11 @@ flag { is_fixed_read_only: true bug: "304574518" } + +flag { + namespace: "window_surfaces" + name: "sync_screen_capture" + description: "Create a screen capture API that blocks in SurfaceFlinger" + is_fixed_read_only: true + bug: "321263247" +} diff --git a/core/java/android/window/flags/windowing_frontend.aconfig b/core/java/android/window/flags/windowing_frontend.aconfig index 2c5fbd7f3725..f234637a7d82 100644 --- a/core/java/android/window/flags/windowing_frontend.aconfig +++ b/core/java/android/window/flags/windowing_frontend.aconfig @@ -38,14 +38,6 @@ flag { } flag { - name: "draw_magnifier_border_outside_wmlock" - namespace: "windowing_frontend" - description: "Avoid holding WM locks for a long time when executing lockCanvas" - bug: "316075123" - is_fixed_read_only: true -} - -flag { name: "introduce_smoother_dimmer" namespace: "windowing_frontend" description: "Refactor dim to fix flickers" diff --git a/core/java/com/android/internal/accessibility/AccessibilityShortcutController.java b/core/java/com/android/internal/accessibility/AccessibilityShortcutController.java index de0f070b01a3..b4395a77c24b 100644 --- a/core/java/com/android/internal/accessibility/AccessibilityShortcutController.java +++ b/core/java/com/android/internal/accessibility/AccessibilityShortcutController.java @@ -18,7 +18,6 @@ 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; @@ -329,7 +328,8 @@ public class AccessibilityShortcutController { } private AlertDialog createShortcutWarningDialog(int userId) { - List<AccessibilityTarget> targets = getTargets(mContext, ACCESSIBILITY_SHORTCUT_KEY); + List<AccessibilityTarget> targets = getTargets(mContext, + ShortcutConstants.UserShortcutType.HARDWARE); if (targets.size() == 0) { return null; } @@ -541,7 +541,7 @@ public class AccessibilityShortcutController { private ComponentName getShortcutTargetComponentName() { final List<String> shortcutTargets = mFrameworkObjectProvider .getAccessibilityManagerInstance(mContext) - .getAccessibilityShortcutTargets(ACCESSIBILITY_SHORTCUT_KEY); + .getAccessibilityShortcutTargets(ShortcutConstants.UserShortcutType.HARDWARE); 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 7ec8838699b3..353e1826a27a 100644 --- a/core/java/com/android/internal/accessibility/common/ShortcutConstants.java +++ b/core/java/com/android/internal/accessibility/common/ShortcutConstants.java @@ -44,19 +44,27 @@ 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({ - UserShortcutType.DEFAULT, - UserShortcutType.SOFTWARE, - UserShortcutType.HARDWARE, - UserShortcutType.TRIPLETAP, - }) + @IntDef( + flag = true, + value = { + UserShortcutType.DEFAULT, + UserShortcutType.SOFTWARE, + UserShortcutType.HARDWARE, + UserShortcutType.TRIPLETAP, + UserShortcutType.TWO_FINGERS_TRIPLE_TAP, + }) public @interface UserShortcutType { int DEFAULT = 0; - int SOFTWARE = 1; // 1 << 0 - int HARDWARE = 2; // 1 << 1 - int TRIPLETAP = 4; // 1 << 2 + // 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) } /** @@ -64,9 +72,12 @@ 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.TRIPLETAP, + UserShortcutType.TWO_FINGERS_TRIPLE_TAP, + // LINT.ThenChange(:shortcut_type_intdef) }; diff --git a/core/java/com/android/internal/accessibility/dialog/AccessibilityActivityTarget.java b/core/java/com/android/internal/accessibility/dialog/AccessibilityActivityTarget.java index 063154d9a6d6..33048dca12b0 100644 --- a/core/java/com/android/internal/accessibility/dialog/AccessibilityActivityTarget.java +++ b/core/java/com/android/internal/accessibility/dialog/AccessibilityActivityTarget.java @@ -17,14 +17,13 @@ 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; @@ -33,7 +32,8 @@ import com.android.internal.accessibility.common.ShortcutConstants.ShortcutMenuM */ class AccessibilityActivityTarget extends AccessibilityTarget { - AccessibilityActivityTarget(Context context, @ShortcutType int shortcutType, + AccessibilityActivityTarget(Context context, + @ShortcutConstants.UserShortcutType 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(convertToUserType(shortcutType))); + convertToKey(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 7eb09e59601b..e084ebdaf3a8 100644 --- a/core/java/com/android/internal/accessibility/dialog/AccessibilityButtonChooserActivity.java +++ b/core/java/com/android/internal/accessibility/dialog/AccessibilityButtonChooserActivity.java @@ -17,7 +17,6 @@ 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; @@ -36,6 +35,7 @@ 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, ACCESSIBILITY_BUTTON)); + mTargets.addAll(getTargets(this, ShortcutConstants.UserShortcutType.SOFTWARE)); 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 2b6913ca5e5a..7406da42507f 100644 --- a/core/java/com/android/internal/accessibility/dialog/AccessibilityServiceTarget.java +++ b/core/java/com/android/internal/accessibility/dialog/AccessibilityServiceTarget.java @@ -17,14 +17,13 @@ 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; @@ -36,7 +35,9 @@ class AccessibilityServiceTarget extends AccessibilityTarget { private final AccessibilityServiceInfo mAccessibilityServiceInfo; - AccessibilityServiceTarget(Context context, @ShortcutType int shortcutType, + AccessibilityServiceTarget( + Context context, + @ShortcutConstants.UserShortcutType int shortcutType, @AccessibilityFragmentType int fragmentType, @NonNull AccessibilityServiceInfo serviceInfo) { super(context, @@ -48,7 +49,7 @@ class AccessibilityServiceTarget extends AccessibilityTarget { serviceInfo.getResolveInfo().serviceInfo.applicationInfo.uid, serviceInfo.getResolveInfo().loadLabel(context.getPackageManager()), serviceInfo.getResolveInfo().loadIcon(context.getPackageManager()), - convertToKey(convertToUserType(shortcutType))); + convertToKey(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 2e80b7e19516..8e2ec1bfd53d 100644 --- a/core/java/com/android/internal/accessibility/dialog/AccessibilityShortcutChooserActivity.java +++ b/core/java/com/android/internal/accessibility/dialog/AccessibilityShortcutChooserActivity.java @@ -15,10 +15,6 @@ */ 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; @@ -42,6 +38,7 @@ 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; @@ -52,8 +49,8 @@ import java.util.List; * activity or allowlisting feature for volume key shortcut. */ public class AccessibilityShortcutChooserActivity extends Activity { - @ShortcutType - private final int mShortcutType = ACCESSIBILITY_SHORTCUT_KEY; + @ShortcutConstants.UserShortcutType + private final int mShortcutType = ShortcutConstants.UserShortcutType.HARDWARE; private static final String KEY_ACCESSIBILITY_SHORTCUT_MENU_MODE = "accessibility_shortcut_menu_mode"; private final List<AccessibilityTarget> mTargets = new ArrayList<>(); @@ -246,7 +243,7 @@ public class AccessibilityShortcutChooserActivity extends Activity { mTargetAdapter.getShortcutMenuMode() == ShortcutMenuMode.EDIT; final int selectDialogTitleId = R.string.accessibility_select_shortcut_menu_title; final int editDialogTitleId = - mShortcutType == ACCESSIBILITY_BUTTON + mShortcutType == ShortcutConstants.UserShortcutType.SOFTWARE ? 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 652cb5233461..4ab1ee9d6b46 100644 --- a/core/java/com/android/internal/accessibility/dialog/AccessibilityTarget.java +++ b/core/java/com/android/internal/accessibility/dialog/AccessibilityTarget.java @@ -16,10 +16,6 @@ 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; @@ -30,7 +26,6 @@ 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; @@ -47,7 +42,7 @@ import com.android.internal.annotations.VisibleForTesting; public abstract class AccessibilityTarget implements TargetOperations, OnTargetSelectedListener, OnTargetCheckedChangeListener { private Context mContext; - @ShortcutType + @ShortcutConstants.UserShortcutType private int mShortcutType; @AccessibilityFragmentType private int mFragmentType; @@ -61,7 +56,8 @@ public abstract class AccessibilityTarget implements TargetOperations, OnTargetS private CharSequence mStateDescription; @VisibleForTesting - public AccessibilityTarget(Context context, @ShortcutType int shortcutType, + public AccessibilityTarget( + Context context, @ShortcutConstants.UserShortcutType int shortcutType, @AccessibilityFragmentType int fragmentType, boolean isShortcutSwitched, String id, int uid, CharSequence label, Drawable icon, String key) { mContext = context; @@ -99,10 +95,10 @@ public abstract class AccessibilityTarget implements TargetOperations, OnTargetS final AccessibilityManager am = getContext().getSystemService(AccessibilityManager.class); switch (getShortcutType()) { - case ACCESSIBILITY_BUTTON: + case ShortcutConstants.UserShortcutType.SOFTWARE: am.notifyAccessibilityButtonClicked(getContext().getDisplayId(), getId()); return; - case ACCESSIBILITY_SHORTCUT_KEY: + case ShortcutConstants.UserShortcutType.HARDWARE: am.performAccessibilityShortcut(getId()); return; default: @@ -114,9 +110,9 @@ public abstract class AccessibilityTarget implements TargetOperations, OnTargetS public void onCheckedChanged(boolean isChecked) { setShortcutEnabled(isChecked); if (isChecked) { - optInValueToSettings(getContext(), convertToUserType(getShortcutType()), getId()); + optInValueToSettings(getContext(), getShortcutType(), getId()); } else { - optOutValueFromSettings(getContext(), convertToUserType(getShortcutType()), getId()); + optOutValueFromSettings(getContext(), getShortcutType(), getId()); } } @@ -142,7 +138,7 @@ public abstract class AccessibilityTarget implements TargetOperations, OnTargetS return mContext; } - public @ShortcutType int getShortcutType() { + public @ShortcutConstants.UserShortcutType 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 51a5ddfa8dd6..bd63e23a066f 100644 --- a/core/java/com/android/internal/accessibility/dialog/AccessibilityTargetHelper.java +++ b/core/java/com/android/internal/accessibility/dialog/AccessibilityTargetHelper.java @@ -16,8 +16,6 @@ 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; @@ -41,12 +39,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; @@ -70,8 +68,9 @@ public final class AccessibilityTargetHelper { * @return The list of {@link AccessibilityTarget}. * @hide */ - public static List<AccessibilityTarget> getTargets(Context context, - @ShortcutType int shortcutType) { + public static List<AccessibilityTarget> getTargets( + Context context, + @ShortcutConstants.UserShortcutType int shortcutType) { // List all accessibility target final List<AccessibilityTarget> installedTargets = getInstalledTargets(context, shortcutType); @@ -113,7 +112,7 @@ public final class AccessibilityTargetHelper { * @return The list of {@link AccessibilityTarget}. */ static List<AccessibilityTarget> getInstalledTargets(Context context, - @ShortcutType int shortcutType) { + @ShortcutConstants.UserShortcutType int shortcutType) { final List<AccessibilityTarget> targets = new ArrayList<>(); targets.addAll(getAccessibilityFilteredTargets(context, shortcutType)); targets.addAll(getAllowListingFeatureTargets(context, shortcutType)); @@ -122,7 +121,7 @@ public final class AccessibilityTargetHelper { } private static List<AccessibilityTarget> getAccessibilityFilteredTargets(Context context, - @ShortcutType int shortcutType) { + @ShortcutConstants.UserShortcutType int shortcutType) { final List<AccessibilityTarget> serviceTargets = getAccessibilityServiceTargets(context, shortcutType); final List<AccessibilityTarget> activityTargets = @@ -155,7 +154,7 @@ public final class AccessibilityTargetHelper { } private static List<AccessibilityTarget> getAccessibilityServiceTargets(Context context, - @ShortcutType int shortcutType) { + @ShortcutConstants.UserShortcutType int shortcutType) { final AccessibilityManager am = (AccessibilityManager) context.getSystemService( Context.ACCESSIBILITY_SERVICE); final List<AccessibilityServiceInfo> installedServices = @@ -171,7 +170,7 @@ public final class AccessibilityTargetHelper { final boolean hasRequestAccessibilityButtonFlag = (info.flags & AccessibilityServiceInfo.FLAG_REQUEST_ACCESSIBILITY_BUTTON) != 0; if ((targetSdk <= Build.VERSION_CODES.Q) && !hasRequestAccessibilityButtonFlag - && (shortcutType == ACCESSIBILITY_BUTTON)) { + && (shortcutType == ShortcutConstants.UserShortcutType.SOFTWARE)) { continue; } @@ -182,7 +181,7 @@ public final class AccessibilityTargetHelper { } private static List<AccessibilityTarget> getAccessibilityActivityTargets(Context context, - @ShortcutType int shortcutType) { + @ShortcutConstants.UserShortcutType int shortcutType) { final AccessibilityManager am = (AccessibilityManager) context.getSystemService( Context.ACCESSIBILITY_SERVICE); final List<AccessibilityShortcutInfo> installedServices = @@ -201,7 +200,7 @@ public final class AccessibilityTargetHelper { } private static List<AccessibilityTarget> getAllowListingFeatureTargets(Context context, - @ShortcutType int shortcutType) { + @ShortcutConstants.UserShortcutType int shortcutType) { final List<AccessibilityTarget> targets = new ArrayList<>(); final int uid = context.getApplicationInfo().uid; @@ -281,8 +280,10 @@ public final class AccessibilityTargetHelper { return targets; } - private static AccessibilityTarget createAccessibilityServiceTarget(Context context, - @ShortcutType int shortcutType, @NonNull AccessibilityServiceInfo info) { + private static AccessibilityTarget createAccessibilityServiceTarget( + Context context, + @ShortcutConstants.UserShortcutType 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 1bc8b84e6869..641a9f18e3d6 100644 --- a/core/java/com/android/internal/accessibility/dialog/InvisibleToggleAccessibilityServiceTarget.java +++ b/core/java/com/android/internal/accessibility/dialog/InvisibleToggleAccessibilityServiceTarget.java @@ -16,9 +16,6 @@ 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; @@ -28,7 +25,6 @@ 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; @@ -45,7 +41,7 @@ import java.util.Set; public class InvisibleToggleAccessibilityServiceTarget extends AccessibilityServiceTarget { public InvisibleToggleAccessibilityServiceTarget( - Context context, @ShortcutType int shortcutType, + Context context, @UserShortcutType int shortcutType, @NonNull AccessibilityServiceInfo serviceInfo) { super(context, shortcutType, @@ -72,10 +68,10 @@ public class InvisibleToggleAccessibilityServiceTarget extends AccessibilityServ private boolean isComponentIdExistingInOtherShortcut() { switch (getShortcutType()) { - case ACCESSIBILITY_BUTTON: + case UserShortcutType.SOFTWARE: return isComponentIdExistingInSettings(getContext(), UserShortcutType.HARDWARE, getId()); - case ACCESSIBILITY_SHORTCUT_KEY: + case UserShortcutType.HARDWARE: 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 c22f17dfa967..2204c0beb4fd 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,7 +28,8 @@ import com.android.internal.accessibility.common.ShortcutConstants.Accessibility */ class InvisibleToggleAllowListingFeatureTarget extends AccessibilityTarget { - InvisibleToggleAllowListingFeatureTarget(Context context, @ShortcutType int shortcutType, + InvisibleToggleAllowListingFeatureTarget(Context context, + @ShortcutConstants.UserShortcutType 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 a4ffef6bfbc2..a6ef73e90772 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,7 +45,8 @@ class ToggleAccessibilityServiceTarget extends AccessibilityServiceTarget { float DISABLED = 0.5f; } - ToggleAccessibilityServiceTarget(Context context, @ShortcutType int shortcutType, + ToggleAccessibilityServiceTarget(Context context, + @ShortcutConstants.UserShortcutType 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 11e668f51774..2a9c555efce8 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,7 +34,8 @@ import com.android.internal.accessibility.dialog.TargetAdapter.ViewHolder; */ class ToggleAllowListingFeatureTarget extends AccessibilityTarget { - ToggleAllowListingFeatureTarget(Context context, @ShortcutType int shortcutType, + ToggleAllowListingFeatureTarget(Context context, + @ShortcutConstants.UserShortcutType 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 04f5061fbd8e..4926e7218530 100644 --- a/core/java/com/android/internal/accessibility/dialog/VolumeShortcutToggleAccessibilityServiceTarget.java +++ b/core/java/com/android/internal/accessibility/dialog/VolumeShortcutToggleAccessibilityServiceTarget.java @@ -16,9 +16,6 @@ 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; @@ -27,7 +24,6 @@ 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; @@ -39,7 +35,8 @@ import com.android.internal.accessibility.common.ShortcutConstants.Accessibility */ class VolumeShortcutToggleAccessibilityServiceTarget extends AccessibilityServiceTarget { - VolumeShortcutToggleAccessibilityServiceTarget(Context context, @ShortcutType int shortcutType, + VolumeShortcutToggleAccessibilityServiceTarget(Context context, + @UserShortcutType int shortcutType, @NonNull AccessibilityServiceInfo serviceInfo) { super(context, shortcutType, @@ -50,10 +47,10 @@ class VolumeShortcutToggleAccessibilityServiceTarget extends AccessibilityServic @Override public void onCheckedChanged(boolean isChecked) { switch (getShortcutType()) { - case ACCESSIBILITY_BUTTON: + case UserShortcutType.SOFTWARE: onCheckedFromAccessibilityButton(isChecked); return; - case ACCESSIBILITY_SHORTCUT_KEY: + case UserShortcutType.HARDWARE: 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 6b074a610818..1e4bcf21c635 100644 --- a/core/java/com/android/internal/accessibility/util/AccessibilityStatsLogUtils.java +++ b/core/java/com/android/internal/accessibility/util/AccessibilityStatsLogUtils.java @@ -21,8 +21,6 @@ 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; @@ -47,9 +45,8 @@ 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. */ @@ -71,15 +68,15 @@ public final class AccessibilityStatsLogUtils { /** * Logs accessibility feature name that is assigned to the given {@code shortcutType}. - * Calls this when clicking the shortcut {@link AccessibilityManager#ACCESSIBILITY_BUTTON} or - * {@link AccessibilityManager#ACCESSIBILITY_SHORTCUT_KEY}. + * Calls this when clicking the shortcut {@link ShortcutConstants.UserShortcutType.SOFTWARE} or + * {@link ShortcutConstants.UserShortcutType.HARDWARE}. * * @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, @ShortcutType int shortcutType) { + ComponentName componentName, @ShortcutConstants.UserShortcutType int shortcutType) { logAccessibilityShortcutActivatedInternal(componentName, convertToLoggingShortcutType(context, shortcutType), UNKNOWN_STATUS); } @@ -87,8 +84,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 AccessibilityManager#ACCESSIBILITY_BUTTON} - * or {@link AccessibilityManager#ACCESSIBILITY_SHORTCUT_KEY}. + * Calls this when clicking the shortcut {@link ShortcutConstants.UserShortcutType.SOFTWARE} + * or {@link ShortcutConstants.UserShortcutType.HARDWARE}. * * @param context context used to retrieve the {@link Settings} provider * @param componentName component name of the accessibility feature @@ -96,7 +93,8 @@ public final class AccessibilityStatsLogUtils { * @param serviceEnabled {@code true} if the service is enabled */ public static void logAccessibilityShortcutActivated(Context context, - ComponentName componentName, @ShortcutType int shortcutType, boolean serviceEnabled) { + ComponentName componentName, @ShortcutConstants.UserShortcutType int shortcutType, + boolean serviceEnabled) { logAccessibilityShortcutActivatedInternal(componentName, convertToLoggingShortcutType(context, shortcutType), convertToLoggingServiceStatus(serviceEnabled)); @@ -235,9 +233,9 @@ public final class AccessibilityStatsLogUtils { } private static int convertToLoggingShortcutType(Context context, - @ShortcutType int shortcutType) { + @ShortcutConstants.UserShortcutType int shortcutType) { switch (shortcutType) { - case ACCESSIBILITY_BUTTON: + case ShortcutConstants.UserShortcutType.SOFTWARE: if (isAccessibilityFloatingMenuEnabled(context)) { return ACCESSIBILITY_SHORTCUT_REPORTED__SHORTCUT_TYPE__A11Y_FLOATING_MENU; } else if (isAccessibilityGestureEnabled(context)) { @@ -245,7 +243,7 @@ public final class AccessibilityStatsLogUtils { } else { return ACCESSIBILITY_SHORTCUT_REPORTED__SHORTCUT_TYPE__A11Y_BUTTON; } - case ACCESSIBILITY_SHORTCUT_KEY: + case ShortcutConstants.UserShortcutType.HARDWARE: 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 3fd303038c57..276c5c4ffdea 100644 --- a/core/java/com/android/internal/accessibility/util/ShortcutUtils.java +++ b/core/java/com/android/internal/accessibility/util/ShortcutUtils.java @@ -16,9 +16,6 @@ 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; @@ -33,7 +30,6 @@ 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; @@ -144,7 +140,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, @ShortcutType int shortcutType, + public static boolean isShortcutContained(Context context, @UserShortcutType int shortcutType, @NonNull String componentId) { final AccessibilityManager am = (AccessibilityManager) context.getSystemService( Context.ACCESSIBILITY_SERVICE); @@ -166,6 +162,8 @@ 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); @@ -173,24 +171,6 @@ 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> @@ -255,12 +235,13 @@ 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)) { + if (Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_ENABLED.equals(targetKey) + || Settings.Secure.ACCESSIBILITY_MAGNIFICATION_TWO_FINGER_TRIPLE_TAP_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/app/ConfirmUserCreationActivity.java b/core/java/com/android/internal/app/ConfirmUserCreationActivity.java index 0a28997885ff..b4e87498f09b 100644 --- a/core/java/com/android/internal/app/ConfirmUserCreationActivity.java +++ b/core/java/com/android/internal/app/ConfirmUserCreationActivity.java @@ -116,6 +116,14 @@ public class ConfirmUserCreationActivity extends AlertActivity if (cantCreateUser) { setResult(UserManager.USER_CREATION_FAILED_NOT_PERMITTED); return null; + } else if (!(isUserPropertyWithinLimit(mUserName, UserManager.MAX_USER_NAME_LENGTH) + && isUserPropertyWithinLimit(mAccountName, UserManager.MAX_ACCOUNT_STRING_LENGTH) + && isUserPropertyWithinLimit(mAccountType, UserManager.MAX_ACCOUNT_STRING_LENGTH)) + || (mAccountOptions != null && !mAccountOptions.isBundleContentsWithinLengthLimit( + UserManager.MAX_ACCOUNT_OPTIONS_LENGTH))) { + setResult(UserManager.USER_CREATION_FAILED_NOT_PERMITTED); + Log.i(TAG, "User properties must not exceed their character limits"); + return null; } else if (cantCreateAnyMoreUsers) { setResult(UserManager.USER_CREATION_FAILED_NO_MORE_USERS); return null; @@ -144,4 +152,8 @@ public class ConfirmUserCreationActivity extends AlertActivity } finish(); } + + private boolean isUserPropertyWithinLimit(String property, int limit) { + return property == null || property.length() <= limit; + } } diff --git a/core/java/com/android/internal/display/BrightnessSynchronizer.java b/core/java/com/android/internal/display/BrightnessSynchronizer.java index 37aaa72cb7a0..006849034fbd 100644 --- a/core/java/com/android/internal/display/BrightnessSynchronizer.java +++ b/core/java/com/android/internal/display/BrightnessSynchronizer.java @@ -47,6 +47,7 @@ import java.io.PrintWriter; * (new) system for storing the brightness. It has methods to convert between the two and also * observes for when one of the settings is changed and syncs this with the other. */ +@android.ravenwood.annotation.RavenwoodKeepPartialClass public class BrightnessSynchronizer { private static final String TAG = "BrightnessSynchronizer"; @@ -282,6 +283,7 @@ public class BrightnessSynchronizer { * @param b second float to compare * @return whether the two values are within a small enough tolerance value */ + @android.ravenwood.annotation.RavenwoodKeep public static boolean floatEquals(float a, float b) { if (a == b) { return true; diff --git a/core/java/com/android/internal/jank/Cuj.java b/core/java/com/android/internal/jank/Cuj.java index 96740c59ec06..7b3565bff533 100644 --- a/core/java/com/android/internal/jank/Cuj.java +++ b/core/java/com/android/internal/jank/Cuj.java @@ -121,10 +121,11 @@ public class Cuj { public static final int CUJ_PREDICTIVE_BACK_CROSS_TASK = 85; public static final int CUJ_PREDICTIVE_BACK_HOME = 86; public static final int CUJ_LAUNCHER_SEARCH_QSB_OPEN = 87; + public static final int CUJ_BACK_PANEL_ARROW = 88; // When adding a CUJ, update this and make sure to also update CUJ_TO_STATSD_INTERACTION_TYPE. @VisibleForTesting - static final int LAST_CUJ = CUJ_LAUNCHER_SEARCH_QSB_OPEN; + static final int LAST_CUJ = CUJ_BACK_PANEL_ARROW; /** @hide */ @IntDef({ @@ -207,6 +208,7 @@ public class Cuj { CUJ_PREDICTIVE_BACK_CROSS_TASK, CUJ_PREDICTIVE_BACK_HOME, CUJ_LAUNCHER_SEARCH_QSB_OPEN, + CUJ_BACK_PANEL_ARROW, }) @Retention(RetentionPolicy.SOURCE) public @interface CujType { @@ -298,8 +300,8 @@ public class Cuj { CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_PREDICTIVE_BACK_CROSS_ACTIVITY] = FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__PREDICTIVE_BACK_CROSS_ACTIVITY; CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_PREDICTIVE_BACK_CROSS_TASK] = FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__PREDICTIVE_BACK_CROSS_TASK; CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_PREDICTIVE_BACK_HOME] = FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__PREDICTIVE_BACK_HOME; - CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_LAUNCHER_SEARCH_QSB_OPEN] = - FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LAUNCHER_SEARCH_QSB_OPEN; + CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_LAUNCHER_SEARCH_QSB_OPEN] = FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LAUNCHER_SEARCH_QSB_OPEN; + CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_BACK_PANEL_ARROW] = FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__BACK_PANEL_ARROW; } private Cuj() { @@ -474,6 +476,8 @@ public class Cuj { return "PREDICTIVE_BACK_HOME"; case CUJ_LAUNCHER_SEARCH_QSB_OPEN: return "LAUNCHER_SEARCH_QSB_OPEN"; + case CUJ_BACK_PANEL_ARROW: + return "BACK_PANEL_ARROW"; } return "UNKNOWN"; } diff --git a/core/java/com/android/internal/jank/InteractionMonitorDebugOverlay.java b/core/java/com/android/internal/jank/InteractionMonitorDebugOverlay.java index f3f16a0c662d..d9cac12c3372 100644 --- a/core/java/com/android/internal/jank/InteractionMonitorDebugOverlay.java +++ b/core/java/com/android/internal/jank/InteractionMonitorDebugOverlay.java @@ -28,6 +28,7 @@ import android.graphics.RecordingCanvas; import android.graphics.Rect; import android.os.Handler; import android.os.Trace; +import android.util.Log; import android.util.SparseArray; import android.util.SparseIntArray; import android.view.WindowCallbacks; @@ -52,6 +53,7 @@ import com.android.internal.jank.FrameTracker.Reasons; * @hide */ class InteractionMonitorDebugOverlay implements WindowCallbacks { + private static final String TAG = "InteractionMonitorDebug"; private static final int REASON_STILL_RUNNING = -1000; private final Object mLock; // Sparse array where the key in the CUJ and the value is the session status, or null if @@ -77,7 +79,7 @@ class InteractionMonitorDebugOverlay implements WindowCallbacks { mDebugPaint.setAntiAlias(false); mDebugFontMetrics = new Paint.FontMetrics(); final Context context = ActivityThread.currentApplication(); - mPackageName = context.getPackageName(); + mPackageName = context == null ? "null" : context.getPackageName(); } @UiThread @@ -153,8 +155,14 @@ class InteractionMonitorDebugOverlay implements WindowCallbacks { SparseArray<InteractionJankMonitor.RunningTracker> runningTrackers) { synchronized (mLock) { mRunningCujs.put(removedCuj, reason); + boolean isLoggable = Log.isLoggable(TAG, Log.DEBUG); + if (isLoggable) { + String cujName = Cuj.getNameOfCuj(removedCuj); + Log.d(TAG, cujName + (reason == REASON_END_NORMAL ? " ended" : " cancelled")); + } // If REASON_STILL_RUNNING is not in mRunningCujs, then all CUJs have ended if (mRunningCujs.indexOfValue(REASON_STILL_RUNNING) < 0) { + if (isLoggable) Log.d(TAG, "All CUJs ended"); mRunningCujs.clear(); dispose(); } else { @@ -186,6 +194,10 @@ class InteractionMonitorDebugOverlay implements WindowCallbacks { @UiThread void onTrackerAdded(@Cuj.CujType int addedCuj, InteractionJankMonitor.RunningTracker tracker) { + if (Log.isLoggable(TAG, Log.DEBUG)) { + String cujName = Cuj.getNameOfCuj(addedCuj); + Log.d(TAG, cujName + " started"); + } synchronized (mLock) { // Use REASON_STILL_RUNNING (not technically one of the '@Reasons') to indicate the CUJ // is still running diff --git a/core/java/com/android/internal/pm/pkg/component/ParsedIntentInfoUtils.java b/core/java/com/android/internal/pm/pkg/component/ParsedIntentInfoUtils.java index c6683cfc8331..05728eee174f 100644 --- a/core/java/com/android/internal/pm/pkg/component/ParsedIntentInfoUtils.java +++ b/core/java/com/android/internal/pm/pkg/component/ParsedIntentInfoUtils.java @@ -18,9 +18,13 @@ package com.android.internal.pm.pkg.component; import static com.android.internal.pm.pkg.parsing.ParsingUtils.ANDROID_RES_NAMESPACE; +import android.annotation.FlaggedApi; import android.annotation.NonNull; import android.content.Intent; import android.content.IntentFilter; +import android.content.UriRelativeFilter; +import android.content.UriRelativeFilterGroup; +import android.content.pm.Flags; import android.content.pm.parsing.result.ParseInput; import android.content.pm.parsing.result.ParseResult; import android.content.res.Resources; @@ -132,6 +136,11 @@ public class ParsedIntentInfoUtils { case "data": result = parseData(intentInfo, res, parser, allowGlobs, input); break; + case "uri-relative-filter-group": + if (Flags.relativeReferenceIntentFilters()) { + result = parseRelRefGroup(intentInfo, pkg, res, parser, allowGlobs, input); + break; + } default: result = ParsingUtils.unknownTag("<intent-filter>", pkg, parser, input); break; @@ -163,6 +172,197 @@ public class ParsedIntentInfoUtils { } @NonNull + @FlaggedApi(Flags.FLAG_RELATIVE_REFERENCE_INTENT_FILTERS) + private static ParseResult<ParsedIntentInfo> parseRelRefGroup(ParsedIntentInfo intentInfo, + ParsingPackage pkg, Resources res, XmlResourceParser parser, boolean allowGlobs, + ParseInput input) throws XmlPullParserException, IOException { + IntentFilter intentFilter = intentInfo.getIntentFilter(); + TypedArray sa = res.obtainAttributes(parser, + R.styleable.AndroidManifestUriRelativeFilterGroup); + UriRelativeFilterGroup group; + try { + int action = UriRelativeFilterGroup.ACTION_ALLOW; + if (!sa.getBoolean(R.styleable.AndroidManifestUriRelativeFilterGroup_allow, true)) { + action = UriRelativeFilterGroup.ACTION_BLOCK; + } + group = new UriRelativeFilterGroup(action); + } finally { + sa.recycle(); + } + final int depth = parser.getDepth(); + int type; + while ((type = parser.next()) != XmlPullParser.END_DOCUMENT + && (type != XmlPullParser.END_TAG + || parser.getDepth() > depth)) { + if (type != XmlPullParser.START_TAG) { + continue; + } + + final ParseResult result; + String nodeName = parser.getName(); + switch (nodeName) { + case "data": + result = parseRelRefGroupData(group, res, parser, allowGlobs, input); + break; + default: + result = ParsingUtils.unknownTag("<uri-relative-filter-group>", + pkg, parser, input); + break; + } + + if (result.isError()) { + return input.error(result); + } + } + + if (group.getUriRelativeFilters().size() > 0) { + intentFilter.addUriRelativeFilterGroup(group); + } + return input.success(null); + } + + @NonNull + @FlaggedApi(Flags.FLAG_RELATIVE_REFERENCE_INTENT_FILTERS) + private static ParseResult<ParsedIntentInfo> parseRelRefGroupData(UriRelativeFilterGroup group, + Resources res, XmlResourceParser parser, boolean allowGlobs, ParseInput input) { + TypedArray sa = res.obtainAttributes(parser, R.styleable.AndroidManifestData); + try { + String str = sa.getNonConfigurationString( + R.styleable.AndroidManifestData_path, 0); + if (str != null) { + group.addUriRelativeFilter(new UriRelativeFilter(UriRelativeFilter.PATH, + PatternMatcher.PATTERN_LITERAL, str)); + } + + str = sa.getNonConfigurationString( + R.styleable.AndroidManifestData_pathPrefix, 0); + if (str != null) { + group.addUriRelativeFilter(new UriRelativeFilter(UriRelativeFilter.PATH, + PatternMatcher.PATTERN_PREFIX, str)); + } + + str = sa.getNonConfigurationString( + R.styleable.AndroidManifestData_pathPattern, 0); + if (str != null) { + if (!allowGlobs) { + return input.error( + "pathPattern not allowed here; path must be literal"); + } + group.addUriRelativeFilter(new UriRelativeFilter(UriRelativeFilter.PATH, + PatternMatcher.PATTERN_SIMPLE_GLOB, str)); + } + + str = sa.getNonConfigurationString( + R.styleable.AndroidManifestData_pathAdvancedPattern, 0); + if (str != null) { + if (!allowGlobs) { + return input.error( + "pathAdvancedPattern not allowed here; path must be literal"); + } + group.addUriRelativeFilter(new UriRelativeFilter(UriRelativeFilter.PATH, + PatternMatcher.PATTERN_ADVANCED_GLOB, str)); + } + + str = sa.getNonConfigurationString( + R.styleable.AndroidManifestData_pathSuffix, 0); + if (str != null) { + group.addUriRelativeFilter(new UriRelativeFilter(UriRelativeFilter.PATH, + PatternMatcher.PATTERN_SUFFIX, str)); + } + + str = sa.getNonConfigurationString( + R.styleable.AndroidManifestData_fragment, 0); + if (str != null) { + group.addUriRelativeFilter(new UriRelativeFilter(UriRelativeFilter.FRAGMENT, + PatternMatcher.PATTERN_LITERAL, str)); + } + + str = sa.getNonConfigurationString( + R.styleable.AndroidManifestData_fragmentPrefix, 0); + if (str != null) { + group.addUriRelativeFilter(new UriRelativeFilter(UriRelativeFilter.FRAGMENT, + PatternMatcher.PATTERN_PREFIX, str)); + } + + str = sa.getNonConfigurationString( + R.styleable.AndroidManifestData_fragmentPattern, 0); + if (str != null) { + if (!allowGlobs) { + return input.error( + "fragmentPattern not allowed here; fragment must be literal"); + } + group.addUriRelativeFilter(new UriRelativeFilter(UriRelativeFilter.FRAGMENT, + PatternMatcher.PATTERN_SIMPLE_GLOB, str)); + } + + str = sa.getNonConfigurationString( + R.styleable.AndroidManifestData_fragmentAdvancedPattern, 0); + if (str != null) { + if (!allowGlobs) { + return input.error( + "fragmentAdvancedPattern not allowed here; fragment must be literal"); + } + group.addUriRelativeFilter(new UriRelativeFilter(UriRelativeFilter.FRAGMENT, + PatternMatcher.PATTERN_ADVANCED_GLOB, str)); + } + + str = sa.getNonConfigurationString( + R.styleable.AndroidManifestData_fragmentSuffix, 0); + if (str != null) { + group.addUriRelativeFilter(new UriRelativeFilter(UriRelativeFilter.FRAGMENT, + PatternMatcher.PATTERN_SUFFIX, str)); + } + + str = sa.getNonConfigurationString( + R.styleable.AndroidManifestData_query, 0); + if (str != null) { + group.addUriRelativeFilter(new UriRelativeFilter(UriRelativeFilter.QUERY, + PatternMatcher.PATTERN_LITERAL, str)); + } + + str = sa.getNonConfigurationString( + R.styleable.AndroidManifestData_queryPrefix, 0); + if (str != null) { + group.addUriRelativeFilter(new UriRelativeFilter(UriRelativeFilter.QUERY, + PatternMatcher.PATTERN_PREFIX, str)); + } + + str = sa.getNonConfigurationString( + R.styleable.AndroidManifestData_queryPattern, 0); + if (str != null) { + if (!allowGlobs) { + return input.error( + "queryPattern not allowed here; query must be literal"); + } + group.addUriRelativeFilter(new UriRelativeFilter(UriRelativeFilter.QUERY, + PatternMatcher.PATTERN_SIMPLE_GLOB, str)); + } + + str = sa.getNonConfigurationString( + R.styleable.AndroidManifestData_queryAdvancedPattern, 0); + if (str != null) { + if (!allowGlobs) { + return input.error( + "queryAdvancedPattern not allowed here; query must be literal"); + } + group.addUriRelativeFilter(new UriRelativeFilter(UriRelativeFilter.QUERY, + PatternMatcher.PATTERN_ADVANCED_GLOB, str)); + } + + str = sa.getNonConfigurationString( + R.styleable.AndroidManifestData_querySuffix, 0); + if (str != null) { + group.addUriRelativeFilter(new UriRelativeFilter(UriRelativeFilter.QUERY, + PatternMatcher.PATTERN_SUFFIX, str)); + } + + return input.success(null); + } finally { + sa.recycle(); + } + } + + @NonNull private static ParseResult<ParsedIntentInfo> parseData(ParsedIntentInfo intentInfo, Resources resources, XmlResourceParser parser, boolean allowGlobs, ParseInput input) { IntentFilter intentFilter = intentInfo.getIntentFilter(); diff --git a/core/java/com/android/internal/policy/SystemBarUtils.java b/core/java/com/android/internal/policy/SystemBarUtils.java index 7a1ac071a625..efa369715373 100644 --- a/core/java/com/android/internal/policy/SystemBarUtils.java +++ b/core/java/com/android/internal/policy/SystemBarUtils.java @@ -19,8 +19,9 @@ package com.android.internal.policy; import android.content.Context; import android.content.res.Resources; import android.graphics.Insets; -import android.util.RotationUtils; +import android.view.Display; import android.view.DisplayCutout; +import android.view.DisplayInfo; import android.view.Surface; import com.android.internal.R; @@ -56,21 +57,21 @@ public final class SystemBarUtils { */ public static int getStatusBarHeightForRotation( Context context, @Surface.Rotation int targetRot) { - final int rotation = context.getDisplay().getRotation(); - final DisplayCutout cutout = context.getDisplay().getCutout(); - - Insets insets = cutout == null ? Insets.NONE : Insets.of(cutout.getSafeInsets()); - Insets waterfallInsets = cutout == null ? Insets.NONE : cutout.getWaterfallInsets(); - // rotate insets to target rotation if needed. - if (rotation != targetRot) { - if (!insets.equals(Insets.NONE)) { - insets = RotationUtils.rotateInsets( - insets, RotationUtils.deltaRotation(rotation, targetRot)); - } - if (!waterfallInsets.equals(Insets.NONE)) { - waterfallInsets = RotationUtils.rotateInsets( - waterfallInsets, RotationUtils.deltaRotation(rotation, targetRot)); - } + final Display display = context.getDisplay(); + final int rotation = display.getRotation(); + final DisplayCutout cutout = display.getCutout(); + DisplayInfo info = new DisplayInfo(); + display.getDisplayInfo(info); + Insets insets; + Insets waterfallInsets; + if (cutout == null) { + insets = Insets.NONE; + waterfallInsets = Insets.NONE; + } else { + DisplayCutout rotated = + cutout.getRotated(info.logicalWidth, info.logicalHeight, rotation, targetRot); + insets = Insets.of(rotated.getSafeInsets()); + waterfallInsets = rotated.getWaterfallInsets(); } final int defaultSize = context.getResources().getDimensionPixelSize(R.dimen.status_bar_height_default); diff --git a/core/java/com/android/internal/widget/ConversationLayout.java b/core/java/com/android/internal/widget/ConversationLayout.java index 42be784d8baa..a8d0d37f78bd 100644 --- a/core/java/com/android/internal/widget/ConversationLayout.java +++ b/core/java/com/android/internal/widget/ConversationLayout.java @@ -105,6 +105,9 @@ public class ConversationLayout extends FrameLayout private int mConversationIconTopPaddingExpandedGroup; private int mConversationIconTopPadding; private int mExpandedGroupMessagePadding; + // TODO (b/217799515) Currently, mConversationText shows the conversation title, the actual + // conversation text is inside of mMessagingLinearLayout, which is misleading, we should rename + // this to mConversationTitleView private TextView mConversationText; private View mConversationIconBadge; private CachingIconView mConversationIconBadgeBg; @@ -125,6 +128,11 @@ public class ConversationLayout extends FrameLayout private int mNotificationBackgroundColor; private CharSequence mFallbackChatName; private CharSequence mFallbackGroupChatName; + //TODO (b/217799515) Currently, Notification.MessagingStyle, ConversationLayout, and + // HybridConversationNotificationView, each has their own definition of "ConversationTitle". + // What make things worse is that the term of "ConversationTitle" often confuses with + // "ConversationText". + // We need to unify them or differentiate the namings. private CharSequence mConversationTitle; private int mMessageSpacingStandard; private int mMessageSpacingGroup; @@ -160,12 +168,12 @@ public class ConversationLayout extends FrameLayout } public ConversationLayout(@NonNull Context context, @Nullable AttributeSet attrs, - @AttrRes int defStyleAttr) { + @AttrRes int defStyleAttr) { super(context, attrs, defStyleAttr); } public ConversationLayout(@NonNull Context context, @Nullable AttributeSet attrs, - @AttrRes int defStyleAttr, @StyleRes int defStyleRes) { + @AttrRes int defStyleAttr, @StyleRes int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); } @@ -297,13 +305,17 @@ public class ConversationLayout extends FrameLayout mNameReplacement = nameReplacement; } - /** Sets this conversation as "important", adding some additional UI treatment. */ + /** + * Sets this conversation as "important", adding some additional UI treatment. + */ @RemotableViewMethod public void setIsImportantConversation(boolean isImportantConversation) { setIsImportantConversation(isImportantConversation, false); } - /** @hide **/ + /** + * @hide + **/ public void setIsImportantConversation(boolean isImportantConversation, boolean animate) { mImportantConversation = isImportantConversation; mImportanceRingView.setVisibility(isImportantConversation && mIcon.getVisibility() != GONE @@ -386,6 +398,7 @@ public class ConversationLayout extends FrameLayout /** * Set conversation data + * * @param extras Bundle contains conversation data */ @RemotableViewMethod(asyncImpl = "setDataAsync") @@ -427,6 +440,7 @@ public class ConversationLayout extends FrameLayout * RemotableViewMethod's asyncImpl of {@link #setData(Bundle)}. * This should be called on a background thread, and returns a Runnable which is then must be * called on the main thread to complete the operation and set text. + * * @param extras Bundle contains conversation data * @hide */ @@ -449,6 +463,7 @@ public class ConversationLayout extends FrameLayout /** * enable/disable precomputed text usage + * * @hide */ public void setPrecomputedTextEnabled(boolean precomputedTextEnabled) { @@ -466,7 +481,9 @@ public class ConversationLayout extends FrameLayout mImageResolver = resolver; } - /** @hide */ + /** + * @hide + */ public void setUnreadCount(int unreadCount) { mExpandButton.setNumber(unreadCount); } @@ -795,6 +812,10 @@ public class ConversationLayout extends FrameLayout mConversationTitle = conversationTitle != null ? conversationTitle.toString() : null; } + // TODO (b/217799515) getConversationTitle is not consistent with setConversationTitle + // if you call getConversationTitle() immediately after setConversationTitle(), the result + // will not correctly reflect the new change without calling updateConversationLayout, for + // example. public CharSequence getConversationTitle() { return mConversationText.getText(); } @@ -914,7 +935,7 @@ public class ConversationLayout extends FrameLayout } private void createGroupViews(List<List<MessagingMessage>> groups, - List<Person> senders, boolean showSpinner) { + List<Person> senders, boolean showSpinner) { mGroups.clear(); for (int groupIndex = 0; groupIndex < groups.size(); groupIndex++) { List<MessagingMessage> group = groups.get(groupIndex); @@ -963,8 +984,8 @@ public class ConversationLayout extends FrameLayout } private void findGroups(List<MessagingMessage> historicMessages, - List<MessagingMessage> messages, List<List<MessagingMessage>> groups, - List<Person> senders) { + List<MessagingMessage> messages, List<List<MessagingMessage>> groups, + List<Person> senders) { CharSequence currentSenderKey = null; List<MessagingMessage> currentGroup = null; int histSize = historicMessages.size(); diff --git a/core/java/com/android/internal/widget/ImageFloatingTextView.java b/core/java/com/android/internal/widget/ImageFloatingTextView.java index ae8f0255a192..5da64350619c 100644 --- a/core/java/com/android/internal/widget/ImageFloatingTextView.java +++ b/core/java/com/android/internal/widget/ImageFloatingTextView.java @@ -251,7 +251,7 @@ public class ImageFloatingTextView extends TextView { */ private int isTextAPrecomputedText() { final CharSequence text = getText(); - if (text == null || text.isEmpty()) { + if (text == null) { return 0; } diff --git a/core/java/com/android/internal/widget/PeopleHelper.java b/core/java/com/android/internal/widget/PeopleHelper.java index 85cedc362b99..3f5b4a0d61fe 100644 --- a/core/java/com/android/internal/widget/PeopleHelper.java +++ b/core/java/com/android/internal/widget/PeopleHelper.java @@ -22,6 +22,8 @@ import static com.android.internal.widget.MessagingPropertyAnimator.ALPHA_OUT; import android.annotation.ColorInt; import android.annotation.NonNull; import android.annotation.Nullable; +import android.app.Notification; +import android.app.Person; import android.content.Context; import android.graphics.Bitmap; import android.graphics.Canvas; @@ -222,6 +224,72 @@ public class PeopleHelper { } /** + * A class that represents a map from unique sender names in the groups to the string 1- or + * 2-character prefix strings for the names. This class uses the String value of the + * CharSequence Names as the key. + */ + public class NameToPrefixMap { + Map<String, String> mMap; + NameToPrefixMap(Map<String, String> map) { + this.mMap = map; + } + + /** + * @param name the name + * @return the prefix of the given name + */ + public String getPrefix(CharSequence name) { + return mMap.get(name.toString()); + } + } + + /** + * Same functionality as mapUniqueNamesToPrefix, but takes list-represented message groups as + * the input. This method is better when inflating MessagingGroup from the UI thread is not + * an option. + * @param groups message groups represented by lists. A message group is some consecutive + * messages (>=3) from the same sender in a conversation. + */ + public NameToPrefixMap mapUniqueNamesToPrefixWithGroupList( + List<List<Notification.MessagingStyle.Message>> groups) { + // Map of unique names to their prefix + ArrayMap<String, String> uniqueNames = new ArrayMap<>(); + // Map of single-character string prefix to the only name which uses it, or null if multiple + ArrayMap<String, CharSequence> uniqueCharacters = new ArrayMap<>(); + for (int i = 0; i < groups.size(); i++) { + List<Notification.MessagingStyle.Message> group = groups.get(i); + if (group.isEmpty()) continue; + Person sender = group.get(0).getSenderPerson(); + if (sender == null) continue; + CharSequence senderName = sender.getName(); + if (sender.getIcon() != null || TextUtils.isEmpty(senderName)) { + continue; + } + String senderNameString = senderName.toString(); + if (!uniqueNames.containsKey(senderNameString)) { + String charPrefix = findNamePrefix(senderName, null); + if (charPrefix == null) { + continue; + } + if (uniqueCharacters.containsKey(charPrefix)) { + // this character was already used, lets make it more unique. We first need to + // resolve the existing character if it exists + CharSequence existingName = uniqueCharacters.get(charPrefix); + if (existingName != null) { + uniqueNames.put(existingName.toString(), findNameSplit(existingName)); + uniqueCharacters.put(charPrefix, null); + } + uniqueNames.put(senderNameString, findNameSplit(senderName)); + } else { + uniqueNames.put(senderNameString, charPrefix); + uniqueCharacters.put(charPrefix, senderName); + } + } + } + return new NameToPrefixMap(uniqueNames); + } + + /** * Update whether the groups can hide the sender if they are first * (happens only for 1:1 conversations where the given title matches the sender's name) */ diff --git a/core/jni/android_util_Process.cpp b/core/jni/android_util_Process.cpp index 6a640a5ab23b..d2e58bb62c46 100644 --- a/core/jni/android_util_Process.cpp +++ b/core/jni/android_util_Process.cpp @@ -1165,12 +1165,11 @@ static jlong android_os_Process_getPss(JNIEnv* env, jobject clazz, jint pid) static jlongArray android_os_Process_getRss(JNIEnv* env, jobject clazz, jint pid) { - // total, file, anon, swap - jlong rss[4] = {0, 0, 0, 0}; + // total, file, anon, swap, shmem + jlong rss[5] = {0, 0, 0, 0, 0}; std::string status_path = android::base::StringPrintf("/proc/%d/status", pid); UniqueFile file = MakeUniqueFile(status_path.c_str(), "re"); - char line[256]; while (file != nullptr && fgets(line, sizeof(line), file.get())) { jlong v; @@ -1182,17 +1181,18 @@ static jlongArray android_os_Process_getRss(JNIEnv* env, jobject clazz, jint pid rss[2] = v; } else if ( sscanf(line, "VmSwap: %" SCNd64 " kB", &v) == 1) { rss[3] = v; + } else if ( sscanf(line, "RssShmem: %" SCNd64 " kB", &v) == 1) { + rss[4] = v; } } - jlongArray rssArray = env->NewLongArray(4); + jlongArray rssArray = env->NewLongArray(5); if (rssArray == NULL) { jniThrowException(env, "java/lang/OutOfMemoryError", NULL); return NULL; } - env->SetLongArrayRegion(rssArray, 0, 4, rss); - + env->SetLongArrayRegion(rssArray, 0, 5, rss); return rssArray; } diff --git a/core/jni/android_window_ScreenCapture.cpp b/core/jni/android_window_ScreenCapture.cpp index 6e903b3ab56d..1031542eb2e6 100644 --- a/core/jni/android_window_ScreenCapture.cpp +++ b/core/jni/android_window_ScreenCapture.cpp @@ -211,7 +211,7 @@ static jint nativeCaptureDisplay(JNIEnv* env, jclass clazz, jobject displayCaptu } static jint nativeCaptureLayers(JNIEnv* env, jclass clazz, jobject layerCaptureArgsObject, - jlong screenCaptureListenerObject) { + jlong screenCaptureListenerObject, jboolean sync) { LayerCaptureArgs captureArgs; getCaptureArgs(env, layerCaptureArgsObject, captureArgs); @@ -227,7 +227,7 @@ static jint nativeCaptureLayers(JNIEnv* env, jclass clazz, jobject layerCaptureA sp<gui::IScreenCaptureListener> captureListener = reinterpret_cast<gui::IScreenCaptureListener*>(screenCaptureListenerObject); - return ScreenshotClient::captureLayers(captureArgs, captureListener); + return ScreenshotClient::captureLayers(captureArgs, captureListener, sync); } static jlong nativeCreateScreenCaptureListener(JNIEnv* env, jclass clazz, jobject consumerObj) { @@ -281,7 +281,7 @@ static const JNINativeMethod sScreenCaptureMethods[] = { // clang-format off {"nativeCaptureDisplay", "(Landroid/window/ScreenCapture$DisplayCaptureArgs;J)I", (void*)nativeCaptureDisplay }, - {"nativeCaptureLayers", "(Landroid/window/ScreenCapture$LayerCaptureArgs;J)I", + {"nativeCaptureLayers", "(Landroid/window/ScreenCapture$LayerCaptureArgs;JZ)I", (void*)nativeCaptureLayers }, {"nativeCreateScreenCaptureListener", "(Ljava/util/function/ObjIntConsumer;)J", (void*)nativeCreateScreenCaptureListener }, diff --git a/core/proto/android/content/intent.proto b/core/proto/android/content/intent.proto index 75e29082508b..1d1f88b01838 100644 --- a/core/proto/android/content/intent.proto +++ b/core/proto/android/content/intent.proto @@ -66,7 +66,7 @@ message IntentProto { optional string identifier = 13 [ (.android.privacy).dest = DEST_EXPLICIT ]; } -// Next Tag: 12 +// Next Tag: 14 message IntentFilterProto { option (.android.msg_privacy).dest = DEST_AUTOMATIC; @@ -89,6 +89,7 @@ message IntentFilterProto { optional bool get_auto_verify = 10; repeated string mime_groups = 11; optional android.os.PersistableBundleProto extras = 12; + repeated UriRelativeFilterGroupProto uri_relative_filter_groups = 13; } message AuthorityEntryProto { @@ -98,3 +99,23 @@ message AuthorityEntryProto { optional bool wild = 2; optional int32 port = 3; } + +message UriRelativeFilterGroupProto { + option (.android.msg_privacy).dest = DEST_AUTOMATIC; + + enum Action { + ACTION_ALLOW = 0; + ACTION_BLOCK = 1; + } + + optional Action action = 1; + repeated UriRelativeFilterProto uri_relative_filters = 2; +} + +message UriRelativeFilterProto { + option (.android.msg_privacy).dest = DEST_AUTOMATIC; + + required int32 uri_part = 1; + required int32 pattern_type = 2; + required string filter = 3; +} diff --git a/core/proto/android/server/windowmanagerservice.proto b/core/proto/android/server/windowmanagerservice.proto index b63021d52f1c..c92435f61ab1 100644 --- a/core/proto/android/server/windowmanagerservice.proto +++ b/core/proto/android/server/windowmanagerservice.proto @@ -464,6 +464,7 @@ message WindowStateProto { repeated .android.graphics.RectProto keep_clear_areas = 45; repeated .android.graphics.RectProto unrestricted_keep_clear_areas = 46; repeated .android.view.InsetsSourceProto mergedLocalInsetsSources = 47; + optional int32 requested_visible_types = 48; } message IdentifierProto { diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index 070e2cb0dc5e..d972556df015 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -6654,7 +6654,14 @@ <!-- Allows the system to control the BiometricDialog (SystemUI). Reserved for the system. @hide --> <permission android:name="android.permission.MANAGE_BIOMETRIC_DIALOG" - android:protectionLevel="signature" /> + android:protectionLevel="signature" /> + + <!-- Allows an application to set the BiometricDialog (SystemUI) logo . + <p>Not for use by third-party applications. + @FlaggedApi("android.hardware.biometrics.custom_biometric_prompt") + --> + <permission android:name="android.permission.SET_BIOMETRIC_DIALOG_LOGO" + android:protectionLevel="signature" /> <!-- Allows an application to control keyguard. Only allowed for system processes. @hide --> @@ -7801,6 +7808,16 @@ <permission android:name="android.permission.RUN_USER_INITIATED_JOBS" 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 + 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. + <p>Protection level: signature|privileged|appop + --> + <permission android:name="android.permission.RUN_BACKUP_JOBS" + android:protectionLevel="signature|privileged|appop"/> + <!-- Allows an app access to the installer provided app metadata. @SystemApi @hide @@ -7968,6 +7985,16 @@ <permission android:name="android.permission.READ_SYSTEM_GRAMMATICAL_GENDER" android:protectionLevel="signature|privileged"/> + <!-- @SystemApi + @FlaggedApi("android.content.pm.emergency_install_permission") + Allows each app store in the system image to designate another app in the system image to + update the app store + <p>Protection level: signature|privileged + @hide + --> + <permission android:name="android.permission.EMERGENCY_INSTALL_PACKAGES" + android:protectionLevel="signature|privileged"/> + <!-- Attribution for Geofencing service. --> <attribution android:tag="GeofencingService" android:label="@string/geofencing_service"/> <!-- Attribution for Country Detector. --> @@ -8374,6 +8401,16 @@ </intent-filter> </receiver> + <!-- Broadcast Receiver listens to sufficient verifier broadcast from Package Manager + when installing new SDK. Verification of SDK code during installation time is run + to determine compatibility with privacy sandbox restrictions. --> + <receiver android:name="com.android.server.sdksandbox.SdkSandboxVerifierReceiver" + android:exported="false"> + <intent-filter> + <action android:name="android.intent.action.PACKAGE_NEEDS_VERIFICATION"/> + </intent-filter> + </receiver> + <service android:name="android.hardware.location.GeofenceHardwareService" android:permission="android.permission.LOCATION_HARDWARE" android:exported="false" /> @@ -8415,6 +8452,10 @@ android:permission="android.permission.BIND_JOB_SERVICE"> </service> + <service android:name="com.android.server.selinux.SelinuxAuditLogsService" + android:permission="android.permission.BIND_JOB_SERVICE"> + </service> + <service android:name="com.android.server.compos.IsolatedCompilationJobService" android:permission="android.permission.BIND_JOB_SERVICE"> </service> diff --git a/core/res/res/color-night/notification_expand_button_state_tint.xml b/core/res/res/color-night/notification_expand_button_state_tint.xml deleted file mode 100644 index a794d53c7e71..000000000000 --- a/core/res/res/color-night/notification_expand_button_state_tint.xml +++ /dev/null @@ -1,21 +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. - --> - -<selector xmlns:android="http://schemas.android.com/apk/res/android"> - <item android:state_pressed="true" android:color="@android:color/system_on_surface_dark" android:alpha="0.06"/> - <item android:state_hovered="true" android:color="@android:color/system_on_surface_dark" android:alpha="0.03"/> - <item android:color="@android:color/system_on_surface_dark" android:alpha="0.00"/> -</selector>
\ No newline at end of file diff --git a/core/res/res/color/notification_expand_button_state_tint.xml b/core/res/res/color/notification_expand_button_state_tint.xml index 67b2c2568bb1..5a8594f0e461 100644 --- a/core/res/res/color/notification_expand_button_state_tint.xml +++ b/core/res/res/color/notification_expand_button_state_tint.xml @@ -14,8 +14,11 @@ ~ limitations under the License. --> -<selector xmlns:android="http://schemas.android.com/apk/res/android"> - <item android:state_pressed="true" android:color="@android:color/system_on_surface_light" android:alpha="0.12"/> - <item android:state_hovered="true" android:color="@android:color/system_on_surface_light" android:alpha="0.08"/> - <item android:color="@android:color/system_on_surface_light" android:alpha="0.00"/> +<selector xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"> + <item android:state_pressed="true" android:color="?androidprv:attr/materialColorOnPrimaryFixed" + android:alpha="0.15"/> + <item android:state_hovered="true" android:color="?androidprv:attr/materialColorOnPrimaryFixed" + android:alpha="0.11"/> + <item android:color="@color/transparent" /> </selector>
\ No newline at end of file diff --git a/core/res/res/values/attrs_manifest.xml b/core/res/res/values/attrs_manifest.xml index 35276bf8ead2..6884fc0057d9 100644 --- a/core/res/res/values/attrs_manifest.xml +++ b/core/res/res/values/attrs_manifest.xml @@ -3438,6 +3438,20 @@ <!-- Attributes that can be supplied in an AndroidManifest.xml <code>data</code> tag, a child of the {@link #AndroidManifestIntentFilter intent-filter} tag, describing + a group matching rule consisting of one or more + {@link #AndroidManifestData data} tags that must all match. This + tag can be specified multiple times to create multiple groups that + will be matched in the order they are defined. --> + <declare-styleable name="AndroidManifestUriRelativeFilterGroup" + parent="AndroidManifestIntentFilter"> + <!-- Specify if this group is allow rule or disallow rule. If this + attribute is not specified then it is assumed to be true --> + <attr name="allow" format="boolean"/> + </declare-styleable> + + <!-- Attributes that can be supplied in an AndroidManifest.xml + <code>data</code> tag, a child of the + {@link #AndroidManifestIntentFilter intent-filter} tag, describing the types of data that match. This tag can be specified multiple times to supply multiple data options, as described in the {@link android.content.IntentFilter} class. Note that all such @@ -3445,7 +3459,8 @@ <code><data android:scheme="myscheme" android:host="me.com" /></code> is equivalent to <code><data android:scheme="myscheme" /> <data android:host="me.com" /></code>. --> - <declare-styleable name="AndroidManifestData" parent="AndroidManifestIntentFilter"> + <declare-styleable name="AndroidManifestData" + parent="AndroidManifestIntentFilter AndroidManifestUriRelativeFilterGroup"> <!-- Specify a MIME type that is handled, as per {@link android.content.IntentFilter#addDataType IntentFilter.addDataType()}. @@ -3549,6 +3564,70 @@ IntentFilter.addDataPath()} with {@link android.os.PatternMatcher#PATTERN_SUFFIX}. --> <attr name="pathSuffix" /> + <!-- Specify a URI query that must exactly match, as a + {@link android.content.UriRelativeFilter UriRelativeFilter} with + {@link android.os.PatternMatcher#PATTERN_LITERAL}. --> + <attr name="query" format="string" /> + <!-- Specify a URI query that must be a prefix to match, as a + {@link android.content.UriRelativeFilter UriRelativeFilter} with + {@link android.os.PatternMatcher#PATTERN_PREFIX}. --> + <attr name="queryPrefix" format="string" /> + <!-- Specify a URI query that matches a simple pattern, as a + {@link android.content.UriRelativeFilter UriRelativeFilter} with + {@link android.os.PatternMatcher#PATTERN_SIMPLE_GLOB}. + Note that because '\' is used as an escape character when + reading the string from XML (before it is parsed as a pattern), + you will need to double-escape: for example a literal "*" would + be written as "\\*" and a literal "\" would be written as + "\\\\". This is basically the same as what you would need to + write if constructing the string in Java code. --> + <attr name="queryPattern" format="string" /> + <!-- Specify a URI query that matches an advanced pattern, as a + {@link android.content.UriRelativeFilter UriRelativeFilter} with + {@link android.os.PatternMatcher#PATTERN_ADVANCED_GLOB}. + Note that because '\' is used as an escape character when + reading the string from XML (before it is parsed as a pattern), + you will need to double-escape: for example a literal "*" would + be written as "\\*" and a literal "\" would be written as + "\\\\". This is basically the same as what you would need to + write if constructing the string in Java code. --> + <attr name="queryAdvancedPattern" format="string" /> + <!-- Specify a URI query that must be a suffix to match, as a + {@link android.content.UriRelativeFilter UriRelativeFilter} with + {@link android.os.PatternMatcher#PATTERN_SUFFIX}. --> + <attr name="querySuffix" format="string" /> + <!-- Specify a URI fragment that must exactly match, as a + {@link android.content.UriRelativeFilter UriRelativeFilter} with + {@link android.os.PatternMatcher#PATTERN_LITERAL}. --> + <attr name="fragment" format="string" /> + <!-- Specify a URI fragment that must be a prefix to match, as a + {@link android.content.UriRelativeFilter UriRelativeFilter} with + {@link android.os.PatternMatcher#PATTERN_PREFIX}. --> + <attr name="fragmentPrefix" format="string" /> + <!-- Specify a URI fragment that matches a simple pattern, as a + {@link android.content.UriRelativeFilter UriRelativeFilter} with + {@link android.os.PatternMatcher#PATTERN_SIMPLE_GLOB}. + Note that because '\' is used as an escape character when + reading the string from XML (before it is parsed as a pattern), + you will need to double-escape: for example a literal "*" would + be written as "\\*" and a literal "\" would be written as + "\\\\". This is basically the same as what you would need to + write if constructing the string in Java code. --> + <attr name="fragmentPattern" format="string" /> + <!-- Specify a URI fragment that matches an advanced pattern, as a + {@link android.content.UriRelativeFilter UriRelativeFilter} with + {@link android.os.PatternMatcher#PATTERN_ADVANCED_GLOB}. + Note that because '\' is used as an escape character when + reading the string from XML (before it is parsed as a pattern), + you will need to double-escape: for example a literal "*" would + be written as "\\*" and a literal "\" would be written as + "\\\\". This is basically the same as what you would need to + write if constructing the string in Java code. --> + <attr name="fragmentAdvancedPattern" format="string" /> + <!-- Specify a URI fragment that must be a suffix to match, as a + {@link android.content.UriRelativeFilter UriRelativeFilter} with + {@link android.os.PatternMatcher#PATTERN_SUFFIX}. --> + <attr name="fragmentSuffix" format="string" /> </declare-styleable> <!-- Attributes that can be supplied in an AndroidManifest.xml diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index 0d1a98785695..23c78fdf108e 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -6611,7 +6611,7 @@ </string-array> <!-- Whether or not the monitoring on the apps' background battery drain is enabled --> - <bool name="config_bg_current_drain_monitor_enabled">true</bool> + <bool name="config_bg_current_drain_monitor_enabled">false</bool> <!-- The threshold of the background current drain (in percentage) to the restricted standby bucket. diff --git a/core/res/res/values/public-staging.xml b/core/res/res/values/public-staging.xml index b8fc052a2fa9..830e99ca907b 100644 --- a/core/res/res/values/public-staging.xml +++ b/core/res/res/values/public-staging.xml @@ -123,6 +123,26 @@ <public name="featureFlag"/> <!-- @FlaggedApi("android.multiuser.enable_system_user_only_for_services_and_providers") --> <public name="systemUserOnly"/> + <!-- @FlaggedApi("android.content.pm.relative_reference_intent_filters") --> + <public name="allow"/> + <!-- @FlaggedApi("android.content.pm.relative_reference_intent_filters") --> + <public name="query"/> + <!-- @FlaggedApi("android.content.pm.relative_reference_intent_filters") --> + <public name="queryPrefix"/> + <!-- @FlaggedApi("android.content.pm.relative_reference_intent_filters") --> + <public name="queryPattern"/> + <!-- @FlaggedApi("android.content.pm.relative_reference_intent_filters") --> + <public name="queryAdvancedPattern"/> + <!-- @FlaggedApi("android.content.pm.relative_reference_intent_filters") --> + <public name="querySuffix"/> + <!-- @FlaggedApi("android.content.pm.relative_reference_intent_filters") --> + <public name="fragmentPrefix"/> + <!-- @FlaggedApi("android.content.pm.relative_reference_intent_filters") --> + <public name="fragmentPattern"/> + <!-- @FlaggedApi("android.content.pm.relative_reference_intent_filters") --> + <public name="fragmentAdvancedPattern"/> + <!-- @FlaggedApi("android.content.pm.relative_reference_intent_filters") --> + <public name="fragmentSuffix"/> </staging-public-group> <staging-public-group type="id" first-id="0x01bc0000"> diff --git a/core/res/res/xml/sms_short_codes.xml b/core/res/res/xml/sms_short_codes.xml index 3a2e50aa06e8..9bb249999d99 100644 --- a/core/res/res/xml/sms_short_codes.xml +++ b/core/res/res/xml/sms_short_codes.xml @@ -34,7 +34,7 @@ http://smscoin.net/software/engine/WordPress/Paid+SMS-registration/ --> <!-- Arab Emirates --> - <shortcode country="ae" pattern="\\d{1,5}" free="1017|1355|3214" /> + <shortcode country="ae" pattern="\\d{1,5}" free="1017|1355|3214|6253" /> <!-- Albania: 5 digits, known short codes listed --> <shortcode country="al" pattern="\\d{5}" premium="15191|55[56]00" /> @@ -86,7 +86,7 @@ <shortcode country="cn" premium="1066.*" free="1065.*" /> <!-- Colombia: 1-6 digits (not confirmed) --> - <shortcode country="co" pattern="\\d{1,6}" free="890350|908160|892255|898002|898880|899960|899948|87739|85517" /> + <shortcode country="co" pattern="\\d{1,6}" free="890350|908160|892255|898002|898880|899960|899948|87739|85517|491289" /> <!-- Cyprus: 4-6 digits (not confirmed), known premium codes listed, plus EU --> <shortcode country="cy" pattern="\\d{4,6}" premium="7510" free="116\\d{3}" /> @@ -104,6 +104,12 @@ <!-- Denmark: see http://iprs.webspacecommerce.com/Denmark-Premium-Rate-Numbers --> <shortcode country="dk" pattern="\\d{4,5}" premium="1\\d{3}" free="116\\d{3}|4665" /> + <!-- Dominican Republic: 1-6 digits (standard system default, not country specific) --> + <shortcode country="do" pattern="\\d{1,6}" free="912892" /> + + <!-- Ecuador: 1-6 digits (standard system default, not country specific) --> + <shortcode country="ec" pattern="\\d{1,6}" free="466453" /> + <!-- Estonia: short codes 3-5 digits starting with 1, plus premium 7 digit numbers starting with 90, plus EU. http://www.tja.ee/public/documents/Elektrooniline_side/Oigusaktid/ENG/Estonian_Numbering_Plan_annex_06_09_2010.mht --> <shortcode country="ee" pattern="1\\d{2,4}" premium="90\\d{5}|15330|1701[0-3]" free="116\\d{3}|95034" /> @@ -154,8 +160,8 @@ http://www.comreg.ie/_fileupload/publications/ComReg1117.pdf --> <shortcode country="ie" pattern="\\d{5}" premium="5[3-9]\\d{3}" free="50\\d{3}|116\\d{3}" standard="5[12]\\d{3}" /> - <!-- Israel: 4 digits, known premium codes listed --> - <shortcode country="il" pattern="\\d{4}" premium="4422|4545" /> + <!-- Israel: 1-5 digits, known premium codes listed --> + <shortcode country="il" pattern="\\d{1,5}" premium="4422|4545" free="37477|6681" /> <!-- Italy: 5 digits (premium=41xxx,42xxx), plus EU: https://www.itu.int/dms_pub/itu-t/oth/02/02/T020200006B0001PDFE.pdf --> @@ -193,11 +199,14 @@ <shortcode country="mk" pattern="\\d{1,6}" free="129005|122" /> <!-- Mexico: 4-5 digits (not confirmed), known premium codes listed --> - <shortcode country="mx" pattern="\\d{4,5}" premium="53035|7766" free="26259|46645|50025|50052|5050|76551|88778|9963|91101|45453" /> + <shortcode country="mx" pattern="\\d{4,6}" premium="53035|7766" free="26259|46645|50025|50052|5050|76551|88778|9963|91101|45453|550346" /> <!-- Malaysia: 5 digits: http://www.skmm.gov.my/attachment/Consumer_Regulation/Mobile_Content_Services_FAQs.pdf --> <shortcode country="my" pattern="\\d{5}" premium="32298|33776" free="22099|28288|66668" /> + <!-- Namibia: 1-5 digits (standard system default, not country specific) --> + <shortcode country="na" pattern="\\d{1,5}" free="40005" /> + <!-- The Netherlands, 4 digits, known premium codes listed, plus EU --> <shortcode country="nl" pattern="\\d{4}" premium="4466|5040" free="116\\d{3}|2223|6225|2223|1662" /> diff --git a/core/tests/InputMethodCoreTests/Android.bp b/core/tests/InputMethodCoreTests/Android.bp new file mode 100644 index 000000000000..ac6462589e16 --- /dev/null +++ b/core/tests/InputMethodCoreTests/Android.bp @@ -0,0 +1,66 @@ +package { + // See: http://go/android-license-faq + // A large-scale-change added 'default_applicable_licenses' to import + // all of the 'license_kinds' from "frameworks_base_license" + // to get the below license kinds: + // SPDX-license-identifier-Apache-2.0 + default_applicable_licenses: ["frameworks_base_license"], +} + +android_test { + name: "InputMethodCoreTests", + + srcs: [ + "src/**/*.java", + "src/**/*.kt", + "src/**/I*.aidl", + ], + + dxflags: ["--core-library"], + + static_libs: [ + "collector-device-lib-platform", + "android-common", + "frameworks-core-util-lib", + "androidx.core_core", + "androidx.core_core-ktx", + "androidx.test.ext.junit", + "androidx.test.runner", + "androidx.test.rules", + "flag-junit", + "junit-params", + "kotlin-test", + "mockito-target-minus-junit4", + "platform-test-annotations", + "platform-compat-test-rules", + "truth", + "print-test-util-lib", + "testng", + "device-time-shell-utils", + "testables", + "flag-junit", + ], + + libs: [ + "android.test.runner", + "android.test.base", + "android.test.mock", + "framework", + "ext", + "framework-res", + ], + + sdk_version: "core_platform", + test_suites: [ + "device-tests", + "automotive-tests", + ], + + certificate: "platform", + + resource_dirs: ["res"], + + data: [ + ":com.android.cts.helpers.aosp", + ], +} diff --git a/core/tests/InputMethodCoreTests/AndroidManifest.xml b/core/tests/InputMethodCoreTests/AndroidManifest.xml new file mode 100644 index 000000000000..8d00d0f755bf --- /dev/null +++ b/core/tests/InputMethodCoreTests/AndroidManifest.xml @@ -0,0 +1,31 @@ +<?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. +--> + +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + android:installLocation="internalOnly" + package="com.android.frameworks.inputmethodcoretests" + android:sharedUserId="com.android.uid.test"> + + <application + android:supportsRtl="true" + android:enableOnBackInvokedCallback="true"> + <uses-library android:name="android.test.runner" /> + </application> + + <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner" + android:targetPackage="com.android.frameworks.inputmethodcoretests" + android:label="InputMethod Core Tests" /> +</manifest> diff --git a/core/tests/InputMethodCoreTests/AndroidTest.xml b/core/tests/InputMethodCoreTests/AndroidTest.xml new file mode 100644 index 000000000000..fa585d8d1075 --- /dev/null +++ b/core/tests/InputMethodCoreTests/AndroidTest.xml @@ -0,0 +1,38 @@ +<?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. +--> +<configuration description="Runs InputMethod Core Tests."> + <option name="test-suite-tag" value="apct" /> + <option name="test-suite-tag" value="apct-instrumentation" /> + + <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller"> + <option name="cleanup-apks" value="true" /> + <option name="test-file-name" value="InputMethodCoreTests.apk" /> + </target_preparer> + + <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer"> + <!-- TODO(b/254155965): Design a mechanism to finally remove this command. --> + <option name="run-command" value="settings put global device_config_sync_disabled 0" /> + </target_preparer> + + <target_preparer class="com.android.compatibility.common.tradefed.targetprep.DeviceInteractionHelperInstaller" /> + + <option name="test-tag" value="InputMethodCoreTests" /> + <test class="com.android.tradefed.testtype.AndroidJUnitTest" > + <option name="package" value="com.android.frameworks.inputmethodcoretests" /> + <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" /> + <option name="hidden-api-checks" value="false"/> + </test> +</configuration> diff --git a/core/tests/InputMethodCoreTests/OWNERS b/core/tests/InputMethodCoreTests/OWNERS new file mode 100644 index 000000000000..5deb2ce8f24b --- /dev/null +++ b/core/tests/InputMethodCoreTests/OWNERS @@ -0,0 +1 @@ +include /core/java/android/view/inputmethod/OWNERS diff --git a/core/tests/coretests/res/xml/ime_meta.xml b/core/tests/InputMethodCoreTests/res/xml/ime_meta.xml index a975718c50e9..a975718c50e9 100644 --- a/core/tests/coretests/res/xml/ime_meta.xml +++ b/core/tests/InputMethodCoreTests/res/xml/ime_meta.xml diff --git a/core/tests/coretests/res/xml/ime_meta_inline_suggestions.xml b/core/tests/InputMethodCoreTests/res/xml/ime_meta_inline_suggestions.xml index e67bf6331bfe..e67bf6331bfe 100644 --- a/core/tests/coretests/res/xml/ime_meta_inline_suggestions.xml +++ b/core/tests/InputMethodCoreTests/res/xml/ime_meta_inline_suggestions.xml diff --git a/core/tests/coretests/res/xml/ime_meta_inline_suggestions_with_touch_exploration.xml b/core/tests/InputMethodCoreTests/res/xml/ime_meta_inline_suggestions_with_touch_exploration.xml index 34402089b47d..34402089b47d 100644 --- a/core/tests/coretests/res/xml/ime_meta_inline_suggestions_with_touch_exploration.xml +++ b/core/tests/InputMethodCoreTests/res/xml/ime_meta_inline_suggestions_with_touch_exploration.xml diff --git a/core/tests/coretests/res/xml/ime_meta_sw_next.xml b/core/tests/InputMethodCoreTests/res/xml/ime_meta_sw_next.xml index 2e2ee33e9933..2e2ee33e9933 100644 --- a/core/tests/coretests/res/xml/ime_meta_sw_next.xml +++ b/core/tests/InputMethodCoreTests/res/xml/ime_meta_sw_next.xml diff --git a/core/tests/coretests/res/xml/ime_meta_virtual_device_only.xml b/core/tests/InputMethodCoreTests/res/xml/ime_meta_virtual_device_only.xml index 1905365808bc..1905365808bc 100644 --- a/core/tests/coretests/res/xml/ime_meta_virtual_device_only.xml +++ b/core/tests/InputMethodCoreTests/res/xml/ime_meta_virtual_device_only.xml diff --git a/core/tests/coretests/res/xml/ime_meta_vr_only.xml b/core/tests/InputMethodCoreTests/res/xml/ime_meta_vr_only.xml index 653a8ffcb944..653a8ffcb944 100644 --- a/core/tests/coretests/res/xml/ime_meta_vr_only.xml +++ b/core/tests/InputMethodCoreTests/res/xml/ime_meta_vr_only.xml diff --git a/core/tests/coretests/src/android/view/inputmethod/BaseInputConnectionTest.java b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/BaseInputConnectionTest.java index f04f603564f6..f04f603564f6 100644 --- a/core/tests/coretests/src/android/view/inputmethod/BaseInputConnectionTest.java +++ b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/BaseInputConnectionTest.java diff --git a/core/tests/coretests/src/android/view/inputmethod/CursorAnchorInfoTest.java b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/CursorAnchorInfoTest.java index 9d7d71d8d539..9d7d71d8d539 100644 --- a/core/tests/coretests/src/android/view/inputmethod/CursorAnchorInfoTest.java +++ b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/CursorAnchorInfoTest.java diff --git a/core/tests/coretests/src/android/view/inputmethod/DeleteRangeGestureTest.java b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/DeleteRangeGestureTest.java index d7b911dda672..d7b911dda672 100644 --- a/core/tests/coretests/src/android/view/inputmethod/DeleteRangeGestureTest.java +++ b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/DeleteRangeGestureTest.java diff --git a/core/tests/coretests/src/android/view/inputmethod/EditorInfoTest.java b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/EditorInfoTest.java index 4839dd27b283..4839dd27b283 100644 --- a/core/tests/coretests/src/android/view/inputmethod/EditorInfoTest.java +++ b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/EditorInfoTest.java diff --git a/core/tests/coretests/src/android/view/inputmethod/InputMethodInfoTest.java b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/InputMethodInfoTest.java index 909af7b4c5fb..a3f537ef5f1c 100644 --- a/core/tests/coretests/src/android/view/inputmethod/InputMethodInfoTest.java +++ b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/InputMethodInfoTest.java @@ -32,7 +32,7 @@ import androidx.test.InstrumentationRegistry; import androidx.test.filters.SmallTest; import androidx.test.runner.AndroidJUnit4; -import com.android.frameworks.coretests.R; +import com.android.frameworks.inputmethodcoretests.R; import org.junit.Rule; import org.junit.Test; diff --git a/core/tests/coretests/src/android/view/inputmethod/InputMethodManagerTest.java b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/InputMethodManagerTest.java index d70572444128..d70572444128 100644 --- a/core/tests/coretests/src/android/view/inputmethod/InputMethodManagerTest.java +++ b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/InputMethodManagerTest.java diff --git a/core/tests/coretests/src/android/view/inputmethod/InputMethodSubtypeArrayTest.java b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/InputMethodSubtypeArrayTest.java index e7b1110f898a..e7b1110f898a 100644 --- a/core/tests/coretests/src/android/view/inputmethod/InputMethodSubtypeArrayTest.java +++ b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/InputMethodSubtypeArrayTest.java diff --git a/core/tests/coretests/src/android/view/inputmethod/InputMethodSubtypeTest.java b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/InputMethodSubtypeTest.java index 5095cad1b607..5095cad1b607 100644 --- a/core/tests/coretests/src/android/view/inputmethod/InputMethodSubtypeTest.java +++ b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/InputMethodSubtypeTest.java diff --git a/core/tests/coretests/src/android/view/inputmethod/InsertGestureTest.java b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/InsertGestureTest.java index 47a724d36038..47a724d36038 100644 --- a/core/tests/coretests/src/android/view/inputmethod/InsertGestureTest.java +++ b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/InsertGestureTest.java diff --git a/core/tests/coretests/src/android/view/inputmethod/InsertModeGestureTest.java b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/InsertModeGestureTest.java index a94f8772fcd0..a94f8772fcd0 100644 --- a/core/tests/coretests/src/android/view/inputmethod/InsertModeGestureTest.java +++ b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/InsertModeGestureTest.java diff --git a/core/tests/coretests/src/android/view/inputmethod/ParcelableHandwritingGestureTest.java b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/ParcelableHandwritingGestureTest.java index 90f7d06857c7..90f7d06857c7 100644 --- a/core/tests/coretests/src/android/view/inputmethod/ParcelableHandwritingGestureTest.java +++ b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/ParcelableHandwritingGestureTest.java diff --git a/core/tests/coretests/src/android/view/inputmethod/SelectGestureTest.java b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/SelectGestureTest.java index b2eb07c0a9e7..b2eb07c0a9e7 100644 --- a/core/tests/coretests/src/android/view/inputmethod/SelectGestureTest.java +++ b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/SelectGestureTest.java diff --git a/core/tests/coretests/src/android/view/inputmethod/SelectRangeGestureTest.java b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/SelectRangeGestureTest.java index df63a4aaaefe..df63a4aaaefe 100644 --- a/core/tests/coretests/src/android/view/inputmethod/SelectRangeGestureTest.java +++ b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/SelectRangeGestureTest.java diff --git a/core/tests/coretests/src/android/view/inputmethod/SparseRectFArrayTest.java b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/SparseRectFArrayTest.java index f264cc630dc5..f264cc630dc5 100644 --- a/core/tests/coretests/src/android/view/inputmethod/SparseRectFArrayTest.java +++ b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/SparseRectFArrayTest.java diff --git a/core/tests/coretests/src/android/view/inputmethod/SurroundingTextTest.java b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/SurroundingTextTest.java index 047f33074460..047f33074460 100644 --- a/core/tests/coretests/src/android/view/inputmethod/SurroundingTextTest.java +++ b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/SurroundingTextTest.java diff --git a/core/tests/coretests/src/android/view/inputmethod/TextAppearanceInfoTest.java b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/TextAppearanceInfoTest.java index 0750cf1a64ab..0750cf1a64ab 100644 --- a/core/tests/coretests/src/android/view/inputmethod/TextAppearanceInfoTest.java +++ b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/TextAppearanceInfoTest.java diff --git a/core/tests/coretests/src/com/android/internal/inputmethod/CompletableFutureUtilTest.kt b/core/tests/InputMethodCoreTests/src/com/android/internal/inputmethod/CompletableFutureUtilTest.kt index 07471f08e0f5..07471f08e0f5 100644 --- a/core/tests/coretests/src/com/android/internal/inputmethod/CompletableFutureUtilTest.kt +++ b/core/tests/InputMethodCoreTests/src/com/android/internal/inputmethod/CompletableFutureUtilTest.kt diff --git a/core/tests/coretests/src/com/android/internal/inputmethod/InputConnectionWrapperTest.java b/core/tests/InputMethodCoreTests/src/com/android/internal/inputmethod/InputConnectionWrapperTest.java index a6262944e8b0..a6262944e8b0 100644 --- a/core/tests/coretests/src/com/android/internal/inputmethod/InputConnectionWrapperTest.java +++ b/core/tests/InputMethodCoreTests/src/com/android/internal/inputmethod/InputConnectionWrapperTest.java diff --git a/core/tests/coretests/src/com/android/internal/inputmethod/InputMethodDebugTest.java b/core/tests/InputMethodCoreTests/src/com/android/internal/inputmethod/InputMethodDebugTest.java index 32bfdcb7b217..32bfdcb7b217 100644 --- a/core/tests/coretests/src/com/android/internal/inputmethod/InputMethodDebugTest.java +++ b/core/tests/InputMethodCoreTests/src/com/android/internal/inputmethod/InputMethodDebugTest.java diff --git a/core/tests/coretests/src/com/android/internal/inputmethod/InputMethodSubtypeHandleTest.java b/core/tests/InputMethodCoreTests/src/com/android/internal/inputmethod/InputMethodSubtypeHandleTest.java index f111bf6fcd64..f111bf6fcd64 100644 --- a/core/tests/coretests/src/com/android/internal/inputmethod/InputMethodSubtypeHandleTest.java +++ b/core/tests/InputMethodCoreTests/src/com/android/internal/inputmethod/InputMethodSubtypeHandleTest.java diff --git a/core/tests/coretests/src/com/android/internal/inputmethod/SubtypeLocaleUtilsTest.java b/core/tests/InputMethodCoreTests/src/com/android/internal/inputmethod/SubtypeLocaleUtilsTest.java index ba6390808151..ba6390808151 100644 --- a/core/tests/coretests/src/com/android/internal/inputmethod/SubtypeLocaleUtilsTest.java +++ b/core/tests/InputMethodCoreTests/src/com/android/internal/inputmethod/SubtypeLocaleUtilsTest.java diff --git a/core/tests/coretests/Android.bp b/core/tests/coretests/Android.bp index 440630287b7c..f47679991418 100644 --- a/core/tests/coretests/Android.bp +++ b/core/tests/coretests/Android.bp @@ -209,9 +209,13 @@ android_ravenwood_test { "testng", ], srcs: [ + "src/android/content/pm/PackageManagerTest.java", + "src/android/content/pm/UserInfoTest.java", "src/android/database/CursorWindowTest.java", "src/android/os/**/*.java", + "src/android/telephony/PinResultTest.java", "src/android/util/**/*.java", + "src/android/view/DisplayInfoTest.java", "src/com/android/internal/logging/**/*.java", "src/com/android/internal/os/**/*.java", "src/com/android/internal/util/**/*.java", diff --git a/core/tests/coretests/src/android/content/pm/PackageManagerTest.java b/core/tests/coretests/src/android/content/pm/PackageManagerTest.java new file mode 100644 index 000000000000..20421d105db8 --- /dev/null +++ b/core/tests/coretests/src/android/content/pm/PackageManagerTest.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.content.pm; + +import static com.google.common.truth.Truth.assertThat; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class PackageManagerTest { + @Test + public void testPackageInfoFlags() throws Exception { + assertThat(PackageManager.PackageInfoFlags.of(42L).getValue()).isEqualTo(42L); + } + + @Test + public void testApplicationInfoFlags() throws Exception { + assertThat(PackageManager.ApplicationInfoFlags.of(42L).getValue()).isEqualTo(42L); + } + + @Test + public void testComponentInfoFlags() throws Exception { + assertThat(PackageManager.ComponentInfoFlags.of(42L).getValue()).isEqualTo(42L); + } + + @Test + public void testResolveInfoFlags() throws Exception { + assertThat(PackageManager.ResolveInfoFlags.of(42L).getValue()).isEqualTo(42L); + } +} diff --git a/core/tests/coretests/src/android/content/pm/UserInfoTest.java b/core/tests/coretests/src/android/content/pm/UserInfoTest.java new file mode 100644 index 000000000000..af36dbb36379 --- /dev/null +++ b/core/tests/coretests/src/android/content/pm/UserInfoTest.java @@ -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 android.content.pm; + +import static com.google.common.truth.Truth.assertThat; + +import android.os.UserHandle; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class UserInfoTest { + @Test + public void testSimple() throws Exception { + final UserInfo ui = new UserInfo(10, "Test", UserInfo.FLAG_GUEST); + + assertThat(ui.getUserHandle()).isEqualTo(UserHandle.of(10)); + assertThat(ui.name).isEqualTo("Test"); + + // Derived based on userType field + assertThat(ui.isManagedProfile()).isEqualTo(false); + assertThat(ui.isGuest()).isEqualTo(true); + assertThat(ui.isRestricted()).isEqualTo(false); + assertThat(ui.isDemo()).isEqualTo(false); + assertThat(ui.isCloneProfile()).isEqualTo(false); + assertThat(ui.isCommunalProfile()).isEqualTo(false); + assertThat(ui.isPrivateProfile()).isEqualTo(false); + + // Derived based on flags field + assertThat(ui.isPrimary()).isEqualTo(false); + assertThat(ui.isAdmin()).isEqualTo(false); + assertThat(ui.isProfile()).isEqualTo(false); + assertThat(ui.isEnabled()).isEqualTo(true); + assertThat(ui.isQuietModeEnabled()).isEqualTo(false); + assertThat(ui.isEphemeral()).isEqualTo(false); + assertThat(ui.isForTesting()).isEqualTo(false); + assertThat(ui.isInitialized()).isEqualTo(false); + assertThat(ui.isFull()).isEqualTo(false); + assertThat(ui.isMain()).isEqualTo(false); + + // Derived dynamically + assertThat(ui.canHaveProfile()).isEqualTo(false); + } + + @Test + public void testDebug() throws Exception { + final UserInfo ui = new UserInfo(10, "Test", UserInfo.FLAG_GUEST); + + assertThat(ui.toString()).isNotEmpty(); + assertThat(ui.toFullString()).isNotEmpty(); + } +} diff --git a/core/tests/coretests/src/android/content/res/FontScaleConverterFactoryTest.kt b/core/tests/coretests/src/android/content/res/FontScaleConverterFactoryTest.kt index e32a57b1aefe..a2a5433eca24 100644 --- a/core/tests/coretests/src/android/content/res/FontScaleConverterFactoryTest.kt +++ b/core/tests/coretests/src/android/content/res/FontScaleConverterFactoryTest.kt @@ -145,7 +145,6 @@ class FontScaleConverterFactoryTest { fun unnecessaryFontScalesReturnsNull() { assertThat(FontScaleConverterFactory.forScale(0F)).isNull() assertThat(FontScaleConverterFactory.forScale(1F)).isNull() - assertThat(FontScaleConverterFactory.forScale(1.1F)).isNull() assertThat(FontScaleConverterFactory.forScale(0.85F)).isNull() } @@ -176,7 +175,7 @@ class FontScaleConverterFactoryTest { assertThat(FontScaleConverterFactory.isNonLinearFontScalingActive(-1f)).isFalse() assertThat(FontScaleConverterFactory.isNonLinearFontScalingActive(0.85f)).isFalse() assertThat(FontScaleConverterFactory.isNonLinearFontScalingActive(1.02f)).isFalse() - assertThat(FontScaleConverterFactory.isNonLinearFontScalingActive(1.10f)).isFalse() + assertThat(FontScaleConverterFactory.isNonLinearFontScalingActive(1.10f)).isTrue() assertThat(FontScaleConverterFactory.isNonLinearFontScalingActive(1.15f)).isTrue() assertThat(FontScaleConverterFactory.isNonLinearFontScalingActive(1.1499999f)) .isTrue() diff --git a/core/tests/coretests/src/android/telephony/PinResultTest.java b/core/tests/coretests/src/android/telephony/PinResultTest.java new file mode 100644 index 000000000000..c260807e5cbc --- /dev/null +++ b/core/tests/coretests/src/android/telephony/PinResultTest.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 android.telephony; + +import static com.google.common.truth.Truth.assertThat; + +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +public class PinResultTest { + @Test + public void testSimple() throws Exception { + final PinResult res = new PinResult(PinResult.PIN_RESULT_TYPE_SUCCESS, 5); + assertThat(res.getResult()).isEqualTo(PinResult.PIN_RESULT_TYPE_SUCCESS); + assertThat(res.getAttemptsRemaining()).isEqualTo(5); + } +} diff --git a/core/tests/coretests/src/android/util/SingletonTest.java b/core/tests/coretests/src/android/util/SingletonTest.java new file mode 100644 index 000000000000..8c5a9639c23a --- /dev/null +++ b/core/tests/coretests/src/android/util/SingletonTest.java @@ -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 android.util; + +import static org.junit.Assert.assertTrue; + +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +public class SingletonTest { + @Test + public void testSimple() throws Exception { + final Singleton<Object> singleton = new Singleton<>() { + @Override + protected Object create() { + return new Object(); + } + }; + + final Object first = singleton.get(); + final Object second = singleton.get(); + assertTrue(first == second); + } +} diff --git a/core/tests/coretests/src/android/view/DisplayInfoTest.java b/core/tests/coretests/src/android/view/DisplayInfoTest.java index 803d38c4208a..4c5b7e508e34 100644 --- a/core/tests/coretests/src/android/view/DisplayInfoTest.java +++ b/core/tests/coretests/src/android/view/DisplayInfoTest.java @@ -21,9 +21,12 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; +import android.platform.test.ravenwood.RavenwoodRule; + import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; @@ -32,6 +35,9 @@ import org.junit.runner.RunWith; public class DisplayInfoTest { private static final float FLOAT_EQUAL_DELTA = 0.0001f; + @Rule + public final RavenwoodRule mRavenwood = new RavenwoodRule(); + @Test public void testDefaultDisplayInfosAreEqual() { DisplayInfo displayInfo1 = new DisplayInfo(); diff --git a/core/tests/coretests/src/android/window/WindowOnBackInvokedDispatcherTest.java b/core/tests/coretests/src/android/window/WindowOnBackInvokedDispatcherTest.java index a709d7be898b..52ff0d4037e8 100644 --- a/core/tests/coretests/src/android/window/WindowOnBackInvokedDispatcherTest.java +++ b/core/tests/coretests/src/android/window/WindowOnBackInvokedDispatcherTest.java @@ -20,6 +20,7 @@ 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; @@ -358,7 +359,7 @@ public class WindowOnBackInvokedDispatcherTest { } @Test - public void onDetachFromWindow_cancelCallbackAndIgnoreOnBackInvoked() throws RemoteException { + public void onDetachFromWindow_cancelsBackAnimation() throws RemoteException { mDispatcher.registerOnBackInvokedCallback(PRIORITY_DEFAULT, mCallback1); OnBackInvokedCallbackInfo callbackInfo = assertSetCallbackInfo(); @@ -368,13 +369,12 @@ public class WindowOnBackInvokedDispatcherTest { waitForIdle(); verify(mCallback1).onBackStarted(any(BackEvent.class)); - // This should trigger mCallback1.onBackCancelled() + // This should trigger mCallback1.onBackCancelled() and unset the callback in WM 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 75b0d4a159d9..145aa60dc495 100644 --- a/core/tests/coretests/src/com/android/internal/accessibility/AccessibilityShortcutControllerTest.java +++ b/core/tests/coretests/src/com/android/internal/accessibility/AccessibilityShortcutControllerTest.java @@ -21,7 +21,6 @@ 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; @@ -83,6 +82,7 @@ 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(ACCESSIBILITY_SHORTCUT_KEY)) + .getAccessibilityShortcutTargets(ShortcutConstants.UserShortcutType.HARDWARE)) .thenReturn(Collections.emptyList()); Settings.Secure.putString(mContentResolver, ACCESSIBILITY_SHORTCUT_TARGET_SERVICE, ""); } private void configureValidShortcutService() throws Exception { when(mAccessibilityManagerService - .getAccessibilityShortcutTargets(ACCESSIBILITY_SHORTCUT_KEY)) + .getAccessibilityShortcutTargets(ShortcutConstants.UserShortcutType.HARDWARE)) .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(ACCESSIBILITY_SHORTCUT_KEY)) + .getAccessibilityShortcutTargets(ShortcutConstants.UserShortcutType.HARDWARE)) .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(ACCESSIBILITY_SHORTCUT_KEY)) + .getAccessibilityShortcutTargets(ShortcutConstants.UserShortcutType.HARDWARE)) .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 69b6a9b7aa87..2ea044ccfb52 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,6 +39,7 @@ 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; @@ -99,7 +100,7 @@ public class InvisibleToggleAccessibilityServiceTargetTest { mSut = new InvisibleToggleAccessibilityServiceTarget( mContextSpy, - AccessibilityManager.ACCESSIBILITY_SHORTCUT_KEY, accessibilityServiceInfo); + ShortcutConstants.UserShortcutType.HARDWARE, accessibilityServiceInfo); } @Test diff --git a/data/etc/privapp-permissions-platform.xml b/data/etc/privapp-permissions-platform.xml index 2de305f8e925..28734283e9b7 100644 --- a/data/etc/privapp-permissions-platform.xml +++ b/data/etc/privapp-permissions-platform.xml @@ -450,7 +450,7 @@ applications that come with the platform <!-- Permissions required for CTS test - android.server.biometrics --> <permission name="android.permission.USE_BIOMETRIC" /> <permission name="android.permission.TEST_BIOMETRIC" /> - <permission name="android.permission.MANAGE_BIOMETRIC_DIALOG" /> + <permission name="android.permission.SET_BIOMETRIC_DIALOG_LOGO" /> <permission name="android.permission.USE_BACKGROUND_FACE_AUTHENTICATION" /> <!-- Permissions required for CTS test - CtsContactsProviderTestCases --> <permission name="android.contacts.permission.MANAGE_SIM_ACCOUNTS" /> @@ -642,5 +642,6 @@ applications that come with the platform <privapp-permissions package="com.android.devicediagnostics"> <permission name="android.permission.READ_PRIVILEGED_PHONE_STATE"/> + <permission name="android.permission.BATTERY_STATS"/> </privapp-permissions> </permissions> diff --git a/libs/WindowManager/Shell/multivalentTests/OWNERS b/libs/WindowManager/Shell/multivalentTests/OWNERS new file mode 100644 index 000000000000..24c1a3a6d400 --- /dev/null +++ b/libs/WindowManager/Shell/multivalentTests/OWNERS @@ -0,0 +1,4 @@ +atsjenk@google.com +liranb@google.com +madym@google.com + diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipDoubleTapHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipDoubleTapHelper.java index 1b1ebc39b558..4cbb78f2dae2 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipDoubleTapHelper.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipDoubleTapHelper.java @@ -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. @@ -14,14 +14,12 @@ * limitations under the License. */ -package com.android.wm.shell.pip.phone; +package com.android.wm.shell.common.pip; import android.annotation.IntDef; import android.annotation.NonNull; import android.graphics.Rect; -import com.android.wm.shell.common.pip.PipBoundsState; - import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -50,9 +48,9 @@ public class PipDoubleTapHelper { @Retention(RetentionPolicy.SOURCE) @interface PipSizeSpec {} - static final int SIZE_SPEC_DEFAULT = 0; - static final int SIZE_SPEC_MAX = 1; - static final int SIZE_SPEC_CUSTOM = 2; + public static final int SIZE_SPEC_DEFAULT = 0; + public static final int SIZE_SPEC_MAX = 1; + public static final int SIZE_SPEC_CUSTOM = 2; /** * Returns MAX or DEFAULT {@link PipSizeSpec} to toggle to/from. @@ -84,7 +82,7 @@ public class PipDoubleTapHelper { * @return pip screen size to switch to */ @PipSizeSpec - static int nextSizeSpec(@NonNull PipBoundsState mPipBoundsState, + public static int nextSizeSpec(@NonNull PipBoundsState mPipBoundsState, @NonNull Rect userResizeBounds) { // is pip screen at its maximum boolean isScreenMax = mPipBoundsState.getBounds().width() diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManager.java index 2dd27430e348..dbf7186def8a 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManager.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManager.java @@ -80,8 +80,7 @@ class CompatUIWindowManager extends CompatUIWindowManagerAbstract { Consumer<Pair<TaskInfo, ShellTaskOrganizer.TaskListener>> onRestartButtonClicked) { super(context, taskInfo, syncQueue, taskListener, displayLayout); mCallback = callback; - mHasSizeCompat = taskInfo.appCompatTaskInfo.topActivityInSizeCompat - && shouldShowSizeCompatRestartButton(taskInfo); + mHasSizeCompat = taskInfo.appCompatTaskInfo.topActivityInSizeCompat; mCameraCompatControlState = taskInfo.appCompatTaskInfo.cameraCompatControlState; mCompatUIHintsState = compatUIHintsState; mCompatUIConfiguration = compatUIConfiguration; @@ -106,7 +105,8 @@ class CompatUIWindowManager extends CompatUIWindowManagerAbstract { @Override protected boolean eligibleToShowLayout() { - return mHasSizeCompat || shouldShowCameraControl(); + return (mHasSizeCompat && shouldShowSizeCompatRestartButton(getLastTaskInfo())) + || shouldShowCameraControl(); } @Override @@ -114,11 +114,6 @@ class CompatUIWindowManager extends CompatUIWindowManagerAbstract { mLayout = inflateLayout(); mLayout.inject(this); - final TaskInfo taskInfo = getLastTaskInfo(); - if (taskInfo != null) { - mHasSizeCompat = mHasSizeCompat && shouldShowSizeCompatRestartButton(taskInfo); - } - updateVisibilityOfViews(); if (mHasSizeCompat) { @@ -139,8 +134,7 @@ class CompatUIWindowManager extends CompatUIWindowManagerAbstract { boolean canShow) { final boolean prevHasSizeCompat = mHasSizeCompat; final int prevCameraCompatControlState = mCameraCompatControlState; - mHasSizeCompat = taskInfo.appCompatTaskInfo.topActivityInSizeCompat - && shouldShowSizeCompatRestartButton(taskInfo); + mHasSizeCompat = taskInfo.appCompatTaskInfo.topActivityInSizeCompat; mCameraCompatControlState = taskInfo.appCompatTaskInfo.cameraCompatControlState; if (!super.updateCompatInfo(taskInfo, taskListener, canShow)) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManagerAbstract.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManagerAbstract.java index 180498c50c78..0564c95aef5c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManagerAbstract.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManagerAbstract.java @@ -332,7 +332,7 @@ public abstract class CompatUIWindowManagerAbstract extends WindowlessWindowMana updateSurfacePosition(); } - @Nullable + @NonNull protected TaskInfo getLastTaskInfo() { return mTaskInfo; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java index 3b48c67a5bbd..7b98fa6523cb 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java @@ -50,15 +50,16 @@ import java.util.Optional; public abstract class Pip2Module { @WMSingleton @Provides - static PipTransition providePipTransition(@NonNull ShellInit shellInit, + static PipTransition providePipTransition(Context context, + @NonNull ShellInit shellInit, @NonNull ShellTaskOrganizer shellTaskOrganizer, @NonNull Transitions transitions, PipBoundsState pipBoundsState, PipBoundsAlgorithm pipBoundsAlgorithm, Optional<PipController> pipController, @NonNull PipScheduler pipScheduler) { - return new PipTransition(shellInit, shellTaskOrganizer, transitions, pipBoundsState, null, - pipBoundsAlgorithm, pipScheduler); + return new PipTransition(context, shellInit, shellTaskOrganizer, transitions, + pipBoundsState, null, pipBoundsAlgorithm, pipScheduler); } @WMSingleton diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java index 04911c0bc064..0e7073688ec4 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java @@ -47,6 +47,7 @@ import com.android.wm.shell.transition.Transitions; import java.io.PrintWriter; import java.util.ArrayList; import java.util.List; +import java.util.function.Consumer; /** * Responsible supplying PiP Transitions. @@ -116,6 +117,17 @@ public abstract class PipTransitionController implements Transitions.TransitionH } /** + * Called when the Shell wants to start resizing Pip transition/animation. + * + * @param onFinishResizeCallback callback guaranteed to execute when animation ends and + * client completes any potential draws upon WM state updates. + */ + public void startResizeTransition(WindowContainerTransaction wct, + Consumer<Rect> onFinishResizeCallback) { + // Default implementation does nothing. + } + + /** * Called when the transition animation can't continue (eg. task is removed during * animation) */ diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchHandler.java index 452a41696fcf..81705e20a1df 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchHandler.java @@ -52,6 +52,7 @@ import com.android.wm.shell.common.FloatingContentCoordinator; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.pip.PipBoundsAlgorithm; import com.android.wm.shell.common.pip.PipBoundsState; +import com.android.wm.shell.common.pip.PipDoubleTapHelper; import com.android.wm.shell.common.pip.PipUiEventLogger; import com.android.wm.shell.common.pip.PipUtils; import com.android.wm.shell.common.pip.SizeSpecSource; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipScheduler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipScheduler.java index 0b8f60e44c7e..57b73b3019f4 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipScheduler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipScheduler.java @@ -24,10 +24,12 @@ import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; +import android.graphics.Rect; import android.view.SurfaceControl; import android.window.WindowContainerToken; import android.window.WindowContainerTransaction; +import androidx.annotation.IntDef; import androidx.annotation.Nullable; import androidx.core.content.ContextCompat; @@ -36,6 +38,10 @@ import com.android.wm.shell.common.pip.PipBoundsState; import com.android.wm.shell.common.pip.PipUtils; import com.android.wm.shell.pip.PipTransitionController; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.function.Consumer; + /** * Scheduler for Shell initiated PiP transitions and animations. */ @@ -58,13 +64,37 @@ public class PipScheduler { private SurfaceControl mPinnedTaskLeash; /** - * A temporary broadcast receiver to initiate exit PiP via expand. - * This will later be modified to be triggered by the PiP menu. + * Temporary PiP CUJ codes to schedule PiP related transitions directly from Shell. + * This is used for a broadcast receiver to resolve intents. This should be removed once + * there is an equivalent of PipTouchHandler and PipResizeGestureHandler for PiP2. + */ + private static final int PIP_EXIT_VIA_EXPAND_CODE = 0; + private static final int PIP_DOUBLE_TAP = 1; + + @IntDef(value = { + PIP_EXIT_VIA_EXPAND_CODE, + PIP_DOUBLE_TAP + }) + @Retention(RetentionPolicy.SOURCE) + @interface PipUserJourneyCode {} + + /** + * A temporary broadcast receiver to initiate PiP CUJs. */ private class PipSchedulerReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { - scheduleExitPipViaExpand(); + int userJourneyCode = intent.getIntExtra("cuj_code_extra", 0); + switch (userJourneyCode) { + case PIP_EXIT_VIA_EXPAND_CODE: + scheduleExitPipViaExpand(); + break; + case PIP_DOUBLE_TAP: + scheduleDoubleTapToResize(); + break; + default: + throw new IllegalStateException("unexpected CUJ code=" + userJourneyCode); + } } } @@ -121,6 +151,23 @@ public class PipScheduler { } } + /** + * Schedules resize PiP via double tap. + */ + public void scheduleDoubleTapToResize() {} + + /** + * Animates resizing of the pinned stack given the duration. + */ + public void scheduleAnimateResizePip(Rect toBounds, Consumer<Rect> onFinishResizeCallback) { + if (mPipTaskToken == null) { + return; + } + WindowContainerTransaction wct = new WindowContainerTransaction(); + wct.setBounds(mPipTaskToken, toBounds); + mPipTransitionController.startResizeTransition(wct, onFinishResizeCallback); + } + void onExitPip() { mPipTaskToken = null; mPinnedTaskLeash = null; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java index 3b0e7c139bed..f3d178aef4ea 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java @@ -22,10 +22,12 @@ import static android.view.WindowManager.TRANSIT_PIP; import static android.view.WindowManager.TRANSIT_TO_FRONT; import static com.android.wm.shell.transition.Transitions.TRANSIT_EXIT_PIP; +import static com.android.wm.shell.transition.Transitions.TRANSIT_RESIZE_PIP; import android.annotation.NonNull; import android.app.ActivityManager; import android.app.PictureInPictureParams; +import android.content.Context; import android.graphics.Rect; import android.os.IBinder; import android.view.SurfaceControl; @@ -36,6 +38,7 @@ import android.window.WindowContainerTransaction; import androidx.annotation.Nullable; +import com.android.wm.shell.R; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.common.pip.PipBoundsAlgorithm; import com.android.wm.shell.common.pip.PipBoundsState; @@ -45,25 +48,29 @@ import com.android.wm.shell.pip.PipTransitionController; import com.android.wm.shell.sysui.ShellInit; import com.android.wm.shell.transition.Transitions; +import java.util.function.Consumer; + /** * Implementation of transitions for PiP on phone. */ public class PipTransition extends PipTransitionController { private static final String TAG = PipTransition.class.getSimpleName(); + private final Context mContext; private final PipScheduler mPipScheduler; @Nullable private WindowContainerToken mPipTaskToken; @Nullable private IBinder mEnterTransition; @Nullable - private IBinder mAutoEnterButtonNavTransition; - @Nullable private IBinder mExitViaExpandTransition; @Nullable - private IBinder mLegacyEnterTransition; + private IBinder mResizeTransition; + + private Consumer<Rect> mFinishResizeCallback; public PipTransition( + Context context, @NonNull ShellInit shellInit, @NonNull ShellTaskOrganizer shellTaskOrganizer, @NonNull Transitions transitions, @@ -74,6 +81,7 @@ public class PipTransition extends PipTransitionController { super(shellInit, shellTaskOrganizer, transitions, pipBoundsState, pipMenuController, pipBoundsAlgorithm); + mContext = context; mPipScheduler = pipScheduler; mPipScheduler.setPipTransitionController(this); } @@ -87,7 +95,7 @@ public class PipTransition extends PipTransitionController { @Override public void startExitTransition(int type, WindowContainerTransaction out, - @android.annotation.Nullable Rect destinationBounds) { + @Nullable Rect destinationBounds) { if (out == null) { return; } @@ -97,6 +105,16 @@ public class PipTransition extends PipTransitionController { } } + @Override + public void startResizeTransition(WindowContainerTransaction wct, + Consumer<Rect> onFinishResizeCallback) { + if (wct == null) { + return; + } + mResizeTransition = mTransitions.startTransition(TRANSIT_RESIZE_PIP, wct, this); + mFinishResizeCallback = onFinishResizeCallback; + } + @Nullable @Override public WindowContainerTransaction handleRequest(@NonNull IBinder transition, @@ -126,43 +144,6 @@ public class PipTransition extends PipTransitionController { public void onTransitionConsumed(@NonNull IBinder transition, boolean aborted, @Nullable SurfaceControl.Transaction finishT) {} - private WindowContainerTransaction getEnterPipTransaction(@NonNull IBinder transition, - @NonNull TransitionRequestInfo request) { - // cache the original task token to check for multi-activity case later - final ActivityManager.RunningTaskInfo pipTask = request.getPipTask(); - PictureInPictureParams pipParams = pipTask.pictureInPictureParams; - mPipBoundsState.setBoundsStateForEntry(pipTask.topActivity, pipTask.topActivityInfo, - pipParams, mPipBoundsAlgorithm); - - // calculate the entry bounds and notify core to move task to pinned with final bounds - final Rect entryBounds = mPipBoundsAlgorithm.getEntryDestinationBounds(); - mPipBoundsState.setBounds(entryBounds); - - WindowContainerTransaction wct = new WindowContainerTransaction(); - wct.movePipActivityToPinnedRootTask(pipTask.token, entryBounds); - return wct; - } - - private boolean isAutoEnterInButtonNavigation(@NonNull TransitionRequestInfo requestInfo) { - final ActivityManager.RunningTaskInfo pipTask = requestInfo.getPipTask(); - if (pipTask == null) { - return false; - } - if (pipTask.pictureInPictureParams == null) { - return false; - } - - // Assuming auto-enter is enabled and pipTask is non-null, the TRANSIT_OPEN request type - // implies that we are entering PiP in button navigation mode. This is guaranteed by - // TaskFragment#startPausing()` in Core which wouldn't get called in gesture nav. - return requestInfo.getType() == TRANSIT_OPEN - && pipTask.pictureInPictureParams.isAutoEnterEnabled(); - } - - private boolean isEnterPictureInPictureModeRequest(@NonNull TransitionRequestInfo requestInfo) { - return requestInfo.getType() == TRANSIT_PIP; - } - @Override public boolean startAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, @@ -182,16 +163,48 @@ public class PipTransition extends PipTransitionController { } else if (transition == mExitViaExpandTransition) { mExitViaExpandTransition = null; return startExpandAnimation(info, startTransaction, finishTransaction, finishCallback); + } else if (transition == mResizeTransition) { + mResizeTransition = null; + return startResizeAnimation(info, startTransaction, finishTransaction, finishCallback); } return false; } - private boolean isLegacyEnter(@NonNull TransitionInfo info) { + private boolean startResizeAnimation(@NonNull TransitionInfo info, + @NonNull SurfaceControl.Transaction startTransaction, + @NonNull SurfaceControl.Transaction finishTransaction, + @NonNull Transitions.TransitionFinishCallback finishCallback) { TransitionInfo.Change pipChange = getPipChange(info); - // If the only change in the changes list is a TO_FRONT mode PiP task, - // then this is legacy-enter PiP. - return pipChange != null && pipChange.getMode() == TRANSIT_TO_FRONT - && info.getChanges().size() == 1; + if (pipChange == null) { + return false; + } + SurfaceControl pipLeash = pipChange.getLeash(); + Rect destinationBounds = pipChange.getEndAbsBounds(); + + // Even though the final bounds and crop are applied with finishTransaction since + // this is a visible change, we still need to handle the app draw coming in. Snapshot + // covering app draw during collection will be removed by startTransaction. So we make + // the crop equal to the final bounds and then scale the leash back to starting bounds. + startTransaction.setWindowCrop(pipLeash, pipChange.getEndAbsBounds().width(), + pipChange.getEndAbsBounds().height()); + startTransaction.setScale(pipLeash, + (float) mPipBoundsState.getBounds().width() / destinationBounds.width(), + (float) mPipBoundsState.getBounds().height() / destinationBounds.height()); + startTransaction.apply(); + + finishTransaction.setScale(pipLeash, + (float) mPipBoundsState.getBounds().width() / destinationBounds.width(), + (float) mPipBoundsState.getBounds().height() / destinationBounds.height()); + + // We are done with the transition, but will continue animating leash to final bounds. + finishCallback.onTransitionFinished(null); + + // Animate the pip leash with the new buffer + final int duration = mContext.getResources().getInteger( + R.integer.config_pipResizeAnimationDuration); + // TODO: b/275910498 Couple this routine with a new implementation of the PiP animator. + startResizeAnimation(pipLeash, mPipBoundsState.getBounds(), destinationBounds, duration); + return true; } private boolean startBoundsTypeEnterAnimation(@NonNull TransitionInfo info, @@ -251,6 +264,57 @@ public class PipTransition extends PipTransitionController { return null; } + private WindowContainerTransaction getEnterPipTransaction(@NonNull IBinder transition, + @NonNull TransitionRequestInfo request) { + // cache the original task token to check for multi-activity case later + final ActivityManager.RunningTaskInfo pipTask = request.getPipTask(); + PictureInPictureParams pipParams = pipTask.pictureInPictureParams; + mPipBoundsState.setBoundsStateForEntry(pipTask.topActivity, pipTask.topActivityInfo, + pipParams, mPipBoundsAlgorithm); + + // calculate the entry bounds and notify core to move task to pinned with final bounds + final Rect entryBounds = mPipBoundsAlgorithm.getEntryDestinationBounds(); + mPipBoundsState.setBounds(entryBounds); + + WindowContainerTransaction wct = new WindowContainerTransaction(); + wct.movePipActivityToPinnedRootTask(pipTask.token, entryBounds); + return wct; + } + + private boolean isAutoEnterInButtonNavigation(@NonNull TransitionRequestInfo requestInfo) { + final ActivityManager.RunningTaskInfo pipTask = requestInfo.getPipTask(); + if (pipTask == null) { + return false; + } + if (pipTask.pictureInPictureParams == null) { + return false; + } + + // Assuming auto-enter is enabled and pipTask is non-null, the TRANSIT_OPEN request type + // implies that we are entering PiP in button navigation mode. This is guaranteed by + // TaskFragment#startPausing()` in Core which wouldn't get called in gesture nav. + return requestInfo.getType() == TRANSIT_OPEN + && pipTask.pictureInPictureParams.isAutoEnterEnabled(); + } + + private boolean isEnterPictureInPictureModeRequest(@NonNull TransitionRequestInfo requestInfo) { + return requestInfo.getType() == TRANSIT_PIP; + } + + private boolean isLegacyEnter(@NonNull TransitionInfo info) { + TransitionInfo.Change pipChange = getPipChange(info); + // If the only change in the changes list is a TO_FRONT mode PiP task, + // then this is legacy-enter PiP. + return pipChange != null && pipChange.getMode() == TRANSIT_TO_FRONT + && info.getChanges().size() == 1; + } + + /** + * TODO: b/275910498 Use a new implementation of the PiP animator here. + */ + private void startResizeAnimation(SurfaceControl leash, Rect startBounds, + Rect endBounds, int duration) {} + private void onExitPip() { mPipTaskToken = null; mPipScheduler.onExitPip(); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/ISplitScreen.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/ISplitScreen.aidl index 253acc49071a..0ca244c4b96a 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/ISplitScreen.aidl +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/ISplitScreen.aidl @@ -158,5 +158,10 @@ interface ISplitScreen { * does not expect split to currently be running. */ RemoteAnimationTarget[] onStartingSplitLegacy(in RemoteAnimationTarget[] appTargets) = 14; + + /** + * Reverse the split. + */ + oneway void switchSplitPosition() = 22; } -// Last id = 21
\ No newline at end of file +// Last id = 22
\ No newline at end of file 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 2ec52bb028c6..70cb2fc6d52c 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 @@ -1109,6 +1109,12 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, mStageCoordinator.onDroppedToSplit(position, dragSessionId); } + void switchSplitPosition(String reason) { + if (isSplitScreenVisible()) { + mStageCoordinator.switchSplitPosition(reason); + } + } + /** * Return the {@param exitReason} as a string. */ @@ -1473,5 +1479,11 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, true /* blocking */); return out[0]; } + + @Override + public void switchSplitPosition() { + executeRemoteCallWithTaskPermission(mController, "switchSplitPosition", + (controller) -> controller.switchSplitPosition("remoteCall")); + } } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenShellCommandHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenShellCommandHandler.java index 7fd03a9a306b..7f16c5e3592e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenShellCommandHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenShellCommandHandler.java @@ -43,6 +43,8 @@ public class SplitScreenShellCommandHandler implements return runRemoveFromSideStage(args, pw); case "setSideStagePosition": return runSetSideStagePosition(args, pw); + case "switchSplitPosition": + return runSwitchSplitPosition(); default: pw.println("Invalid command: " + args[0]); return false; @@ -84,6 +86,11 @@ public class SplitScreenShellCommandHandler implements return true; } + private boolean runSwitchSplitPosition() { + mController.switchSplitPosition("shellCommand"); + return true; + } + @Override public void printShellCommandHelp(PrintWriter pw, String prefix) { pw.println(prefix + "moveToSideStage <taskId> <SideStagePosition>"); @@ -92,5 +99,7 @@ public class SplitScreenShellCommandHandler implements pw.println(prefix + " Remove a task with given id in split-screen mode."); pw.println(prefix + "setSideStagePosition <SideStagePosition>"); pw.println(prefix + " Sets the position of the side-stage."); + pw.println(prefix + "switchSplitPosition"); + pw.println(prefix + " Reverses the split."); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java index 3fb0dbfaa63d..67fc7e2b4ea6 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java @@ -175,6 +175,9 @@ public class Transitions implements RemoteCallable<Transitions>, /** Transition to animate task to desktop. */ public static final int TRANSIT_MOVE_TO_DESKTOP = WindowManager.TRANSIT_FIRST_CUSTOM + 15; + /** Transition to resize PiP task. */ + public static final int TRANSIT_RESIZE_PIP = TRANSIT_FIRST_CUSTOM + 16; + private final ShellTaskOrganizer mOrganizer; private final Context mContext; private final ShellExecutor mMainExecutor; 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 554b1fb99550..4ba05ce8aef1 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 @@ -32,6 +32,7 @@ import static com.android.wm.shell.windowdecor.MoveToDesktopAnimator.DRAG_FREEFO import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.ValueAnimator; +import android.annotation.NonNull; import android.app.ActivityManager; import android.app.ActivityManager.RunningTaskInfo; import android.app.ActivityTaskManager; @@ -60,7 +61,6 @@ import android.view.ViewConfiguration; import android.window.WindowContainerToken; import android.window.WindowContainerTransaction; -import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.android.internal.annotations.VisibleForTesting; @@ -544,12 +544,22 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { return true; } + /** + * Perform a task size toggle on release of the double-tap, assuming no drag event + * was handled during the double-tap. + * @param e The motion event that occurred during the double-tap gesture. + * @return true if the event should be consumed, false if not + */ @Override - public boolean onDoubleTap(@NonNull MotionEvent e) { + public boolean onDoubleTapEvent(@NonNull MotionEvent e) { + final int action = e.getActionMasked(); + if (mIsDragging || (action != MotionEvent.ACTION_UP + && action != MotionEvent.ACTION_CANCEL)) { + return false; + } final RunningTaskInfo taskInfo = mTaskOrganizer.getRunningTaskInfo(mTaskId); - mDesktopTasksController.ifPresent(c -> { - c.toggleDesktopTaskSize(taskInfo, mWindowDecorByTaskId.get(taskInfo.taskId)); - }); + mDesktopTasksController.ifPresent(c -> c.toggleDesktopTaskSize(taskInfo, + mWindowDecorByTaskId.get(taskInfo.taskId))); return true; } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUILayoutTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUILayoutTest.java index 4ddc539eb220..dd358e757fde 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUILayoutTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUILayoutTest.java @@ -30,6 +30,7 @@ import static org.mockito.Mockito.verify; import android.app.ActivityManager; import android.app.AppCompatTaskInfo.CameraCompatControlState; import android.app.TaskInfo; +import android.graphics.Rect; import android.testing.AndroidTestingRunner; import android.util.Pair; import android.view.LayoutInflater; @@ -83,6 +84,7 @@ public class CompatUILayoutTest extends ShellTestCase { @Before public void setUp() { MockitoAnnotations.initMocks(this); + doReturn(100).when(mCompatUIConfiguration).getHideSizeCompatRestartButtonTolerance(); mTaskInfo = createTaskInfo(/* hasSizeCompat= */ false, CAMERA_COMPAT_CONTROL_HIDDEN); mWindowManager = new CompatUIWindowManager(mContext, mTaskInfo, mSyncTransactionQueue, mCallback, mTaskListener, new DisplayLayout(), new CompatUIHintsState(), @@ -127,7 +129,6 @@ public class CompatUILayoutTest extends ShellTestCase { @Test public void testOnClickForSizeCompatHint() { mWindowManager.mHasSizeCompat = true; - doReturn(true).when(mWindowManager).shouldShowSizeCompatRestartButton(mTaskInfo); mWindowManager.createLayout(/* canShow= */ true); final LinearLayout sizeCompatHint = mLayout.findViewById(R.id.size_compat_hint); sizeCompatHint.performClick(); @@ -222,6 +223,9 @@ public class CompatUILayoutTest extends ShellTestCase { taskInfo.taskId = TASK_ID; taskInfo.appCompatTaskInfo.topActivityInSizeCompat = hasSizeCompat; taskInfo.appCompatTaskInfo.cameraCompatControlState = cameraCompatControlState; + taskInfo.appCompatTaskInfo.topActivityLetterboxHeight = 1000; + taskInfo.appCompatTaskInfo.topActivityLetterboxWidth = 1000; + taskInfo.configuration.windowConfiguration.setBounds(new Rect(0, 0, 2000, 2000)); return taskInfo; } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIWindowManagerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIWindowManagerTest.java index 2acfd83084ab..4f261cd79d39 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIWindowManagerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIWindowManagerTest.java @@ -20,7 +20,6 @@ import static android.app.AppCompatTaskInfo.CAMERA_COMPAT_CONTROL_DISMISSED; import static android.app.AppCompatTaskInfo.CAMERA_COMPAT_CONTROL_HIDDEN; import static android.app.AppCompatTaskInfo.CAMERA_COMPAT_CONTROL_TREATMENT_APPLIED; import static android.app.AppCompatTaskInfo.CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED; -import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; import static android.platform.test.flag.junit.SetFlagsRule.DefaultInitValueType.DEVICE_DEFAULT; import static android.view.WindowInsets.Type.navigationBars; @@ -86,6 +85,8 @@ public class CompatUIWindowManagerTest extends ShellTestCase { public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(DEVICE_DEFAULT); private static final int TASK_ID = 1; + private static final int TASK_WIDTH = 2000; + private static final int TASK_HEIGHT = 2000; @Mock private SyncTransactionQueue mSyncTransactionQueue; @Mock private CompatUIController.CompatUICallback mCallback; @@ -101,6 +102,7 @@ public class CompatUIWindowManagerTest extends ShellTestCase { @Before public void setUp() { MockitoAnnotations.initMocks(this); + doReturn(100).when(mCompatUIConfiguration).getHideSizeCompatRestartButtonTolerance(); mTaskInfo = createTaskInfo(/* hasSizeCompat= */ false, CAMERA_COMPAT_CONTROL_HIDDEN); mWindowManager = new CompatUIWindowManager(mContext, mTaskInfo, mSyncTransactionQueue, mCallback, mTaskListener, new DisplayLayout(), new CompatUIHintsState(), @@ -115,7 +117,6 @@ public class CompatUIWindowManagerTest extends ShellTestCase { public void testCreateSizeCompatButton() { // Doesn't create layout if show is false. mWindowManager.mHasSizeCompat = true; - doReturn(true).when(mWindowManager).shouldShowSizeCompatRestartButton(mTaskInfo); assertTrue(mWindowManager.createLayout(/* canShow= */ false)); verify(mWindowManager, never()).inflateLayout(); @@ -147,6 +148,13 @@ public class CompatUIWindowManagerTest extends ShellTestCase { mWindowManager.mHasSizeCompat = false; assertFalse(mWindowManager.createLayout(/* canShow= */ true)); + // Returns false and doesn't create layout if restart button should be hidden. + clearInvocations(mWindowManager); + mWindowManager.mHasSizeCompat = true; + mTaskInfo.appCompatTaskInfo.topActivityLetterboxWidth = TASK_WIDTH; + mTaskInfo.appCompatTaskInfo.topActivityLetterboxHeight = TASK_HEIGHT; + assertFalse(mWindowManager.createLayout(/* canShow= */ true)); + verify(mWindowManager, never()).inflateLayout(); } @@ -293,8 +301,6 @@ public class CompatUIWindowManagerTest extends ShellTestCase { @Test public void testUpdateCompatInfoLayoutNotInflatedYet() { - mWindowManager.mHasSizeCompat = true; - doReturn(true).when(mWindowManager).shouldShowSizeCompatRestartButton(any()); mWindowManager.createLayout(/* canShow= */ false); verify(mWindowManager, never()).inflateLayout(); @@ -314,6 +320,15 @@ public class CompatUIWindowManagerTest extends ShellTestCase { mWindowManager.updateCompatInfo(taskInfo, mTaskListener, /* canShow= */ true); verify(mWindowManager).inflateLayout(); + + // Change shouldShowSizeCompatRestartButton to false and pass canShow true, layout + // shouldn't be inflated + clearInvocations(mWindowManager); + taskInfo.appCompatTaskInfo.topActivityLetterboxWidth = TASK_WIDTH; + taskInfo.appCompatTaskInfo.topActivityLetterboxHeight = TASK_HEIGHT; + mWindowManager.updateCompatInfo(taskInfo, mTaskListener, /* canShow= */ true); + + verify(mWindowManager, never()).inflateLayout(); } @Test @@ -364,7 +379,6 @@ public class CompatUIWindowManagerTest extends ShellTestCase { // Create button if it is not created. mWindowManager.mLayout = null; mWindowManager.mHasSizeCompat = true; - doReturn(true).when(mWindowManager).shouldShowSizeCompatRestartButton(mTaskInfo); mWindowManager.updateVisibility(/* canShow= */ true); verify(mWindowManager).createLayout(/* canShow= */ true); @@ -489,7 +503,6 @@ public class CompatUIWindowManagerTest extends ShellTestCase { TaskInfo taskInfo = createTaskInfo(true, CAMERA_COMPAT_CONTROL_HIDDEN); taskInfo.configuration.windowConfiguration.setBounds(new Rect(0, 0, 2000, 2000)); taskInfo.appCompatTaskInfo.topActivityLetterboxHeight = 2000; - taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN); taskInfo.appCompatTaskInfo.topActivityLetterboxWidth = 1850; assertFalse(mWindowManager.shouldShowSizeCompatRestartButton(taskInfo)); @@ -514,6 +527,11 @@ public class CompatUIWindowManagerTest extends ShellTestCase { taskInfo.appCompatTaskInfo.topActivityInSizeCompat = hasSizeCompat; taskInfo.appCompatTaskInfo.cameraCompatControlState = cameraCompatControlState; taskInfo.configuration.uiMode &= ~Configuration.UI_MODE_TYPE_DESK; + // Letterboxed activity that takes half the screen should show size compat restart button + taskInfo.configuration.windowConfiguration.setBounds( + new Rect(0, 0, TASK_WIDTH, TASK_HEIGHT)); + taskInfo.appCompatTaskInfo.topActivityLetterboxHeight = 1000; + taskInfo.appCompatTaskInfo.topActivityLetterboxWidth = 1000; return taskInfo; } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipDoubleTapHelperTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipDoubleTapHelperTest.java index 0f8db85dcef4..b583acda1c9a 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipDoubleTapHelperTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipDoubleTapHelperTest.java @@ -16,10 +16,10 @@ package com.android.wm.shell.pip.phone; -import static com.android.wm.shell.pip.phone.PipDoubleTapHelper.SIZE_SPEC_CUSTOM; -import static com.android.wm.shell.pip.phone.PipDoubleTapHelper.SIZE_SPEC_DEFAULT; -import static com.android.wm.shell.pip.phone.PipDoubleTapHelper.SIZE_SPEC_MAX; -import static com.android.wm.shell.pip.phone.PipDoubleTapHelper.nextSizeSpec; +import static com.android.wm.shell.common.pip.PipDoubleTapHelper.SIZE_SPEC_CUSTOM; +import static com.android.wm.shell.common.pip.PipDoubleTapHelper.SIZE_SPEC_DEFAULT; +import static com.android.wm.shell.common.pip.PipDoubleTapHelper.SIZE_SPEC_MAX; +import static com.android.wm.shell.common.pip.PipDoubleTapHelper.nextSizeSpec; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -30,6 +30,7 @@ import android.testing.AndroidTestingRunner; import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.common.pip.PipBoundsState; +import com.android.wm.shell.common.pip.PipDoubleTapHelper; import org.junit.Assert; import org.junit.Before; @@ -38,7 +39,7 @@ import org.junit.runner.RunWith; import org.mockito.Mock; /** - * Unit test against {@link PipDoubleTapHelper}. + * Unit test against {@link com.android.wm.shell.common.pip.PipDoubleTapHelper}. */ @RunWith(AndroidTestingRunner.class) public class PipDoubleTapHelperTest extends ShellTestCase { diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java index 12a5594ae1da..7f3bfbb0e81d 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java @@ -421,6 +421,15 @@ public class SplitScreenControllerTests extends ShellTestCase { assertEquals(false, controller.supportsMultiInstanceSplit(component)); } + @Test + public void testSwitchSplitPosition_checksIsSplitScreenVisible() { + final String reason = "test"; + when(mSplitScreenController.isSplitScreenVisible()).thenReturn(true, false); + mSplitScreenController.switchSplitPosition(reason); + mSplitScreenController.switchSplitPosition(reason); + verify(mStageCoordinator, times(1)).switchSplitPosition(reason); + } + private Intent createStartIntent(String activityName) { Intent intent = new Intent(); intent.setComponent(new ComponentName(mContext, activityName)); diff --git a/libs/hwui/Android.bp b/libs/hwui/Android.bp index b40b73c111d0..4e330da417be 100644 --- a/libs/hwui/Android.bp +++ b/libs/hwui/Android.bp @@ -229,15 +229,6 @@ filegroup { path: "apex/java", } -java_api_contribution { - name: "framework-graphics-public-stubs", - api_surface: "public", - api_file: "api/current.txt", - visibility: [ - "//build/orchestrator/apis", - ], -} - // ------------------------ // APEX // ------------------------ @@ -638,14 +629,6 @@ cc_defaults { // Allow implicit fallthroughs in HardwareBitmapUploader.cpp until they are fixed. cflags: ["-Wno-implicit-fallthrough"], }, - host: { - srcs: [ - "utils/HostColorSpace.cpp", - ], - export_static_lib_headers: [ - "libarect", - ], - }, }, } diff --git a/libs/hwui/utils/HostColorSpace.cpp b/libs/hwui/utils/HostColorSpace.cpp deleted file mode 100644 index 77a6820c6999..000000000000 --- a/libs/hwui/utils/HostColorSpace.cpp +++ /dev/null @@ -1,417 +0,0 @@ -/* - * Copyright (C) 2019 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -// This is copied from framework/native/libs/ui in order not to include libui in host build - -#include <ui/ColorSpace.h> - -using namespace std::placeholders; - -namespace android { - -static constexpr float linearResponse(float v) { - return v; -} - -static constexpr float rcpResponse(float x, const ColorSpace::TransferParameters& p) { - return x >= p.d * p.c ? (std::pow(x, 1.0f / p.g) - p.b) / p.a : x / p.c; -} - -static constexpr float response(float x, const ColorSpace::TransferParameters& p) { - return x >= p.d ? std::pow(p.a * x + p.b, p.g) : p.c * x; -} - -static constexpr float rcpFullResponse(float x, const ColorSpace::TransferParameters& p) { - return x >= p.d * p.c ? (std::pow(x - p.e, 1.0f / p.g) - p.b) / p.a : (x - p.f) / p.c; -} - -static constexpr float fullResponse(float x, const ColorSpace::TransferParameters& p) { - return x >= p.d ? std::pow(p.a * x + p.b, p.g) + p.e : p.c * x + p.f; -} - -static float absRcpResponse(float x, float g,float a, float b, float c, float d) { - float xx = std::abs(x); - return std::copysign(xx >= d * c ? (std::pow(xx, 1.0f / g) - b) / a : xx / c, x); -} - -static float absResponse(float x, float g, float a, float b, float c, float d) { - float xx = std::abs(x); - return std::copysign(xx >= d ? std::pow(a * xx + b, g) : c * xx, x); -} - -static float safePow(float x, float e) { - return powf(x < 0.0f ? 0.0f : x, e); -} - -static ColorSpace::transfer_function toOETF(const ColorSpace::TransferParameters& parameters) { - if (parameters.e == 0.0f && parameters.f == 0.0f) { - return std::bind(rcpResponse, _1, parameters); - } - return std::bind(rcpFullResponse, _1, parameters); -} - -static ColorSpace::transfer_function toEOTF( const ColorSpace::TransferParameters& parameters) { - if (parameters.e == 0.0f && parameters.f == 0.0f) { - return std::bind(response, _1, parameters); - } - return std::bind(fullResponse, _1, parameters); -} - -static ColorSpace::transfer_function toOETF(float gamma) { - if (gamma == 1.0f) { - return linearResponse; - } - return std::bind(safePow, _1, 1.0f / gamma); -} - -static ColorSpace::transfer_function toEOTF(float gamma) { - if (gamma == 1.0f) { - return linearResponse; - } - return std::bind(safePow, _1, gamma); -} - -static constexpr std::array<float2, 3> computePrimaries(const mat3& rgbToXYZ) { - float3 r(rgbToXYZ * float3{1, 0, 0}); - float3 g(rgbToXYZ * float3{0, 1, 0}); - float3 b(rgbToXYZ * float3{0, 0, 1}); - - return {{r.xy / dot(r, float3{1}), - g.xy / dot(g, float3{1}), - b.xy / dot(b, float3{1})}}; -} - -static constexpr float2 computeWhitePoint(const mat3& rgbToXYZ) { - float3 w(rgbToXYZ * float3{1}); - return w.xy / dot(w, float3{1}); -} - -ColorSpace::ColorSpace( - const std::string& name, - const mat3& rgbToXYZ, - transfer_function OETF, - transfer_function EOTF, - clamping_function clamper) noexcept - : mName(name) - , mRGBtoXYZ(rgbToXYZ) - , mXYZtoRGB(inverse(rgbToXYZ)) - , mOETF(std::move(OETF)) - , mEOTF(std::move(EOTF)) - , mClamper(std::move(clamper)) - , mPrimaries(computePrimaries(rgbToXYZ)) - , mWhitePoint(computeWhitePoint(rgbToXYZ)) { -} - -ColorSpace::ColorSpace( - const std::string& name, - const mat3& rgbToXYZ, - const TransferParameters parameters, - clamping_function clamper) noexcept - : mName(name) - , mRGBtoXYZ(rgbToXYZ) - , mXYZtoRGB(inverse(rgbToXYZ)) - , mParameters(parameters) - , mOETF(toOETF(mParameters)) - , mEOTF(toEOTF(mParameters)) - , mClamper(std::move(clamper)) - , mPrimaries(computePrimaries(rgbToXYZ)) - , mWhitePoint(computeWhitePoint(rgbToXYZ)) { -} - -ColorSpace::ColorSpace( - const std::string& name, - const mat3& rgbToXYZ, - float gamma, - clamping_function clamper) noexcept - : mName(name) - , mRGBtoXYZ(rgbToXYZ) - , mXYZtoRGB(inverse(rgbToXYZ)) - , mParameters({gamma, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}) - , mOETF(toOETF(gamma)) - , mEOTF(toEOTF(gamma)) - , mClamper(std::move(clamper)) - , mPrimaries(computePrimaries(rgbToXYZ)) - , mWhitePoint(computeWhitePoint(rgbToXYZ)) { -} - -ColorSpace::ColorSpace( - const std::string& name, - const std::array<float2, 3>& primaries, - const float2& whitePoint, - transfer_function OETF, - transfer_function EOTF, - clamping_function clamper) noexcept - : mName(name) - , mRGBtoXYZ(computeXYZMatrix(primaries, whitePoint)) - , mXYZtoRGB(inverse(mRGBtoXYZ)) - , mOETF(std::move(OETF)) - , mEOTF(std::move(EOTF)) - , mClamper(std::move(clamper)) - , mPrimaries(primaries) - , mWhitePoint(whitePoint) { -} - -ColorSpace::ColorSpace( - const std::string& name, - const std::array<float2, 3>& primaries, - const float2& whitePoint, - const TransferParameters parameters, - clamping_function clamper) noexcept - : mName(name) - , mRGBtoXYZ(computeXYZMatrix(primaries, whitePoint)) - , mXYZtoRGB(inverse(mRGBtoXYZ)) - , mParameters(parameters) - , mOETF(toOETF(mParameters)) - , mEOTF(toEOTF(mParameters)) - , mClamper(std::move(clamper)) - , mPrimaries(primaries) - , mWhitePoint(whitePoint) { -} - -ColorSpace::ColorSpace( - const std::string& name, - const std::array<float2, 3>& primaries, - const float2& whitePoint, - float gamma, - clamping_function clamper) noexcept - : mName(name) - , mRGBtoXYZ(computeXYZMatrix(primaries, whitePoint)) - , mXYZtoRGB(inverse(mRGBtoXYZ)) - , mParameters({gamma, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}) - , mOETF(toOETF(gamma)) - , mEOTF(toEOTF(gamma)) - , mClamper(std::move(clamper)) - , mPrimaries(primaries) - , mWhitePoint(whitePoint) { -} - -constexpr mat3 ColorSpace::computeXYZMatrix( - const std::array<float2, 3>& primaries, const float2& whitePoint) { - const float2& R = primaries[0]; - const float2& G = primaries[1]; - const float2& B = primaries[2]; - const float2& W = whitePoint; - - float oneRxRy = (1 - R.x) / R.y; - float oneGxGy = (1 - G.x) / G.y; - float oneBxBy = (1 - B.x) / B.y; - float oneWxWy = (1 - W.x) / W.y; - - float RxRy = R.x / R.y; - float GxGy = G.x / G.y; - float BxBy = B.x / B.y; - float WxWy = W.x / W.y; - - float BY = - ((oneWxWy - oneRxRy) * (GxGy - RxRy) - (WxWy - RxRy) * (oneGxGy - oneRxRy)) / - ((oneBxBy - oneRxRy) * (GxGy - RxRy) - (BxBy - RxRy) * (oneGxGy - oneRxRy)); - float GY = (WxWy - RxRy - BY * (BxBy - RxRy)) / (GxGy - RxRy); - float RY = 1 - GY - BY; - - float RYRy = RY / R.y; - float GYGy = GY / G.y; - float BYBy = BY / B.y; - - return { - float3{RYRy * R.x, RY, RYRy * (1 - R.x - R.y)}, - float3{GYGy * G.x, GY, GYGy * (1 - G.x - G.y)}, - float3{BYBy * B.x, BY, BYBy * (1 - B.x - B.y)} - }; -} - -const ColorSpace ColorSpace::sRGB() { - return { - "sRGB IEC61966-2.1", - {{float2{0.640f, 0.330f}, {0.300f, 0.600f}, {0.150f, 0.060f}}}, - {0.3127f, 0.3290f}, - {2.4f, 1 / 1.055f, 0.055f / 1.055f, 1 / 12.92f, 0.04045f, 0.0f, 0.0f} - }; -} - -const ColorSpace ColorSpace::linearSRGB() { - return { - "sRGB IEC61966-2.1 (Linear)", - {{float2{0.640f, 0.330f}, {0.300f, 0.600f}, {0.150f, 0.060f}}}, - {0.3127f, 0.3290f} - }; -} - -const ColorSpace ColorSpace::extendedSRGB() { - return { - "scRGB-nl IEC 61966-2-2:2003", - {{float2{0.640f, 0.330f}, {0.300f, 0.600f}, {0.150f, 0.060f}}}, - {0.3127f, 0.3290f}, - std::bind(absRcpResponse, _1, 2.4f, 1 / 1.055f, 0.055f / 1.055f, 1 / 12.92f, 0.04045f), - std::bind(absResponse, _1, 2.4f, 1 / 1.055f, 0.055f / 1.055f, 1 / 12.92f, 0.04045f), - std::bind(clamp<float>, _1, -0.799f, 2.399f) - }; -} - -const ColorSpace ColorSpace::linearExtendedSRGB() { - return { - "scRGB IEC 61966-2-2:2003", - {{float2{0.640f, 0.330f}, {0.300f, 0.600f}, {0.150f, 0.060f}}}, - {0.3127f, 0.3290f}, - 1.0f, - std::bind(clamp<float>, _1, -0.5f, 7.499f) - }; -} - -const ColorSpace ColorSpace::NTSC() { - return { - "NTSC (1953)", - {{float2{0.67f, 0.33f}, {0.21f, 0.71f}, {0.14f, 0.08f}}}, - {0.310f, 0.316f}, - {1 / 0.45f, 1 / 1.099f, 0.099f / 1.099f, 1 / 4.5f, 0.081f, 0.0f, 0.0f} - }; -} - -const ColorSpace ColorSpace::BT709() { - return { - "Rec. ITU-R BT.709-5", - {{float2{0.640f, 0.330f}, {0.300f, 0.600f}, {0.150f, 0.060f}}}, - {0.3127f, 0.3290f}, - {1 / 0.45f, 1 / 1.099f, 0.099f / 1.099f, 1 / 4.5f, 0.081f, 0.0f, 0.0f} - }; -} - -const ColorSpace ColorSpace::BT2020() { - return { - "Rec. ITU-R BT.2020-1", - {{float2{0.708f, 0.292f}, {0.170f, 0.797f}, {0.131f, 0.046f}}}, - {0.3127f, 0.3290f}, - {1 / 0.45f, 1 / 1.099f, 0.099f / 1.099f, 1 / 4.5f, 0.081f, 0.0f, 0.0f} - }; -} - -const ColorSpace ColorSpace::AdobeRGB() { - return { - "Adobe RGB (1998)", - {{float2{0.64f, 0.33f}, {0.21f, 0.71f}, {0.15f, 0.06f}}}, - {0.3127f, 0.3290f}, - 2.2f - }; -} - -const ColorSpace ColorSpace::ProPhotoRGB() { - return { - "ROMM RGB ISO 22028-2:2013", - {{float2{0.7347f, 0.2653f}, {0.1596f, 0.8404f}, {0.0366f, 0.0001f}}}, - {0.34567f, 0.35850f}, - {1.8f, 1.0f, 0.0f, 1 / 16.0f, 0.031248f, 0.0f, 0.0f} - }; -} - -const ColorSpace ColorSpace::DisplayP3() { - return { - "Display P3", - {{float2{0.680f, 0.320f}, {0.265f, 0.690f}, {0.150f, 0.060f}}}, - {0.3127f, 0.3290f}, - {2.4f, 1 / 1.055f, 0.055f / 1.055f, 1 / 12.92f, 0.039f, 0.0f, 0.0f} - }; -} - -const ColorSpace ColorSpace::DCIP3() { - return { - "SMPTE RP 431-2-2007 DCI (P3)", - {{float2{0.680f, 0.320f}, {0.265f, 0.690f}, {0.150f, 0.060f}}}, - {0.314f, 0.351f}, - 2.6f - }; -} - -const ColorSpace ColorSpace::ACES() { - return { - "SMPTE ST 2065-1:2012 ACES", - {{float2{0.73470f, 0.26530f}, {0.0f, 1.0f}, {0.00010f, -0.0770f}}}, - {0.32168f, 0.33767f}, - 1.0f, - std::bind(clamp<float>, _1, -65504.0f, 65504.0f) - }; -} - -const ColorSpace ColorSpace::ACEScg() { - return { - "Academy S-2014-004 ACEScg", - {{float2{0.713f, 0.293f}, {0.165f, 0.830f}, {0.128f, 0.044f}}}, - {0.32168f, 0.33767f}, - 1.0f, - std::bind(clamp<float>, _1, -65504.0f, 65504.0f) - }; -} - -std::unique_ptr<float3[]> ColorSpace::createLUT(uint32_t size, const ColorSpace& src, - const ColorSpace& dst) { - size = clamp(size, 2u, 256u); - float m = 1.0f / float(size - 1); - - std::unique_ptr<float3[]> lut(new float3[size * size * size]); - float3* data = lut.get(); - - ColorSpaceConnector connector(src, dst); - - for (uint32_t z = 0; z < size; z++) { - for (int32_t y = int32_t(size - 1); y >= 0; y--) { - for (uint32_t x = 0; x < size; x++) { - *data++ = connector.transform({x * m, y * m, z * m}); - } - } - } - - return lut; -} - -static const float2 ILLUMINANT_D50_XY = {0.34567f, 0.35850f}; -static const float3 ILLUMINANT_D50_XYZ = {0.964212f, 1.0f, 0.825188f}; -static const mat3 BRADFORD = mat3{ - float3{ 0.8951f, -0.7502f, 0.0389f}, - float3{ 0.2664f, 1.7135f, -0.0685f}, - float3{-0.1614f, 0.0367f, 1.0296f} -}; - -static mat3 adaptation(const mat3& matrix, const float3& srcWhitePoint, const float3& dstWhitePoint) { - float3 srcLMS = matrix * srcWhitePoint; - float3 dstLMS = matrix * dstWhitePoint; - return inverse(matrix) * mat3{dstLMS / srcLMS} * matrix; -} - -ColorSpaceConnector::ColorSpaceConnector( - const ColorSpace& src, - const ColorSpace& dst) noexcept - : mSource(src) - , mDestination(dst) { - - if (all(lessThan(abs(src.getWhitePoint() - dst.getWhitePoint()), float2{1e-3f}))) { - mTransform = dst.getXYZtoRGB() * src.getRGBtoXYZ(); - } else { - mat3 rgbToXYZ(src.getRGBtoXYZ()); - mat3 xyzToRGB(dst.getXYZtoRGB()); - - float3 srcXYZ = ColorSpace::XYZ(float3{src.getWhitePoint(), 1}); - float3 dstXYZ = ColorSpace::XYZ(float3{dst.getWhitePoint(), 1}); - - if (any(greaterThan(abs(src.getWhitePoint() - ILLUMINANT_D50_XY), float2{1e-3f}))) { - rgbToXYZ = adaptation(BRADFORD, srcXYZ, ILLUMINANT_D50_XYZ) * src.getRGBtoXYZ(); - } - - if (any(greaterThan(abs(dst.getWhitePoint() - ILLUMINANT_D50_XY), float2{1e-3f}))) { - xyzToRGB = inverse(adaptation(BRADFORD, dstXYZ, ILLUMINANT_D50_XYZ) * dst.getRGBtoXYZ()); - } - - mTransform = xyzToRGB * rgbToXYZ; - } -} - -}; // namespace android diff --git a/location/java/android/location/altitude/AltitudeConverter.java b/location/java/android/location/altitude/AltitudeConverter.java index 3dc024efef56..6f8891216bed 100644 --- a/location/java/android/location/altitude/AltitudeConverter.java +++ b/location/java/android/location/altitude/AltitudeConverter.java @@ -19,9 +19,11 @@ package android.location.altitude; import android.annotation.NonNull; import android.annotation.WorkerThread; import android.content.Context; +import android.frameworks.location.altitude.GetGeoidHeightRequest; +import android.frameworks.location.altitude.GetGeoidHeightResponse; import android.location.Location; -import com.android.internal.location.altitude.GeoidHeightMap; +import com.android.internal.location.altitude.GeoidMap; import com.android.internal.location.altitude.S2CellIdUtils; import com.android.internal.location.altitude.nano.MapParamsProto; import com.android.internal.util.Preconditions; @@ -37,7 +39,7 @@ import java.io.IOException; * <pre> * Brian Julian and Michael Angermann. * "Resource efficient and accurate altitude conversion to Mean Sea Level." - * To appear in 2023 IEEE/ION Position, Location and Navigation Symposium (PLANS). + * 2023 IEEE/ION Position, Location and Navigation Symposium (PLANS). * </pre> */ public final class AltitudeConverter { @@ -45,8 +47,8 @@ public final class AltitudeConverter { private static final double MAX_ABS_VALID_LATITUDE = 90; private static final double MAX_ABS_VALID_LONGITUDE = 180; - /** Manages a mapping of geoid heights associated with S2 cells. */ - private final GeoidHeightMap mGeoidHeightMap = new GeoidHeightMap(); + /** Manages a mapping of geoid heights and expiration distances associated with S2 cells. */ + private final GeoidMap mGeoidMap = new GeoidMap(); /** * Creates an instance that manages an independent cache to optimized conversions of locations @@ -78,75 +80,87 @@ public final class AltitudeConverter { /** * Returns the four S2 cell IDs for the map square associated with the {@code location}. * - * <p>The first map cell contains the location, while the others are located horizontally, - * vertically, and diagonally, in that order, with respect to the S2 (i,j) coordinate system. If - * the diagonal map cell does not exist (i.e., the location is near an S2 cube vertex), its - * corresponding ID is set to zero. + * <p>The first map cell, denoted z11 in the appendix of the referenced paper above, contains + * the location. The others are the map cells denoted z21, z12, and z22, in that order. */ - @NonNull - private static long[] findMapSquare(@NonNull MapParamsProto params, + private static long[] findMapSquare(@NonNull MapParamsProto geoidHeightParams, @NonNull Location location) { long s2CellId = S2CellIdUtils.fromLatLngDegrees(location.getLatitude(), location.getLongitude()); // Cell-space properties and coordinates. - int sizeIj = 1 << (S2CellIdUtils.MAX_LEVEL - params.mapS2Level); + int sizeIj = 1 << (S2CellIdUtils.MAX_LEVEL - geoidHeightParams.mapS2Level); int maxIj = 1 << S2CellIdUtils.MAX_LEVEL; - long s0 = S2CellIdUtils.getParent(s2CellId, params.mapS2Level); - int f0 = S2CellIdUtils.getFace(s2CellId); - int i0 = S2CellIdUtils.getI(s2CellId); - int j0 = S2CellIdUtils.getJ(s2CellId); - int i1 = i0 + sizeIj; - int j1 = j0 + sizeIj; + long z11 = S2CellIdUtils.getParent(s2CellId, geoidHeightParams.mapS2Level); + int f11 = S2CellIdUtils.getFace(s2CellId); + int i1 = S2CellIdUtils.getI(s2CellId); + int j1 = S2CellIdUtils.getJ(s2CellId); + int i2 = i1 + sizeIj; + int j2 = j1 + sizeIj; // Non-boundary region calculation - simplest and most common case. - if (i1 < maxIj && j1 < maxIj) { - return new long[]{ - s0, - S2CellIdUtils.getParent(S2CellIdUtils.fromFij(f0, i1, j0), params.mapS2Level), - S2CellIdUtils.getParent(S2CellIdUtils.fromFij(f0, i0, j1), params.mapS2Level), - S2CellIdUtils.getParent(S2CellIdUtils.fromFij(f0, i1, j1), params.mapS2Level) - }; + if (i2 < maxIj && j2 < maxIj) { + return new long[]{z11, S2CellIdUtils.getParent(S2CellIdUtils.fromFij(f11, i2, j1), + geoidHeightParams.mapS2Level), S2CellIdUtils.getParent( + S2CellIdUtils.fromFij(f11, i1, j2), geoidHeightParams.mapS2Level), + S2CellIdUtils.getParent(S2CellIdUtils.fromFij(f11, i2, j2), + geoidHeightParams.mapS2Level)}; } - // Boundary region calculation. + // Boundary region calculation long[] edgeNeighbors = new long[4]; - S2CellIdUtils.getEdgeNeighbors(s0, edgeNeighbors); - long s1 = edgeNeighbors[1]; - long s2 = edgeNeighbors[2]; - long s3; - if (f0 % 2 == 1) { - S2CellIdUtils.getEdgeNeighbors(s1, edgeNeighbors); - if (i1 < maxIj) { - s3 = edgeNeighbors[2]; - } else { - s3 = s1; - s1 = edgeNeighbors[1]; - } - } else { - S2CellIdUtils.getEdgeNeighbors(s2, edgeNeighbors); - if (j1 < maxIj) { - s3 = edgeNeighbors[1]; - } else { - s3 = s2; - s2 = edgeNeighbors[3]; - } - } + S2CellIdUtils.getEdgeNeighbors(z11, edgeNeighbors); + long z11W = edgeNeighbors[0]; + long z11S = edgeNeighbors[1]; + long z11E = edgeNeighbors[2]; + long z11N = edgeNeighbors[3]; + + long[] otherEdgeNeighbors = new long[4]; + S2CellIdUtils.getEdgeNeighbors(z11W, otherEdgeNeighbors); + S2CellIdUtils.getEdgeNeighbors(z11S, edgeNeighbors); + long z11Sw = findCommonNeighbor(edgeNeighbors, otherEdgeNeighbors, z11); + S2CellIdUtils.getEdgeNeighbors(z11E, otherEdgeNeighbors); + long z11Se = findCommonNeighbor(edgeNeighbors, otherEdgeNeighbors, z11); + S2CellIdUtils.getEdgeNeighbors(z11N, edgeNeighbors); + long z11Ne = findCommonNeighbor(edgeNeighbors, otherEdgeNeighbors, z11); + + long z21 = (f11 % 2 == 1 && i2 >= maxIj) ? z11Sw : z11S; + long z12 = (f11 % 2 == 0 && j2 >= maxIj) ? z11Ne : z11E; + long z22 = (z21 == z11Sw) ? z11S : (z12 == z11Ne) ? z11E : z11Se; // Reuse edge neighbors' array to avoid an extra allocation. - edgeNeighbors[0] = s0; - edgeNeighbors[1] = s1; - edgeNeighbors[2] = s2; - edgeNeighbors[3] = s3; + edgeNeighbors[0] = z11; + edgeNeighbors[1] = z21; + edgeNeighbors[2] = z12; + edgeNeighbors[3] = z22; return edgeNeighbors; } /** + * Returns the first common non-z11 neighbor found between the two arrays of edge neighbors. If + * such a common neighbor does not exist, returns z11. + */ + private static long findCommonNeighbor(long[] edgeNeighbors, long[] otherEdgeNeighbors, + long z11) { + for (long edgeNeighbor : edgeNeighbors) { + if (edgeNeighbor == z11) { + continue; + } + for (long otherEdgeNeighbor : otherEdgeNeighbors) { + if (edgeNeighbor == otherEdgeNeighbor) { + return edgeNeighbor; + } + } + } + return z11; + } + + /** * Adds to {@code location} the bilinearly interpolated Mean Sea Level altitude. In addition, a * Mean Sea Level altitude accuracy is added if the {@code location} has a valid vertical * accuracy; otherwise, does not add a corresponding accuracy. */ - private static void addMslAltitude(@NonNull MapParamsProto params, + private static void addMslAltitude(@NonNull MapParamsProto geoidHeightParams, @NonNull double[] geoidHeightsMeters, @NonNull Location location) { double h0 = geoidHeightsMeters[0]; double h1 = geoidHeightsMeters[1]; @@ -158,7 +172,7 @@ public final class AltitudeConverter { // employ the simplified unit square formulation. long s2CellId = S2CellIdUtils.fromLatLngDegrees(location.getLatitude(), location.getLongitude()); - double sizeIj = 1 << (S2CellIdUtils.MAX_LEVEL - params.mapS2Level); + double sizeIj = 1 << (S2CellIdUtils.MAX_LEVEL - geoidHeightParams.mapS2Level); double wi = (S2CellIdUtils.getI(s2CellId) % sizeIj) / sizeIj; double wj = (S2CellIdUtils.getJ(s2CellId) % sizeIj) / sizeIj; double offsetMeters = h0 + (h1 - h0) * wi + (h2 - h0) * wj + (h3 - h1 - h2 + h0) * wi * wj; @@ -167,8 +181,8 @@ public final class AltitudeConverter { if (location.hasVerticalAccuracy()) { double verticalAccuracyMeters = location.getVerticalAccuracyMeters(); if (Double.isFinite(verticalAccuracyMeters) && verticalAccuracyMeters >= 0) { - location.setMslAltitudeAccuracyMeters( - (float) Math.hypot(verticalAccuracyMeters, params.modelRmseMeters)); + location.setMslAltitudeAccuracyMeters((float) Math.hypot(verticalAccuracyMeters, + geoidHeightParams.modelRmseMeters)); } } } @@ -191,10 +205,11 @@ public final class AltitudeConverter { public void addMslAltitudeToLocation(@NonNull Context context, @NonNull Location location) throws IOException { validate(location); - MapParamsProto params = GeoidHeightMap.getParams(context); - long[] s2CellIds = findMapSquare(params, location); - double[] geoidHeightsMeters = mGeoidHeightMap.readGeoidHeights(params, context, s2CellIds); - addMslAltitude(params, geoidHeightsMeters, location); + MapParamsProto geoidHeightParams = GeoidMap.getGeoidHeightParams(context); + long[] mapCells = findMapSquare(geoidHeightParams, location); + double[] geoidHeightsMeters = mGeoidMap.readGeoidHeights(geoidHeightParams, context, + mapCells); + addMslAltitude(geoidHeightParams, geoidHeightsMeters, location); } /** @@ -206,18 +221,68 @@ public final class AltitudeConverter { */ public boolean addMslAltitudeToLocation(@NonNull Location location) { validate(location); - MapParamsProto params = GeoidHeightMap.getParams(); - if (params == null) { + MapParamsProto geoidHeightParams = GeoidMap.getGeoidHeightParams(); + if (geoidHeightParams == null) { return false; } - long[] s2CellIds = findMapSquare(params, location); - double[] geoidHeightsMeters = mGeoidHeightMap.readGeoidHeights(params, s2CellIds); + long[] mapCells = findMapSquare(geoidHeightParams, location); + double[] geoidHeightsMeters = mGeoidMap.readGeoidHeights(geoidHeightParams, mapCells); if (geoidHeightsMeters == null) { return false; } - addMslAltitude(params, geoidHeightsMeters, location); + addMslAltitude(geoidHeightParams, geoidHeightsMeters, location); return true; } + + /** + * Returns the geoid height (a.k.a. geoid undulation) at the location specified in {@code + * request}. The geoid height at a location is defined as the difference between an altitude + * measured above the World Geodetic System 1984 reference ellipsoid (WGS84) and its + * corresponding Mean Sea Level altitude. + * + * <p>Must be called off the main thread as data may be loaded from raw assets. + * + * @throws IOException if an I/O error occurs when loading data from raw assets. + * @throws IllegalArgumentException if the {@code request} has an invalid latitude or longitude. + * Specifically, the latitude must be between -90 and 90 (both + * inclusive), and the longitude must be between -180 and 180 + * (both inclusive). + * @hide + */ + @WorkerThread + public @NonNull GetGeoidHeightResponse getGeoidHeight(@NonNull Context context, + @NonNull GetGeoidHeightRequest request) throws IOException { + // Create a valid location from which the geoid height and its accuracy will be extracted. + Location location = new Location(""); + location.setLatitude(request.latitudeDegrees); + location.setLongitude(request.longitudeDegrees); + location.setAltitude(0.0); + location.setVerticalAccuracyMeters(0.0f); + + addMslAltitudeToLocation(context, location); + // The geoid height for a location with zero WGS84 altitude is equal in value to the + // negative of corresponding MSL altitude. + double geoidHeightMeters = -location.getMslAltitudeMeters(); + // The geoid height error for a location with zero vertical accuracy is equal in value to + // the corresponding MSL altitude accuracy. + float geoidHeightErrorMeters = location.getMslAltitudeAccuracyMeters(); + + MapParamsProto expirationDistanceParams = GeoidMap.getExpirationDistanceParams(context); + long s2CellId = S2CellIdUtils.fromLatLngDegrees(location.getLatitude(), + location.getLongitude()); + long[] mapCell = {S2CellIdUtils.getParent(s2CellId, expirationDistanceParams.mapS2Level)}; + double expirationDistanceMeters = mGeoidMap.readExpirationDistances( + expirationDistanceParams, context, mapCell)[0]; + float additionalGeoidHeightErrorMeters = (float) expirationDistanceParams.modelRmseMeters; + + GetGeoidHeightResponse response = new GetGeoidHeightResponse(); + response.geoidHeightMeters = geoidHeightMeters; + response.geoidHeightErrorMeters = geoidHeightErrorMeters; + response.expirationDistanceMeters = expirationDistanceMeters; + response.additionalGeoidHeightErrorMeters = additionalGeoidHeightErrorMeters; + response.success = true; + return response; + } } diff --git a/location/java/com/android/internal/location/altitude/GeoidHeightMap.java b/location/java/com/android/internal/location/altitude/GeoidMap.java index 8067050d9da3..9bf5689c1028 100644 --- a/location/java/com/android/internal/location/altitude/GeoidHeightMap.java +++ b/location/java/com/android/internal/location/altitude/GeoidMap.java @@ -34,11 +34,12 @@ import java.nio.ByteBuffer; import java.util.Objects; /** - * Manages a mapping of geoid heights associated with S2 cells, referred to as MAP CELLS. + * Manages a mapping of geoid heights and expiration distances associated with S2 cells, referred to + * as MAP CELLS. * * <p>Tiles are used extensively to reduce the number of entries needed to be stored in memory and - * on disk. A tile associates geoid heights with all map cells of a common parent at a specified S2 - * level. + * on disk. A tile associates geoid heights or expiration distances with all map cells of a common + * parent at a specified S2 level. * * <p>Since bilinear interpolation considers at most four map cells at a time, at most four tiles * are simultaneously stored in memory. These tiles, referred to as CACHE TILES, are each keyed by @@ -48,42 +49,79 @@ import java.util.Objects; * The latter tiles, referred to as DISK TILES, are each keyed by its common parent's S2 cell token, * referred to as a DISK TOKEN. */ -public final class GeoidHeightMap { +public final class GeoidMap { - private static final Object sLock = new Object(); + private static final Object GEOID_HEIGHT_PARAMS_LOCK = new Object(); - @GuardedBy("sLock") + private static final Object EXPIRATION_DISTANCE_PARAMS_LOCK = new Object(); + + @GuardedBy("GEOID_HEIGHT_PARAMS_LOCK") + @Nullable + private static MapParamsProto sGeoidHeightParams; + + @GuardedBy("EXPIRATION_DISTANCE_PARAMS_LOCK") @Nullable - private static MapParamsProto sParams; + private static MapParamsProto sExpirationDistanceParams; + + /** + * Defines a cache large enough to hold all geoid height cache tiles needed for interpolation. + */ + private final LruCache<Long, S2TileProto> mGeoidHeightCacheTiles = new LruCache<>(4); - /** Defines a cache large enough to hold all cache tiles needed for interpolation. */ - private final LruCache<Long, S2TileProto> mCacheTiles = new LruCache<>(4); + /** + * Defines a cache large enough to hold all expiration distance cache tiles needed for + * interpolation. + */ + private final LruCache<Long, S2TileProto> mExpirationDistanceCacheTiles = new LruCache<>(4); /** - * Returns the singleton parameter instance for a spherically projected geoid height map and its - * corresponding tile management. + * Returns the singleton parameter instance for geoid height parameters of a spherically + * projected map. */ @NonNull - public static MapParamsProto getParams(@NonNull Context context) throws IOException { - synchronized (sLock) { - if (sParams == null) { - try (InputStream is = context.getApplicationContext().getAssets().open( - "geoid_height_map/map-params.pb")) { - sParams = MapParamsProto.parseFrom(is.readAllBytes()); - } + 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); + } + return sGeoidHeightParams; + } + } + + /** + * Returns the singleton parameter instance for expiration distance parameters of a spherically + * projected + * map. + */ + @NonNull + public static MapParamsProto getExpirationDistanceParams(@NonNull Context context) + 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); } - return sParams; + return sExpirationDistanceParams; + } + } + + @NonNull + private static MapParamsProto parseParams(@NonNull Context context) throws IOException { + try (InputStream is = context.getApplicationContext().getAssets().open( + "geoid_height_map/map-params.pb")) { + return MapParamsProto.parseFrom(is.readAllBytes()); } } /** - * Same as {@link #getParams(Context)} except that null is returned if the singleton parameter - * instance is not yet initialized. + * Same as {@link #getGeoidHeightParams(Context)} except that null is returned if the singleton + * parameter instance is not yet initialized. */ @Nullable - public static MapParamsProto getParams() { - synchronized (sLock) { - return sParams; + public static MapParamsProto getGeoidHeightParams() { + synchronized (GEOID_HEIGHT_PARAMS_LOCK) { + return sGeoidHeightParams; } } @@ -93,18 +131,17 @@ public final class GeoidHeightMap { @NonNull private static String getDiskToken(@NonNull MapParamsProto params, long s2CellId) { - return S2CellIdUtils.getToken( - S2CellIdUtils.getParent(s2CellId, params.diskTileS2Level)); + return S2CellIdUtils.getToken(S2CellIdUtils.getParent(s2CellId, params.diskTileS2Level)); } /** * Adds to {@code values} values in the unit interval [0, 1] for the map cells identified by - * {@code s2CellIds}. Returns true if values are present for all IDs; otherwise, returns false - * and adds NaNs for absent values. + * {@code s2CellIds}. Returns true if values are present for all IDs; otherwise, adds NaNs for + * absent values and returns false. */ private static boolean getUnitIntervalValues(@NonNull MapParamsProto params, - @NonNull TileFunction tileFunction, - @NonNull long[] s2CellIds, @NonNull double[] values) { + @NonNull TileFunction tileFunction, @NonNull long[] s2CellIds, + @NonNull double[] values) { int len = s2CellIds.length; S2TileProto[] tiles = new S2TileProto[len]; @@ -137,9 +174,8 @@ public final class GeoidHeightMap { @SuppressWarnings("ReferenceEquality") private static void mergeByteBufferValues(@NonNull MapParamsProto params, - @NonNull long[] s2CellIds, - @NonNull S2TileProto[] tiles, - int tileIndex, @NonNull double[] values) { + @NonNull long[] s2CellIds, @NonNull S2TileProto[] tiles, int tileIndex, + @NonNull double[] values) { byte[] bytes = tiles[tileIndex].byteBuffer; if (bytes == null || bytes.length == 0) { return; @@ -163,24 +199,22 @@ public final class GeoidHeightMap { } private static void mergeByteJpegValues(@NonNull MapParamsProto params, - @NonNull long[] s2CellIds, - @NonNull S2TileProto[] tiles, - int tileIndex, @NonNull double[] values) { + @NonNull long[] s2CellIds, @NonNull S2TileProto[] tiles, int tileIndex, + @NonNull double[] values) { mergeByteImageValues(params, tiles[tileIndex].byteJpeg, s2CellIds, tiles, tileIndex, values); } private static void mergeBytePngValues(@NonNull MapParamsProto params, - @NonNull long[] s2CellIds, - @NonNull S2TileProto[] tiles, - int tileIndex, @NonNull double[] values) { + @NonNull long[] s2CellIds, @NonNull S2TileProto[] tiles, int tileIndex, + @NonNull double[] values) { mergeByteImageValues(params, tiles[tileIndex].bytePng, s2CellIds, tiles, tileIndex, values); } @SuppressWarnings("ReferenceEquality") private static void mergeByteImageValues(@NonNull MapParamsProto params, @NonNull byte[] bytes, - @NonNull long[] s2CellIds, - @NonNull S2TileProto[] tiles, int tileIndex, @NonNull double[] values) { + @NonNull long[] s2CellIds, @NonNull S2TileProto[] tiles, int tileIndex, + @NonNull double[] values) { if (bytes == null || bytes.length == 0) { return; } @@ -219,7 +253,7 @@ public final class GeoidHeightMap { * ID. */ private static void validate(@NonNull MapParamsProto params, @NonNull long[] s2CellIds) { - Preconditions.checkArgument(s2CellIds.length == 4); + Preconditions.checkArgument(s2CellIds.length <= 4); for (long s2CellId : s2CellIds) { Preconditions.checkArgument(S2CellIdUtils.getLevel(s2CellId) == params.mapS2Level); } @@ -233,15 +267,38 @@ public final class GeoidHeightMap { @NonNull public double[] readGeoidHeights(@NonNull MapParamsProto params, @NonNull Context context, @NonNull long[] s2CellIds) throws IOException { + return readMapValues(params, context, s2CellIds, mGeoidHeightCacheTiles); + } + + /** + * Returns the expiration distances in meters associated with the map cells identified by + * {@code s2CellIds}. Throws an {@link IOException} if a geoid height cannot be calculated for + * an ID. + */ + @NonNull + public double[] readExpirationDistances(@NonNull MapParamsProto params, + @NonNull Context context, @NonNull long[] s2CellIds) throws IOException { + return readMapValues(params, context, s2CellIds, mExpirationDistanceCacheTiles); + } + + /** + * Returns the map values in meters associated with the map cells identified by + * {@code s2CellIds}. Throws an {@link IOException} if a map value cannot be calculated for an + * ID. + */ + @NonNull + private static double[] readMapValues(@NonNull MapParamsProto params, @NonNull Context context, + @NonNull long[] s2CellIds, @NonNull LruCache<Long, S2TileProto> cacheTiles) + throws IOException { validate(params, s2CellIds); - double[] heightsMeters = new double[s2CellIds.length]; - if (getGeoidHeights(params, mCacheTiles::get, s2CellIds, heightsMeters)) { - return heightsMeters; + double[] mapValuesMeters = new double[s2CellIds.length]; + if (getMapValues(params, cacheTiles::get, s2CellIds, mapValuesMeters)) { + return mapValuesMeters; } - TileFunction loadedTiles = loadFromCacheAndDisk(params, context, s2CellIds); - if (getGeoidHeights(params, loadedTiles, s2CellIds, heightsMeters)) { - return heightsMeters; + TileFunction loadedTiles = loadFromCacheAndDisk(params, context, s2CellIds, cacheTiles); + if (getMapValues(params, loadedTiles, s2CellIds, mapValuesMeters)) { + return mapValuesMeters; } throw new IOException("Unable to calculate geoid heights from raw assets."); } @@ -255,32 +312,33 @@ public final class GeoidHeightMap { public double[] readGeoidHeights(@NonNull MapParamsProto params, @NonNull long[] s2CellIds) { validate(params, s2CellIds); double[] heightsMeters = new double[s2CellIds.length]; - if (getGeoidHeights(params, mCacheTiles::get, s2CellIds, heightsMeters)) { + if (getMapValues(params, mGeoidHeightCacheTiles::get, s2CellIds, heightsMeters)) { return heightsMeters; } return null; } /** - * Adds to {@code heightsMeters} the geoid heights in meters associated with the map cells + * Adds to {@code mapValuesMeters} the map values in meters associated with the map cells * identified by {@code s2CellIds}. Returns true if heights are present for all IDs; otherwise, - * returns false and adds NaNs for absent heights. + * adds NaNs for absent heights and returns false. */ - private boolean getGeoidHeights(@NonNull MapParamsProto params, + private static boolean getMapValues(@NonNull MapParamsProto params, @NonNull TileFunction tileFunction, @NonNull long[] s2CellIds, - @NonNull double[] heightsMeters) { - boolean allFound = getUnitIntervalValues(params, tileFunction, s2CellIds, heightsMeters); - for (int i = 0; i < heightsMeters.length; i++) { + @NonNull double[] mapValuesMeters) { + boolean allFound = getUnitIntervalValues(params, tileFunction, s2CellIds, mapValuesMeters); + for (int i = 0; i < mapValuesMeters.length; i++) { // NaNs are properly preserved. - heightsMeters[i] *= params.modelAMeters; - heightsMeters[i] += params.modelBMeters; + mapValuesMeters[i] *= params.modelAMeters; + mapValuesMeters[i] += params.modelBMeters; } return allFound; } @NonNull - private TileFunction loadFromCacheAndDisk(@NonNull MapParamsProto params, - @NonNull Context context, @NonNull long[] s2CellIds) throws IOException { + private static TileFunction loadFromCacheAndDisk(@NonNull MapParamsProto params, + @NonNull Context context, @NonNull long[] s2CellIds, + @NonNull LruCache<Long, S2TileProto> cacheTiles) throws IOException { int len = s2CellIds.length; // Enable batch loading by finding all cache keys upfront. @@ -296,7 +354,7 @@ public final class GeoidHeightMap { if (diskTokens[i] != null) { continue; } - loadedTiles[i] = mCacheTiles.get(cacheKeys[i]); + loadedTiles[i] = cacheTiles.get(cacheKeys[i]); diskTokens[i] = getDiskToken(params, cacheKeys[i]); // Batch across common cache key. @@ -319,7 +377,7 @@ public final class GeoidHeightMap { "geoid_height_map/tile-" + diskTokens[i] + ".pb")) { tile = S2TileProto.parseFrom(is.readAllBytes()); } - mergeFromDiskTile(params, tile, cacheKeys, diskTokens, i, loadedTiles); + mergeFromDiskTile(params, tile, cacheKeys, diskTokens, i, loadedTiles, cacheTiles); } return cacheKey -> { @@ -332,9 +390,10 @@ public final class GeoidHeightMap { }; } - private void mergeFromDiskTile(@NonNull MapParamsProto params, @NonNull S2TileProto diskTile, - @NonNull long[] cacheKeys, @NonNull String[] diskTokens, int diskTokenIndex, - @NonNull S2TileProto[] loadedTiles) throws IOException { + private static void mergeFromDiskTile(@NonNull MapParamsProto params, + @NonNull S2TileProto diskTile, @NonNull long[] cacheKeys, @NonNull String[] diskTokens, + int diskTokenIndex, @NonNull S2TileProto[] loadedTiles, + @NonNull LruCache<Long, S2TileProto> cacheTiles) throws IOException { int len = cacheKeys.length; int numMapCellsPerCacheTile = 1 << (2 * (params.mapS2Level - params.cacheTileS2Level)); @@ -375,7 +434,7 @@ public final class GeoidHeightMap { } // Side load into tile cache. - mCacheTiles.put(cacheKeys[i], loadedTiles[i]); + cacheTiles.put(cacheKeys[i], loadedTiles[i]); } } diff --git a/media/java/android/media/MediaRouter2.java b/media/java/android/media/MediaRouter2.java index 691aa7784d7a..425db06ce55f 100644 --- a/media/java/android/media/MediaRouter2.java +++ b/media/java/android/media/MediaRouter2.java @@ -183,23 +183,23 @@ public final class MediaRouter2 { * preference} passed by a proxy router. Use {@link RouteDiscoveryPreference#EMPTY} when * setting a route callback. * <li> - * <p>Methods returning non-system {@link RoutingController controllers} always return - * new instances with the latest data. Do not attempt to compare or store them. Instead, - * use {@link #getController(String)} or {@link #getControllers()} to query the most + * <p>Methods returning non-system {@link RoutingController controllers} always return new + * instances with the latest data. Do not attempt to compare or store them. Instead, use + * {@link #getController(String)} or {@link #getControllers()} to query the most * up-to-date state. * <li> * <p>Calls to {@link #setOnGetControllerHintsListener} are ignored. * </ul> * * @param clientPackageName the package name of the app to control - * @throws SecurityException if the caller doesn't have {@link - * Manifest.permission#MEDIA_CONTENT_CONTROL MEDIA_CONTENT_CONTROL} permission. - * @hide + * @return a proxy MediaRouter2 instance if {@code clientPackageName} exists or {@code null}. */ - // TODO (b/311711420): Deprecate once #getInstance(Context, String, UserHandle) reaches public - // SDK. - @SystemApi - @RequiresPermission(Manifest.permission.MEDIA_CONTENT_CONTROL) + @FlaggedApi(FLAG_ENABLE_CROSS_USER_ROUTING_IN_MEDIA_ROUTER2) + @RequiresPermission( + anyOf = { + Manifest.permission.MEDIA_CONTENT_CONTROL, + Manifest.permission.MEDIA_ROUTING_CONTROL + }) @Nullable public static MediaRouter2 getInstance( @NonNull Context context, @NonNull String clientPackageName) { @@ -226,9 +226,9 @@ public final class MediaRouter2 { * {@link RouteDiscoveryPreference.Builder#setPreferredFeatures(List) preferred features} * when setting a route callback. * <li> - * <p>Methods returning non-system {@link RoutingController controllers} always return - * new instances with the latest data. Do not attempt to compare or store them. Instead, - * use {@link #getController(String)} or {@link #getControllers()} to query the most + * <p>Methods returning non-system {@link RoutingController controllers} always return new + * instances with the latest data. Do not attempt to compare or store them. Instead, use + * {@link #getController(String)} or {@link #getControllers()} to query the most * up-to-date state. * <li> * <p>Calls to {@link #setOnGetControllerHintsListener} are ignored. @@ -242,8 +242,8 @@ public final class MediaRouter2 { * @throws SecurityException if {@code user} does not match {@link Process#myUserHandle()} and * the caller does not hold {@code Manifest.permission#INTERACT_ACROSS_USERS_FULL}. * @throws IllegalArgumentException if {@code clientPackageName} does not exist in {@code user}. + * @hide */ - @FlaggedApi(FLAG_ENABLE_CROSS_USER_ROUTING_IN_MEDIA_ROUTER2) @RequiresPermission( anyOf = { Manifest.permission.MEDIA_CONTENT_CONTROL, @@ -251,9 +251,7 @@ public final class MediaRouter2 { }) @NonNull public static MediaRouter2 getInstance( - @NonNull Context context, - @NonNull String clientPackageName, - @NonNull UserHandle user) { + @NonNull Context context, @NonNull String clientPackageName, @NonNull UserHandle user) { return findOrCreateProxyInstanceForCallingUser(context, clientPackageName, user); } diff --git a/media/java/android/media/RoutingSessionInfo.java b/media/java/android/media/RoutingSessionInfo.java index d28c26df6749..2202766ef016 100644 --- a/media/java/android/media/RoutingSessionInfo.java +++ b/media/java/android/media/RoutingSessionInfo.java @@ -182,7 +182,7 @@ public final class RoutingSessionInfo implements Parcelable { mControlHints = src.readBundle(); mIsSystemSession = src.readBoolean(); mTransferReason = src.readInt(); - mTransferInitiatorUserHandle = src.readParcelable(null, android.os.UserHandle.class); + mTransferInitiatorUserHandle = UserHandle.readFromParcel(src); mTransferInitiatorPackageName = src.readString(); } @@ -417,11 +417,7 @@ public final class RoutingSessionInfo implements Parcelable { dest.writeBundle(mControlHints); dest.writeBoolean(mIsSystemSession); dest.writeInt(mTransferReason); - if (mTransferInitiatorUserHandle != null) { - mTransferInitiatorUserHandle.writeToParcel(dest, /* flags= */ 0); - } else { - dest.writeParcelable(null, /* flags= */ 0); - } + UserHandle.writeToParcel(mTransferInitiatorUserHandle, dest); dest.writeString(mTransferInitiatorPackageName); } diff --git a/media/java/android/media/flags/editing.aconfig b/media/java/android/media/flags/editing.aconfig new file mode 100644 index 000000000000..c3997e94622d --- /dev/null +++ b/media/java/android/media/flags/editing.aconfig @@ -0,0 +1,8 @@ +package: "com.android.media.editing.flags" + +flag { + name: "add_media_metrics_editing" + namespace: "media_solutions" + description: "Add media metrics for transcoding/editing events." + bug: "297487694" +} diff --git a/media/java/android/media/metrics/EditingEndedEvent.aidl b/media/java/android/media/metrics/EditingEndedEvent.aidl new file mode 100644 index 000000000000..e099deaa6836 --- /dev/null +++ b/media/java/android/media/metrics/EditingEndedEvent.aidl @@ -0,0 +1,19 @@ +/* + * 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.media.metrics; + +parcelable EditingEndedEvent; diff --git a/media/java/android/media/metrics/EditingEndedEvent.java b/media/java/android/media/metrics/EditingEndedEvent.java new file mode 100644 index 000000000000..72e6db8d987f --- /dev/null +++ b/media/java/android/media/metrics/EditingEndedEvent.java @@ -0,0 +1,325 @@ +/* + * 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.media.metrics; + +import static com.android.media.editing.flags.Flags.FLAG_ADD_MEDIA_METRICS_EDITING; + +import android.annotation.FlaggedApi; +import android.annotation.IntDef; +import android.annotation.IntRange; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; + +import java.lang.annotation.Retention; +import java.util.Objects; + +/** Event for an editing operation having ended. */ +@FlaggedApi(FLAG_ADD_MEDIA_METRICS_EDITING) +public final class EditingEndedEvent extends Event implements Parcelable { + + // The special value 0 is reserved for the field being unspecified in the proto. + + /** The editing operation was successful. */ + public static final int FINAL_STATE_SUCCEEDED = 1; + + /** The editing operation was canceled. */ + public static final int FINAL_STATE_CANCELED = 2; + + /** The editing operation failed due to an error. */ + public static final int FINAL_STATE_ERROR = 3; + + /** @hide */ + @IntDef( + prefix = {"FINAL_STATE_"}, + value = { + FINAL_STATE_SUCCEEDED, + FINAL_STATE_CANCELED, + FINAL_STATE_ERROR, + }) + @Retention(java.lang.annotation.RetentionPolicy.SOURCE) + public @interface FinalState {} + + private final @FinalState int mFinalState; + + // The special value 0 is reserved for the field being unspecified in the proto. + + /** Special value representing that no error occurred. */ + public static final int ERROR_CODE_NONE = 1; + + /** Error code for unexpected runtime errors. */ + public static final int ERROR_CODE_FAILED_RUNTIME_CHECK = 2; + + /** Error code for non-specific errors during input/output. */ + public static final int ERROR_CODE_IO_UNSPECIFIED = 3; + + /** Error code for network connection failures. */ + public static final int ERROR_CODE_IO_NETWORK_CONNECTION_FAILED = 4; + + /** Error code for network timeouts. */ + public static final int ERROR_CODE_IO_NETWORK_CONNECTION_TIMEOUT = 5; + + /** Caused by an HTTP server returning an unexpected HTTP response status code. */ + public static final int ERROR_CODE_IO_BAD_HTTP_STATUS = 6; + + /** Caused by a non-existent file. */ + public static final int ERROR_CODE_IO_FILE_NOT_FOUND = 7; + + /** + * Caused by lack of permission to perform an IO operation. For example, lack of permission to + * access internet or external storage. + */ + public static final int ERROR_CODE_IO_NO_PERMISSION = 8; + + /** */ + public static final int ERROR_CODE_IO_CLEARTEXT_NOT_PERMITTED = 9; + + /** Caused by reading data out of the data bounds. */ + public static final int ERROR_CODE_IO_READ_POSITION_OUT_OF_RANGE = 10; + + /** Caused by a decoder initialization failure. */ + public static final int ERROR_CODE_DECODER_INIT_FAILED = 11; + + /** Caused by a failure while trying to decode media samples. */ + public static final int ERROR_CODE_DECODING_FAILED = 12; + + /** Caused by trying to decode content whose format is not supported. */ + public static final int ERROR_CODE_DECODING_FORMAT_UNSUPPORTED = 13; + + /** Caused by an encoder initialization failure. */ + public static final int ERROR_CODE_ENCODER_INIT_FAILED = 14; + + /** Caused by a failure while trying to encode media samples. */ + public static final int ERROR_CODE_ENCODING_FAILED = 15; + + /** Caused by trying to encode content whose format is not supported. */ + public static final int ERROR_CODE_ENCODING_FORMAT_UNSUPPORTED = 16; + + /** Caused by a video frame processing failure. */ + public static final int ERROR_CODE_VIDEO_FRAME_PROCESSING_FAILED = 17; + + /** Caused by an audio processing failure. */ + public static final int ERROR_CODE_AUDIO_PROCESSING_FAILED = 18; + + /** Caused by a failure while muxing media samples. */ + public static final int ERROR_CODE_MUXING_FAILED = 19; + + /** @hide */ + @IntDef( + prefix = {"ERROR_CODE_"}, + value = { + ERROR_CODE_NONE, + ERROR_CODE_FAILED_RUNTIME_CHECK, + ERROR_CODE_IO_UNSPECIFIED, + ERROR_CODE_IO_NETWORK_CONNECTION_FAILED, + ERROR_CODE_IO_NETWORK_CONNECTION_TIMEOUT, + ERROR_CODE_IO_BAD_HTTP_STATUS, + ERROR_CODE_IO_FILE_NOT_FOUND, + ERROR_CODE_IO_NO_PERMISSION, + ERROR_CODE_IO_CLEARTEXT_NOT_PERMITTED, + ERROR_CODE_IO_READ_POSITION_OUT_OF_RANGE, + ERROR_CODE_DECODER_INIT_FAILED, + ERROR_CODE_DECODING_FAILED, + ERROR_CODE_DECODING_FORMAT_UNSUPPORTED, + ERROR_CODE_ENCODER_INIT_FAILED, + ERROR_CODE_ENCODING_FAILED, + ERROR_CODE_ENCODING_FORMAT_UNSUPPORTED, + ERROR_CODE_VIDEO_FRAME_PROCESSING_FAILED, + ERROR_CODE_AUDIO_PROCESSING_FAILED, + ERROR_CODE_MUXING_FAILED, + }) + @Retention(java.lang.annotation.RetentionPolicy.SOURCE) + public @interface ErrorCode {} + + private final @ErrorCode int mErrorCode; + @SuppressWarnings("HidingField") // Hiding field from superclass as for playback events. + private final long mTimeSinceCreatedMillis; + + private EditingEndedEvent( + @FinalState int finalState, + @ErrorCode int errorCode, + long timeSinceCreatedMillis, + @NonNull Bundle extras) { + mFinalState = finalState; + mErrorCode = errorCode; + mTimeSinceCreatedMillis = timeSinceCreatedMillis; + mMetricsBundle = extras.deepCopy(); + } + + /** Returns the state of the editing session when it ended. */ + @FinalState + public int getFinalState() { + return mFinalState; + } + + /** Returns the error code for a {@linkplain #FINAL_STATE_ERROR failed} editing session. */ + @ErrorCode + public int getErrorCode() { + return mErrorCode; + } + + /** + * Gets the elapsed time since creating of the editing session, in milliseconds, or -1 if + * unknown. + * + * @return The elapsed time since creating the editing session, in milliseconds, or -1 if + * unknown. + * @see LogSessionId + * @see EditingSession + */ + @Override + @IntRange(from = -1) + public long getTimeSinceCreatedMillis() { + return mTimeSinceCreatedMillis; + } + + /** + * Gets metrics-related information that is not supported by dedicated methods. + * + * <p>It is intended to be used for backwards compatibility by the metrics infrastructure. + */ + @Override + @NonNull + public Bundle getMetricsBundle() { + return mMetricsBundle; + } + + @Override + @NonNull + public String toString() { + return "PlaybackErrorEvent { " + + "finalState = " + + mFinalState + + ", " + + "errorCode = " + + mErrorCode + + ", " + + "timeSinceCreatedMillis = " + + mTimeSinceCreatedMillis + + " }"; + } + + @Override + public boolean equals(@Nullable Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + EditingEndedEvent that = (EditingEndedEvent) o; + return mFinalState == that.mFinalState + && mErrorCode == that.mErrorCode + && mTimeSinceCreatedMillis == that.mTimeSinceCreatedMillis; + } + + @Override + public int hashCode() { + return Objects.hash(mFinalState, mErrorCode, mTimeSinceCreatedMillis); + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeInt(mFinalState); + dest.writeInt(mErrorCode); + dest.writeLong(mTimeSinceCreatedMillis); + dest.writeBundle(mMetricsBundle); + } + + @Override + public int describeContents() { + return 0; + } + + private EditingEndedEvent(@NonNull Parcel in) { + int finalState = in.readInt(); + int errorCode = in.readInt(); + long timeSinceCreatedMillis = in.readLong(); + Bundle metricsBundle = in.readBundle(); + + mFinalState = finalState; + mErrorCode = errorCode; + mTimeSinceCreatedMillis = timeSinceCreatedMillis; + mMetricsBundle = metricsBundle; + } + + public static final @NonNull Creator<EditingEndedEvent> CREATOR = + new Creator<>() { + @Override + public EditingEndedEvent[] newArray(int size) { + return new EditingEndedEvent[size]; + } + + @Override + public EditingEndedEvent createFromParcel(@NonNull Parcel in) { + return new EditingEndedEvent(in); + } + }; + + /** Builder for {@link EditingEndedEvent} */ + @FlaggedApi(FLAG_ADD_MEDIA_METRICS_EDITING) + public static final class Builder { + private final @FinalState int mFinalState; + private @ErrorCode int mErrorCode; + private long mTimeSinceCreatedMillis; + private Bundle mMetricsBundle; + + /** + * Creates a new Builder. + * + * @param finalState The state of the editing session when it ended. + */ + public Builder(@FinalState int finalState) { + mFinalState = finalState; + mErrorCode = ERROR_CODE_NONE; + mTimeSinceCreatedMillis = -1; + mMetricsBundle = new Bundle(); + } + + /** + * Sets the elapsed time since creating the editing session, in milliseconds. + * + * @param timeSinceCreatedMillis The elapsed time since creating the editing session, in + * milliseconds, or -1 if the value is unknown. + * @see #getTimeSinceCreatedMillis() + */ + public @NonNull Builder setTimeSinceCreatedMillis( + @IntRange(from = -1) long timeSinceCreatedMillis) { + mTimeSinceCreatedMillis = timeSinceCreatedMillis; + return this; + } + + /** Sets the error code for a {@linkplain #FINAL_STATE_ERROR failed} editing session. */ + public @NonNull Builder setErrorCode(@ErrorCode int value) { + mErrorCode = value; + return this; + } + + /** + * Sets metrics-related information that is not supported by dedicated methods. + * + * <p>Used for backwards compatibility by the metrics infrastructure. + */ + public @NonNull Builder setMetricsBundle(@NonNull Bundle metricsBundle) { + mMetricsBundle = metricsBundle; + return this; + } + + /** Builds an instance. */ + public @NonNull EditingEndedEvent build() { + return new EditingEndedEvent( + mFinalState, mErrorCode, mTimeSinceCreatedMillis, mMetricsBundle); + } + } +} diff --git a/media/java/android/media/metrics/EditingSession.java b/media/java/android/media/metrics/EditingSession.java index 2ddf623b1ed3..964e12cfcc05 100644 --- a/media/java/android/media/metrics/EditingSession.java +++ b/media/java/android/media/metrics/EditingSession.java @@ -16,6 +16,9 @@ package android.media.metrics; +import static com.android.media.editing.flags.Flags.FLAG_ADD_MEDIA_METRICS_EDITING; + +import android.annotation.FlaggedApi; import android.annotation.NonNull; import android.annotation.Nullable; @@ -24,7 +27,8 @@ import com.android.internal.util.AnnotationValidations; import java.util.Objects; /** - * An instances of this class represents a session of media editing. + * Represents a session of media editing, for example, transcoding between formats, transmuxing or + * applying trimming or audio/video effects to a stream. */ public final class EditingSession implements AutoCloseable { private final @NonNull String mId; @@ -40,6 +44,13 @@ public final class EditingSession implements AutoCloseable { mLogSessionId = new LogSessionId(mId); } + /** Reports that an editing operation ended. */ + @FlaggedApi(FLAG_ADD_MEDIA_METRICS_EDITING) + public void reportEditingEndedEvent(@NonNull EditingEndedEvent editingEndedEvent) { + mManager.reportEditingEndedEvent(mId, editingEndedEvent); + } + + /** Returns the identifier for logging this session. */ public @NonNull LogSessionId getSessionId() { return mLogSessionId; } diff --git a/media/java/android/media/metrics/IMediaMetricsManager.aidl b/media/java/android/media/metrics/IMediaMetricsManager.aidl index 51b1cc27c8a7..e07ca67296b3 100644 --- a/media/java/android/media/metrics/IMediaMetricsManager.aidl +++ b/media/java/android/media/metrics/IMediaMetricsManager.aidl @@ -16,6 +16,7 @@ package android.media.metrics; +import android.media.metrics.EditingEndedEvent; import android.media.metrics.NetworkEvent; import android.media.metrics.PlaybackErrorEvent; import android.media.metrics.PlaybackMetrics; @@ -24,7 +25,7 @@ import android.media.metrics.TrackChangeEvent; import android.os.PersistableBundle; /** - * Interface to the playback manager service. + * Interface to the media metrics manager service. * @hide */ interface IMediaMetricsManager { @@ -37,6 +38,8 @@ interface IMediaMetricsManager { void reportPlaybackStateEvent(in String sessionId, in PlaybackStateEvent event, int userId); void reportTrackChangeEvent(in String sessionId, in TrackChangeEvent event, int userId); + void reportEditingEndedEvent(in String sessionId, in EditingEndedEvent event, int userId); + String getTranscodingSessionId(int userId); String getEditingSessionId(int userId); String getBundleSessionId(int userId); diff --git a/media/java/android/media/metrics/MediaMetricsManager.java b/media/java/android/media/metrics/MediaMetricsManager.java index 0898874c2f65..622b0c158b8a 100644 --- a/media/java/android/media/metrics/MediaMetricsManager.java +++ b/media/java/android/media/metrics/MediaMetricsManager.java @@ -193,4 +193,18 @@ public final class MediaMetricsManager { throw e.rethrowFromSystemServer(); } } + + /** + * Reports the event of an editing session ending. + * + * @hide + */ + public void reportEditingEndedEvent( + @NonNull String sessionId, EditingEndedEvent editingEndedEvent) { + try { + mService.reportEditingEndedEvent(sessionId, editingEndedEvent, mUserId); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } } diff --git a/media/java/android/media/tv/interactive/ITvInteractiveAppClient.aidl b/media/java/android/media/tv/interactive/ITvInteractiveAppClient.aidl index e3dba03d6093..7b5853169923 100644 --- a/media/java/android/media/tv/interactive/ITvInteractiveAppClient.aidl +++ b/media/java/android/media/tv/interactive/ITvInteractiveAppClient.aidl @@ -61,7 +61,8 @@ oneway interface ITvInteractiveAppClient { void onSetTvRecordingInfo(in String recordingId, in TvRecordingInfo recordingInfo, int seq); void onRequestTvRecordingInfo(in String recordingId, int seq); void onRequestTvRecordingInfoList(in int type, int seq); - void onRequestSigning( - in String id, in String algorithm, in String alias, in byte[] data, int seq); + void onRequestSigning(in String id, in String algorithm, in String alias, in byte[] data, + int seq); + void onRequestCertificate(in String host, int port, int seq); void onAdRequest(in AdRequest request, int Seq); } diff --git a/media/java/android/media/tv/interactive/ITvInteractiveAppManager.aidl b/media/java/android/media/tv/interactive/ITvInteractiveAppManager.aidl index 0f58b29247bb..1b9450b240ac 100644 --- a/media/java/android/media/tv/interactive/ITvInteractiveAppManager.aidl +++ b/media/java/android/media/tv/interactive/ITvInteractiveAppManager.aidl @@ -58,6 +58,8 @@ interface ITvInteractiveAppManager { void sendAvailableSpeeds(in IBinder sessionToken, in float[] speeds, int userId); void sendSigningResult(in IBinder sessionToken, in String signingId, in byte[] result, int userId); + void sendCertificate(in IBinder sessionToken, in String host, int port, + in Bundle certBundle, int userId); void sendTvRecordingInfo(in IBinder sessionToken, in TvRecordingInfo recordingInfo, int userId); void sendTvRecordingInfoList(in IBinder sessionToken, in List<TvRecordingInfo> recordingInfoList, int userId); diff --git a/media/java/android/media/tv/interactive/ITvInteractiveAppSession.aidl b/media/java/android/media/tv/interactive/ITvInteractiveAppSession.aidl index 06808c9ff915..3969315ab655 100644 --- a/media/java/android/media/tv/interactive/ITvInteractiveAppSession.aidl +++ b/media/java/android/media/tv/interactive/ITvInteractiveAppSession.aidl @@ -49,6 +49,7 @@ oneway interface ITvInteractiveAppSession { void sendTimeShiftMode(int mode); void sendAvailableSpeeds(in float[] speeds); void sendSigningResult(in String signingId, in byte[] result); + void sendCertificate(in String host, int port, in Bundle certBundle); void sendTvRecordingInfo(in TvRecordingInfo recordingInfo); void sendTvRecordingInfoList(in List<TvRecordingInfo> recordingInfoList); void notifyError(in String errMsg, in Bundle params); diff --git a/media/java/android/media/tv/interactive/ITvInteractiveAppSessionCallback.aidl b/media/java/android/media/tv/interactive/ITvInteractiveAppSessionCallback.aidl index 416b8f12d5ea..cb89181fd714 100644 --- a/media/java/android/media/tv/interactive/ITvInteractiveAppSessionCallback.aidl +++ b/media/java/android/media/tv/interactive/ITvInteractiveAppSessionCallback.aidl @@ -61,5 +61,6 @@ oneway interface ITvInteractiveAppSessionCallback { void onRequestTvRecordingInfo(in String recordingId); void onRequestTvRecordingInfoList(in int type); void onRequestSigning(in String id, in String algorithm, in String alias, in byte[] data); + void onRequestCertificate(in String host, int port); void onAdRequest(in AdRequest request); } diff --git a/media/java/android/media/tv/interactive/ITvInteractiveAppSessionWrapper.java b/media/java/android/media/tv/interactive/ITvInteractiveAppSessionWrapper.java index 77730aa46d0a..ec6c2bfab576 100644 --- a/media/java/android/media/tv/interactive/ITvInteractiveAppSessionWrapper.java +++ b/media/java/android/media/tv/interactive/ITvInteractiveAppSessionWrapper.java @@ -104,6 +104,7 @@ public class ITvInteractiveAppSessionWrapper private static final int DO_SEND_AVAILABLE_SPEEDS = 47; private static final int DO_SEND_SELECTED_TRACK_INFO = 48; private static final int DO_NOTIFY_VIDEO_FREEZE_UPDATED = 49; + private static final int DO_SEND_CERTIFICATE = 50; private final HandlerCaller mCaller; private Session mSessionImpl; @@ -369,6 +370,13 @@ public class ITvInteractiveAppSessionWrapper mSessionImpl.notifyVideoFreezeUpdated((Boolean) msg.obj); break; } + case DO_SEND_CERTIFICATE: { + SomeArgs args = (SomeArgs) msg.obj; + mSessionImpl.sendCertificate((String) args.arg1, (Integer) args.arg2, + (Bundle) args.arg3); + args.recycle(); + break; + } default: { Log.w(TAG, "Unhandled message code: " + msg.what); break; @@ -483,6 +491,12 @@ public class ITvInteractiveAppSessionWrapper } @Override + public void sendCertificate(@NonNull String host, int port, @NonNull Bundle certBundle) { + mCaller.executeOrSendMessage( + mCaller.obtainMessageOOO(DO_SEND_CERTIFICATE, host, port, certBundle)); + } + + @Override public void notifyError(@NonNull String errMsg, @NonNull Bundle params) { mCaller.executeOrSendMessage( mCaller.obtainMessageOO(DO_NOTIFY_ERROR, errMsg, params)); diff --git a/media/java/android/media/tv/interactive/TvInteractiveAppManager.java b/media/java/android/media/tv/interactive/TvInteractiveAppManager.java index 8a340f6862bb..011744f94edb 100755 --- a/media/java/android/media/tv/interactive/TvInteractiveAppManager.java +++ b/media/java/android/media/tv/interactive/TvInteractiveAppManager.java @@ -34,6 +34,7 @@ import android.media.tv.TvInputManager; import android.media.tv.TvRecordingInfo; import android.media.tv.TvTrackInfo; import android.net.Uri; +import android.net.http.SslCertificate; import android.os.Bundle; import android.os.Handler; import android.os.IBinder; @@ -656,6 +657,18 @@ public final class TvInteractiveAppManager { } @Override + public void onRequestCertificate(String host, int port, int seq) { + synchronized (mSessionCallbackRecordMap) { + SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq); + if (record == null) { + Log.e(TAG, "Callback not found for seq " + seq); + return; + } + record.postRequestCertificate(host, port); + } + } + + @Override public void onSessionStateChanged(int state, int err, int seq) { synchronized (mSessionCallbackRecordMap) { SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq); @@ -1328,6 +1341,19 @@ public final class TvInteractiveAppManager { } } + void sendCertificate(@NonNull String host, int port, @NonNull SslCertificate cert) { + if (mToken == null) { + Log.w(TAG, "The session has been already released"); + return; + } + try { + mService.sendCertificate(mToken, host, port, SslCertificate.saveState(cert), + mUserId); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + void notifyError(@NonNull String errMsg, @NonNull Bundle params) { if (mToken == null) { Log.w(TAG, "The session has been already released"); @@ -2232,6 +2258,15 @@ public final class TvInteractiveAppManager { }); } + void postRequestCertificate(String host, int port) { + mHandler.post(new Runnable() { + @Override + public void run() { + mSessionCallback.onRequestCertificate(mSession, host, port); + } + }); + } + void postRequestTvRecordingInfo(String recordingId) { mHandler.post(new Runnable() { @Override @@ -2574,6 +2609,17 @@ public final class TvInteractiveAppManager { } /** + * This is called when the service requests a SSL certificate for client validation. + * + * @param session A {@link TvInteractiveAppService.Session} associated with this callback. + * @param host the host name of the SSL authentication server. + * @param port the port of the SSL authentication server. E.g., 443 + * @hide + */ + public void onRequestCertificate(Session session, String host, int port) { + } + + /** * This is called when {@link TvInteractiveAppService.Session#notifySessionStateChanged} is * called. * diff --git a/media/java/android/media/tv/interactive/TvInteractiveAppService.java b/media/java/android/media/tv/interactive/TvInteractiveAppService.java index 5247a0ebe6e0..054b272d820f 100755 --- a/media/java/android/media/tv/interactive/TvInteractiveAppService.java +++ b/media/java/android/media/tv/interactive/TvInteractiveAppService.java @@ -46,6 +46,7 @@ import android.media.tv.TvTrackInfo; import android.media.tv.TvView; import android.media.tv.interactive.TvInteractiveAppView.TvInteractiveAppCallback; import android.net.Uri; +import android.net.http.SslCertificate; import android.os.AsyncTask; import android.os.Bundle; import android.os.Handler; @@ -734,6 +735,17 @@ public abstract class TvInteractiveAppService extends Service { } /** + * Receives the requested Certificate + * + * @param host the host name of the SSL authentication server. + * @param port the port of the SSL authentication server. E.g., 443 + * @param cert the SSL certificate received. + * @hide + */ + public void onCertificate(@NonNull String host, int port, @NonNull SslCertificate cert) { + } + + /** * Called when the application sends information of an error. * * @param errMsg the message of the error. @@ -1633,6 +1645,32 @@ public abstract class TvInteractiveAppService extends Service { } /** + * Requests a SSL certificate for client validation. + * + * @param host the host name of the SSL authentication server. + * @param port the port of the SSL authentication server. E.g., 443 + * @hide + */ + public void requestCertificate(@NonNull String host, int port) { + executeOrPostRunnableOnMainThread(new Runnable() { + @MainThread + @Override + public void run() { + try { + if (DEBUG) { + Log.d(TAG, "requestCertificate"); + } + if (mSessionCallback != null) { + mSessionCallback.onRequestCertificate(host, port); + } + } catch (RemoteException e) { + Log.w(TAG, "error in requestCertificate", e); + } + } + }); + } + + /** * Sends an advertisement request to be processed by the related TV input. * * @param request The advertisement request @@ -1725,6 +1763,11 @@ public abstract class TvInteractiveAppService extends Service { onSigningResult(signingId, result); } + void sendCertificate(String host, int port, Bundle certBundle) { + SslCertificate cert = SslCertificate.restoreState(certBundle); + onCertificate(host, port, cert); + } + void notifyError(String errMsg, Bundle params) { onError(errMsg, params); } diff --git a/media/java/android/media/tv/interactive/TvInteractiveAppView.java b/media/java/android/media/tv/interactive/TvInteractiveAppView.java index 5bb61c261ae2..3b295742c244 100755 --- a/media/java/android/media/tv/interactive/TvInteractiveAppView.java +++ b/media/java/android/media/tv/interactive/TvInteractiveAppView.java @@ -34,6 +34,7 @@ import android.media.tv.interactive.TvInteractiveAppManager.Session; import android.media.tv.interactive.TvInteractiveAppManager.Session.FinishedInputEventCallback; import android.media.tv.interactive.TvInteractiveAppManager.SessionCallback; import android.net.Uri; +import android.net.http.SslCertificate; import android.os.Bundle; import android.os.Handler; import android.util.AttributeSet; @@ -756,6 +757,22 @@ public class TvInteractiveAppView extends ViewGroup { } /** + * Send the requested SSL certificate to the TV Interactive App + * @param host the host name of the SSL authentication server. + * @param port the port of the SSL authentication server. E.g., 443 + * @param cert the SSL certificate requested + * @hide + */ + public void sendCertificate(@NonNull String host, int port, @NonNull SslCertificate cert) { + if (DEBUG) { + Log.d(TAG, "sendCertificate"); + } + if (mSession != null) { + mSession.sendCertificate(host, port, cert); + } + } + + /** * Notifies the corresponding {@link TvInteractiveAppService} when there is an error. * * @param errMsg the message of the error. diff --git a/native/graphics/jni/Android.bp b/native/graphics/jni/Android.bp index 10c570b30d7a..8ea46329af58 100644 --- a/native/graphics/jni/Android.bp +++ b/native/graphics/jni/Android.bp @@ -72,6 +72,9 @@ cc_library_shared { ], }, }, + stubs: { + symbol_file: "libjnigraphics.map.txt", + }, } // The headers module is in frameworks/native/Android.bp. @@ -93,15 +96,18 @@ cc_defaults { ], static_libs: ["libarect"], fuzz_config: { - cc: ["dichenzhang@google.com","scroggo@google.com"], + cc: [ + "dichenzhang@google.com", + "scroggo@google.com", + ], asan_options: [ "detect_odr_violation=1", ], hwasan_options: [ - // Image decoders may attempt to allocate a large amount of memory - // (especially if the encoded image is large). This doesn't - // necessarily mean there is a bug. Set allocator_may_return_null=1 - // for hwasan so the fuzzer can continue running. + // Image decoders may attempt to allocate a large amount of memory + // (especially if the encoded image is large). This doesn't + // necessarily mean there is a bug. Set allocator_may_return_null=1 + // for hwasan so the fuzzer can continue running. "allocator_may_return_null = 1", ], }, diff --git a/nfc/jarjar-rules.txt b/nfc/jarjar-rules.txt index 4cd652d6af7f..99ae14414dad 100644 --- a/nfc/jarjar-rules.txt +++ b/nfc/jarjar-rules.txt @@ -4,6 +4,7 @@ rule android.content.ComponentNameProto* com.android.nfc.x.@0 rule android.content.IntentProto* com.android.nfc.x.@0 rule android.content.IntentFilterProto* com.android.nfc.x.@0 rule android.content.AuthorityEntryProto* com.android.nfc.x.@0 +rule android.content.UriRelativeFilter* com.android.nfc.x.@0 rule android.nfc.cardemulation.AidGroupProto* com.android.nfc.x.@0 rule android.nfc.cardemulation.ApduServiceInfoProto* com.android.nfc.x.@0 rule android.nfc.cardemulation.NfcFServiceInfoProto* com.android.nfc.x.@0 diff --git a/nfc/java/android/nfc/cardemulation/CardEmulation.java b/nfc/java/android/nfc/cardemulation/CardEmulation.java index 9d38e4c5b297..1f41b812164c 100644 --- a/nfc/java/android/nfc/cardemulation/CardEmulation.java +++ b/nfc/java/android/nfc/cardemulation/CardEmulation.java @@ -338,8 +338,10 @@ public final class CardEmulation { } } /** - * Sets whether the system should default to observe mode or not when - * the service is in the foreground or the default payment service. + * Sets whether the system should default to observe mode or not when the service is in the + * foreground or the default payment service. The default is to not enable observe mode when + * a service either the foreground default service or the default payment service so not + * calling this method will preserve that behavior. * * @param service The component name of the service * @param enable Whether the servic should default to observe mode or not diff --git a/packages/CrashRecovery/aconfig/flags.aconfig b/packages/CrashRecovery/aconfig/flags.aconfig new file mode 100644 index 000000000000..563626634068 --- /dev/null +++ b/packages/CrashRecovery/aconfig/flags.aconfig @@ -0,0 +1,9 @@ +package: "android.crashrecovery.flags" + +flag { + name: "recoverability_detection" + namespace: "package_watchdog" + description: "Feature flag for recoverability detection" + bug: "310236690" + is_fixed_read_only: true +}
\ No newline at end of file diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/CustomizedAppBar.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/CustomizedAppBar.kt index 3a0e51b24c2c..4f6196648634 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/CustomizedAppBar.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/CustomizedAppBar.kt @@ -64,6 +64,8 @@ import androidx.compose.ui.layout.layoutId import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.semantics.clearAndSetSemantics +import androidx.compose.ui.semantics.heading +import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.Constraints @@ -129,7 +131,8 @@ private fun Title(title: String, maxLines: Int = Int.MAX_VALUE) { modifier = Modifier.padding( start = SettingsDimension.itemPaddingAround, end = SettingsDimension.itemPaddingEnd, - ), + ) + .semantics { heading() }, overflow = TextOverflow.Ellipsis, maxLines = maxLines, ) diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/common/BroadcastReceiverFlow.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/common/BroadcastReceiverFlow.kt index e099f1124bf1..0a424bc4230a 100644 --- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/common/BroadcastReceiverFlow.kt +++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/common/BroadcastReceiverFlow.kt @@ -37,10 +37,11 @@ private const val TAG = "BroadcastReceiverFlow" fun Context.broadcastReceiverFlow(intentFilter: IntentFilter): Flow<Intent> = callbackFlow { val broadcastReceiver = object : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { + Log.d(TAG, "onReceive: $intent") trySend(intent) } } - registerReceiver(broadcastReceiver, intentFilter, Context.RECEIVER_NOT_EXPORTED) + registerReceiver(broadcastReceiver, intentFilter, Context.RECEIVER_VISIBLE_TO_INSTANT_APPS) awaitClose { unregisterReceiver(broadcastReceiver) } }.catch { e -> diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppRepository.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppRepository.kt index 81a8b324f70f..cea3d13e27ab 100644 --- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppRepository.kt +++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppRepository.kt @@ -37,6 +37,7 @@ fun rememberAppRepository(): AppRepository = rememberContext(::AppRepositoryImpl interface AppRepository { fun loadLabel(app: ApplicationInfo): String + @Suppress("ABSTRACT_COMPOSABLE_DEFAULT_PARAMETER_VALUE") @Composable fun produceLabel(app: ApplicationInfo, isClonedAppPage: Boolean = false): State<String> { val context = LocalContext.current diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/framework/common/BroadcastReceiverFlowTest.kt b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/framework/common/BroadcastReceiverFlowTest.kt index eef5225aef42..772f925c0a77 100644 --- a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/framework/common/BroadcastReceiverFlowTest.kt +++ b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/framework/common/BroadcastReceiverFlowTest.kt @@ -43,7 +43,7 @@ class BroadcastReceiverFlowTest { private val context = mock<Context> { on { - registerReceiver(any(), eq(INTENT_FILTER), eq(Context.RECEIVER_NOT_EXPORTED)) + registerReceiver(any(), eq(INTENT_FILTER), eq(Context.RECEIVER_VISIBLE_TO_INSTANT_APPS)) } doAnswer { registeredBroadcastReceiver = it.arguments[0] as BroadcastReceiver null diff --git a/packages/SettingsLib/res/drawable/ic_external_display.xml b/packages/SettingsLib/res/drawable/ic_external_display.xml new file mode 100644 index 000000000000..de50de8c07c8 --- /dev/null +++ b/packages/SettingsLib/res/drawable/ic_external_display.xml @@ -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. + --> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="25dp" + android:viewportWidth="24" + android:viewportHeight="25"> + <group> + <clip-path + android:pathData="M0,0.307h24v24h-24z"/> + <path + android:pathData="M8,21.307V19.307H10V17.307H4C3.45,17.307 2.975,17.115 2.575,16.732C2.192,16.332 2,15.857 2,15.307V5.307C2,4.757 2.192,4.29 2.575,3.907C2.975,3.507 3.45,3.307 4,3.307H20C20.55,3.307 21.017,3.507 21.4,3.907C21.8,4.29 22,4.757 22,5.307V15.307C22,15.857 21.8,16.332 21.4,16.732C21.017,17.115 20.55,17.307 20,17.307H14V19.307H16V21.307H8ZM4,15.307H20V5.307H4V15.307ZM4,15.307V5.307V15.307Z" + android:fillColor="#E5E3D6"/> + </group> +</vector> diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/HapClientProfile.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/HapClientProfile.java index cf4d6be9a042..0613676113f5 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/HapClientProfile.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/HapClientProfile.java @@ -99,6 +99,8 @@ public class HapClientProfile implements LocalBluetoothProfile { device.refresh(); } + // Check current list of CachedDevices to see if any are hearing aid devices. + mDeviceManager.updateHearingAidsDevices(); mIsProfileReady = true; mProfileManager.callServiceConnectedListeners(); } diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidDeviceManager.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidDeviceManager.java index 3a15b7108e3f..9fd174d4586c 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidDeviceManager.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidDeviceManager.java @@ -15,8 +15,8 @@ */ package com.android.settingslib.bluetooth; -import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothHearingAid; +import android.bluetooth.BluetoothLeAudio; import android.bluetooth.BluetoothProfile; import android.bluetooth.BluetoothUuid; import android.bluetooth.le.ScanFilter; @@ -68,14 +68,9 @@ public class HearingAidDeviceManager { void initHearingAidDeviceIfNeeded(CachedBluetoothDevice newDevice, List<ScanFilter> leScanFilters) { - long hiSyncId = getHiSyncId(newDevice.getDevice()); - if (isValidHiSyncId(hiSyncId)) { - // Once hiSyncId is valid, assign hearing aid info - final HearingAidInfo.Builder infoBuilder = new HearingAidInfo.Builder() - .setAshaDeviceSide(getDeviceSide(newDevice.getDevice())) - .setAshaDeviceMode(getDeviceMode(newDevice.getDevice())) - .setHiSyncId(hiSyncId); - newDevice.setHearingAidInfo(infoBuilder.build()); + HearingAidInfo info = generateHearingAidInfo(newDevice); + if (info != null) { + newDevice.setHearingAidInfo(info); } else if (leScanFilters != null && !newDevice.isHearingAidDevice()) { // If the device is added with hearing aid scan filter during pairing, set an empty // hearing aid info to indicate it's a hearing aid device. The info will be updated @@ -94,38 +89,6 @@ public class HearingAidDeviceManager { } } - private long getHiSyncId(BluetoothDevice device) { - final LocalBluetoothProfileManager profileManager = mBtManager.getProfileManager(); - final HearingAidProfile profileProxy = profileManager.getHearingAidProfile(); - if (profileProxy == null) { - return BluetoothHearingAid.HI_SYNC_ID_INVALID; - } - - return profileProxy.getHiSyncId(device); - } - - private int getDeviceSide(BluetoothDevice device) { - final LocalBluetoothProfileManager profileManager = mBtManager.getProfileManager(); - final HearingAidProfile profileProxy = profileManager.getHearingAidProfile(); - if (profileProxy == null) { - Log.w(TAG, "HearingAidProfile is not supported and not ready to fetch device side"); - return HearingAidProfile.DeviceSide.SIDE_INVALID; - } - - return profileProxy.getDeviceSide(device); - } - - private int getDeviceMode(BluetoothDevice device) { - final LocalBluetoothProfileManager profileManager = mBtManager.getProfileManager(); - final HearingAidProfile profileProxy = profileManager.getHearingAidProfile(); - if (profileProxy == null) { - Log.w(TAG, "HearingAidProfile is not supported and not ready to fetch device mode"); - return HearingAidProfile.DeviceMode.MODE_INVALID; - } - - return profileProxy.getDeviceMode(device); - } - boolean setSubDeviceIfNeeded(CachedBluetoothDevice newDevice) { final long hiSyncId = newDevice.getHiSyncId(); if (isValidHiSyncId(hiSyncId)) { @@ -157,21 +120,17 @@ public class HearingAidDeviceManager { // To collect all HearingAid devices and call #onHiSyncIdChanged to group device by HiSyncId void updateHearingAidsDevices() { - final Set<Long> newSyncIdSet = new HashSet<Long>(); + final Set<Long> newSyncIdSet = new HashSet<>(); for (CachedBluetoothDevice cachedDevice : mCachedDevices) { // Do nothing if HiSyncId has been assigned - if (!isValidHiSyncId(cachedDevice.getHiSyncId())) { - final long newHiSyncId = getHiSyncId(cachedDevice.getDevice()); - // Do nothing if there is no HiSyncId on Bluetooth device - if (isValidHiSyncId(newHiSyncId)) { - // Once hiSyncId is valid, assign hearing aid info - final HearingAidInfo.Builder infoBuilder = new HearingAidInfo.Builder() - .setAshaDeviceSide(getDeviceSide(cachedDevice.getDevice())) - .setAshaDeviceMode(getDeviceMode(cachedDevice.getDevice())) - .setHiSyncId(newHiSyncId); - cachedDevice.setHearingAidInfo(infoBuilder.build()); - - newSyncIdSet.add(newHiSyncId); + if (isValidHiSyncId(cachedDevice.getHiSyncId())) { + continue; + } + HearingAidInfo info = generateHearingAidInfo(cachedDevice); + if (info != null) { + cachedDevice.setHearingAidInfo(info); + if (isValidHiSyncId(info.getHiSyncId())) { + newSyncIdSet.add(info.getHiSyncId()); } } } @@ -378,6 +337,54 @@ public class HearingAidDeviceManager { return null; } + private boolean isLeAudioHearingAid(CachedBluetoothDevice cachedDevice) { + List<LocalBluetoothProfile> profiles = cachedDevice.getProfiles(); + boolean supportLeAudio = profiles.stream().anyMatch(p -> p instanceof LeAudioProfile); + boolean supportHapClient = profiles.stream().anyMatch(p -> p instanceof HapClientProfile); + return supportLeAudio && supportHapClient; + } + + private boolean isAshaHearingAid(CachedBluetoothDevice cachedDevice) { + return cachedDevice.getProfiles().stream().anyMatch(p -> p instanceof HearingAidProfile); + } + + private HearingAidInfo generateHearingAidInfo(CachedBluetoothDevice cachedDevice) { + final LocalBluetoothProfileManager profileManager = mBtManager.getProfileManager(); + if (isAshaHearingAid(cachedDevice)) { + final HearingAidProfile asha = profileManager.getHearingAidProfile(); + if (asha == null) { + Log.w(TAG, "HearingAidProfile is not supported on this device"); + } else { + long hiSyncId = asha.getHiSyncId(cachedDevice.getDevice()); + if (isValidHiSyncId(hiSyncId)) { + final HearingAidInfo.Builder infoBuilder = new HearingAidInfo.Builder() + .setAshaDeviceSide(asha.getDeviceSide(cachedDevice.getDevice())) + .setAshaDeviceMode(asha.getDeviceMode(cachedDevice.getDevice())) + .setHiSyncId(hiSyncId); + return infoBuilder.build(); + } + } + } + if (isLeAudioHearingAid(cachedDevice)) { + final HapClientProfile hapClientProfile = profileManager.getHapClientProfile(); + final LeAudioProfile leAudioProfile = profileManager.getLeAudioProfile(); + if (hapClientProfile == null || leAudioProfile == null) { + Log.w(TAG, "HapClientProfile or LeAudioProfile is not supported on this device"); + } else { + int audioLocation = leAudioProfile.getAudioLocation(cachedDevice.getDevice()); + int hearingAidType = hapClientProfile.getHearingAidType(cachedDevice.getDevice()); + if (audioLocation != BluetoothLeAudio.AUDIO_LOCATION_INVALID + && hearingAidType != HapClientProfile.HearingAidType.TYPE_INVALID) { + final HearingAidInfo.Builder infoBuilder = new HearingAidInfo.Builder() + .setLeAudioLocation(audioLocation) + .setHapDeviceType(hearingAidType); + return infoBuilder.build(); + } + } + } + return null; + } + private void log(String msg) { if (DEBUG) { Log.d(TAG, msg); diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidProfile.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidProfile.java index 14fab16de3e6..f2450de60878 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidProfile.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidProfile.java @@ -109,7 +109,7 @@ public class HearingAidProfile implements LocalBluetoothProfile { device.refresh(); } - // Check current list of CachedDevices to see if any are Hearing Aid devices. + // Check current list of CachedDevices to see if any are hearing aid devices. mDeviceManager.updateHearingAidsDevices(); mIsProfileReady = true; mProfileManager.callServiceConnectedListeners(); diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LeAudioProfile.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LeAudioProfile.java index 931a6f149b84..6be4336178eb 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LeAudioProfile.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LeAudioProfile.java @@ -83,6 +83,8 @@ public class LeAudioProfile implements LocalBluetoothProfile { device.refresh(); } + // Check current list of CachedDevices to see if any are hearing aid devices. + mDeviceManager.updateHearingAidsDevices(); mProfileManager.callServiceConnectedListeners(); mIsProfileReady = true; } diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java index 934870507a20..1d2f7902b781 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java @@ -759,6 +759,20 @@ public class LocalBluetoothLeBroadcast implements LocalBluetoothProfile { } /** + * Update the LE Broadcast by calling {@link BluetoothLeBroadcast#updateBroadcast(int, + * BluetoothLeAudioContentMetadata)}, currently only updates programInfo. + */ + public void updateBroadcast() { + if (mServiceBroadcast == null) { + Log.d(TAG, "The BluetoothLeBroadcast is null when updating the broadcast."); + return; + } + String programInfo = getProgramInfo(); + mBluetoothLeAudioContentMetadata = mBuilder.setProgramInfo(programInfo).build(); + mServiceBroadcast.updateBroadcast(mBroadcastId, mBluetoothLeAudioContentMetadata); + } + + /** * Register Broadcast Callbacks to track its state and receivers * * @param executor Executor object for callback diff --git a/packages/SettingsLib/src/com/android/settingslib/media/DeviceIconUtil.java b/packages/SettingsLib/src/com/android/settingslib/media/DeviceIconUtil.java index cf224dc3be5f..3de49336f427 100644 --- a/packages/SettingsLib/src/com/android/settingslib/media/DeviceIconUtil.java +++ b/packages/SettingsLib/src/com/android/settingslib/media/DeviceIconUtil.java @@ -71,15 +71,15 @@ public class DeviceIconUtil { new Device( AudioDeviceInfo.TYPE_HDMI, MediaRoute2Info.TYPE_HDMI, - mIsTv ? R.drawable.ic_tv : R.drawable.ic_headphone), + mIsTv ? R.drawable.ic_tv : R.drawable.ic_external_display), new Device( AudioDeviceInfo.TYPE_HDMI_ARC, MediaRoute2Info.TYPE_HDMI_ARC, - mIsTv ? R.drawable.ic_hdmi : R.drawable.ic_headphone), + mIsTv ? R.drawable.ic_hdmi : R.drawable.ic_external_display), new Device( AudioDeviceInfo.TYPE_HDMI_EARC, MediaRoute2Info.TYPE_HDMI_EARC, - mIsTv ? R.drawable.ic_hdmi : R.drawable.ic_headphone), + mIsTv ? R.drawable.ic_hdmi : R.drawable.ic_external_display), new Device( AudioDeviceInfo.TYPE_WIRED_HEADSET, MediaRoute2Info.TYPE_WIRED_HEADSET, diff --git a/packages/SettingsLib/src/com/android/settingslib/media/RouterInfoMediaManager.java b/packages/SettingsLib/src/com/android/settingslib/media/RouterInfoMediaManager.java index 8a122fcddcb3..aef09ac236f3 100644 --- a/packages/SettingsLib/src/com/android/settingslib/media/RouterInfoMediaManager.java +++ b/packages/SettingsLib/src/com/android/settingslib/media/RouterInfoMediaManager.java @@ -26,11 +26,13 @@ import android.media.MediaRouter2Manager; import android.media.RouteDiscoveryPreference; import android.media.RouteListingPreference; import android.media.RoutingSessionInfo; +import android.os.Process; import android.text.TextUtils; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import com.android.media.flags.Flags; import com.android.settingslib.bluetooth.LocalBluetoothManager; import java.util.ArrayList; @@ -62,21 +64,33 @@ public final class RouterInfoMediaManager extends InfoMediaManager { refreshDevices(); }; - // TODO: b/192657812 - Create factory method in InfoMediaManager to return - // RouterInfoMediaManager or ManagerInfoMediaManager based on flag. + // TODO (b/321969740): Plumb target UserHandle between UMO and RouterInfoMediaManager. public RouterInfoMediaManager( Context context, String packageName, Notification notification, - LocalBluetoothManager localBluetoothManager) throws PackageNotAvailableException { + LocalBluetoothManager localBluetoothManager) + throws PackageNotAvailableException { super(context, packageName, notification, localBluetoothManager); - mRouter = MediaRouter2.getInstance(context, packageName); + MediaRouter2 router = null; - if (mRouter == null) { + if (Flags.enableCrossUserRoutingInMediaRouter2()) { + try { + router = MediaRouter2.getInstance(context, packageName, Process.myUserHandle()); + } catch (IllegalArgumentException ex) { + // Do nothing + } + } else { + router = MediaRouter2.getInstance(context, packageName); + } + if (router == null) { throw new PackageNotAvailableException( "Package name " + packageName + " does not exist."); } + // We have to defer initialization because mRouter is final. + mRouter = router; + mRouterManager = MediaRouter2Manager.getInstance(context); } 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 new file mode 100644 index 000000000000..3355fb395ca0 --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/AudioRepository.kt @@ -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.settingslib.volume.data.repository + +import android.media.AudioManager +import com.android.internal.util.ConcurrentUtils +import kotlin.coroutines.CoroutineContext +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch + +/** Provides audio managing functionality and data. */ +interface AudioRepository { + + /** Current [AudioManager.getMode]. */ + val mode: StateFlow<Int> +} + +class AudioRepositoryImpl( + private val audioManager: AudioManager, + backgroundCoroutineContext: CoroutineContext, + coroutineScope: CoroutineScope, +) : AudioRepository { + + override val mode: StateFlow<Int> = + callbackFlow { + val listener = + AudioManager.OnModeChangedListener { newMode -> launch { send(newMode) } } + audioManager.addOnModeChangedListener(ConcurrentUtils.DIRECT_EXECUTOR, listener) + awaitClose { audioManager.removeOnModeChangedListener(listener) } + } + .flowOn(backgroundCoroutineContext) + .stateIn(coroutineScope, SharingStarted.WhileSubscribed(), audioManager.mode) +} diff --git a/packages/SettingsLib/src/com/android/settingslib/volume/domain/interactor/AudioModeInteractor.kt b/packages/SettingsLib/src/com/android/settingslib/volume/domain/interactor/AudioModeInteractor.kt new file mode 100644 index 000000000000..053c59b15067 --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/volume/domain/interactor/AudioModeInteractor.kt @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settingslib.volume.domain.interactor + +import android.media.AudioManager +import com.android.settingslib.volume.data.repository.AudioRepository +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + +class AudioModeInteractor(repository: AudioRepository) { + + private val ongoingCallModes = + setOf( + AudioManager.MODE_RINGTONE, + AudioManager.MODE_IN_CALL, + AudioManager.MODE_IN_COMMUNICATION, + ) + + /** Returns if current [AudioManager.getMode] call is an ongoing call */ + val isOngoingCall: Flow<Boolean> = repository.mode.map { it in ongoingCallModes } +} diff --git a/packages/SettingsLib/tests/integ/Android.bp b/packages/SettingsLib/tests/integ/Android.bp index 644b72c383ac..ce3a7baf6be6 100644 --- a/packages/SettingsLib/tests/integ/Android.bp +++ b/packages/SettingsLib/tests/integ/Android.bp @@ -57,6 +57,7 @@ android_test { "SettingsLibSettingsSpinner", "SettingsLibUsageProgressBarPreference", "settingslib_media_flags_lib", + "kotlinx_coroutines_test", ], dxflags: ["--multi-dex"], 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 new file mode 100644 index 000000000000..686362fadf02 --- /dev/null +++ b/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/FakeAudioRepository.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 kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow + +class FakeAudioRepository : AudioRepository { + + private val mutableMode = MutableStateFlow(0) + override val mode: StateFlow<Int> + get() = mutableMode.asStateFlow() + + fun setMode(newMode: Int) { + mutableMode.value = newMode + } +} 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 new file mode 100644 index 000000000000..3bc1edc9c944 --- /dev/null +++ b/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/domain/interactor/AudioModeInteractorTest.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.settingslib.volume.domain.interactor + +import android.media.AudioManager +import androidx.test.filters.SmallTest +import androidx.test.runner.AndroidJUnit4 +import com.android.settingslib.volume.data.repository.FakeAudioRepository +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.Test +import org.junit.runner.RunWith + +@OptIn(ExperimentalCoroutinesApi::class) +@RunWith(AndroidJUnit4::class) +@SmallTest +class AudioModeInteractorTest { + + private val testScope = TestScope() + private val fakeAudioRepository = FakeAudioRepository() + + private val underTest = AudioModeInteractor(fakeAudioRepository) + + @Test + fun ongoingCallModes_isOnGoingCall() { + testScope.runTest { + for (mode in ongoingCallModes) { + var isOngoingCall = false + underTest.isOngoingCall.onEach { isOngoingCall = it }.launchIn(backgroundScope) + + fakeAudioRepository.setMode(mode) + runCurrent() + + assertThat(isOngoingCall).isTrue() + } + } + } + + @Test + fun notOngoingCallModes_isNotOnGoingCall() { + testScope.runTest { + var isOngoingCall = true + underTest.isOngoingCall.onEach { isOngoingCall = it }.launchIn(backgroundScope) + + fakeAudioRepository.setMode(AudioManager.MODE_CURRENT) + runCurrent() + + assertThat(isOngoingCall).isFalse() + } + } + + private companion object { + private val ongoingCallModes = + setOf( + AudioManager.MODE_RINGTONE, + AudioManager.MODE_IN_CALL, + AudioManager.MODE_IN_COMMUNICATION, + ) + } +} diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/HearingAidDeviceManagerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/HearingAidDeviceManagerTest.java index e7487e857464..aa5a2984e70c 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/HearingAidDeviceManagerTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/HearingAidDeviceManagerTest.java @@ -15,6 +15,15 @@ */ package com.android.settingslib.bluetooth; +import static android.bluetooth.BluetoothHearingAid.HI_SYNC_ID_INVALID; +import static android.bluetooth.BluetoothLeAudio.AUDIO_LOCATION_FRONT_LEFT; +import static android.bluetooth.BluetoothLeAudio.AUDIO_LOCATION_INVALID; + +import static com.android.settingslib.bluetooth.HapClientProfile.HearingAidType.TYPE_BINAURAL; +import static com.android.settingslib.bluetooth.HapClientProfile.HearingAidType.TYPE_INVALID; +import static com.android.settingslib.bluetooth.HearingAidProfile.DeviceMode.MODE_BINAURAL; +import static com.android.settingslib.bluetooth.HearingAidProfile.DeviceSide.SIDE_RIGHT; + import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.any; @@ -32,7 +41,6 @@ import static org.mockito.Mockito.when; import android.bluetooth.BluetoothClass; import android.bluetooth.BluetoothDevice; -import android.bluetooth.BluetoothHearingAid; import android.bluetooth.BluetoothProfile; import android.bluetooth.BluetoothUuid; import android.bluetooth.le.ScanFilter; @@ -92,6 +100,10 @@ public class HearingAidDeviceManagerTest { @Mock private HearingAidProfile mHearingAidProfile; @Mock + private LeAudioProfile mLeAudioProfile; + @Mock + private HapClientProfile mHapClientProfile; + @Mock private AudioProductStrategy mAudioStrategy; @Mock private BluetoothDevice mDevice1; @@ -123,6 +135,8 @@ public class HearingAidDeviceManagerTest { when(mLocalBluetoothManager.getEventManager()).thenReturn(mBluetoothEventManager); when(mLocalBluetoothManager.getProfileManager()).thenReturn(mLocalProfileManager); when(mLocalProfileManager.getHearingAidProfile()).thenReturn(mHearingAidProfile); + when(mLocalProfileManager.getLeAudioProfile()).thenReturn(mLeAudioProfile); + when(mLocalProfileManager.getHapClientProfile()).thenReturn(mHapClientProfile); when(mAudioStrategy.getAudioAttributesForLegacyStreamType( AudioManager.STREAM_MUSIC)) .thenReturn((new AudioAttributes.Builder()).build()); @@ -140,34 +154,43 @@ public class HearingAidDeviceManagerTest { } /** - * Test initHearingAidDeviceIfNeeded, set HearingAid's information, including HiSyncId, - * deviceSide, deviceMode. + * Test initHearingAidDeviceIfNeeded + * + * Conditions: + * 1) ASHA hearing aid + * 2) Valid HiSyncId + * Result: + * Set hearing aid info to the device. */ @Test - public void initHearingAidDeviceIfNeeded_validHiSyncId_setHearingAidInfo() { + public void initHearingAidDeviceIfNeeded_asha_validHiSyncId_setHearingAidInfo() { + when(mCachedDevice1.getProfiles()).thenReturn(List.of(mHearingAidProfile)); when(mHearingAidProfile.getHiSyncId(mDevice1)).thenReturn(HISYNCID1); - when(mHearingAidProfile.getDeviceMode(mDevice1)).thenReturn( - HearingAidProfile.DeviceMode.MODE_BINAURAL); - when(mHearingAidProfile.getDeviceSide(mDevice1)).thenReturn( - HearingAidProfile.DeviceSide.SIDE_RIGHT); + when(mHearingAidProfile.getDeviceMode(mDevice1)).thenReturn(MODE_BINAURAL); + when(mHearingAidProfile.getDeviceSide(mDevice1)).thenReturn(SIDE_RIGHT); assertThat(mCachedDevice1.getHiSyncId()).isNotEqualTo(HISYNCID1); mHearingAidDeviceManager.initHearingAidDeviceIfNeeded(mCachedDevice1, null); assertThat(mCachedDevice1.getHiSyncId()).isEqualTo(HISYNCID1); + assertThat(mCachedDevice1.getDeviceSide()).isEqualTo(HearingAidInfo.DeviceSide.SIDE_RIGHT); assertThat(mCachedDevice1.getDeviceMode()).isEqualTo( HearingAidInfo.DeviceMode.MODE_BINAURAL); - assertThat(mCachedDevice1.getDeviceSide()).isEqualTo( - HearingAidInfo.DeviceSide.SIDE_RIGHT); } /** - * Test initHearingAidDeviceIfNeeded, an invalid HiSyncId will not be assigned + * Test initHearingAidDeviceIfNeeded + * + * Conditions: + * 1) ASHA hearing aid + * 2) Invalid HiSyncId + * Result: + * Do not set hearing aid info to the device. */ @Test - public void initHearingAidDeviceIfNeeded_invalidHiSyncId_notToSetHearingAidInfo() { - when(mHearingAidProfile.getHiSyncId(mDevice1)).thenReturn( - BluetoothHearingAid.HI_SYNC_ID_INVALID); + public void initHearingAidDeviceIfNeeded_asha_invalidHiSyncId_notToSetHearingAidInfo() { + when(mCachedDevice1.getProfiles()).thenReturn(List.of(mHearingAidProfile)); + when(mHearingAidProfile.getHiSyncId(mDevice1)).thenReturn(HI_SYNC_ID_INVALID); mHearingAidDeviceManager.initHearingAidDeviceIfNeeded(mCachedDevice1, null); @@ -175,34 +198,89 @@ public class HearingAidDeviceManagerTest { } /** - * Test initHearingAidDeviceIfNeeded, an invalid HiSyncId and hearing aid scan filter, set an - * empty hearing aid info on the device. + * Test initHearingAidDeviceIfNeeded + * + * Conditions: + * 1) ASHA hearing aid + * 2) Invalid HiSyncId + * 3) ASHA uuid scan filter + * Result: + * Set an empty hearing aid info to the device. */ @Test - public void initHearingAidDeviceIfNeeded_hearingAidScanFilter_setHearingAidInfo() { - when(mHearingAidProfile.getHiSyncId(mDevice1)).thenReturn( - BluetoothHearingAid.HI_SYNC_ID_INVALID); + public void initHearingAidDeviceIfNeeded_asha_scanFilterNotNull_setEmptyHearingAidInfo() { + when(mCachedDevice1.getProfiles()).thenReturn(List.of(mHearingAidProfile)); + when(mHearingAidProfile.getHiSyncId(mDevice1)).thenReturn(HI_SYNC_ID_INVALID); final ScanFilter scanFilter = new ScanFilter.Builder() .setServiceData(BluetoothUuid.HEARING_AID, new byte[]{0}).build(); mHearingAidDeviceManager.initHearingAidDeviceIfNeeded(mCachedDevice1, List.of(scanFilter)); - assertThat(mCachedDevice1.isHearingAidDevice()).isTrue(); + verify(mCachedDevice1).setHearingAidInfo(new HearingAidInfo.Builder().build()); } /** - * Test initHearingAidDeviceIfNeeded, an invalid HiSyncId and random scan filter, not to set - * hearing aid info on the device. + * Test initHearingAidDeviceIfNeeded + * + * Conditions: + * 1) Asha hearing aid + * 2) Invalid HiSyncId + * 3) Random scan filter + * Result: + * Do not set hearing aid info to the device. */ @Test - public void initHearingAidDeviceIfNeeded_randomScanFilter_setHearingAidInfo() { - when(mHearingAidProfile.getHiSyncId(mDevice1)).thenReturn( - BluetoothHearingAid.HI_SYNC_ID_INVALID); + public void initHearingAidDeviceIfNeeded_asha_randomScanFilter_notToSetHearingAidInfo() { + when(mCachedDevice1.getProfiles()).thenReturn(List.of(mHearingAidProfile)); + when(mHearingAidProfile.getHiSyncId(mDevice1)).thenReturn(HI_SYNC_ID_INVALID); final ScanFilter scanFilter = new ScanFilter.Builder().build(); mHearingAidDeviceManager.initHearingAidDeviceIfNeeded(mCachedDevice1, List.of(scanFilter)); - assertThat(mCachedDevice1.isHearingAidDevice()).isFalse(); + verify(mCachedDevice1, never()).setHearingAidInfo(any(HearingAidInfo.class)); + } + + /** + * Test initHearingAidDeviceIfNeeded + * + * Conditions: + * 1) LeAudio hearing aid + * 2) Valid audio location and device type + * Result: + * Set hearing aid info to the device. + */ + @Test + public void initHearingAidDeviceIfNeeded_leAudio_validInfo_setHearingAidInfo() { + when(mCachedDevice1.getProfiles()).thenReturn(List.of(mLeAudioProfile, mHapClientProfile)); + when(mLeAudioProfile.getAudioLocation(mDevice1)).thenReturn(AUDIO_LOCATION_FRONT_LEFT); + when(mHapClientProfile.getHearingAidType(mDevice1)).thenReturn(TYPE_BINAURAL); + + mHearingAidDeviceManager.initHearingAidDeviceIfNeeded(mCachedDevice1, null); + + verify(mCachedDevice1).setHearingAidInfo(any(HearingAidInfo.class)); + assertThat(mCachedDevice1.getDeviceSide()).isEqualTo(HearingAidInfo.DeviceSide.SIDE_LEFT); + assertThat(mCachedDevice1.getDeviceMode()).isEqualTo( + HearingAidInfo.DeviceMode.MODE_BINAURAL); + } + + /** + * Test initHearingAidDeviceIfNeeded + * + * Conditions: + * 1) LeAudio hearing aid + * 2) Invalid audio location and device type + * Result: + * Do not set hearing aid info to the device. + */ + @Test + public void initHearingAidDeviceIfNeeded_leAudio_invalidInfo_notToSetHearingAidInfo() { + when(mCachedDevice1.getProfiles()).thenReturn(List.of(mLeAudioProfile, mHapClientProfile)); + when(mLeAudioProfile.getAudioLocation(mDevice1)).thenReturn(AUDIO_LOCATION_INVALID); + when(mHapClientProfile.getHearingAidType(mDevice1)).thenReturn(TYPE_INVALID); + + mHearingAidDeviceManager.initHearingAidDeviceIfNeeded(mCachedDevice1, null); + + verify(mCachedDevice1, never()).setHearingAidInfo(any(HearingAidInfo.class)); } /** @@ -234,13 +312,20 @@ public class HearingAidDeviceManagerTest { } /** - * Test updateHearingAidsDevices, to link two devices with the same HiSyncId. - * When first paired devices is connected and second paired device is disconnected, first - * paired device would be set as main device and second device will be removed from - * CachedDevices list. + * Test updateHearingAidsDevices + * + * Conditions: + * 1) Two ASHA hearing aids with the same HiSyncId + * 2) First paired devices is connected + * 3) Second paired device is disconnected + * Result: + * First paired device would be set as main device and second paired device will be set + * as sub device and removed from CachedDevices list. */ @Test - public void updateHearingAidsDevices_firstPairedDevicesConnected_verifySubDevice() { + public void updateHearingAidsDevices_asha_firstPairedDevicesConnected_verifySubDevice() { + when(mCachedDevice1.getProfiles()).thenReturn(List.of(mHearingAidProfile)); + when(mCachedDevice2.getProfiles()).thenReturn(List.of(mHearingAidProfile)); when(mHearingAidProfile.getHiSyncId(mDevice1)).thenReturn(HISYNCID1); when(mHearingAidProfile.getHiSyncId(mDevice2)).thenReturn(HISYNCID1); when(mCachedDevice1.isConnected()).thenReturn(true); @@ -257,13 +342,20 @@ public class HearingAidDeviceManagerTest { } /** - * Test updateHearingAidsDevices, to link two devices with the same HiSyncId. - * When second paired devices is connected and first paired device is disconnected, second - * paired device would be set as main device and first device will be removed from - * CachedDevices list. + * Test updateHearingAidsDevices + * + * Conditions: + * 1) Two ASHA hearing aids with the same HiSyncId + * 2) First paired devices is disconnected + * 3) Second paired device is connected + * Result: + * Second paired device would be set as main device and first paired device will be set + * as sub device and removed from CachedDevices list. */ @Test - public void updateHearingAidsDevices_secondPairedDeviceConnected_verifySubDevice() { + public void updateHearingAidsDevices_asha_secondPairedDeviceConnected_verifySubDevice() { + when(mCachedDevice1.getProfiles()).thenReturn(List.of(mHearingAidProfile)); + when(mCachedDevice2.getProfiles()).thenReturn(List.of(mHearingAidProfile)); when(mHearingAidProfile.getHiSyncId(mDevice1)).thenReturn(HISYNCID1); when(mHearingAidProfile.getHiSyncId(mDevice2)).thenReturn(HISYNCID1); when(mCachedDevice1.isConnected()).thenReturn(false); @@ -280,12 +372,20 @@ public class HearingAidDeviceManagerTest { } /** - * Test updateHearingAidsDevices, to link two devices with the same HiSyncId. - * When both devices are connected, to build up main and sub relationship and to remove sub - * device from CachedDevices list. + * Test updateHearingAidsDevices + * + * Conditions: + * 1) Two ASHA hearing aids with the same HiSyncId + * 2) First paired devices is connected + * 3) Second paired device is connected + * Result: + * First paired device would be set as main device and second paired device will be set + * as sub device and removed from CachedDevices list. */ @Test - public void updateHearingAidsDevices_BothConnected_verifySubDevice() { + public void updateHearingAidsDevices_asha_bothConnected_verifySubDevice() { + when(mCachedDevice1.getProfiles()).thenReturn(List.of(mHearingAidProfile)); + when(mCachedDevice2.getProfiles()).thenReturn(List.of(mHearingAidProfile)); when(mHearingAidProfile.getHiSyncId(mDevice1)).thenReturn(HISYNCID1); when(mHearingAidProfile.getHiSyncId(mDevice2)).thenReturn(HISYNCID1); when(mCachedDevice1.isConnected()).thenReturn(true); @@ -302,46 +402,64 @@ public class HearingAidDeviceManagerTest { } /** - * Test updateHearingAidsDevices, dispatch callback + * Test updateHearingAidsDevices + * + * Conditions: + * 1) Two ASHA hearing aids with the same HiSyncId + * Result: + * Dispatch device removed callback */ @Test - public void updateHearingAidsDevices_dispatchDeviceRemovedCallback() { + public void updateHearingAidsDevices_asha_dispatchDeviceRemovedCallback() { + when(mCachedDevice1.getProfiles()).thenReturn(List.of(mHearingAidProfile)); + when(mCachedDevice2.getProfiles()).thenReturn(List.of(mHearingAidProfile)); when(mHearingAidProfile.getHiSyncId(mDevice1)).thenReturn(HISYNCID1); when(mHearingAidProfile.getHiSyncId(mDevice2)).thenReturn(HISYNCID1); mCachedDeviceManager.mCachedDevices.add(mCachedDevice1); mCachedDeviceManager.mCachedDevices.add(mCachedDevice2); + mHearingAidDeviceManager.updateHearingAidsDevices(); verify(mBluetoothEventManager).dispatchDeviceRemoved(mCachedDevice1); } /** - * Test updateHearingAidsDevices, do nothing when HiSyncId is invalid + * Test updateHearingAidsDevices + * + * Conditions: + * 1) Two ASHA hearing aids with invalid HiSyncId + * Result: + * Do nothing */ @Test - public void updateHearingAidsDevices_invalidHiSyncId_doNothing() { - when(mHearingAidProfile.getHiSyncId(mDevice1)). - thenReturn(BluetoothHearingAid.HI_SYNC_ID_INVALID); - when(mHearingAidProfile.getHiSyncId(mDevice2)). - thenReturn(BluetoothHearingAid.HI_SYNC_ID_INVALID); + public void updateHearingAidsDevices_asha_invalidHiSyncId_doNothing() { + when(mCachedDevice1.getProfiles()).thenReturn(List.of(mHearingAidProfile)); + when(mCachedDevice2.getProfiles()).thenReturn(List.of(mHearingAidProfile)); + when(mHearingAidProfile.getHiSyncId(mDevice1)).thenReturn(HI_SYNC_ID_INVALID); + when(mHearingAidProfile.getHiSyncId(mDevice2)).thenReturn(HI_SYNC_ID_INVALID); mCachedDeviceManager.mCachedDevices.add(mCachedDevice1); mCachedDeviceManager.mCachedDevices.add(mCachedDevice2); + mHearingAidDeviceManager.updateHearingAidsDevices(); verify(mHearingAidDeviceManager, never()).onHiSyncIdChanged(anyLong()); } /** - * Test updateHearingAidsDevices, set HearingAid's information, including HiSyncId, deviceSide, - * deviceMode. + * Test updateHearingAidsDevices + * + * Conditions: + * 1) ASHA hearing aids + * 2) Valid HiSync Id + * Result: + * Set hearing aid info to the device. */ @Test - public void updateHearingAidsDevices_validHiSyncId_setHearingAidInfos() { + public void updateHearingAidsDevices_asha_validHiSyncId_setHearingAidInfo() { + when(mCachedDevice1.getProfiles()).thenReturn(List.of(mHearingAidProfile)); when(mHearingAidProfile.getHiSyncId(mDevice1)).thenReturn(HISYNCID1); - when(mHearingAidProfile.getDeviceMode(mDevice1)).thenReturn( - HearingAidProfile.DeviceMode.MODE_BINAURAL); - when(mHearingAidProfile.getDeviceSide(mDevice1)).thenReturn( - HearingAidProfile.DeviceSide.SIDE_RIGHT); + when(mHearingAidProfile.getDeviceMode(mDevice1)).thenReturn(MODE_BINAURAL); + when(mHearingAidProfile.getDeviceSide(mDevice1)).thenReturn(SIDE_RIGHT); mCachedDeviceManager.mCachedDevices.add(mCachedDevice1); mHearingAidDeviceManager.updateHearingAidsDevices(); @@ -355,6 +473,51 @@ public class HearingAidDeviceManagerTest { } /** + * Test updateHearingAidsDevices + * + * Conditions: + * 1) LeAudio hearing aid + * 2) Valid audio location and device type + * Result: + * Set hearing aid info to the device. + */ + @Test + public void updateHearingAidsDevices_leAudio_validInfo_setHearingAidInfo() { + when(mCachedDevice1.getProfiles()).thenReturn(List.of(mLeAudioProfile, mHapClientProfile)); + when(mLeAudioProfile.getAudioLocation(mDevice1)).thenReturn(AUDIO_LOCATION_FRONT_LEFT); + when(mHapClientProfile.getHearingAidType(mDevice1)).thenReturn(TYPE_BINAURAL); + mCachedDeviceManager.mCachedDevices.add(mCachedDevice1); + + mHearingAidDeviceManager.updateHearingAidsDevices(); + + verify(mCachedDevice1).setHearingAidInfo(any(HearingAidInfo.class)); + assertThat(mCachedDevice1.getDeviceSide()).isEqualTo(HearingAidInfo.DeviceSide.SIDE_LEFT); + assertThat(mCachedDevice1.getDeviceMode()).isEqualTo( + HearingAidInfo.DeviceMode.MODE_BINAURAL); + } + + /** + * Test updateHearingAidsDevices + * + * Conditions: + * 1) LeAudio hearing aid + * 2) Invalid audio location and device type + * Result: + * Do not set hearing aid info to the device. + */ + @Test + public void updateHearingAidsDevices_leAudio_invalidInfo_notToSetHearingAidInfo() { + when(mCachedDevice1.getProfiles()).thenReturn(List.of(mLeAudioProfile, mHapClientProfile)); + when(mLeAudioProfile.getAudioLocation(mDevice1)).thenReturn(AUDIO_LOCATION_INVALID); + when(mHapClientProfile.getHearingAidType(mDevice1)).thenReturn(TYPE_INVALID); + mCachedDeviceManager.mCachedDevices.add(mCachedDevice1); + + mHearingAidDeviceManager.updateHearingAidsDevices(); + + verify(mCachedDevice1, never()).setHearingAidInfo(any(HearingAidInfo.class)); + } + + /** * Test onProfileConnectionStateChangedIfProcessed. * When first hearing aid device is connected, to process it same as other generic devices. * No need to process it. diff --git a/packages/SettingsProvider/res/values/defaults.xml b/packages/SettingsProvider/res/values/defaults.xml index 89a8dd95d3c3..17d9f1b87fac 100644 --- a/packages/SettingsProvider/res/values/defaults.xml +++ b/packages/SettingsProvider/res/values/defaults.xml @@ -335,4 +335,7 @@ <!-- Default for Settings.BATTERY_CHARGING_STATE_ENFORCE_LEVEL. -1 means system internal default value is used. --> <integer name="def_battery_charging_state_enforce_level">-1</integer> + + <!-- Value to use as default scale for fonts --> + <item name="def_device_font_scale" format="float" type="dimen">1.0</item> </resources> diff --git a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java index 8ae50eb7ffad..8ad5f244b659 100644 --- a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java +++ b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java @@ -74,6 +74,7 @@ public class SecureSettings { Settings.Secure.TTS_DEFAULT_LOCALE, Settings.Secure.SHOW_IME_WITH_HARD_KEYBOARD, Settings.Secure.ACCESSIBILITY_BOUNCE_KEYS, + Settings.Secure.ACCESSIBILITY_SLOW_KEYS, Settings.Secure.ACCESSIBILITY_STICKY_KEYS, Settings.Secure.WIFI_NETWORKS_AVAILABLE_NOTIFICATION_ON, // moved to global Settings.Secure.WIFI_NETWORKS_AVAILABLE_REPEAT_DELAY, // moved to global diff --git a/packages/SettingsProvider/src/android/provider/settings/backup/SystemSettings.java b/packages/SettingsProvider/src/android/provider/settings/backup/SystemSettings.java index e7d7bb01e180..38ec931612f0 100644 --- a/packages/SettingsProvider/src/android/provider/settings/backup/SystemSettings.java +++ b/packages/SettingsProvider/src/android/provider/settings/backup/SystemSettings.java @@ -48,6 +48,7 @@ public class SystemSettings { Settings.System.WIFI_STATIC_DNS2, Settings.System.BLUETOOTH_DISCOVERABILITY, Settings.System.BLUETOOTH_DISCOVERABILITY_TIMEOUT, + Settings.System.DEFAULT_DEVICE_FONT_SCALE, Settings.System.FONT_SCALE, Settings.System.DIM_SCREEN, Settings.System.SCREEN_OFF_TIMEOUT, diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java index 285c8c969343..d854df38a9ef 100644 --- a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java +++ b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java @@ -120,6 +120,7 @@ public class SecureSettingsValidators { VALIDATORS.put(Secure.TTS_DEFAULT_LOCALE, TTS_LIST_VALIDATOR); VALIDATORS.put(Secure.SHOW_IME_WITH_HARD_KEYBOARD, BOOLEAN_VALIDATOR); VALIDATORS.put(Secure.ACCESSIBILITY_BOUNCE_KEYS, ANY_INTEGER_VALIDATOR); + VALIDATORS.put(Secure.ACCESSIBILITY_SLOW_KEYS, ANY_INTEGER_VALIDATOR); VALIDATORS.put(Secure.ACCESSIBILITY_STICKY_KEYS, BOOLEAN_VALIDATOR); VALIDATORS.put(Secure.WIFI_NETWORKS_AVAILABLE_NOTIFICATION_ON, BOOLEAN_VALIDATOR); VALIDATORS.put(Secure.WIFI_NETWORKS_AVAILABLE_REPEAT_DELAY, NON_NEGATIVE_INTEGER_VALIDATOR); diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/SettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/SettingsValidators.java index a8a659ee1e5c..677c81ad9271 100644 --- a/packages/SettingsProvider/src/android/provider/settings/validators/SettingsValidators.java +++ b/packages/SettingsProvider/src/android/provider/settings/validators/SettingsValidators.java @@ -45,6 +45,9 @@ public class SettingsValidators { } }; + public static final Validator FONT_SCALE_VALIDATOR = new InclusiveFloatRangeValidator(0.25f, + 5.0f); + public static final Validator NON_NEGATIVE_INTEGER_VALIDATOR = new Validator() { @Override public boolean validate(@Nullable String value) { diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/SystemSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/SystemSettingsValidators.java index 572303a813bf..98941c7cc116 100644 --- a/packages/SettingsProvider/src/android/provider/settings/validators/SystemSettingsValidators.java +++ b/packages/SettingsProvider/src/android/provider/settings/validators/SystemSettingsValidators.java @@ -20,6 +20,7 @@ import static android.provider.settings.validators.SettingsValidators.ANY_INTEGE import static android.provider.settings.validators.SettingsValidators.ANY_STRING_VALIDATOR; import static android.provider.settings.validators.SettingsValidators.BOOLEAN_VALIDATOR; import static android.provider.settings.validators.SettingsValidators.COMPONENT_NAME_VALIDATOR; +import static android.provider.settings.validators.SettingsValidators.FONT_SCALE_VALIDATOR; import static android.provider.settings.validators.SettingsValidators.LENIENT_IP_ADDRESS_VALIDATOR; import static android.provider.settings.validators.SettingsValidators.NON_NEGATIVE_FLOAT_VALIDATOR; import static android.provider.settings.validators.SettingsValidators.NON_NEGATIVE_INTEGER_VALIDATOR; @@ -31,7 +32,6 @@ import android.compat.annotation.UnsupportedAppUsage; import android.content.ComponentName; import android.hardware.display.ColorDisplayManager; import android.os.BatteryManager; -import android.provider.Settings.Global; import android.provider.Settings.System; import android.util.ArrayMap; @@ -93,7 +93,8 @@ public class SystemSettingsValidators { return value == null || value.length() < MAX_LENGTH; } }); - VALIDATORS.put(System.FONT_SCALE, new InclusiveFloatRangeValidator(0.25f, 5.0f)); + VALIDATORS.put(System.DEFAULT_DEVICE_FONT_SCALE, FONT_SCALE_VALIDATOR); + VALIDATORS.put(System.FONT_SCALE, FONT_SCALE_VALIDATOR); VALIDATORS.put(System.DIM_SCREEN, BOOLEAN_VALIDATOR); VALIDATORS.put( System.DISPLAY_COLOR_MODE, diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java index 3a46f4e96ccb..febce97031bb 100644 --- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java +++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java @@ -3812,7 +3812,7 @@ public class SettingsProvider extends ContentProvider { } private final class UpgradeController { - private static final int SETTINGS_VERSION = 224; + private static final int SETTINGS_VERSION = 225; private final int mUserId; @@ -6004,6 +6004,13 @@ public class SettingsProvider extends ContentProvider { currentVersion = 224; } + // Version 224: Update the default font scale depending on the + // R.dimen.def_device_font_scale configuration property. + if (currentVersion == 224) { + handleDefaultFontScale(getSystemSettingsLocked(userId)); + currentVersion = 225; + } + // vXXX: Add new settings above this point. if (currentVersion != newVersion) { @@ -6021,6 +6028,32 @@ public class SettingsProvider extends ContentProvider { return currentVersion; } + @SuppressWarnings("GuardedBy") + @GuardedBy("mLock") + private void handleDefaultFontScale(@NonNull SettingsState systemSettings) { + final float defaultFontScale = getContext().getResources() + .getFloat(R.dimen.def_device_font_scale); + // Persist the value for future use (e.g. Reset Settings option) + systemSettings.insertSettingLocked( + Settings.System.DEFAULT_DEVICE_FONT_SCALE, + String.valueOf(defaultFontScale), + /* tag= */ null, + /* makeDefault= */ false, + SettingsState.SYSTEM_PACKAGE_NAME); + // We verify if there is a pre existing value for font_scale. + final Setting existingFontScale = systemSettings.getSettingLocked( + Settings.System.FONT_SCALE); + if (existingFontScale == null || existingFontScale.isNull()) { + // Set the default value only if it didn't exist before + systemSettings.insertSettingLocked( + Settings.System.FONT_SCALE, + String.valueOf(defaultFontScale), + /* tag= */ null, + /* makeDefault= */ false, + SettingsState.SYSTEM_PACKAGE_NAME); + } + } + @GuardedBy("mLock") private void initGlobalSettingsDefaultValLocked(String key, boolean val) { initGlobalSettingsDefaultValLocked(key, val ? "1" : "0"); diff --git a/packages/Shell/AndroidManifest.xml b/packages/Shell/AndroidManifest.xml index d38454221f76..cdb4aea6ee79 100644 --- a/packages/Shell/AndroidManifest.xml +++ b/packages/Shell/AndroidManifest.xml @@ -561,7 +561,7 @@ <uses-permission android:name="android.permission.TEST_BIOMETRIC" /> <!-- Permission required for CTS test - android.server.biometrics --> - <uses-permission android:name="android.permission.MANAGE_BIOMETRIC_DIALOG" /> + <uses-permission android:name="android.permission.SET_BIOMETRIC_DIALOG_LOGO" /> <!-- Permission required for CTS test - android.server.biometrics --> <uses-permission android:name="android.permission.USE_BACKGROUND_FACE_AUTHENTICATION" /> @@ -902,6 +902,9 @@ <!-- Permission required for BinaryTransparencyService shell API and host test --> <uses-permission android:name="android.permission.GET_BACKGROUND_INSTALLED_PACKAGES" /> + <!-- Permissions required for CTS test - CtsPermissionUiTestCases --> + <uses-permission android:name="android.permission.MANAGE_ENHANCED_CONFIRMATION_STATES" /> + <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 80656e9253db..90593d6b1937 100644 --- a/packages/SystemUI/Android.bp +++ b/packages/SystemUI/Android.bp @@ -330,6 +330,7 @@ android_library { "androidx.core_core-animation-testing-nodeps", "androidx.compose.ui_ui", "flag-junit", + "ravenwood-junit", "platform-test-annotations", "notification_flags_lib", ], @@ -386,7 +387,8 @@ android_library { android_app { name: "SystemUIRobo-stub", - use_resource_processor: true, + // SystemUiRavenTests references the .aapt.srcjar + use_resource_processor: false, defaults: [ "platform_app_defaults", "SystemUI_optimized_defaults", @@ -458,6 +460,34 @@ android_robolectric_test { ], } +android_ravenwood_test { + name: "SystemUiRavenTests", + srcs: [ + ":SystemUI-tests-utils", + ":SystemUI-tests-multivalent", + // TODO(b/294256649): pivot to using {.aapt.jar} and re-enable + // use_resource_processor: true when better supported by soong + ":SystemUIRobo-stub{.aapt.srcjar}", + ], + static_libs: [ + "SystemUI-core", + "SystemUI-res", + "SystemUI-tests-base", + "androidx.test.uiautomator_uiautomator", + "androidx.core_core-animation-testing", + "androidx.test.ext.junit", + ], + libs: [ + "android.test.runner", + "android.test.base", + "android.test.mock", + ], + auto_gen_config: true, + plugins: [ + "dagger2-compiler", + ], +} + // Opt-out config for optimizing the SystemUI target using R8. // Disabled via `export SYSTEMUI_OPTIMIZE_JAVA=false`, or explicitly in Make via // `SYSTEMUI_OPTIMIZE_JAVA := false`. diff --git a/packages/SystemUI/aconfig/accessibility.aconfig b/packages/SystemUI/aconfig/accessibility.aconfig index 7ba889bc8fee..866aa8945525 100644 --- a/packages/SystemUI/aconfig/accessibility.aconfig +++ b/packages/SystemUI/aconfig/accessibility.aconfig @@ -17,6 +17,13 @@ flag { } flag { + name: "floating_menu_drag_to_edit" + namespace: "accessibility" + description: "adds a second drag button to allow the user edit the shortcut." + bug: "297583708" +} + +flag { name: "floating_menu_ime_displacement_animation" namespace: "accessibility" description: "Adds an animation for when the FAB is displaced by an IME becoming visible." diff --git a/packages/SystemUI/aconfig/biometrics_framework.aconfig b/packages/SystemUI/aconfig/biometrics_framework.aconfig index 5fd3b485e9ed..7cc0c83abfea 100644 --- a/packages/SystemUI/aconfig/biometrics_framework.aconfig +++ b/packages/SystemUI/aconfig/biometrics_framework.aconfig @@ -7,4 +7,11 @@ flag { namespace: "biometrics_framework" description: "Adds talkback directional guidance when using UDFPS with biometric prompt" bug: "310044658" +} + +flag { + name: "constraint_bp" + namespace: "biometrics_framework" + description: "Refactors Biometric Prompt to use a ConstraintLayout" + bug: "288175072" }
\ No newline at end of file diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig index 7eca04a5f85b..a2530d59e2e6 100644 --- a/packages/SystemUI/aconfig/systemui.aconfig +++ b/packages/SystemUI/aconfig/systemui.aconfig @@ -366,8 +366,8 @@ flag { } flag { - name: "enable_notif_linearlayout_optimized" - namespace: "systemui" - description: "Enables notification specific LinearLayout optimization" - bug: "316110233" + name: "keyguard_wm_state_refactor" + namespace: "systemui" + description: "Enables refactored logic for SysUI+WM unlock/occlusion code paths" + bug: "278086361" } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt index a22fecf3688d..ff5a69801c56 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt @@ -64,14 +64,6 @@ fun CommunalContainer( transitions = sceneTransitions, ) - // Don't show hub mode UI if communal is not available. Communal is only available if it has - // been enabled via settings and either keyguard is showing, or, the device is currently - // dreaming. - val isCommunalAvailable by viewModel.isCommunalAvailable.collectAsState() - if (!isCommunalAvailable) { - return - } - // This effect exposes the SceneTransitionLayout's observable transition state to the rest of // the system, and unsets it when the view is disposed to avoid a memory leak. DisposableEffect(viewModel, sceneTransitionLayoutState) { @@ -84,13 +76,14 @@ fun CommunalContainer( SceneTransitionLayout( state = sceneTransitionLayoutState, modifier = modifier.fillMaxSize(), - edgeDetector = FixedSizeEdgeDetector(ContainerDimensions.EdgeSwipeSize), + swipeSourceDetector = FixedSizeEdgeDetector(ContainerDimensions.EdgeSwipeSize), ) { scene( TransitionSceneKey.Blank, userActions = mapOf( - Swipe(SwipeDirection.Left, fromEdge = Edge.Right) to TransitionSceneKey.Communal + Swipe(SwipeDirection.Left, fromSource = Edge.Right) to + TransitionSceneKey.Communal ) ) { // This scene shows nothing only allowing for transitions to the communal scene. @@ -101,7 +94,7 @@ fun CommunalContainer( TransitionSceneKey.Communal, userActions = mapOf( - Swipe(SwipeDirection.Right, fromEdge = Edge.Left) to TransitionSceneKey.Blank + Swipe(SwipeDirection.Right, fromSource = Edge.Left) to TransitionSceneKey.Blank ), ) { CommunalScene(viewModel, modifier = modifier) 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 761e74e52237..576596fa67fe 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 @@ -48,7 +48,6 @@ import androidx.compose.foundation.lazy.grid.rememberLazyGridState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add -import androidx.compose.material.icons.filled.Edit import androidx.compose.material.icons.outlined.Delete import androidx.compose.material.icons.outlined.Edit import androidx.compose.material.icons.outlined.TouchApp @@ -60,7 +59,6 @@ import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults import androidx.compose.material3.FilledIconButton import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton import androidx.compose.material3.IconButtonColors import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedButton @@ -130,12 +128,11 @@ fun CommunalHub( val gridState = rememberLazyGridState() val contentListState = rememberContentListState(widgetConfigurator, communalContent, viewModel) val reorderingWidgets by viewModel.reorderingWidgets.collectAsState() - val selectedIndex = viewModel.selectedIndex.collectAsState() + val selectedKey = viewModel.selectedKey.collectAsState() val removeButtonEnabled by remember { - derivedStateOf { selectedIndex.value != null || reorderingWidgets } + derivedStateOf { selectedKey.value != null || reorderingWidgets } } - val (isButtonToEditWidgetsShowing, setIsButtonToEditWidgetsShowing) = - remember { mutableStateOf(false) } + var isButtonToEditWidgetsShowing by remember { mutableStateOf(false) } val contentPadding = gridContentPadding(viewModel.isEditMode, toolbarSize) val contentOffset = beforeContentPadding(contentPadding).toOffset() @@ -150,22 +147,30 @@ fun CommunalHub( if (!viewModel.isEditMode) return@pointerInput observeTapsWithoutConsuming { offset -> val adjustedOffset = offset - contentOffset - val index = - gridState.layoutInfo.visibleItemsInfo - .firstItemAtOffset(adjustedOffset) - ?.index - val newIndex = - if (index?.let(contentListState::isItemEditable) == true) { - index - } else { - null - } - viewModel.setSelectedIndex(newIndex) + val index = firstIndexAtOffset(gridState, adjustedOffset) + val key = index?.let { keyAtIndexIfEditable(contentListState.list, index) } + viewModel.setSelectedKey(key) } } .thenIf(!viewModel.isEditMode) { - Modifier.pointerInput(Unit) { - detectLongPressGesture { offset -> setIsButtonToEditWidgetsShowing(true) } + Modifier.pointerInput( + gridState, + contentOffset, + communalContent, + 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) } + val key = index?.let { keyAtIndexIfEditable(communalContent, index) } + viewModel.setSelectedKey(key) + } } }, ) { @@ -186,7 +191,7 @@ fun CommunalHub( onOpenWidgetPicker = onOpenWidgetPicker, gridState = gridState, contentListState = contentListState, - selectedIndex = selectedIndex, + selectedKey = selectedKey, widgetConfigurator = widgetConfigurator, ) @@ -198,18 +203,18 @@ fun CommunalHub( onEditDone = onEditDone, onOpenWidgetPicker = onOpenWidgetPicker, onRemoveClicked = { - selectedIndex.value?.let { index -> - contentListState.onRemove(index) + val index = + selectedKey.value?.let { key -> + contentListState.list.indexOfFirst { it.key == key } + } + index?.let { + contentListState.onRemove(it) contentListState.onSaveList() - viewModel.setSelectedIndex(null) + viewModel.setSelectedKey(null) } }, removeEnabled = removeButtonEnabled ) - } else { - IconButton(onClick = viewModel::onOpenWidgetEditor) { - Icon(Icons.Default.Edit, stringResource(R.string.button_to_open_widget_editor)) - } } if (isPopupOnDismissCtaShowing) { @@ -219,10 +224,10 @@ fun CommunalHub( if (isButtonToEditWidgetsShowing) { ButtonToEditWidgets( onClick = { - setIsButtonToEditWidgetsShowing(false) - viewModel.onOpenWidgetEditor() + isButtonToEditWidgetsShowing = false + viewModel.onOpenWidgetEditor(selectedKey.value) }, - onHide = { setIsButtonToEditWidgetsShowing(false) }, + onHide = { isButtonToEditWidgetsShowing = false }, ) } @@ -244,7 +249,7 @@ private fun BoxScope.CommunalHubLazyGrid( communalContent: List<CommunalContentModel>, viewModel: BaseCommunalViewModel, contentPadding: PaddingValues, - selectedIndex: State<Int?>, + selectedKey: State<String?>, contentOffset: Offset, gridState: LazyGridState, contentListState: ContentListState, @@ -253,7 +258,8 @@ private fun BoxScope.CommunalHubLazyGrid( onOpenWidgetPicker: (() -> Unit)? = null, widgetConfigurator: WidgetConfigurator?, ) { - var gridModifier = Modifier.align(Alignment.CenterStart) + var gridModifier = + Modifier.align(Alignment.CenterStart).onGloballyPositioned { setGridCoordinates(it) } var list = communalContent var dragDropState: GridDragDropState? = null if (viewModel.isEditMode && viewModel is CommunalEditModeViewModel) { @@ -266,10 +272,7 @@ private fun BoxScope.CommunalHubLazyGrid( updateDragPositionForRemove = updateDragPositionForRemove ) gridModifier = - gridModifier - .fillMaxSize() - .dragContainer(dragDropState, contentOffset, viewModel) - .onGloballyPositioned { setGridCoordinates(it) } + gridModifier.fillMaxSize().dragContainer(dragDropState, contentOffset, viewModel) // for widgets dropped from other activities val dragAndDropTargetState = rememberDragAndDropTargetState( @@ -307,7 +310,8 @@ private fun BoxScope.CommunalHubLazyGrid( list[index].size.dp().value, ) if (viewModel.isEditMode && dragDropState != null) { - val selected by remember(index) { derivedStateOf { index == selectedIndex.value } } + val selected by + remember(index) { derivedStateOf { list[index].key == selectedKey.value } } DraggableItem( dragDropState = dragDropState, selected = selected, @@ -378,7 +382,7 @@ private fun Toolbar( colors = filledButtonColors(), contentPadding = Dimensions.ButtonPadding ) { - Icon(Icons.Default.Add, stringResource(R.string.button_to_open_widget_editor)) + Icon(Icons.Default.Add, stringResource(R.string.hub_mode_add_widget_button_text)) Spacer(spacerModifier) Text( text = stringResource(R.string.hub_mode_add_widget_button_text), @@ -832,6 +836,13 @@ private fun CommunalContentSize.dp(): Dp { } } +private fun firstIndexAtOffset(gridState: LazyGridState, offset: Offset): Int? = + gridState.layoutInfo.visibleItemsInfo.firstItemAtOffset(offset)?.index + +/** Returns the key of item if it's editable at the given index. Only widget is editable. */ +private fun keyAtIndexIfEditable(list: List<CommunalContentModel>, index: Int): String? = + if (index in list.indices && list[index].isWidget()) list[index].key else null + data class ContentPaddingInPx(val start: Float, val top: Float) { fun toOffset(): Offset = Offset(start, top) } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/LockSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/LockSection.kt index 2a6bea75791c..be6f022d8d52 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/LockSection.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/LockSection.kt @@ -33,6 +33,7 @@ import com.android.keyguard.LockIconView import com.android.keyguard.LockIconViewController import com.android.systemui.Flags.keyguardBottomAreaRefactor import com.android.systemui.biometrics.AuthController +import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.deviceentry.shared.DeviceEntryUdfpsRefactor import com.android.systemui.flags.FeatureFlagsClassic import com.android.systemui.flags.Flags @@ -47,10 +48,12 @@ import com.android.systemui.res.R import com.android.systemui.statusbar.VibratorHelper import dagger.Lazy import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope class LockSection @Inject constructor( + @Application private val applicationScope: CoroutineScope, private val windowManager: WindowManager, private val authController: AuthController, private val featureFlags: FeatureFlagsClassic, @@ -76,6 +79,7 @@ constructor( DeviceEntryIconView(context, null).apply { id = R.id.device_entry_icon_view DeviceEntryIconViewBinder.bind( + applicationScope, this, deviceEntryIconViewModel.get(), deviceEntryForegroundViewModel.get(), diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt index c35202cd830a..9f9e1f5bb56a 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt @@ -183,7 +183,7 @@ private fun UserAction.toTransitionUserAction(): SceneTransitionUserAction { is UserAction.Swipe -> Swipe( pointerCount = pointerCount, - fromEdge = + fromSource = when (this.fromEdge) { null -> null Edge.LEFT -> SceneTransitionEdge.Left diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/EdgeDetector.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/EdgeDetector.kt index 82d4239d7eb5..b0dc3a144533 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/EdgeDetector.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/EdgeDetector.kt @@ -23,24 +23,19 @@ import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.dp -interface EdgeDetector { - /** - * Return the [Edge] associated to [position] inside a layout of size [layoutSize], given - * [density] and [orientation]. - */ - fun edge( - layoutSize: IntSize, - position: IntOffset, - density: Density, - orientation: Orientation, - ): Edge? +/** The edge of a [SceneTransitionLayout]. */ +enum class Edge : SwipeSource { + Left, + Right, + Top, + Bottom, } val DefaultEdgeDetector = FixedSizeEdgeDetector(40.dp) -/** An [EdgeDetector] that detects edges assuming a fixed edge size of [size]. */ -class FixedSizeEdgeDetector(val size: Dp) : EdgeDetector { - override fun edge( +/** An [SwipeSourceDetector] that detects edges assuming a fixed edge size of [size]. */ +class FixedSizeEdgeDetector(val size: Dp) : SwipeSourceDetector { + override fun source( layoutSize: IntSize, position: IntOffset, density: Density, diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Key.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Key.kt index 90f46bd4dcaa..9d4b69c51690 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Key.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Key.kt @@ -44,7 +44,7 @@ sealed class Key(val debugName: String, val identity: Any) { class SceneKey( name: String, identity: Any = Object(), -) : Key(name, identity) { +) : Key(name, identity), UserActionResult { @VisibleForTesting // TODO(b/240432457): Make internal once PlatformComposeSceneTransitionLayoutTestsUtils can // access internal members. @@ -53,6 +53,10 @@ class SceneKey( /** The unique [ElementKey] identifying this scene's root element. */ val rootElementKey = ElementKey(name, identity) + // Implementation of [UserActionResult]. + override val toScene: SceneKey = this + override val distance: UserActionDistance? = null + override fun toString(): String { return "SceneKey(debugName=$debugName)" } diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt index 38738782c889..8552aaf0ebff 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt @@ -64,8 +64,8 @@ import androidx.compose.ui.util.fastForEach @Stable internal fun Modifier.multiPointerDraggable( orientation: Orientation, - enabled: Boolean, - startDragImmediately: Boolean, + enabled: () -> Boolean, + startDragImmediately: () -> Boolean, onDragStarted: (startedPosition: Offset, overSlop: Float, pointersDown: Int) -> Unit, onDragDelta: (delta: Float) -> Unit, onDragStopped: (velocity: Float) -> Unit, @@ -83,8 +83,8 @@ internal fun Modifier.multiPointerDraggable( private data class MultiPointerDraggableElement( private val orientation: Orientation, - private val enabled: Boolean, - private val startDragImmediately: Boolean, + private val enabled: () -> Boolean, + private val startDragImmediately: () -> Boolean, private val onDragStarted: (startedPosition: Offset, overSlop: Float, pointersDown: Int) -> Unit, private val onDragDelta: (Float) -> Unit, @@ -110,10 +110,10 @@ private data class MultiPointerDraggableElement( } } -private class MultiPointerDraggableNode( +internal class MultiPointerDraggableNode( orientation: Orientation, - enabled: Boolean, - var startDragImmediately: Boolean, + enabled: () -> Boolean, + var startDragImmediately: () -> Boolean, var onDragStarted: (startedPosition: Offset, overSlop: Float, pointersDown: Int) -> Unit, var onDragDelta: (Float) -> Unit, var onDragStopped: (velocity: Float) -> Unit, @@ -122,7 +122,7 @@ private class MultiPointerDraggableNode( private val delegate = delegate(SuspendingPointerInputModifierNode(pointerInputHandler)) private val velocityTracker = VelocityTracker() - var enabled: Boolean = enabled + var enabled: () -> Boolean = enabled set(value) { // Reset the pointer input whenever enabled changed. if (value != field) { @@ -133,7 +133,7 @@ private class MultiPointerDraggableNode( var orientation: Orientation = orientation set(value) { - // Reset the pointer input whenever enabled orientation. + // Reset the pointer input whenever orientation changed. if (value != field) { field = value delegate.resetPointerInputHandler() @@ -149,7 +149,7 @@ private class MultiPointerDraggableNode( ) = delegate.onPointerEvent(pointerEvent, pass, bounds) private suspend fun PointerInputScope.pointerInput() { - if (!enabled) { + if (!enabled()) { return } @@ -163,8 +163,7 @@ private class MultiPointerDraggableNode( val onDragEnd: () -> Unit = { val maxFlingVelocity = currentValueOf(LocalViewConfiguration).maximumFlingVelocity.let { max -> - val maxF = max.toFloat() - Velocity(maxF, maxF) + Velocity(max, max) } val velocity = velocityTracker.calculateVelocity(maxFlingVelocity) @@ -183,7 +182,7 @@ private class MultiPointerDraggableNode( detectDragGestures( orientation = orientation, - startDragImmediately = { startDragImmediately }, + startDragImmediately = startDragImmediately, onDragStart = onDragStart, onDragEnd = onDragEnd, onDragCancel = onDragCancel, diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Scene.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Scene.kt index f67df54b088c..af51cee2a255 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Scene.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Scene.kt @@ -38,7 +38,7 @@ internal class Scene( val key: SceneKey, layoutImpl: SceneTransitionLayoutImpl, content: @Composable SceneScope.() -> Unit, - actions: Map<UserAction, SceneKey>, + actions: Map<UserAction, UserActionResult>, zIndex: Float, ) { internal val scope = SceneScopeImpl(layoutImpl, this) diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneGestureHandler.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneGestureHandler.kt index ff054786cf52..aed04f688628 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneGestureHandler.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneGestureHandler.kt @@ -28,6 +28,8 @@ import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.round import com.android.compose.nestedscroll.PriorityNestedScrollConnection @@ -75,23 +77,25 @@ internal class SceneGestureHandler( internal var currentSource: Any? = null - /** The [UserAction]s associated to the current swipe. */ - private var actionUpOrLeft: UserAction? = null - private var actionDownOrRight: UserAction? = null - private var actionUpOrLeftNoEdge: UserAction? = null - private var actionDownOrRightNoEdge: UserAction? = null - private var upOrLeftScene: SceneKey? = null - private var downOrRightScene: SceneKey? = null + /** The [Swipes] associated to the current gesture. */ + private var swipes: Swipes? = null + + /** The [UserActionResult] associated to up and down swipes. */ + private var upOrLeftResult: UserActionResult? = null + private var downOrRightResult: UserActionResult? = null internal fun onDragStarted(pointersDown: Int, startedPosition: Offset?, overSlop: Float) { if (isDrivingTransition) { // This [transition] was already driving the animation: simply take over it. // Stop animating and start from where the current offset. swipeTransition.cancelOffsetAnimation() - updateTargetScenes(swipeTransition._fromScene) + updateSwipesResults(swipeTransition._fromScene) return } + check(overSlop != 0f) { + "onDragStarted() called while isDrivingTransition=false overSlop=0f" + } val transitionState = layoutState.transitionState if (transitionState is TransitionState.Transition) { // TODO(b/290184746): Better handle interruptions here if state != idle. @@ -104,18 +108,25 @@ internal class SceneGestureHandler( } val fromScene = layoutImpl.scene(transitionState.currentScene) - setCurrentActions(fromScene, startedPosition, pointersDown) + updateSwipes(fromScene, startedPosition, pointersDown) val (targetScene, distance) = - findTargetSceneAndDistance(fromScene, overSlop, updateScenes = true) ?: return - + findTargetSceneAndDistance(fromScene, overSlop, updateSwipesResults = true) ?: return updateTransition(SwipeTransition(fromScene, targetScene, distance), force = true) } - private fun setCurrentActions(fromScene: Scene, startedPosition: Offset?, pointersDown: Int) { - val fromEdge = + private fun updateSwipes(fromScene: Scene, startedPosition: Offset?, pointersDown: Int) { + this.swipes = computeSwipes(fromScene, startedPosition, pointersDown) + } + + private fun computeSwipes( + fromScene: Scene, + startedPosition: Offset?, + pointersDown: Int + ): Swipes { + val fromSource = startedPosition?.let { position -> - layoutImpl.edgeDetector.edge( + layoutImpl.swipeSourceDetector.source( fromScene.targetSize, position.round(), layoutImpl.density, @@ -131,7 +142,7 @@ internal class SceneGestureHandler( Orientation.Vertical -> SwipeDirection.Up }, pointerCount = pointersDown, - fromEdge = fromEdge, + fromSource = fromSource, ) val downOrRight = @@ -142,33 +153,31 @@ internal class SceneGestureHandler( Orientation.Vertical -> SwipeDirection.Down }, pointerCount = pointersDown, - fromEdge = fromEdge, + fromSource = fromSource, ) - if (fromEdge == null) { - actionUpOrLeft = null - actionDownOrRight = null - actionUpOrLeftNoEdge = upOrLeft - actionDownOrRightNoEdge = downOrRight + return if (fromSource == null) { + Swipes( + upOrLeft = null, + downOrRight = null, + upOrLeftNoSource = upOrLeft, + downOrRightNoSource = downOrRight, + ) } else { - actionUpOrLeft = upOrLeft - actionDownOrRight = downOrRight - actionUpOrLeftNoEdge = upOrLeft.copy(fromEdge = null) - actionDownOrRightNoEdge = downOrRight.copy(fromEdge = null) + Swipes( + upOrLeft = upOrLeft, + downOrRight = downOrRight, + upOrLeftNoSource = upOrLeft.copy(fromSource = null), + downOrRightNoSource = downOrRight.copy(fromSource = null), + ) } } - /** - * Use the layout size in the swipe orientation for swipe distance. - * - * TODO(b/290184746): Also handle custom distances for transitions. With smaller distances, we - * will also have to make sure that we correctly handle overscroll. - */ - private fun Scene.getAbsoluteDistance(): Float { - return when (orientation) { - Orientation.Horizontal -> targetSize.width - Orientation.Vertical -> targetSize.height - }.toFloat() + private fun Scene.getAbsoluteDistance(distance: UserActionDistance?): Float { + val targetSize = this.targetSize + return with(distance ?: DefaultSwipeDistance) { + layoutImpl.density.absoluteDistance(targetSize, orientation) + } } internal fun onDrag(delta: Float) { @@ -183,7 +192,7 @@ internal class SceneGestureHandler( findTargetSceneAndDistance( fromScene, swipeTransition.dragOffset, - updateScenes = isNewFromScene, + updateSwipesResults = isNewFromScene, ) ?: run { onDragStopped(delta, true) @@ -200,9 +209,31 @@ internal class SceneGestureHandler( } } - private fun updateTargetScenes(fromScene: Scene) { - upOrLeftScene = fromScene.upOrLeft() - downOrRightScene = fromScene.downOrRight() + private fun updateSwipesResults(fromScene: Scene) { + val (upOrLeftResult, downOrRightResult) = + swipesResults( + fromScene, + this.swipes ?: error("updateSwipes() should be called before updateSwipesResults()") + ) + + this.upOrLeftResult = upOrLeftResult + this.downOrRightResult = downOrRightResult + } + + private fun swipesResults( + fromScene: Scene, + swipes: Swipes + ): Pair<UserActionResult?, UserActionResult?> { + val userActions = fromScene.userActions + fun sceneToSwipePair(swipe: Swipe?): UserActionResult? { + return userActions[swipe ?: return null] + } + + val upOrLeftResult = + sceneToSwipePair(swipes.upOrLeft) ?: sceneToSwipePair(swipes.upOrLeftNoSource) + val downOrRightResult = + sceneToSwipePair(swipes.downOrRight) ?: sceneToSwipePair(swipes.downOrRightNoSource) + return Pair(upOrLeftResult, downOrRightResult) } /** @@ -229,9 +260,9 @@ internal class SceneGestureHandler( // If the offset is past the distance then let's change fromScene so that the user can swipe // to the next screen or go back to the previous one. val offset = swipeTransition.dragOffset - return if (offset <= -absoluteDistance && upOrLeftScene == toScene.key) { + return if (offset <= -absoluteDistance && upOrLeftResult?.toScene == toScene.key) { Pair(toScene, absoluteDistance) - } else if (offset >= absoluteDistance && downOrRightScene == toScene.key) { + } else if (offset >= absoluteDistance && downOrRightResult?.toScene == toScene.key) { Pair(toScene, -absoluteDistance) } else { Pair(fromScene, 0f) @@ -244,31 +275,41 @@ internal class SceneGestureHandler( * @param fromScene the scene from which we look for the target * @param directionOffset signed float that indicates the direction. Positive is down or right * negative is up or left. - * @param updateScenes whether the target scenes should be updated to the current values held in - * the Scenes map. Usually we don't want to update them while doing a drag, because this could - * change the target scene (jump cutting) to a different scene, when some system state changed - * the targets the background. However, an update is needed any time we calculate the targets - * for a new fromScene. + * @param updateSwipesResults whether the target scenes should be updated to the current values + * held in the Scenes map. Usually we don't want to update them while doing a drag, because + * this could change the target scene (jump cutting) to a different scene, when some system + * state changed the targets the background. However, an update is needed any time we + * calculate the targets for a new fromScene. * @return null when there are no targets in either direction. If one direction is null and you * drag into the null direction this function will return the opposite direction, assuming * that the users intention is to start the drag into the other direction eventually. If * [directionOffset] is 0f and both direction are available, it will default to - * [upOrLeftScene]. + * [upOrLeftResult]. */ private inline fun findTargetSceneAndDistance( fromScene: Scene, directionOffset: Float, - updateScenes: Boolean, + updateSwipesResults: Boolean, ): Pair<Scene, Float>? { - if (updateScenes) updateTargetScenes(fromScene) - val absoluteDistance = fromScene.getAbsoluteDistance() + if (updateSwipesResults) updateSwipesResults(fromScene) // Compute the target scene depending on the current offset. return when { - upOrLeftScene == null && downOrRightScene == null -> null - (directionOffset < 0f && upOrLeftScene != null) || downOrRightScene == null -> - Pair(layoutImpl.scene(upOrLeftScene!!), -absoluteDistance) - else -> Pair(layoutImpl.scene(downOrRightScene!!), absoluteDistance) + upOrLeftResult == null && downOrRightResult == null -> null + (directionOffset < 0f && upOrLeftResult != null) || downOrRightResult == null -> + upOrLeftResult?.let { result -> + Pair( + layoutImpl.scene(result.toScene), + -fromScene.getAbsoluteDistance(result.distance) + ) + } + else -> + downOrRightResult?.let { result -> + Pair( + layoutImpl.scene(result.toScene), + fromScene.getAbsoluteDistance(result.distance) + ) + } } } @@ -280,24 +321,25 @@ internal class SceneGestureHandler( fromScene: Scene, directionOffset: Float, ): Pair<Scene, Float>? { - val absoluteDistance = fromScene.getAbsoluteDistance() return when { directionOffset > 0f -> - upOrLeftScene?.let { Pair(layoutImpl.scene(it), -absoluteDistance) } + upOrLeftResult?.let { result -> + Pair( + layoutImpl.scene(result.toScene), + -fromScene.getAbsoluteDistance(result.distance), + ) + } directionOffset < 0f -> - downOrRightScene?.let { Pair(layoutImpl.scene(it), absoluteDistance) } + downOrRightResult?.let { result -> + Pair( + layoutImpl.scene(result.toScene), + fromScene.getAbsoluteDistance(result.distance), + ) + } else -> null } } - private fun Scene.upOrLeft(): SceneKey? { - return userActions[actionUpOrLeft] ?: userActions[actionUpOrLeftNoEdge] - } - - private fun Scene.downOrRight(): SceneKey? { - return userActions[actionDownOrRight] ?: userActions[actionDownOrRightNoEdge] - } - internal fun onDragStopped(velocity: Float, canChangeScene: Boolean) { // The state was changed since the drag started; don't do anything. if (!isDrivingTransition) { @@ -515,6 +557,26 @@ internal class SceneGestureHandler( companion object { private const val TAG = "SceneGestureHandler" } + + private object DefaultSwipeDistance : UserActionDistance { + override fun Density.absoluteDistance( + fromSceneSize: IntSize, + orientation: Orientation, + ): Float { + return when (orientation) { + Orientation.Horizontal -> fromSceneSize.width + Orientation.Vertical -> fromSceneSize.height + }.toFloat() + } + } + + /** The [Swipe] associated to a given fromScene, startedPosition and pointersDown. */ + private class Swipes( + val upOrLeft: Swipe?, + val downOrRight: Swipe?, + val upOrLeftNoSource: Swipe?, + val downOrRightNoSource: Swipe?, + ) } private class SceneDraggableHandler( 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 80f8c1c9e987..7e0aa9c3e2b1 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 @@ -27,6 +27,10 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.NestedScrollConnection import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.IntSize /** * [SceneTransitionLayout] is a container that automatically animates its content whenever its state @@ -38,7 +42,8 @@ import androidx.compose.ui.platform.LocalDensity * UI code. * * @param state the state of this layout. - * @param edgeDetector the edge detector used to detect which edge a swipe is started from, if any. + * @param swipeSourceDetector the edge detector used to detect which edge a swipe is started from, + * if any. * @param transitionInterceptionThreshold used during a scene transition. For the scene to be * intercepted, the progress value must be above the threshold, and below (1 - threshold). * @param scenes the configuration of the different scenes of this layout. @@ -48,14 +53,14 @@ import androidx.compose.ui.platform.LocalDensity fun SceneTransitionLayout( state: SceneTransitionLayoutState, modifier: Modifier = Modifier, - edgeDetector: EdgeDetector = DefaultEdgeDetector, + swipeSourceDetector: SwipeSourceDetector = DefaultEdgeDetector, @FloatRange(from = 0.0, to = 0.5) transitionInterceptionThreshold: Float = 0f, scenes: SceneTransitionLayoutScope.() -> Unit, ) { SceneTransitionLayoutForTesting( state, modifier, - edgeDetector, + swipeSourceDetector, transitionInterceptionThreshold, onLayoutImpl = null, scenes, @@ -76,7 +81,8 @@ fun SceneTransitionLayout( * This is called when the user commits a transition to a new scene because of a [UserAction], for * instance by triggering back navigation or by swiping to a new scene. * @param transitions the definition of the transitions used to animate a change of scene. - * @param edgeDetector the edge detector used to detect which edge a swipe is started from, if any. + * @param swipeSourceDetector the source detector used to detect which source a swipe is started + * from, if any. * @param transitionInterceptionThreshold used during a scene transition. For the scene to be * intercepted, the progress value must be above the threshold, and below (1 - threshold). * @param scenes the configuration of the different scenes of this layout. @@ -87,7 +93,7 @@ fun SceneTransitionLayout( onChangeScene: (SceneKey) -> Unit, transitions: SceneTransitions, modifier: Modifier = Modifier, - edgeDetector: EdgeDetector = DefaultEdgeDetector, + swipeSourceDetector: SwipeSourceDetector = DefaultEdgeDetector, @FloatRange(from = 0.0, to = 0.5) transitionInterceptionThreshold: Float = 0f, scenes: SceneTransitionLayoutScope.() -> Unit, ) { @@ -95,7 +101,7 @@ fun SceneTransitionLayout( SceneTransitionLayout( state, modifier, - edgeDetector, + swipeSourceDetector, transitionInterceptionThreshold, scenes, ) @@ -113,7 +119,7 @@ interface SceneTransitionLayoutScope { */ fun scene( key: SceneKey, - userActions: Map<UserAction, SceneKey> = emptyMap(), + userActions: Map<UserAction, UserActionResult> = emptyMap(), content: @Composable SceneScope.() -> Unit, ) } @@ -335,7 +341,7 @@ data object Back : UserAction data class Swipe( val direction: SwipeDirection, val pointerCount: Int = 1, - val fromEdge: Edge? = null, + val fromSource: SwipeSource? = null, ) : UserAction { companion object { val Left = Swipe(SwipeDirection.Left) @@ -353,6 +359,95 @@ enum class SwipeDirection(val orientation: Orientation) { } /** + * The source of a Swipe. + * + * Important: This can be anything that can be returned by any [SwipeSourceDetector], but this must + * implement [equals] and [hashCode]. Note that those can be trivially implemented using data + * classes. + */ +interface SwipeSource { + // Require equals() and hashCode() to be implemented. + override fun equals(other: Any?): Boolean + + override fun hashCode(): Int +} + +interface SwipeSourceDetector { + /** + * Return the [SwipeSource] associated to [position] inside a layout of size [layoutSize], given + * [density] and [orientation]. + */ + fun source( + layoutSize: IntSize, + position: IntOffset, + density: Density, + orientation: Orientation, + ): SwipeSource? +} + +/** + * The result of performing a [UserAction]. + * + * Note: [UserActionResult] is implemented by [SceneKey], and you can also use [withDistance] to + * easily create a [UserActionResult] with a fixed distance: + * ``` + * SceneTransitionLayout(...) { + * scene( + * Scenes.Foo, + * userActions = + * mapOf( + * Swipe.Right to Scene.Bar, + * Swipe.Down to Scene.Doe withDistance 100.dp, + * ) + * ) + * ) { ... } + * } + * ``` + */ +interface UserActionResult { + /** The scene we should be transitioning to during the [UserAction]. */ + val toScene: SceneKey + + /** + * The distance the action takes to animate from 0% to 100%. + * + * If `null`, a default distance will be used that depends on the [UserAction] performed. + */ + val distance: UserActionDistance? +} + +interface UserActionDistance { + /** + * Return the **absolute** distance of the user action given the size of the scene we are + * animating from and the [orientation]. + */ + fun Density.absoluteDistance(fromSceneSize: IntSize, orientation: Orientation): Float +} + +/** + * A utility function to make it possible to define user actions with a distance using the syntax + * `Swipe.Up to Scene.foo withDistance 100.dp` + */ +infix fun Pair<UserAction, SceneKey>.withDistance( + distance: Dp +): Pair<UserAction, UserActionResult> { + val scene = second + val distance = FixedDistance(distance) + return first to + object : UserActionResult { + override val toScene: SceneKey = scene + override val distance: UserActionDistance = distance + } +} + +/** The user action has a fixed [absoluteDistance]. */ +private class FixedDistance(private val distance: Dp) : UserActionDistance { + override fun Density.absoluteDistance(fromSceneSize: IntSize, orientation: Orientation): Float { + return distance.toPx() + } +} + +/** * An internal version of [SceneTransitionLayout] to be used for tests. * * Important: You should use this only in tests and if you need to access the underlying @@ -362,7 +457,7 @@ enum class SwipeDirection(val orientation: Orientation) { internal fun SceneTransitionLayoutForTesting( state: SceneTransitionLayoutState, modifier: Modifier = Modifier, - edgeDetector: EdgeDetector = DefaultEdgeDetector, + swipeSourceDetector: SwipeSourceDetector = DefaultEdgeDetector, transitionInterceptionThreshold: Float = 0f, onLayoutImpl: ((SceneTransitionLayoutImpl) -> Unit)? = null, scenes: SceneTransitionLayoutScope.() -> Unit, @@ -373,7 +468,7 @@ internal fun SceneTransitionLayoutForTesting( SceneTransitionLayoutImpl( state = state as BaseSceneTransitionLayoutState, density = density, - edgeDetector = edgeDetector, + swipeSourceDetector = swipeSourceDetector, transitionInterceptionThreshold = transitionInterceptionThreshold, builder = scenes, coroutineScope = coroutineScope, @@ -394,7 +489,7 @@ internal fun SceneTransitionLayoutForTesting( } layoutImpl.density = density - layoutImpl.edgeDetector = edgeDetector + layoutImpl.swipeSourceDetector = swipeSourceDetector } layoutImpl.Content(modifier) diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt index 7cc9d2623e9c..8c5a4720e7fb 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt @@ -47,7 +47,7 @@ internal typealias MovableElementContent = internal class SceneTransitionLayoutImpl( internal val state: BaseSceneTransitionLayoutState, internal var density: Density, - internal var edgeDetector: EdgeDetector, + internal var swipeSourceDetector: SwipeSourceDetector, internal var transitionInterceptionThreshold: Float, builder: SceneTransitionLayoutScope.() -> Unit, private val coroutineScope: CoroutineScope, @@ -140,7 +140,7 @@ internal class SceneTransitionLayoutImpl( object : SceneTransitionLayoutScope { override fun scene( key: SceneKey, - userActions: Map<UserAction, SceneKey>, + userActions: Map<UserAction, UserActionResult>, content: @Composable SceneScope.() -> Unit, ) { scenesToRemove.remove(key) @@ -229,8 +229,10 @@ internal class SceneTransitionLayoutImpl( // Handle back events. // TODO(b/290184746): Make sure that this works with SystemUI once we use // SceneTransitionLayout in Flexiglass. - scene(state.transitionState.currentScene).userActions[Back]?.let { backScene -> - BackHandler { with(state) { coroutineScope.onChangeScene(backScene) } } + scene(state.transitionState.currentScene).userActions[Back]?.let { result -> + // TODO(b/290184746): Handle predictive back and use result.distance if + // specified. + BackHandler { with(state) { coroutineScope.onChangeScene(result.toScene) } } } Box { diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt index 0d3bc7d0cd85..b9c4ac0cd006 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt @@ -17,40 +17,98 @@ package com.android.compose.animation.scene import androidx.compose.foundation.gestures.Orientation +import androidx.compose.runtime.Stable import androidx.compose.ui.Modifier +import androidx.compose.ui.input.pointer.PointerEvent +import androidx.compose.ui.input.pointer.PointerEventPass +import androidx.compose.ui.node.DelegatingNode +import androidx.compose.ui.node.ModifierNodeElement +import androidx.compose.ui.node.PointerInputModifierNode +import androidx.compose.ui.unit.IntSize /** * Configures the swipeable behavior of a [SceneTransitionLayout] depending on the current state. */ +@Stable internal fun Modifier.swipeToScene(gestureHandler: SceneGestureHandler): Modifier { - /** Whether swipe should be enabled in the given [orientation]. */ - fun Scene.shouldEnableSwipes(orientation: Orientation): Boolean = - userActions.keys.any { it is Swipe && it.direction.orientation == orientation } - - val layoutImpl = gestureHandler.layoutImpl - val currentScene = layoutImpl.scene(layoutImpl.state.transitionState.currentScene) - val orientation = gestureHandler.orientation - val canSwipe = currentScene.shouldEnableSwipes(orientation) - val canOppositeSwipe = - currentScene.shouldEnableSwipes( - when (orientation) { - Orientation.Vertical -> Orientation.Horizontal - Orientation.Horizontal -> Orientation.Vertical - } + return this.then(SwipeToSceneElement(gestureHandler)) +} + +private data class SwipeToSceneElement( + val gestureHandler: SceneGestureHandler, +) : ModifierNodeElement<SwipeToSceneNode>() { + override fun create(): SwipeToSceneNode = SwipeToSceneNode(gestureHandler) + + override fun update(node: SwipeToSceneNode) { + node.gestureHandler = gestureHandler + } +} + +private class SwipeToSceneNode( + gestureHandler: SceneGestureHandler, +) : DelegatingNode(), PointerInputModifierNode { + private val delegate = + delegate( + MultiPointerDraggableNode( + orientation = gestureHandler.orientation, + enabled = ::enabled, + startDragImmediately = ::startDragImmediately, + onDragStarted = gestureHandler.draggable::onDragStarted, + onDragDelta = gestureHandler.draggable::onDelta, + onDragStopped = gestureHandler.draggable::onDragStopped, + ) ) - return multiPointerDraggable( - orientation = orientation, - enabled = gestureHandler.isDrivingTransition || canSwipe, - // Immediately start the drag if this our [transition] is currently animating to a scene + var gestureHandler: SceneGestureHandler = gestureHandler + set(value) { + if (value != field) { + field = value + + // Make sure to update the delegate orientation. Note that this will automatically + // reset the underlying pointer input handler, so previous gestures will be + // cancelled. + delegate.orientation = value.orientation + } + } + + override fun onPointerEvent( + pointerEvent: PointerEvent, + pass: PointerEventPass, + bounds: IntSize, + ) = delegate.onPointerEvent(pointerEvent, pass, bounds) + + override fun onCancelPointerInput() = delegate.onCancelPointerInput() + + private fun enabled(): Boolean { + return gestureHandler.isDrivingTransition || + currentScene().shouldEnableSwipes(gestureHandler.orientation) + } + + private fun currentScene(): Scene { + val layoutImpl = gestureHandler.layoutImpl + return layoutImpl.scene(layoutImpl.state.transitionState.currentScene) + } + + /** Whether swipe should be enabled in the given [orientation]. */ + private fun Scene.shouldEnableSwipes(orientation: Orientation): Boolean { + return userActions.keys.any { it is Swipe && it.direction.orientation == orientation } + } + + private fun startDragImmediately(): Boolean { + // Immediately start the drag if this our transition is currently animating to a scene // (i.e. the user released their input pointer after swiping in this orientation) and the // user can't swipe in the other direction. - startDragImmediately = - gestureHandler.isDrivingTransition && - gestureHandler.swipeTransition.isAnimatingOffset && - !canOppositeSwipe, - onDragStarted = gestureHandler.draggable::onDragStarted, - onDragDelta = gestureHandler.draggable::onDelta, - onDragStopped = gestureHandler.draggable::onDragStopped, - ) + return gestureHandler.isDrivingTransition && + gestureHandler.swipeTransition.isAnimatingOffset && + !canOppositeSwipe() + } + + private fun canOppositeSwipe(): Boolean { + val oppositeOrientation = + when (gestureHandler.orientation) { + Orientation.Vertical -> Orientation.Horizontal + Orientation.Horizontal -> Orientation.Vertical + } + return currentScene().shouldEnableSwipes(oppositeOrientation) + } } diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt index dc8505c43889..a764a52723af 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt @@ -320,11 +320,3 @@ interface PropertyTransformationBuilder { anchorHeight: Boolean = true, ) } - -/** The edge of a [SceneTransitionLayout]. */ -enum class Edge { - Left, - Right, - Top, - Bottom, -} diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/nestedscroll/PriorityNestedScrollConnection.kt b/packages/SystemUI/compose/scene/src/com/android/compose/nestedscroll/PriorityNestedScrollConnection.kt index 2841bcf4e40c..ac11d3040d67 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/nestedscroll/PriorityNestedScrollConnection.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/nestedscroll/PriorityNestedScrollConnection.kt @@ -22,6 +22,7 @@ import androidx.compose.ui.input.nestedscroll.NestedScrollConnection import androidx.compose.ui.input.nestedscroll.NestedScrollSource import androidx.compose.ui.unit.Velocity import com.android.compose.ui.util.SpaceVectorConverter +import kotlin.math.sign /** * This [NestedScrollConnection] waits for a child to scroll ([onPreScroll] or [onPostScroll]), and @@ -117,7 +118,12 @@ class PriorityNestedScrollConnection( return Velocity.Zero } - onPriorityStart(available = Offset.Zero) + // The offset passed to onPriorityStart() must be != 0f, so we create a small offset of 1px + // given the available velocity. + // TODO(b/291053278): Remove canStartPostFling() and instead make it possible to define the + // overscroll behavior on the Scene level. + val smallOffset = Offset(available.x.sign, available.y.sign) + onPriorityStart(available = smallOffset) // This is the last event of a scroll gesture. return onPriorityStop(available) diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/FixedSizeEdgeDetectorTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/FixedSizeEdgeDetectorTest.kt index a68282ae78f4..cceaf57ca82b 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/FixedSizeEdgeDetectorTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/FixedSizeEdgeDetectorTest.kt @@ -35,7 +35,7 @@ class FixedSizeEdgeDetectorTest { @Test fun horizontalEdges() { fun horizontalEdge(position: Int): Edge? = - detector.edge( + detector.source( layoutSize, position = IntOffset(position, 0), density, @@ -53,7 +53,7 @@ class FixedSizeEdgeDetectorTest { @Test fun verticalEdges() { fun verticalEdge(position: Int): Edge? = - detector.edge( + detector.source( layoutSize, position = IntOffset(0, position), density, 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 066a3e45fb3c..88363ad24d9a 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 @@ -77,7 +77,7 @@ class SceneGestureHandlerTest { userActions = mapOf( Swipe.Up to SceneB, - Swipe(SwipeDirection.Up, fromEdge = Edge.Bottom) to SceneA + Swipe(SwipeDirection.Up, fromSource = Edge.Bottom) to SceneA ), ) { Text("SceneC") @@ -90,7 +90,7 @@ class SceneGestureHandlerTest { SceneTransitionLayoutImpl( state = layoutState, density = Density(1f), - edgeDetector = DefaultEdgeDetector, + swipeSourceDetector = DefaultEdgeDetector, transitionInterceptionThreshold = transitionInterceptionThreshold, builder = scenesBuilder, coroutineScope = coroutineScope, @@ -192,16 +192,14 @@ class SceneGestureHandlerTest { @Test fun onDragStarted_shouldStartATransition() = runGestureTest { - draggable.onDragStarted() + draggable.onDragStarted(overSlop = down(0.1f)) assertTransition(currentScene = SceneA) } @Test fun afterSceneTransitionIsStarted_interceptDragEvents() = runGestureTest { - draggable.onDragStarted() + draggable.onDragStarted(overSlop = down(0.1f)) assertTransition(currentScene = SceneA) - - draggable.onDelta(pixels = down(0.1f)) assertThat(progress).isEqualTo(0.1f) draggable.onDelta(pixels = down(0.1f)) @@ -210,10 +208,7 @@ class SceneGestureHandlerTest { @Test fun onDragStoppedAfterDrag_velocityLowerThanThreshold_remainSameScene() = runGestureTest { - draggable.onDragStarted() - assertTransition(currentScene = SceneA) - - draggable.onDelta(pixels = down(0.1f)) + draggable.onDragStarted(overSlop = down(0.1f)) assertTransition(currentScene = SceneA) draggable.onDragStopped( @@ -228,10 +223,7 @@ class SceneGestureHandlerTest { @Test fun onDragStoppedAfterDrag_velocityAtLeastThreshold_goToNextScene() = runGestureTest { - draggable.onDragStarted() - assertTransition(currentScene = SceneA) - - draggable.onDelta(pixels = down(0.1f)) + draggable.onDragStarted(overSlop = down(0.1f)) assertTransition(currentScene = SceneA) draggable.onDragStopped(velocity = velocityThreshold) @@ -245,7 +237,7 @@ class SceneGestureHandlerTest { @Test fun onDragStoppedAfterStarted_returnToIdle() = runGestureTest { - draggable.onDragStarted() + draggable.onDragStarted(overSlop = down(0.1f)) assertTransition(currentScene = SceneA) draggable.onDragStopped(velocity = 0f) @@ -256,8 +248,7 @@ class SceneGestureHandlerTest { @Test fun onDragReversedDirection_changeToScene() = runGestureTest { // Drag A -> B with progress 0.6 - draggable.onDragStarted() - draggable.onDelta(up(0.6f)) + draggable.onDragStarted(overSlop = up(0.6f)) assertTransition( currentScene = SceneA, fromScene = SceneA, @@ -366,8 +357,7 @@ class SceneGestureHandlerTest { @Test fun onAccelaratedScroll_scrollToThirdScene() = runGestureTest { // Drag A -> B with progress 0.2 - draggable.onDragStarted() - draggable.onDelta(up(0.2f)) + draggable.onDragStarted(overSlop = up(0.2f)) assertTransition( currentScene = SceneA, fromScene = SceneA, @@ -401,9 +391,7 @@ class SceneGestureHandlerTest { @Test fun onAccelaratedScrollBothTargetsBecomeNull_settlesToIdle() = runGestureTest { - draggable.onDragStarted() - draggable.onDelta(up(0.2f)) - + draggable.onDragStarted(overSlop = up(0.2f)) draggable.onDelta(up(0.2f)) draggable.onDragStopped(velocity = -velocityThreshold) assertTransition(currentScene = SceneB, fromScene = SceneA, toScene = SceneB) @@ -459,16 +447,14 @@ class SceneGestureHandlerTest { draggable.onDragStopped(down(0.1f)) // now target changed to C for new drag that started before previous drag settled to Idle - draggable.onDragStarted(up(0.1f)) + draggable.onDragStarted(overSlop = 0f) + draggable.onDelta(up(0.1f)) assertTransition(fromScene = SceneA, toScene = SceneC, progress = 0.3f) } @Test fun startGestureDuringAnimatingOffset_shouldImmediatelyStopTheAnimation() = runGestureTest { - draggable.onDragStarted() - assertTransition(currentScene = SceneA) - - draggable.onDelta(pixels = down(0.1f)) + draggable.onDragStarted(overSlop = down(0.1f)) assertTransition(currentScene = SceneA) draggable.onDragStopped( @@ -759,10 +745,8 @@ class SceneGestureHandlerTest { @Test fun startNestedScrollWhileDragging() = runGestureTest { val nestedScroll = nestedScrollConnection(nestedScrollBehavior = EdgeAlways) - draggable.onDragStarted() + draggable.onDragStarted(overSlop = down(0.1f)) assertTransition(currentScene = SceneA) - - draggable.onDelta(down(0.1f)) assertThat(progress).isEqualTo(0.1f) // now we can intercept the scroll events diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt index 1ec3c8ba2301..940335853221 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt @@ -17,6 +17,7 @@ package com.android.compose.animation.scene import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.size import androidx.compose.runtime.Composable @@ -89,8 +90,8 @@ class SwipeToSceneTest { mapOf( Swipe.Down to TestScenes.SceneA, Swipe(SwipeDirection.Down, pointerCount = 2) to TestScenes.SceneB, - Swipe(SwipeDirection.Right, fromEdge = Edge.Left) to TestScenes.SceneB, - Swipe(SwipeDirection.Down, fromEdge = Edge.Top) to TestScenes.SceneB, + Swipe(SwipeDirection.Right, fromSource = Edge.Left) to TestScenes.SceneB, + Swipe(SwipeDirection.Down, fromSource = Edge.Top) to TestScenes.SceneB, ), ) { Box(Modifier.fillMaxSize()) @@ -349,4 +350,46 @@ class SwipeToSceneTest { assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Idle::class.java) assertThat(layoutState.transitionState.currentScene).isEqualTo(TestScenes.SceneC) } + + @Test + fun swipeDistance() { + // The draggable touch slop, i.e. the min px distance a touch pointer must move before it is + // detected as a drag event. + var touchSlop = 0f + + val layoutState = MutableSceneTransitionLayoutState(TestScenes.SceneA) + val verticalSwipeDistance = 50.dp + assertThat(verticalSwipeDistance).isNotEqualTo(LayoutHeight) + + rule.setContent { + touchSlop = LocalViewConfiguration.current.touchSlop + + SceneTransitionLayout( + state = layoutState, + modifier = Modifier.size(LayoutWidth, LayoutHeight) + ) { + scene( + TestScenes.SceneA, + userActions = + mapOf(Swipe.Down to TestScenes.SceneB withDistance verticalSwipeDistance), + ) { + Spacer(Modifier.fillMaxSize()) + } + scene(TestScenes.SceneB) { Spacer(Modifier.fillMaxSize()) } + } + } + + assertThat(layoutState.currentTransition).isNull() + + // Swipe by half of verticalSwipeDistance. + rule.onRoot().performTouchInput { + down(middleTop) + moveBy(Offset(0f, touchSlop + (verticalSwipeDistance / 2).toPx()), delayMillis = 1_000) + } + + // We should be at 50% + val transition = layoutState.currentTransition + assertThat(transition).isNotNull() + assertThat(transition!!.progress).isEqualTo(0.5f) + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt index 030d41ddd8fb..c82688c2772a 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt @@ -203,11 +203,17 @@ class KeyguardSecurityContainerControllerTest : SysuiTestCase() { whenever(deviceProvisionedController.isUserSetup(anyInt())).thenReturn(true) featureFlags = FakeFeatureFlags() - featureFlags.set(Flags.KEYGUARD_WM_STATE_REFACTOR, false) featureFlags.set(Flags.REFACTOR_KEYGUARD_DISMISS_INTENT, false) featureFlags.set(Flags.LOCKSCREEN_ENABLE_LANDSCAPE, false) - mSetFlagsRule.enableFlags(AConfigFlags.FLAG_REVAMPED_BOUNCER_MESSAGES) + mSetFlagsRule.enableFlags( + AConfigFlags.FLAG_REVAMPED_BOUNCER_MESSAGES, + ) + mSetFlagsRule.disableFlags( + FLAG_SIDEFPS_CONTROLLER_REFACTOR, + AConfigFlags.FLAG_KEYGUARD_WM_STATE_REFACTOR + ) + keyguardPasswordViewController = KeyguardPasswordViewController( keyguardPasswordView, @@ -238,7 +244,6 @@ class KeyguardSecurityContainerControllerTest : SysuiTestCase() { sceneInteractor.setTransitionState(sceneTransitionStateFlow) deviceEntryInteractor = kosmos.deviceEntryInteractor - mSetFlagsRule.disableFlags(FLAG_SIDEFPS_CONTROLLER_REFACTOR) underTest = KeyguardSecurityContainerController( view, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/AccessibilityQsShortcutsRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/AccessibilityQsShortcutsRepositoryImplTest.kt new file mode 100644 index 000000000000..9287edf4ee51 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/AccessibilityQsShortcutsRepositoryImplTest.kt @@ -0,0 +1,82 @@ +/* + * 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.accessibility.data.repository + +import android.provider.Settings +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.util.settings.FakeSettings +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +class AccessibilityQsShortcutsRepositoryImplTest : SysuiTestCase() { + private val testDispatcher = StandardTestDispatcher() + private val testScope = TestScope(testDispatcher) + private val secureSettings = FakeSettings() + + private val userA11yQsShortcutsRepositoryFactory = + object : UserA11yQsShortcutsRepository.Factory { + override fun create(userId: Int): UserA11yQsShortcutsRepository { + return UserA11yQsShortcutsRepository( + userId, + secureSettings, + testScope.backgroundScope, + testDispatcher, + ) + } + } + + private val underTest = + AccessibilityQsShortcutsRepositoryImpl(userA11yQsShortcutsRepositoryFactory) + + @Test + fun a11yQsShortcutTargetsForCorrectUsers() = + testScope.runTest { + val user0 = 0 + val targetsForUser0 = setOf("a", "b", "c") + val user1 = 1 + val targetsForUser1 = setOf("A") + val targetsFromUser0 by collectLastValue(underTest.a11yQsShortcutTargets(user0)) + val targetsFromUser1 by collectLastValue(underTest.a11yQsShortcutTargets(user1)) + + storeA11yQsShortcutTargetsForUser(targetsForUser0, user0) + storeA11yQsShortcutTargetsForUser(targetsForUser1, user1) + + assertThat(targetsFromUser0).isEqualTo(targetsForUser0) + assertThat(targetsFromUser1).isEqualTo(targetsForUser1) + } + + private fun storeA11yQsShortcutTargetsForUser(a11yQsTargets: Set<String>, forUser: Int) { + secureSettings.putStringForUser( + SETTING_NAME, + a11yQsTargets.joinToString(separator = ":"), + forUser + ) + } + + companion object { + private const val SETTING_NAME = Settings.Secure.ACCESSIBILITY_QS_TARGETS + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/UserA11yQsShortcutsRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/UserA11yQsShortcutsRepositoryTest.kt new file mode 100644 index 000000000000..ce22e288e292 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/UserA11yQsShortcutsRepositoryTest.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.accessibility.data.repository + +import android.provider.Settings +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.util.settings.FakeSettings +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +class UserA11yQsShortcutsRepositoryTest : SysuiTestCase() { + private val secureSettings = FakeSettings() + private val testDispatcher = StandardTestDispatcher() + private val testScope = TestScope(testDispatcher) + + private val underTest = + UserA11yQsShortcutsRepository( + USER_ID, + secureSettings, + testScope.backgroundScope, + testDispatcher + ) + + @Test + fun targetsMatchesSetting() = + testScope.runTest { + val observedTargets by collectLastValue(underTest.targets) + val a11yQsTargets = setOf("a", "b", "c") + secureSettings.putStringForUser( + SETTING_NAME, + a11yQsTargets.joinToString(SEPARATOR), + USER_ID + ) + + assertThat(observedTargets).isEqualTo(a11yQsTargets) + } + + companion object { + private const val USER_ID = 0 + private const val SEPARATOR = ":" + private const val SETTING_NAME = Settings.Secure.ACCESSIBILITY_QS_TARGETS + } +} 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 bb3429e72b35..c979ca63950a 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 @@ -30,7 +30,6 @@ import com.android.systemui.communal.data.db.CommunalWidgetItem import com.android.systemui.communal.shared.CommunalWidgetHost import com.android.systemui.communal.shared.model.CommunalWidgetContentModel import com.android.systemui.communal.widgets.CommunalAppWidgetHost -import com.android.systemui.communal.widgets.WidgetConfigurator import com.android.systemui.communal.widgets.widgetConfiguratorFail import com.android.systemui.communal.widgets.widgetConfiguratorSuccess import com.android.systemui.coroutines.collectLastValue @@ -45,8 +44,7 @@ import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat import java.util.Optional import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.flowOf -import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Before @@ -62,24 +60,17 @@ import org.mockito.MockitoAnnotations @SmallTest @RunWith(AndroidJUnit4::class) class CommunalWidgetRepositoryImplTest : SysuiTestCase() { - @Mock private lateinit var appWidgetManagerOptional: Optional<AppWidgetManager> - @Mock private lateinit var appWidgetManager: AppWidgetManager - @Mock private lateinit var appWidgetHost: CommunalAppWidgetHost - @Mock private lateinit var stopwatchProviderInfo: AppWidgetProviderInfo - @Mock private lateinit var providerInfoA: AppWidgetProviderInfo - @Mock private lateinit var communalWidgetHost: CommunalWidgetHost - @Mock private lateinit var communalWidgetDao: CommunalWidgetDao private lateinit var logBuffer: LogBuffer + private lateinit var fakeWidgets: MutableStateFlow<Map<CommunalItemRank, CommunalWidgetItem>> private val kosmos = testKosmos() - private val testDispatcher = kosmos.testDispatcher private val testScope = kosmos.testScope private val fakeAllowlist = @@ -94,7 +85,7 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { @Before fun setUp() { MockitoAnnotations.initMocks(this) - + fakeWidgets = MutableStateFlow(emptyMap()) logBuffer = logcatLogBuffer(name = "CommunalWidgetRepoImplTest") setAppWidgetIds(emptyList()) @@ -102,13 +93,11 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { overrideResource(R.array.config_communalWidgetAllowlist, fakeAllowlist.toTypedArray()) whenever(stopwatchProviderInfo.loadLabel(any())).thenReturn("Stopwatch") - whenever(communalWidgetDao.getWidgets()).thenReturn(flowOf(emptyMap())) - whenever(appWidgetManagerOptional.isPresent).thenReturn(true) - whenever(appWidgetManagerOptional.get()).thenReturn(appWidgetManager) + whenever(communalWidgetDao.getWidgets()).thenReturn(fakeWidgets) underTest = CommunalWidgetRepositoryImpl( - appWidgetManagerOptional, + Optional.of(appWidgetManager), appWidgetHost, testScope.backgroundScope, kosmos.testDispatcher, @@ -119,30 +108,16 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { } @Test - fun neverQueryDbForWidgets_whenHostIsInactive() = - testScope.runTest { - underTest.updateAppWidgetHostActive(false) - underTest.communalWidgets.launchIn(testScope.backgroundScope) - runCurrent() - - verify(communalWidgetDao, never()).getWidgets() - } - - @Test - fun communalWidgets_whenHostIsActive_queryWidgetsFromDb() = + fun communalWidgets_queryWidgetsFromDb() = testScope.runTest { - underTest.updateAppWidgetHostActive(true) - val communalItemRankEntry = CommunalItemRank(uid = 1L, rank = 1) val communalWidgetItemEntry = CommunalWidgetItem(uid = 1L, 1, "pk_name/cls_name", 1L) - whenever(communalWidgetDao.getWidgets()) - .thenReturn(flowOf(mapOf(communalItemRankEntry to communalWidgetItemEntry))) + fakeWidgets.value = mapOf(communalItemRankEntry to communalWidgetItemEntry) whenever(appWidgetManager.getAppWidgetInfo(anyInt())).thenReturn(providerInfoA) installedProviders(listOf(stopwatchProviderInfo)) val communalWidgets by collectLastValue(underTest.communalWidgets) - runCurrent() verify(communalWidgetDao).getWidgets() assertThat(communalWidgets) .containsExactly( @@ -157,8 +132,6 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { @Test fun addWidget_allocateId_bindWidget_andAddToDb() = testScope.runTest { - underTest.updateAppWidgetHostActive(true) - val provider = ComponentName("pkg_name", "cls_name") val id = 1 val priority = 1 @@ -176,8 +149,6 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { @Test fun addWidget_configurationFails_doNotAddWidgetToDb() = testScope.runTest { - underTest.updateAppWidgetHostActive(true) - val provider = ComponentName("pkg_name", "cls_name") val id = 1 val priority = 1 @@ -195,23 +166,13 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { @Test fun addWidget_configurationThrowsError_doNotAddWidgetToDb() = testScope.runTest { - underTest.updateAppWidgetHostActive(true) - val provider = ComponentName("pkg_name", "cls_name") val id = 1 val priority = 1 whenever(communalWidgetHost.getAppWidgetInfo(id)) .thenReturn(PROVIDER_INFO_REQUIRES_CONFIGURATION) whenever(communalWidgetHost.allocateIdAndBindWidget(provider)).thenReturn(id) - underTest.addWidget( - provider, - priority, - object : WidgetConfigurator { - override suspend fun configureWidget(appWidgetId: Int): Boolean { - throw IllegalStateException("some error") - } - } - ) + underTest.addWidget(provider, priority) { throw IllegalStateException("some error") } runCurrent() verify(communalWidgetHost).allocateIdAndBindWidget(provider) @@ -222,8 +183,6 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { @Test fun addWidget_configurationNotRequired_doesNotConfigure_addWidgetToDb() = testScope.runTest { - underTest.updateAppWidgetHostActive(true) - val provider = ComponentName("pkg_name", "cls_name") val id = 1 val priority = 1 @@ -241,8 +200,6 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { @Test fun deleteWidget_removeWidgetId_andDeleteFromDb() = testScope.runTest { - underTest.updateAppWidgetHostActive(true) - val id = 1 underTest.deleteWidget(id) runCurrent() @@ -254,8 +211,6 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { @Test fun reorderWidgets_queryDb() = testScope.runTest { - underTest.updateAppWidgetHostActive(true) - val widgetIdToPriorityMap = mapOf(104 to 1, 103 to 2, 101 to 3) underTest.updateWidgetOrder(widgetIdToPriorityMap) runCurrent() @@ -263,28 +218,6 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { verify(communalWidgetDao).updateWidgetOrder(widgetIdToPriorityMap) } - @Test - fun appWidgetHost_startListening() = - testScope.runTest { - verify(appWidgetHost, never()).startListening() - - underTest.updateAppWidgetHostActive(true) - - verify(appWidgetHost).startListening() - } - - @Test - fun appWidgetHost_stopListening() = - testScope.runTest { - underTest.updateAppWidgetHostActive(true) - - verify(appWidgetHost).startListening() - - underTest.updateAppWidgetHostActive(false) - - verify(appWidgetHost).stopListening() - } - private fun installedProviders(providers: List<AppWidgetProviderInfo>) { whenever(appWidgetManager.installedProviders).thenReturn(providers) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorCommunalDisabledTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorCommunalDisabledTest.kt index e8216735fb5d..6a3fc2a060eb 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorCommunalDisabledTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorCommunalDisabledTest.kt @@ -80,15 +80,4 @@ class CommunalInteractorCommunalDisabledTest : SysuiTestCase() { assertThat(isCommunalAvailable).isFalse() } - - @Test - fun updateAppWidgetHostActive_whenStorageUnlock_false() = - testScope.runTest { - assertThat(widgetRepository.isHostActive()).isFalse() - - keyguardRepository.setIsEncryptedOrLockdown(false) - runCurrent() - - assertThat(widgetRepository.isHostActive()).isFalse() - } } 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 1b7117f41bbb..ee01bf9c26e4 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 @@ -125,7 +125,7 @@ class CommunalInteractorTest : SysuiTestCase() { keyguardRepository.setIsEncryptedOrLockdown(false) userRepository.setSelectedUserInfo(mainUser) keyguardRepository.setKeyguardShowing(true) - runCurrent() + communalRepository.setCommunalEnabledState(true) assertThat(isAvailable).isTrue() } @@ -138,7 +138,8 @@ class CommunalInteractorTest : SysuiTestCase() { keyguardRepository.setIsEncryptedOrLockdown(true) userRepository.setSelectedUserInfo(mainUser) - runCurrent() + keyguardRepository.setKeyguardShowing(true) + communalRepository.setCommunalEnabledState(true) assertThat(isAvailable).isFalse() } @@ -152,7 +153,7 @@ class CommunalInteractorTest : SysuiTestCase() { keyguardRepository.setIsEncryptedOrLockdown(false) userRepository.setSelectedUserInfo(secondaryUser) keyguardRepository.setKeyguardShowing(true) - runCurrent() + communalRepository.setCommunalEnabledState(true) assertThat(isAvailable).isFalse() } @@ -166,23 +167,23 @@ class CommunalInteractorTest : SysuiTestCase() { keyguardRepository.setIsEncryptedOrLockdown(false) userRepository.setSelectedUserInfo(mainUser) keyguardRepository.setDreaming(true) - runCurrent() + communalRepository.setCommunalEnabledState(true) assertThat(isAvailable).isTrue() } @Test - fun updateAppWidgetHostActive_uponStorageUnlockAsMainUser_true() = + fun isCommunalAvailable_communalDisabled_false() = testScope.runTest { - collectLastValue(underTest.isCommunalAvailable) - assertThat(widgetRepository.isHostActive()).isFalse() + val isAvailable by collectLastValue(underTest.isCommunalAvailable) + assertThat(isAvailable).isFalse() keyguardRepository.setIsEncryptedOrLockdown(false) userRepository.setSelectedUserInfo(mainUser) keyguardRepository.setKeyguardShowing(true) - runCurrent() + communalRepository.setCommunalEnabledState(false) - assertThat(widgetRepository.isHostActive()).isTrue() + assertThat(isAvailable).isFalse() } @Test @@ -647,6 +648,14 @@ class CommunalInteractorTest : SysuiTestCase() { verify(editWidgetsActivityStarter).startActivity() } + @Test + fun showWidgetEditor_withPreselectedKey_startsActivity() = + testScope.runTest { + val widgetKey = CommunalContentModel.KEY.widget(123) + underTest.showWidgetEditor(preselectedKey = widgetKey) + verify(editWidgetsActivityStarter).startActivity(widgetKey) + } + private fun smartspaceTimer(id: String, timestamp: Long = 0L): SmartspaceTarget { val timer = mock(SmartspaceTarget::class.java) whenever(timer.smartspaceTargetId).thenReturn(id) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalTutorialInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalTutorialInteractorTest.kt index 352463f5a198..6c87e0f2eb23 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalTutorialInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalTutorialInteractorTest.kt @@ -34,20 +34,15 @@ import com.android.systemui.kosmos.testScope 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.google.common.truth.Truth.assertThat import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.mockito.Mock -import org.mockito.MockitoAnnotations @SmallTest @RunWith(AndroidJUnit4::class) class CommunalTutorialInteractorTest : SysuiTestCase() { - @Mock lateinit var user: UserInfo - private val kosmos = testKosmos() private val testScope = kosmos.testScope @@ -60,14 +55,14 @@ class CommunalTutorialInteractorTest : SysuiTestCase() { @Before fun setUp() { - MockitoAnnotations.initMocks(this) - keyguardRepository = kosmos.fakeKeyguardRepository communalTutorialRepository = kosmos.fakeCommunalTutorialRepository communalRepository = kosmos.fakeCommunalRepository communalInteractor = kosmos.communalInteractor userRepository = kosmos.fakeUserRepository + userRepository.setUserInfos(listOf(MAIN_USER_INFO)) + underTest = kosmos.communalTutorialInteractor } @@ -204,12 +199,17 @@ class CommunalTutorialInteractorTest : SysuiTestCase() { private suspend fun setCommunalAvailable(available: Boolean) { if (available) { communalRepository.setIsCommunalEnabled(true) + communalRepository.setCommunalEnabledState(true) keyguardRepository.setIsEncryptedOrLockdown(false) - whenever(user.isMain).thenReturn(true) - userRepository.setUserInfos(listOf(user)) - userRepository.setSelectedUserInfo(user) + userRepository.setSelectedUserInfo(MAIN_USER_INFO) + keyguardRepository.setKeyguardShowing(true) } else { - keyguardRepository.setIsEncryptedOrLockdown(true) + communalRepository.setIsCommunalEnabled(false) + communalRepository.setCommunalEnabledState(false) } } + + private companion object { + val MAIN_USER_INFO = UserInfo(0, "primary", UserInfo.FLAG_MAIN) + } } 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 a2dec5ff8830..273d1cd55626 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 @@ -129,6 +129,19 @@ class CommunalEditModeViewModelTest : SysuiTestCase() { } @Test + fun selectedKey_onReorderWidgets_isCleared() = + testScope.runTest { + val selectedKey by collectLastValue(underTest.selectedKey) + + val key = CommunalContentModel.KEY.widget(123) + underTest.setSelectedKey(key) + assertThat(selectedKey).isEqualTo(key) + + underTest.onReorderWidgetStart() + assertThat(selectedKey).isNull() + } + + @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 c814f3f1db6a..0723e8306f71 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 @@ -27,6 +27,7 @@ import com.android.systemui.communal.data.repository.FakeCommunalMediaRepository import com.android.systemui.communal.data.repository.FakeCommunalTutorialRepository import com.android.systemui.communal.data.repository.FakeCommunalWidgetRepository import com.android.systemui.communal.data.repository.fakeCommunalMediaRepository +import com.android.systemui.communal.data.repository.fakeCommunalRepository import com.android.systemui.communal.data.repository.fakeCommunalTutorialRepository import com.android.systemui.communal.data.repository.fakeCommunalWidgetRepository import com.android.systemui.communal.domain.interactor.communalInteractor @@ -90,6 +91,8 @@ class CommunalViewModelTest : SysuiTestCase() { mediaRepository = kosmos.fakeCommunalMediaRepository userRepository = kosmos.fakeUserRepository + kosmos.fakeCommunalRepository.setCommunalEnabledState(true) + underTest = CommunalViewModel( testScope, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartableTest.kt new file mode 100644 index 000000000000..a3654b6e8963 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartableTest.kt @@ -0,0 +1,134 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.communal.widgets + +import android.content.pm.UserInfo +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.communal.data.repository.fakeCommunalRepository +import com.android.systemui.communal.domain.interactor.communalInteractor +import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository +import com.android.systemui.kosmos.applicationCoroutineScope +import com.android.systemui.kosmos.testDispatcher +import com.android.systemui.kosmos.testScope +import com.android.systemui.testKosmos +import com.android.systemui.user.data.repository.fakeUserRepository +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 +import org.mockito.Mock +import org.mockito.Mockito.never +import org.mockito.Mockito.verify +import org.mockito.MockitoAnnotations + +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@RunWith(AndroidJUnit4::class) +class CommunalAppWidgetHostStartableTest : SysuiTestCase() { + private val kosmos = testKosmos() + + @Mock private lateinit var appWidgetHost: CommunalAppWidgetHost + + private lateinit var underTest: CommunalAppWidgetHostStartable + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + kosmos.fakeUserRepository.setUserInfos(listOf(MAIN_USER_INFO)) + + underTest = + CommunalAppWidgetHostStartable( + appWidgetHost, + kosmos.communalInteractor, + kosmos.applicationCoroutineScope, + kosmos.testDispatcher, + ) + } + + @Test + fun editModeShowingStartsAppWidgetHost() = + with(kosmos) { + testScope.runTest { + setCommunalAvailable(false) + communalInteractor.setEditModeOpen(true) + verify(appWidgetHost, never()).startListening() + + underTest.start() + runCurrent() + + verify(appWidgetHost).startListening() + verify(appWidgetHost, never()).stopListening() + + communalInteractor.setEditModeOpen(false) + runCurrent() + + verify(appWidgetHost).stopListening() + } + } + + @Test + fun communalShowingStartsAppWidgetHost() = + with(kosmos) { + testScope.runTest { + setCommunalAvailable(true) + communalInteractor.setEditModeOpen(false) + verify(appWidgetHost, never()).startListening() + + underTest.start() + runCurrent() + + verify(appWidgetHost).startListening() + verify(appWidgetHost, never()).stopListening() + + setCommunalAvailable(false) + runCurrent() + + verify(appWidgetHost).stopListening() + } + } + + @Test + fun communalAndEditModeNotShowingNeverStartListening() = + with(kosmos) { + testScope.runTest { + setCommunalAvailable(false) + communalInteractor.setEditModeOpen(false) + + underTest.start() + runCurrent() + + verify(appWidgetHost, never()).startListening() + verify(appWidgetHost, never()).stopListening() + } + } + + private suspend fun setCommunalAvailable(available: Boolean) = + with(kosmos) { + fakeKeyguardRepository.setIsEncryptedOrLockdown(false) + fakeUserRepository.setSelectedUserInfo(MAIN_USER_INFO) + fakeKeyguardRepository.setKeyguardShowing(true) + fakeCommunalRepository.setCommunalEnabledState(available) + } + + private companion object { + val MAIN_USER_INFO = UserInfo(0, "primary", UserInfo.FLAG_MAIN) + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/data/repository/DeviceEntryFaceAuthRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/data/repository/DeviceEntryFaceAuthRepositoryTest.kt index 6a14220e6a42..6808f5d643a9 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/data/repository/DeviceEntryFaceAuthRepositoryTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/data/repository/DeviceEntryFaceAuthRepositoryTest.kt @@ -38,6 +38,7 @@ import androidx.test.filters.SmallTest import com.android.internal.logging.InstanceId.fakeInstanceId import com.android.internal.logging.UiEventLogger import com.android.keyguard.KeyguardUpdateMonitor +import com.android.systemui.Flags as AConfigFlags import com.android.systemui.SysuiTestCase import com.android.systemui.biometrics.data.repository.FakeDisplayStateRepository import com.android.systemui.biometrics.data.repository.FakeFacePropertyRepository @@ -62,7 +63,6 @@ import com.android.systemui.display.data.repository.FakeDisplayRepository import com.android.systemui.display.data.repository.display import com.android.systemui.dump.DumpManager import com.android.systemui.flags.FakeFeatureFlags -import com.android.systemui.flags.Flags.KEYGUARD_WM_STATE_REFACTOR import com.android.systemui.keyguard.data.repository.BiometricType import com.android.systemui.keyguard.data.repository.FakeBiometricSettingsRepository import com.android.systemui.keyguard.data.repository.FakeCommandQueue @@ -194,7 +194,7 @@ class DeviceEntryFaceAuthRepositoryTest : SysuiTestCase() { biometricSettingsRepository = FakeBiometricSettingsRepository() deviceEntryFingerprintAuthRepository = FakeDeviceEntryFingerprintAuthRepository() trustRepository = FakeTrustRepository() - featureFlags = FakeFeatureFlags().apply { set(KEYGUARD_WM_STATE_REFACTOR, false) } + featureFlags = FakeFeatureFlags() powerRepository = FakePowerRepository() powerInteractor = @@ -252,6 +252,10 @@ class DeviceEntryFaceAuthRepositoryTest : SysuiTestCase() { .thenReturn(listOf(createFaceSensorProperties(supportsFaceDetection = true))) whenever(bypassController.bypassEnabled).thenReturn(true) underTest = createDeviceEntryFaceAuthRepositoryImpl(faceManager, bypassController) + + mSetFlagsRule.disableFlags( + AConfigFlags.FLAG_KEYGUARD_WM_STATE_REFACTOR, + ) } private fun createDeviceEntryFaceAuthRepositoryImpl( @@ -301,7 +305,6 @@ class DeviceEntryFaceAuthRepositoryTest : SysuiTestCase() { faceAuthBuffer, keyguardTransitionInteractor, displayStateInteractor, - featureFlags, dumpManager, ) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/AuthRippleInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/AuthRippleInteractorTest.kt new file mode 100644 index 000000000000..88ad3f37dacd --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/AuthRippleInteractorTest.kt @@ -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.systemui.deviceentry.domain.interactor + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.biometrics.data.repository.fingerprintPropertyRepository +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository +import com.android.systemui.keyguard.shared.model.BiometricUnlockModel +import com.android.systemui.keyguard.shared.model.BiometricUnlockSource +import com.android.systemui.kosmos.testScope +import com.android.systemui.testKosmos +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith + +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@RunWith(AndroidJUnit4::class) +class AuthRippleInteractorTest : SysuiTestCase() { + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + private val deviceEntrySourceInteractor = kosmos.deviceEntrySourceInteractor + private val fingerprintPropertyRepository = kosmos.fingerprintPropertyRepository + private val keyguardRepository = kosmos.fakeKeyguardRepository + private val underTest = kosmos.authRippleInteractor + + @Test + fun enteringDeviceFromDeviceEntryIcon_udfpsNotSupported_doesNotShowAuthRipple() = + testScope.runTest { + val showUnlockRipple by collectLastValue(underTest.showUnlockRipple) + fingerprintPropertyRepository.supportsRearFps() + keyguardRepository.setKeyguardDismissible(true) + runCurrent() + deviceEntrySourceInteractor.attemptEnterDeviceFromDeviceEntryIcon() + assertThat(showUnlockRipple).isNull() + } + + @Test + fun enteringDeviceFromDeviceEntryIcon_udfpsSupported_showsAuthRipple() = + testScope.runTest { + val showUnlockRipple by collectLastValue(underTest.showUnlockRipple) + fingerprintPropertyRepository.supportsUdfps() + keyguardRepository.setKeyguardDismissible(true) + runCurrent() + deviceEntrySourceInteractor.attemptEnterDeviceFromDeviceEntryIcon() + assertThat(showUnlockRipple).isEqualTo(BiometricUnlockSource.FINGERPRINT_SENSOR) + } + + @Test + fun faceUnlocked_showsAuthRipple() = + testScope.runTest { + val showUnlockRipple by collectLastValue(underTest.showUnlockRipple) + keyguardRepository.setBiometricUnlockSource(BiometricUnlockSource.FACE_SENSOR) + keyguardRepository.setBiometricUnlockState(BiometricUnlockModel.WAKE_AND_UNLOCK) + assertThat(showUnlockRipple).isEqualTo(BiometricUnlockSource.FACE_SENSOR) + } + + @Test + fun fingerprintUnlocked_showsAuthRipple() = + testScope.runTest { + val showUnlockRippleFromBiometricUnlock by collectLastValue(underTest.showUnlockRipple) + keyguardRepository.setBiometricUnlockSource(BiometricUnlockSource.FINGERPRINT_SENSOR) + keyguardRepository.setBiometricUnlockState(BiometricUnlockModel.WAKE_AND_UNLOCK) + assertThat(showUnlockRippleFromBiometricUnlock) + .isEqualTo(BiometricUnlockSource.FINGERPRINT_SENSOR) + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntrySourceInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntrySourceInteractorTest.kt new file mode 100644 index 000000000000..d216fa0d0eff --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntrySourceInteractorTest.kt @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.deviceentry.domain.interactor + +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.keyguard.data.repository.fakeKeyguardRepository +import com.android.systemui.keyguard.shared.model.BiometricUnlockModel +import com.android.systemui.keyguard.shared.model.BiometricUnlockSource +import com.android.systemui.kosmos.testScope +import com.android.systemui.testKosmos +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith + +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@RunWith(AndroidJUnit4::class) +class DeviceEntrySourceInteractorTest : SysuiTestCase() { + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + private val keyguardRepository = kosmos.fakeKeyguardRepository + private val underTest = kosmos.deviceEntrySourceInteractor + + @Test + fun deviceEntryFromFaceUnlock() = + testScope.runTest { + val deviceEntryFromBiometricAuthentication by + collectLastValue(underTest.deviceEntryFromBiometricSource) + keyguardRepository.setBiometricUnlockSource(BiometricUnlockSource.FACE_SENSOR) + keyguardRepository.setBiometricUnlockState(BiometricUnlockModel.WAKE_AND_UNLOCK) + runCurrent() + assertThat(deviceEntryFromBiometricAuthentication) + .isEqualTo(BiometricUnlockSource.FACE_SENSOR) + } + + @Test + fun deviceEntryFromFingerprintUnlock() = runTest { + val deviceEntryFromBiometricAuthentication by + collectLastValue(underTest.deviceEntryFromBiometricSource) + keyguardRepository.setBiometricUnlockSource(BiometricUnlockSource.FINGERPRINT_SENSOR) + keyguardRepository.setBiometricUnlockState(BiometricUnlockModel.WAKE_AND_UNLOCK) + runCurrent() + assertThat(deviceEntryFromBiometricAuthentication) + .isEqualTo(BiometricUnlockSource.FINGERPRINT_SENSOR) + } + + @Test + fun noDeviceEntry() = runTest { + val deviceEntryFromBiometricAuthentication by + collectLastValue(underTest.deviceEntryFromBiometricSource) + keyguardRepository.setBiometricUnlockSource(BiometricUnlockSource.FINGERPRINT_SENSOR) + // doesn't dismiss keyguard: + keyguardRepository.setBiometricUnlockState(BiometricUnlockModel.ONLY_WAKE) + runCurrent() + assertThat(deviceEntryFromBiometricAuthentication).isNull() + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt index dc8b97abbfe8..c01c79dd2601 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt @@ -238,10 +238,10 @@ class KeyguardRepositoryImplTest : SysuiTestCase() { } @Test - fun isKeyguardUnlocked() = + fun isKeyguardDismissible() = testScope.runTest { whenever(keyguardStateController.isUnlocked).thenReturn(false) - val isKeyguardUnlocked by collectLastValue(underTest.isKeyguardUnlocked) + val isKeyguardUnlocked by collectLastValue(underTest.isKeyguardDismissible) runCurrent() assertThat(isKeyguardUnlocked).isFalse() @@ -574,7 +574,7 @@ class KeyguardRepositoryImplTest : SysuiTestCase() { @Test fun isEncryptedOrLockdown() = - testScope.runTest { + TestScope(mainDispatcher).runTest { whenever(userTracker.userId).thenReturn(0) whenever(keyguardUpdateMonitor.isEncryptedOrLockdown(0)).thenReturn(true) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/data/repository/AutoAddSettingsRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/data/repository/AutoAddSettingsRepositoryTest.kt index d277fcab3690..1545e7444ffb 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/data/repository/AutoAddSettingsRepositoryTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/data/repository/AutoAddSettingsRepositoryTest.kt @@ -16,6 +16,7 @@ package com.android.systemui.qs.pipeline.data.repository +import android.platform.test.annotations.EnabledOnRavenwood import android.provider.Settings import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest @@ -38,6 +39,7 @@ import org.mockito.MockitoAnnotations @OptIn(ExperimentalCoroutinesApi::class) @SmallTest +@EnabledOnRavenwood @RunWith(AndroidJUnit4::class) class AutoAddSettingsRepositoryTest : SysuiTestCase() { private val secureSettings = FakeSettings() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/data/repository/CustomTileAddedSharedPreferencesRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/data/repository/CustomTileAddedSharedPreferencesRepositoryTest.kt index 3db676d68f42..ee7a97a0846a 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/data/repository/CustomTileAddedSharedPreferencesRepositoryTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/data/repository/CustomTileAddedSharedPreferencesRepositoryTest.kt @@ -18,6 +18,7 @@ package com.android.systemui.qs.pipeline.data.repository import android.content.ComponentName import android.content.SharedPreferences +import android.platform.test.annotations.EnabledOnRavenwood import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase @@ -29,6 +30,7 @@ import org.junit.Test import org.junit.runner.RunWith @SmallTest +@EnabledOnRavenwood @RunWith(AndroidJUnit4::class) class CustomTileAddedSharedPreferencesRepositoryTest : SysuiTestCase() { 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 eb845b2b423c..d9f24b36dac2 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 @@ -35,7 +35,6 @@ 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.coroutines.collectLastValue -import com.android.systemui.kosmos.testDispatcher import com.android.systemui.kosmos.testScope import com.android.systemui.testKosmos import com.android.systemui.util.mockito.any @@ -44,6 +43,8 @@ import com.android.systemui.util.mockito.eq import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test @@ -52,6 +53,7 @@ import org.mockito.ArgumentMatchers.anyInt import org.mockito.Mock import org.mockito.MockitoAnnotations +@OptIn(ExperimentalCoroutinesApi::class) @SmallTest @RunWith(AndroidJUnit4::class) @TestableLooper.RunWithLooper @@ -82,7 +84,7 @@ class InstalledTilesComponentRepositoryImplTest : SysuiTestCase() { underTest = InstalledTilesComponentRepositoryImpl( context, - kosmos.testDispatcher, + testScope.backgroundScope, kosmos.packageChangeRepository ) } @@ -103,6 +105,7 @@ class InstalledTilesComponentRepositoryImplTest : SysuiTestCase() { .thenReturn(listOf(resolveInfo)) val componentNames by collectLastValue(underTest.getInstalledTilesComponents(userId)) + runCurrent() assertThat(componentNames).containsExactly(TEST_COMPONENT) } @@ -115,6 +118,8 @@ class InstalledTilesComponentRepositoryImplTest : SysuiTestCase() { ResolveInfo(TEST_COMPONENT, hasPermission = true, defaultEnabled = true) val componentNames by collectLastValue(underTest.getInstalledTilesComponents(userId)) + runCurrent() + assertThat(componentNames).isEmpty() whenever( @@ -126,6 +131,7 @@ class InstalledTilesComponentRepositoryImplTest : SysuiTestCase() { ) .thenReturn(listOf(resolveInfo)) kosmos.fakePackageChangeRepository.notifyChange(PackageChangeModel.Empty) + runCurrent() assertThat(componentNames).containsExactly(TEST_COMPONENT) } @@ -146,6 +152,8 @@ class InstalledTilesComponentRepositoryImplTest : SysuiTestCase() { .thenReturn(listOf(resolveInfo)) val componentNames by collectLastValue(underTest.getInstalledTilesComponents(userId)) + runCurrent() + assertThat(componentNames).isEmpty() } @@ -165,6 +173,8 @@ class InstalledTilesComponentRepositoryImplTest : SysuiTestCase() { .thenReturn(listOf(resolveInfo)) val componentNames by collectLastValue(underTest.getInstalledTilesComponents(userId)) + runCurrent() + assertThat(componentNames).isEmpty() } @@ -210,10 +220,31 @@ class InstalledTilesComponentRepositoryImplTest : SysuiTestCase() { .thenReturn(listOf(resolveInfo)) val componentNames by collectLastValue(underTest.getInstalledTilesComponents(userId)) + runCurrent() assertThat(componentNames).containsExactly(TEST_COMPONENT) } + @Test + fun loadComponentsForSameUserTwice_returnsSameFlow() = + testScope.runTest { + val flowForUser1 = underTest.getInstalledTilesComponents(1) + val flowForUser1TheSecondTime = underTest.getInstalledTilesComponents(1) + runCurrent() + + assertThat(flowForUser1TheSecondTime).isEqualTo(flowForUser1) + } + + @Test + fun loadComponentsForDifferentUsers_returnsDifferentFlow() = + testScope.runTest { + val flowForUser1 = underTest.getInstalledTilesComponents(1) + val flowForUser2 = underTest.getInstalledTilesComponents(2) + runCurrent() + + assertThat(flowForUser2).isNotEqualTo(flowForUser1) + } + companion object { private val INTENT = Intent(TileService.ACTION_QS_TILE) private val FLAGS = diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/data/repository/TileSpecSettingsRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/data/repository/TileSpecSettingsRepositoryTest.kt index f7c3b213730c..3418977c3211 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/data/repository/TileSpecSettingsRepositoryTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/data/repository/TileSpecSettingsRepositoryTest.kt @@ -16,6 +16,7 @@ package com.android.systemui.qs.pipeline.data.repository +import android.platform.test.annotations.EnabledOnRavenwood import android.provider.Settings import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest @@ -39,6 +40,7 @@ import org.mockito.Mock import org.mockito.MockitoAnnotations @SmallTest +@EnabledOnRavenwood @RunWith(AndroidJUnit4::class) @OptIn(ExperimentalCoroutinesApi::class) class TileSpecSettingsRepositoryTest : SysuiTestCase() { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/data/repository/TilesSettingConverterTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/data/repository/TilesSettingConverterTest.kt index 9516c2181ac0..9e99fc006acc 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/data/repository/TilesSettingConverterTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/data/repository/TilesSettingConverterTest.kt @@ -1,5 +1,6 @@ package com.android.systemui.qs.pipeline.data.repository +import android.platform.test.annotations.EnabledOnRavenwood import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase @@ -9,6 +10,7 @@ import org.junit.Test import org.junit.runner.RunWith @SmallTest +@EnabledOnRavenwood @RunWith(AndroidJUnit4::class) class TilesSettingConverterTest : SysuiTestCase() { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/data/repository/UserAutoAddRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/data/repository/UserAutoAddRepositoryTest.kt index 36e860e37ffa..1ca3c0637824 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/data/repository/UserAutoAddRepositoryTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/data/repository/UserAutoAddRepositoryTest.kt @@ -1,5 +1,6 @@ package com.android.systemui.qs.pipeline.data.repository +import android.platform.test.annotations.EnabledOnRavenwood import android.provider.Settings import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest @@ -24,6 +25,7 @@ import org.mockito.MockitoAnnotations @OptIn(ExperimentalCoroutinesApi::class) @SmallTest +@EnabledOnRavenwood @RunWith(AndroidJUnit4::class) class UserAutoAddRepositoryTest : SysuiTestCase() { private val secureSettings = FakeSettings() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/data/repository/UserTileSpecRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/data/repository/UserTileSpecRepositoryTest.kt index d4a9fabd6806..58fc10917d44 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/data/repository/UserTileSpecRepositoryTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/data/repository/UserTileSpecRepositoryTest.kt @@ -1,5 +1,6 @@ package com.android.systemui.qs.pipeline.data.repository +import android.platform.test.annotations.EnabledOnRavenwood import android.provider.Settings import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest @@ -23,6 +24,7 @@ import org.mockito.Mock import org.mockito.MockitoAnnotations @SmallTest +@EnabledOnRavenwood @OptIn(ExperimentalCoroutinesApi::class) @RunWith(AndroidJUnit4::class) class UserTileSpecRepositoryTest : SysuiTestCase() { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/data/restoreprocessors/WorkTileRestoreProcessorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/data/restoreprocessors/WorkTileRestoreProcessorTest.kt index 30d1822b28da..57bb77f46e40 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/data/restoreprocessors/WorkTileRestoreProcessorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/data/restoreprocessors/WorkTileRestoreProcessorTest.kt @@ -17,6 +17,7 @@ package com.android.systemui.qs.pipeline.data.restoreprocessors import android.os.UserHandle +import android.platform.test.annotations.EnabledOnRavenwood import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase @@ -31,6 +32,7 @@ import org.junit.Test import org.junit.runner.RunWith @SmallTest +@EnabledOnRavenwood @RunWith(AndroidJUnit4::class) @OptIn(ExperimentalCoroutinesApi::class) class WorkTileRestoreProcessorTest : SysuiTestCase() { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/autoaddable/A11yShortcutAutoAddableListTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/autoaddable/A11yShortcutAutoAddableListTest.kt new file mode 100644 index 000000000000..311122d7f8d5 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/autoaddable/A11yShortcutAutoAddableListTest.kt @@ -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.systemui.qs.pipeline.domain.autoaddable + +import android.content.ComponentName +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags +import android.view.accessibility.Flags +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.internal.accessibility.AccessibilityShortcutController +import com.android.systemui.SysuiTestCase +import com.android.systemui.qs.pipeline.shared.TileSpec +import com.android.systemui.qs.tiles.ColorCorrectionTile +import com.android.systemui.qs.tiles.ColorInversionTile +import com.android.systemui.qs.tiles.OneHandedModeTile +import com.android.systemui.qs.tiles.ReduceBrightColorsTile +import com.android.systemui.util.mockito.mock +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +class A11yShortcutAutoAddableListTest : SysuiTestCase() { + + private val factory = + object : A11yShortcutAutoAddable.Factory { + override fun create( + spec: TileSpec, + componentName: ComponentName + ): A11yShortcutAutoAddable { + return A11yShortcutAutoAddable(mock(), mock(), spec, componentName) + } + } + + @Test + @DisableFlags(Flags.FLAG_A11Y_QS_SHORTCUT) + fun getA11yShortcutAutoAddables_withA11yQsShortcutFlagOff_emptyResult() { + val autoAddables = A11yShortcutAutoAddableList.getA11yShortcutAutoAddables(factory) + + assertThat(autoAddables).isEmpty() + } + + @Test + @EnableFlags(Flags.FLAG_A11Y_QS_SHORTCUT) + fun getA11yShortcutAutoAddables_withA11yQsShortcutFlagOn_correctAutoAddables() { + val expected = + setOf( + factory.create( + TileSpec.create(ColorCorrectionTile.TILE_SPEC), + AccessibilityShortcutController.DALTONIZER_COMPONENT_NAME + ), + factory.create( + TileSpec.create(ColorInversionTile.TILE_SPEC), + AccessibilityShortcutController.COLOR_INVERSION_COMPONENT_NAME + ), + factory.create( + TileSpec.create(OneHandedModeTile.TILE_SPEC), + AccessibilityShortcutController.ONE_HANDED_COMPONENT_NAME + ), + factory.create( + TileSpec.create(ReduceBrightColorsTile.TILE_SPEC), + AccessibilityShortcutController.REDUCE_BRIGHT_COLORS_COMPONENT_NAME + ), + ) + + val autoAddables = A11yShortcutAutoAddableList.getA11yShortcutAutoAddables(factory) + + assertThat(autoAddables).isNotEmpty() + assertThat(autoAddables).containsExactlyElementsIn(expected) + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/autoaddable/A11yShortcutAutoAddableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/autoaddable/A11yShortcutAutoAddableTest.kt new file mode 100644 index 000000000000..3b33a43d9341 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/autoaddable/A11yShortcutAutoAddableTest.kt @@ -0,0 +1,175 @@ +/* + * 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.qs.pipeline.domain.autoaddable + +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.accessibility.data.repository.FakeAccessibilityQsShortcutsRepository +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.coroutines.collectValues +import com.android.systemui.qs.pipeline.domain.model.AutoAddSignal +import com.android.systemui.qs.pipeline.domain.model.AutoAddTracking +import com.android.systemui.qs.pipeline.shared.TileSpec +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +class A11yShortcutAutoAddableTest : SysuiTestCase() { + private val testDispatcher = StandardTestDispatcher() + private val testScope = TestScope(testDispatcher) + + private val a11yQsShortcutsRepository = FakeAccessibilityQsShortcutsRepository() + private val underTest = + A11yShortcutAutoAddable(a11yQsShortcutsRepository, testDispatcher, SPEC, TARGET_COMPONENT) + + @Test + fun settingNotSet_noSignal() = + testScope.runTest { + val signal by collectLastValue(underTest.autoAddSignal(USER_ID)) + + assertThat(signal).isNull() // null means no emitted value + } + + @Test + fun settingSetWithTarget_addSignal() = + testScope.runTest { + val signal by collectLastValue(underTest.autoAddSignal(USER_ID)) + assertThat(signal).isNull() + + a11yQsShortcutsRepository.setA11yQsShortcutTargets( + USER_ID, + setOf(TARGET_COMPONENT_FLATTEN) + ) + + assertThat(signal).isEqualTo(AutoAddSignal.Add(SPEC)) + } + + @Test + fun settingSetWithoutTarget_removeSignal() = + testScope.runTest { + val signal by collectLastValue(flow = underTest.autoAddSignal(USER_ID)) + assertThat(signal).isNull() + + a11yQsShortcutsRepository.setA11yQsShortcutTargets( + USER_ID, + setOf(OTHER_COMPONENT_FLATTEN) + ) + + assertThat(signal).isEqualTo(AutoAddSignal.Remove(SPEC)) + } + + @Test + fun settingSetWithMultipleComponents_containsTarget_addSignal() = + testScope.runTest { + val signal by collectLastValue(underTest.autoAddSignal(USER_ID)) + assertThat(signal).isNull() + + a11yQsShortcutsRepository.setA11yQsShortcutTargets( + USER_ID, + setOf(OTHER_COMPONENT_FLATTEN, TARGET_COMPONENT_FLATTEN) + ) + + assertThat(signal).isEqualTo(AutoAddSignal.Add(SPEC)) + } + + @Test + fun settingSetWithMultipleComponents_doesNotContainTarget_removeSignal() = + testScope.runTest { + val signal by collectLastValue(underTest.autoAddSignal(USER_ID)) + assertThat(signal).isNull() + + a11yQsShortcutsRepository.setA11yQsShortcutTargets( + USER_ID, + setOf(OTHER_COMPONENT_FLATTEN, OTHER_COMPONENT_FLATTEN) + ) + + assertThat(signal).isEqualTo(AutoAddSignal.Remove(SPEC)) + } + + @Test + fun multipleChangesWithTarget_onlyOneAddSignal() = + testScope.runTest { + val signals by collectValues(underTest.autoAddSignal(USER_ID)) + assertThat(signals).isEmpty() + + repeat(3) { + a11yQsShortcutsRepository.setA11yQsShortcutTargets( + USER_ID, + setOf(TARGET_COMPONENT_FLATTEN) + ) + } + + assertThat(signals.size).isEqualTo(1) + assertThat(signals[0]).isEqualTo(AutoAddSignal.Add(SPEC)) + } + + @Test + fun multipleChangesWithoutTarget_onlyOneRemoveSignal() = + testScope.runTest { + val signals by collectValues(underTest.autoAddSignal(USER_ID)) + assertThat(signals).isEmpty() + + repeat(3) { + a11yQsShortcutsRepository.setA11yQsShortcutTargets( + USER_ID, + setOf("$OTHER_COMPONENT_FLATTEN$it") + ) + } + + assertThat(signals.size).isEqualTo(1) + assertThat(signals[0]).isEqualTo(AutoAddSignal.Remove(SPEC)) + } + + @Test + fun settingSetWithTargetForUsers_onlySignalInThatUser() = + testScope.runTest { + val otherUserId = USER_ID + 1 + val signalTargetUser by collectLastValue(underTest.autoAddSignal(USER_ID)) + val signalOtherUser by collectLastValue(underTest.autoAddSignal(otherUserId)) + assertThat(signalTargetUser).isNull() + assertThat(signalOtherUser).isNull() + + a11yQsShortcutsRepository.setA11yQsShortcutTargets( + USER_ID, + setOf(TARGET_COMPONENT_FLATTEN) + ) + + assertThat(signalTargetUser).isEqualTo(AutoAddSignal.Add(SPEC)) + assertThat(signalOtherUser).isNull() + } + + @Test + fun strategyAlways() { + assertThat(underTest.autoAddTracking).isEqualTo(AutoAddTracking.Always) + } + + companion object { + private val SPEC = TileSpec.create("spec") + private val TARGET_COMPONENT = ComponentName("FakePkgName", "FakeClassName") + private val TARGET_COMPONENT_FLATTEN = TARGET_COMPONENT.flattenToString() + private val OTHER_COMPONENT_FLATTEN = + ComponentName("FakePkgName", "OtherClassName").flattenToString() + private const val USER_ID = 0 + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/autoaddable/AutoAddableSettingListTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/autoaddable/AutoAddableSettingListTest.kt index 4454a3cb15fc..f185ed57de74 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/autoaddable/AutoAddableSettingListTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/autoaddable/AutoAddableSettingListTest.kt @@ -17,6 +17,7 @@ package com.android.systemui.qs.pipeline.domain.autoaddable import android.content.ComponentName +import android.platform.test.annotations.EnabledOnRavenwood import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.res.R @@ -28,6 +29,7 @@ import org.junit.Test import org.junit.runner.RunWith @SmallTest +@EnabledOnRavenwood @RunWith(AndroidJUnit4::class) class AutoAddableSettingListTest : SysuiTestCase() { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/autoaddable/AutoAddableSettingTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/autoaddable/AutoAddableSettingTest.kt index d153e9d1d361..cfb84a7a6709 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/autoaddable/AutoAddableSettingTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/autoaddable/AutoAddableSettingTest.kt @@ -16,6 +16,7 @@ package com.android.systemui.qs.pipeline.domain.autoaddable +import android.platform.test.annotations.EnabledOnRavenwood import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase @@ -35,6 +36,7 @@ import org.junit.runner.RunWith @OptIn(ExperimentalCoroutinesApi::class) @SmallTest +@EnabledOnRavenwood @RunWith(AndroidJUnit4::class) class AutoAddableSettingTest : SysuiTestCase() { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/autoaddable/CallbackControllerAutoAddableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/autoaddable/CallbackControllerAutoAddableTest.kt index ec139e4c515e..ea8d87358172 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/autoaddable/CallbackControllerAutoAddableTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/autoaddable/CallbackControllerAutoAddableTest.kt @@ -16,6 +16,7 @@ package com.android.systemui.qs.pipeline.domain.autoaddable +import android.platform.test.annotations.EnabledOnRavenwood import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase @@ -35,6 +36,7 @@ import org.junit.runner.RunWith @OptIn(ExperimentalCoroutinesApi::class) @SmallTest +@EnabledOnRavenwood @RunWith(AndroidJUnit4::class) class CallbackControllerAutoAddableTest : SysuiTestCase() { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/autoaddable/CastAutoAddableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/autoaddable/CastAutoAddableTest.kt index 4fae532d4174..9bb591e1ae7c 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/autoaddable/CastAutoAddableTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/autoaddable/CastAutoAddableTest.kt @@ -16,6 +16,7 @@ package com.android.systemui.qs.pipeline.domain.autoaddable +import android.platform.test.annotations.EnabledOnRavenwood import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase @@ -41,6 +42,7 @@ import org.mockito.MockitoAnnotations @OptIn(ExperimentalCoroutinesApi::class) @SmallTest +@EnabledOnRavenwood @RunWith(AndroidJUnit4::class) class CastAutoAddableTest : SysuiTestCase() { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/autoaddable/DataSaverAutoAddableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/autoaddable/DataSaverAutoAddableTest.kt index 9e2d1f885e2d..b925b27bfcc3 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/autoaddable/DataSaverAutoAddableTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/autoaddable/DataSaverAutoAddableTest.kt @@ -16,6 +16,7 @@ package com.android.systemui.qs.pipeline.domain.autoaddable +import android.platform.test.annotations.EnabledOnRavenwood import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase @@ -40,6 +41,7 @@ import org.mockito.MockitoAnnotations @OptIn(ExperimentalCoroutinesApi::class) @SmallTest +@EnabledOnRavenwood @RunWith(AndroidJUnit4::class) class DataSaverAutoAddableTest : SysuiTestCase() { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/autoaddable/DeviceControlsAutoAddableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/autoaddable/DeviceControlsAutoAddableTest.kt index 0116bd9575d8..188c6a949104 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/autoaddable/DeviceControlsAutoAddableTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/autoaddable/DeviceControlsAutoAddableTest.kt @@ -16,6 +16,7 @@ package com.android.systemui.qs.pipeline.domain.autoaddable +import android.platform.test.annotations.EnabledOnRavenwood import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase @@ -43,6 +44,7 @@ import org.mockito.MockitoAnnotations @OptIn(ExperimentalCoroutinesApi::class) @SmallTest +@EnabledOnRavenwood @RunWith(AndroidJUnit4::class) class DeviceControlsAutoAddableTest : SysuiTestCase() { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/autoaddable/HotspotAutoAddableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/autoaddable/HotspotAutoAddableTest.kt index e7ea9a66450c..02699ddad089 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/autoaddable/HotspotAutoAddableTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/autoaddable/HotspotAutoAddableTest.kt @@ -16,6 +16,7 @@ package com.android.systemui.qs.pipeline.domain.autoaddable +import android.platform.test.annotations.EnabledOnRavenwood import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase @@ -40,6 +41,7 @@ import org.mockito.MockitoAnnotations @OptIn(ExperimentalCoroutinesApi::class) @SmallTest +@EnabledOnRavenwood @RunWith(AndroidJUnit4::class) class HotspotAutoAddableTest : SysuiTestCase() { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/autoaddable/ReduceBrightColorsAutoAddableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/autoaddable/ReduceBrightColorsAutoAddableTest.kt index 19ac63c36cab..6d6fd754341d 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/autoaddable/ReduceBrightColorsAutoAddableTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/autoaddable/ReduceBrightColorsAutoAddableTest.kt @@ -16,6 +16,7 @@ package com.android.systemui.qs.pipeline.domain.autoaddable +import android.platform.test.annotations.EnabledOnRavenwood import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase @@ -43,6 +44,7 @@ import org.mockito.MockitoAnnotations @OptIn(ExperimentalCoroutinesApi::class) @SmallTest +@EnabledOnRavenwood @RunWith(AndroidJUnit4::class) class ReduceBrightColorsAutoAddableTest : SysuiTestCase() { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/autoaddable/SafetyCenterAutoAddableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/autoaddable/SafetyCenterAutoAddableTest.kt index d645ee34619b..633e494b3195 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/autoaddable/SafetyCenterAutoAddableTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/autoaddable/SafetyCenterAutoAddableTest.kt @@ -18,6 +18,7 @@ package com.android.systemui.qs.pipeline.domain.autoaddable import android.content.ComponentName import android.content.pm.PackageManager +import android.platform.test.annotations.EnabledOnRavenwood import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.res.R @@ -51,6 +52,7 @@ import org.mockito.MockitoAnnotations @OptIn(ExperimentalCoroutinesApi::class) @SmallTest +@EnabledOnRavenwood @RunWith(AndroidJUnit4::class) class SafetyCenterAutoAddableTest : SysuiTestCase() { private val testDispatcher = StandardTestDispatcher() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/autoaddable/WalletAutoAddableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/autoaddable/WalletAutoAddableTest.kt index 83ff35d8022d..c5c76eb77152 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/autoaddable/WalletAutoAddableTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/autoaddable/WalletAutoAddableTest.kt @@ -16,6 +16,7 @@ package com.android.systemui.qs.pipeline.domain.autoaddable +import android.platform.test.annotations.EnabledOnRavenwood import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase @@ -37,6 +38,7 @@ import org.mockito.MockitoAnnotations @OptIn(ExperimentalCoroutinesApi::class) @SmallTest +@EnabledOnRavenwood @RunWith(AndroidJUnit4::class) class WalletAutoAddableTest : SysuiTestCase() { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/AutoAddInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/AutoAddInteractorTest.kt index 54b03a90229b..2ea12ef4c88f 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/AutoAddInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/AutoAddInteractorTest.kt @@ -16,6 +16,7 @@ package com.android.systemui.qs.pipeline.domain.interactor +import android.platform.test.annotations.EnabledOnRavenwood import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase @@ -46,6 +47,7 @@ import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations @SmallTest +@EnabledOnRavenwood @RunWith(AndroidJUnit4::class) @OptIn(ExperimentalCoroutinesApi::class) class AutoAddInteractorTest : SysuiTestCase() { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/PanelInteractorImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/PanelInteractorImplTest.kt index 0d9711588a1f..d38c19b5bc4a 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/PanelInteractorImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/PanelInteractorImplTest.kt @@ -15,6 +15,7 @@ */ package com.android.systemui.qs.pipeline.domain.interactor +import android.platform.test.annotations.EnabledOnRavenwood import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase @@ -28,6 +29,7 @@ import org.mockito.MockitoAnnotations @RunWith(AndroidJUnit4::class) @SmallTest +@EnabledOnRavenwood class PanelInteractorImplTest : SysuiTestCase() { @Mock private lateinit var shadeController: ShadeController diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/RestoreReconciliationInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/RestoreReconciliationInteractorTest.kt index b2a9783d2e60..0b3144ab3084 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/RestoreReconciliationInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/RestoreReconciliationInteractorTest.kt @@ -1,5 +1,6 @@ package com.android.systemui.qs.pipeline.domain.interactor +import android.platform.test.annotations.EnabledOnRavenwood import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase @@ -26,6 +27,7 @@ import org.mockito.Mockito.inOrder @RunWith(AndroidJUnit4::class) @SmallTest +@EnabledOnRavenwood class RestoreReconciliationInteractorTest : SysuiTestCase() { private val tileSpecRepository = FakeTileSpecRepository() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/shared/TileSpecTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/shared/TileSpecTest.kt index 558e7694b54c..869ab6c24fce 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/shared/TileSpecTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/shared/TileSpecTest.kt @@ -17,6 +17,7 @@ package com.android.systemui.qs.pipeline.shared import android.content.ComponentName +import android.platform.test.annotations.EnabledOnRavenwood import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase @@ -25,6 +26,7 @@ import org.junit.Test import org.junit.runner.RunWith @SmallTest +@EnabledOnRavenwood @RunWith(AndroidJUnit4::class) class TileSpecTest : SysuiTestCase() { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/base/actions/QSTileIntentUserInputHandlerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/base/actions/QSTileIntentUserInputHandlerTest.kt index c1049773cabf..bf48784407b8 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/base/actions/QSTileIntentUserInputHandlerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/base/actions/QSTileIntentUserInputHandlerTest.kt @@ -24,6 +24,7 @@ import android.content.pm.PackageManager import android.content.pm.PackageManager.ResolveInfoFlags import android.content.pm.ResolveInfo import android.os.UserHandle +import android.platform.test.annotations.EnabledOnRavenwood import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase @@ -44,6 +45,7 @@ import org.mockito.Mockito.`when` import org.mockito.MockitoAnnotations @SmallTest +@EnabledOnRavenwood @RunWith(AndroidJUnit4::class) class QSTileIntentUserInputHandlerTest : SysuiTestCase() { @Mock private lateinit var packageManager: PackageManager diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/base/analytics/QSTileAnalyticsTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/base/analytics/QSTileAnalyticsTest.kt index 9861606fd1b1..fd09e3ca4bb8 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/base/analytics/QSTileAnalyticsTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/base/analytics/QSTileAnalyticsTest.kt @@ -16,6 +16,7 @@ package com.android.systemui.qs.tiles.base.analytics +import android.platform.test.annotations.EnabledOnRavenwood import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.internal.logging.InstanceId @@ -33,6 +34,7 @@ import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations @SmallTest +@EnabledOnRavenwood @RunWith(AndroidJUnit4::class) class QSTileAnalyticsTest : SysuiTestCase() { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/base/interactor/DisabledByPolicyInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/base/interactor/DisabledByPolicyInteractorTest.kt index 2bdc154dd885..a1f885c64312 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/base/interactor/DisabledByPolicyInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/base/interactor/DisabledByPolicyInteractorTest.kt @@ -20,6 +20,7 @@ import android.content.ComponentName import android.content.Context import android.content.Intent import android.os.UserHandle +import android.platform.test.annotations.EnabledOnRavenwood import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.settingslib.RestrictedLockUtils @@ -45,6 +46,7 @@ import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations @SmallTest +@EnabledOnRavenwood @RunWith(AndroidJUnit4::class) class DisabledByPolicyInteractorTest : SysuiTestCase() { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/airplate/domain/interactor/AirplaneModeTileDataInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/airplate/domain/interactor/AirplaneModeTileDataInteractorTest.kt index 937744db500e..89b9b7f30297 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/airplate/domain/interactor/AirplaneModeTileDataInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/airplate/domain/interactor/AirplaneModeTileDataInteractorTest.kt @@ -17,6 +17,7 @@ package com.android.systemui.qs.tiles.impl.airplate.domain.interactor import android.os.UserHandle +import android.platform.test.annotations.EnabledOnRavenwood import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase @@ -36,6 +37,7 @@ import org.junit.runner.RunWith @OptIn(ExperimentalCoroutinesApi::class) @SmallTest +@EnabledOnRavenwood @RunWith(AndroidJUnit4::class) class AirplaneModeTileDataInteractorTest : SysuiTestCase() { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/airplate/domain/interactor/AirplaneModeTileUserActionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/airplate/domain/interactor/AirplaneModeTileUserActionInteractorTest.kt index 81bde8188f5e..8982d810ad8a 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/airplate/domain/interactor/AirplaneModeTileUserActionInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/airplate/domain/interactor/AirplaneModeTileUserActionInteractorTest.kt @@ -16,6 +16,7 @@ package com.android.systemui.qs.tiles.impl.airplate.domain.interactor +import android.platform.test.annotations.EnabledOnRavenwood import android.provider.Settings import android.telephony.TelephonyManager import androidx.test.ext.junit.runners.AndroidJUnit4 @@ -37,6 +38,7 @@ import org.junit.Test import org.junit.runner.RunWith @SmallTest +@EnabledOnRavenwood @RunWith(AndroidJUnit4::class) class AirplaneModeTileUserActionInteractorTest : SysuiTestCase() { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/colorcorrection/domain/interactor/ColorCorrectionTileDataInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/colorcorrection/domain/interactor/ColorCorrectionTileDataInteractorTest.kt index 8c612acad887..abaf808f3f91 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/colorcorrection/domain/interactor/ColorCorrectionTileDataInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/colorcorrection/domain/interactor/ColorCorrectionTileDataInteractorTest.kt @@ -17,6 +17,7 @@ package com.android.systemui.qs.tiles.impl.colorcorrection.domain.interactor import android.os.UserHandle +import android.platform.test.annotations.EnabledOnRavenwood import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase @@ -35,6 +36,7 @@ import org.junit.runner.RunWith @OptIn(ExperimentalCoroutinesApi::class) @SmallTest +@EnabledOnRavenwood @RunWith(AndroidJUnit4::class) class ColorCorrectionTileDataInteractorTest : SysuiTestCase() { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/colorcorrection/domain/interactor/ColorCorrectionTileUserActionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/colorcorrection/domain/interactor/ColorCorrectionTileUserActionInteractorTest.kt index 3049cc079a1c..3bc53b273e89 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/colorcorrection/domain/interactor/ColorCorrectionTileUserActionInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/colorcorrection/domain/interactor/ColorCorrectionTileUserActionInteractorTest.kt @@ -17,6 +17,7 @@ package com.android.systemui.qs.tiles.impl.colorcorrection.domain.interactor import android.os.UserHandle +import android.platform.test.annotations.EnabledOnRavenwood import android.provider.Settings import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest @@ -32,6 +33,7 @@ import org.junit.Test import org.junit.runner.RunWith @SmallTest +@EnabledOnRavenwood @RunWith(AndroidJUnit4::class) class ColorCorrectionTileUserActionInteractorTest : SysuiTestCase() { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/flashlight/domain/interactor/FlashlightTileDataInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/flashlight/domain/interactor/FlashlightTileDataInteractorTest.kt index 7f7490d9ecc3..a9e39354d131 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/flashlight/domain/interactor/FlashlightTileDataInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/flashlight/domain/interactor/FlashlightTileDataInteractorTest.kt @@ -17,6 +17,7 @@ package com.android.systemui.qs.tiles.impl.flashlight.domain.interactor import android.os.UserHandle +import android.platform.test.annotations.EnabledOnRavenwood import android.testing.LeakCheck import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest @@ -37,6 +38,7 @@ import org.junit.runner.RunWith @OptIn(ExperimentalCoroutinesApi::class) @SmallTest +@EnabledOnRavenwood @RunWith(AndroidJUnit4::class) class FlashlightTileDataInteractorTest : SysuiTestCase() { private lateinit var controller: FakeFlashlightController diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/inversion/domain/interactor/ColorInversionTileDataInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/inversion/domain/interactor/ColorInversionTileDataInteractorTest.kt index 24c7bfb2d324..75b07eedab95 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/inversion/domain/interactor/ColorInversionTileDataInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/inversion/domain/interactor/ColorInversionTileDataInteractorTest.kt @@ -17,6 +17,7 @@ package com.android.systemui.qs.tiles.impl.inversion.domain.interactor import android.os.UserHandle +import android.platform.test.annotations.EnabledOnRavenwood import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase @@ -35,6 +36,7 @@ import org.junit.runner.RunWith @OptIn(ExperimentalCoroutinesApi::class) @SmallTest +@EnabledOnRavenwood @RunWith(AndroidJUnit4::class) class ColorInversionTileDataInteractorTest : SysuiTestCase() { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/inversion/domain/interactor/ColorInversionUserActionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/inversion/domain/interactor/ColorInversionUserActionInteractorTest.kt index 99bae18d43d4..f574f793fbf2 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/inversion/domain/interactor/ColorInversionUserActionInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/inversion/domain/interactor/ColorInversionUserActionInteractorTest.kt @@ -17,6 +17,7 @@ package com.android.systemui.qs.tiles.impl.inversion.domain.interactor import android.os.UserHandle +import android.platform.test.annotations.EnabledOnRavenwood import android.provider.Settings import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest @@ -32,6 +33,7 @@ import org.junit.Test import org.junit.runner.RunWith @SmallTest +@EnabledOnRavenwood @RunWith(AndroidJUnit4::class) class ColorInversionUserActionInteractorTest : SysuiTestCase() { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/location/interactor/LocationTileDataInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/location/interactor/LocationTileDataInteractorTest.kt index 8fdc93be4ba2..9adf57ac458a 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/location/interactor/LocationTileDataInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/location/interactor/LocationTileDataInteractorTest.kt @@ -17,6 +17,7 @@ package com.android.systemui.qs.tiles.impl.location.interactor import android.os.UserHandle +import android.platform.test.annotations.EnabledOnRavenwood import android.testing.LeakCheck import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest @@ -38,6 +39,7 @@ import org.junit.runner.RunWith @OptIn(ExperimentalCoroutinesApi::class) @SmallTest +@EnabledOnRavenwood @RunWith(AndroidJUnit4::class) class LocationTileDataInteractorTest : SysuiTestCase() { private lateinit var controller: FakeLocationController diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/location/interactor/LocationTileUserActionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/location/interactor/LocationTileUserActionInteractorTest.kt index 0fb8ae697190..8b21cb4a97d5 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/location/interactor/LocationTileUserActionInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/location/interactor/LocationTileUserActionInteractorTest.kt @@ -16,6 +16,7 @@ package com.android.systemui.qs.tiles.impl.location.interactor +import android.platform.test.annotations.EnabledOnRavenwood import android.provider.Settings import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest @@ -42,6 +43,7 @@ import org.mockito.Mockito import org.mockito.MockitoAnnotations @SmallTest +@EnabledOnRavenwood @RunWith(AndroidJUnit4::class) class LocationTileUserActionInteractorTest : SysuiTestCase() { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/saver/domain/DataSaverDialogDelegateTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/saver/domain/DataSaverDialogDelegateTest.kt index 4b9625107745..f24723a2a9f3 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/saver/domain/DataSaverDialogDelegateTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/saver/domain/DataSaverDialogDelegateTest.kt @@ -17,6 +17,7 @@ package com.android.systemui.qs.tiles.impl.saver.domain import android.content.SharedPreferences +import android.platform.test.annotations.EnabledOnRavenwood import android.testing.LeakCheck import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest @@ -37,6 +38,7 @@ import org.mockito.Mockito.verify /** Test [DataSaverDialogDelegate]. */ @SmallTest +@EnabledOnRavenwood @RunWith(AndroidJUnit4::class) class DataSaverDialogDelegateTest : SysuiTestCase() { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/saver/domain/interactor/DataSaverTileDataInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/saver/domain/interactor/DataSaverTileDataInteractorTest.kt index 819bd03437f4..daee22d0c45c 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/saver/domain/interactor/DataSaverTileDataInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/saver/domain/interactor/DataSaverTileDataInteractorTest.kt @@ -17,6 +17,7 @@ package com.android.systemui.qs.tiles.impl.saver.domain.interactor import android.os.UserHandle +import android.platform.test.annotations.EnabledOnRavenwood import android.testing.LeakCheck import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest @@ -36,6 +37,7 @@ import org.junit.runner.RunWith @OptIn(ExperimentalCoroutinesApi::class) @SmallTest +@EnabledOnRavenwood @RunWith(AndroidJUnit4::class) class DataSaverTileDataInteractorTest : SysuiTestCase() { private val controller: FakeDataSaverController = FakeDataSaverController(LeakCheck()) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/uimodenight/domain/UiModeNightTileUserActionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/uimodenight/domain/UiModeNightTileUserActionInteractorTest.kt index 004ec6250e7e..eea6d161d4ff 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/uimodenight/domain/UiModeNightTileUserActionInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/uimodenight/domain/UiModeNightTileUserActionInteractorTest.kt @@ -17,6 +17,7 @@ package com.android.systemui.qs.tiles.impl.uimodenight.domain import android.app.UiModeManager +import android.platform.test.annotations.EnabledOnRavenwood import android.provider.Settings import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest @@ -39,6 +40,7 @@ import org.mockito.Mockito.never import org.mockito.Mockito.verify @SmallTest +@EnabledOnRavenwood @RunWith(AndroidJUnit4::class) class UiModeNightTileUserActionInteractorTest : SysuiTestCase() { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/viewmodel/QSTileConfigProviderTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/viewmodel/QSTileConfigProviderTest.kt index 5eca8caa7d15..40971a87480d 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/viewmodel/QSTileConfigProviderTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/viewmodel/QSTileConfigProviderTest.kt @@ -16,6 +16,7 @@ package com.android.systemui.qs.tiles.viewmodel +import android.platform.test.annotations.EnabledOnRavenwood import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase @@ -27,6 +28,7 @@ import org.junit.Test import org.junit.runner.RunWith @SmallTest +@EnabledOnRavenwood @RunWith(AndroidJUnit4::class) class QSTileConfigProviderTest : SysuiTestCase() { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelTest.kt index 3a0ebdbd6a17..a8bc8d6b36b6 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelTest.kt @@ -17,6 +17,7 @@ package com.android.systemui.qs.tiles.viewmodel import android.os.UserHandle +import android.platform.test.annotations.EnabledOnRavenwood import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.MediumTest import com.android.systemui.SysuiTestCase @@ -50,6 +51,7 @@ import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations @MediumTest +@EnabledOnRavenwood @RunWith(AndroidJUnit4::class) @OptIn(ExperimentalCoroutinesApi::class) class QSTileViewModelTest : SysuiTestCase() { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelUserInputTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelUserInputTest.kt index 22fb152aee44..18cdd71e25b6 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelUserInputTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelUserInputTest.kt @@ -16,6 +16,7 @@ package com.android.systemui.qs.tiles.viewmodel +import android.platform.test.annotations.EnabledOnRavenwood import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.MediumTest import com.android.settingslib.RestrictedLockUtils @@ -53,6 +54,7 @@ import org.mockito.MockitoAnnotations /** Tests all possible [QSTileUserAction]s. If you need */ @MediumTest +@EnabledOnRavenwood @RunWith(AndroidJUnit4::class) @OptIn(ExperimentalCoroutinesApi::class) class QSTileViewModelUserInputTest : SysuiTestCase() { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/adapter/QSSceneAdapterTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/adapter/QSSceneAdapterTest.kt index 18a7320bf29d..d1bc686385a1 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/adapter/QSSceneAdapterTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/adapter/QSSceneAdapterTest.kt @@ -16,6 +16,7 @@ package com.android.systemui.qs.ui.adapter +import android.platform.test.annotations.EnabledOnRavenwood import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase @@ -25,6 +26,7 @@ import org.junit.Test import org.junit.runner.RunWith @SmallTest +@EnabledOnRavenwood @RunWith(AndroidJUnit4::class) class QSSceneAdapterTest : SysuiTestCase() { @Test diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/SystemUIDialogTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/SystemUIDialogTest.java index 7274c0c65e69..452895745f95 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/SystemUIDialogTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/SystemUIDialogTest.java @@ -31,6 +31,7 @@ import android.content.res.Configuration; import android.platform.test.annotations.RequiresFlagsEnabled; import android.platform.test.flag.junit.CheckFlagsRule; import android.platform.test.flag.junit.DeviceFlagsValueProvider; +import android.platform.test.ravenwood.RavenwoodRule; import android.testing.TestableLooper.RunWithLooper; import androidx.test.ext.junit.runners.AndroidJUnit4; @@ -66,8 +67,11 @@ public class SystemUIDialogTest extends SysuiTestCase { @Mock private SystemUIDialog.Delegate mDelegate; + // TODO(b/292141694): build out Ravenwood support for DeviceFlagsValueProvider + // Ravenwood already has solid support for SetFlagsRule, but CheckFlagsRule will be added soon @Rule - public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); + public final CheckFlagsRule mCheckFlagsRule = RavenwoodRule.isOnRavenwood() ? null + : DeviceFlagsValueProvider.createCheckFlagsRule(); @Before public void setup() { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/util/kotlin/BooleanFlowOperatorsTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/util/kotlin/BooleanFlowOperatorsTest.kt new file mode 100644 index 000000000000..b4a0a37ec9ef --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/util/kotlin/BooleanFlowOperatorsTest.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.systemui.util.kotlin + +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.kosmos.testScope +import com.android.systemui.testKosmos +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 com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +class BooleanFlowOperatorsTest : SysuiTestCase() { + + val kosmos = testKosmos() + val testScope = kosmos.testScope + + @Test + fun and_allTrue_returnsTrue() = + testScope.runTest { + val result by collectLastValue(and(TRUE, TRUE)) + assertThat(result).isTrue() + } + + @Test + fun and_anyFalse_returnsFalse() = + testScope.runTest { + val result by collectLastValue(and(TRUE, FALSE, TRUE)) + assertThat(result).isFalse() + } + + @Test + fun and_allFalse_returnsFalse() = + testScope.runTest { + val result by collectLastValue(and(FALSE, FALSE, FALSE)) + assertThat(result).isFalse() + } + + @Test + fun or_allTrue_returnsTrue() = + testScope.runTest { + val result by collectLastValue(or(TRUE, TRUE)) + assertThat(result).isTrue() + } + + @Test + fun or_anyTrue_returnsTrue() = + testScope.runTest { + val result by collectLastValue(or(FALSE, TRUE, FALSE)) + assertThat(result).isTrue() + } + + @Test + fun or_allFalse_returnsFalse() = + testScope.runTest { + val result by collectLastValue(or(FALSE, FALSE, FALSE)) + assertThat(result).isFalse() + } + + @Test + fun not_true_returnsFalse() = + testScope.runTest { + val result by collectLastValue(not(TRUE)) + assertThat(result).isFalse() + } + + @Test + fun not_false_returnsFalse() = + testScope.runTest { + val result by collectLastValue(not(FALSE)) + assertThat(result).isTrue() + } + + private companion object { + val TRUE: Flow<Boolean> + get() = flowOf(true) + val FALSE: Flow<Boolean> + get() = flowOf(false) + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/bottombar/ui/viewmodel/BottomBarViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/bottombar/ui/viewmodel/BottomBarViewModelTest.kt new file mode 100644 index 000000000000..471c8d851879 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/bottombar/ui/viewmodel/BottomBarViewModelTest.kt @@ -0,0 +1,90 @@ +/* + * 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.bottombar.ui.viewmodel + +import android.content.Intent +import android.provider.Settings +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.kosmos.testScope +import com.android.systemui.plugins.activityStarter +import com.android.systemui.testKosmos +import com.android.systemui.util.mockito.capture +import com.android.systemui.util.mockito.eq +import com.android.systemui.volume.panel.volumePanelViewModel +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentCaptor +import org.mockito.Captor +import org.mockito.Mockito.verify +import org.mockito.junit.MockitoJUnit + +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@RunWith(AndroidJUnit4::class) +class BottomBarViewModelTest : SysuiTestCase() { + + @JvmField @Rule var mockitoRule = MockitoJUnit.rule() + + @Captor private lateinit var intentCaptor: ArgumentCaptor<Intent> + + private val kosmos = testKosmos() + + private lateinit var underTest: BottomBarViewModel + + private fun initUnderTest() { + underTest = with(kosmos) { BottomBarViewModel(activityStarter, volumePanelViewModel) } + } + + @Test + fun onDoneClicked_hidesPanel() { + with(kosmos) { + testScope.runTest { + initUnderTest() + underTest.onDoneClicked() + runCurrent() + + val volumePanelState by collectLastValue(volumePanelViewModel.volumePanelState) + assertThat(volumePanelState!!.isVisible).isFalse() + } + } + } + + @Test + fun onSettingsClicked_dismissesPanelAndNavigatesToSettings() { + with(kosmos) { + testScope.runTest { + initUnderTest() + underTest.onSettingsClicked() + + runCurrent() + + val volumePanelState by collectLastValue(volumePanelViewModel.volumePanelState) + assertThat(volumePanelState!!.isVisible).isFalse() + verify(activityStarter).startActivity(capture(intentCaptor), eq(true)) + assertThat(intentCaptor.value.action).isEqualTo(Settings.ACTION_SOUND_SETTINGS) + } + } + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/domain/interactor/ComponentsInteractorImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/domain/interactor/ComponentsInteractorImplTest.kt new file mode 100644 index 000000000000..6256eece65dd --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/domain/interactor/ComponentsInteractorImplTest.kt @@ -0,0 +1,122 @@ +/* + * 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.volume.panel.domain.interactor + +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.kosmos.Kosmos +import com.android.systemui.kosmos.testScope +import com.android.systemui.volume.panel.availableCriteria +import com.android.systemui.volume.panel.criteriaByKey +import com.android.systemui.volume.panel.defaultCriteria +import com.android.systemui.volume.panel.domain.model.ComponentModel +import com.android.systemui.volume.panel.enabledComponents +import com.android.systemui.volume.panel.shared.model.VolumePanelComponentKey +import com.android.systemui.volume.panel.unavailableCriteria +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +class ComponentsInteractorImplTest : SysuiTestCase() { + + private val kosmos = Kosmos() + + private lateinit var underTest: ComponentsInteractor + + private fun initUnderTest() { + underTest = + with(kosmos) { + ComponentsInteractorImpl( + enabledComponents, + defaultCriteria, + testScope.backgroundScope, + criteriaByKey, + ) + } + } + + @Test + fun componentsAvailability_checked() { + with(kosmos) { + testScope.runTest { + enabledComponents = + setOf( + BOTTOM_BAR, + COMPONENT_1, + COMPONENT_2, + ) + criteriaByKey = + mapOf( + BOTTOM_BAR to availableCriteria, + COMPONENT_1 to unavailableCriteria, + COMPONENT_2 to availableCriteria, + ) + initUnderTest() + + val components by collectLastValue(underTest.components) + + assertThat(components) + .containsExactly( + ComponentModel(BOTTOM_BAR, true), + ComponentModel(COMPONENT_1, false), + ComponentModel(COMPONENT_2, true), + ) + } + } + } + + @Test + fun noCriteria_fallbackToDefaultCriteria() { + with(kosmos) { + testScope.runTest { + enabledComponents = + setOf( + BOTTOM_BAR, + COMPONENT_1, + COMPONENT_2, + ) + criteriaByKey = + mapOf( + BOTTOM_BAR to availableCriteria, + COMPONENT_2 to availableCriteria, + ) + defaultCriteria = unavailableCriteria + initUnderTest() + + val components by collectLastValue(underTest.components) + + assertThat(components) + .containsExactly( + ComponentModel(BOTTOM_BAR, true), + ComponentModel(COMPONENT_1, false), + ComponentModel(COMPONENT_2, true), + ) + } + } + } + + private companion object { + const val BOTTOM_BAR: VolumePanelComponentKey = "test_bottom_bar" + const val COMPONENT_1: VolumePanelComponentKey = "test_component:1" + const val COMPONENT_2: VolumePanelComponentKey = "test_component:2" + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/ui/composable/ComponentsFactoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/ui/composable/ComponentsFactoryTest.kt new file mode 100644 index 000000000000..3dbf23e3dc58 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/ui/composable/ComponentsFactoryTest.kt @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.volume.panel.ui.composable + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.testKosmos +import com.android.systemui.volume.panel.componentByKey +import com.android.systemui.volume.panel.mockVolumePanelUiComponentProvider +import com.android.systemui.volume.panel.shared.model.VolumePanelComponentKey +import com.google.common.truth.Truth +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +class ComponentsFactoryTest : SysuiTestCase() { + + private val kosmos = testKosmos() + + private lateinit var underTest: ComponentsFactory + + private fun initUnderTest() { + underTest = ComponentsFactory(kosmos.componentByKey) + } + + @Test + fun existingComponent_created() { + kosmos.componentByKey = mapOf(TEST_COMPONENT to kosmos.mockVolumePanelUiComponentProvider) + initUnderTest() + + val component = underTest.createComponent(TEST_COMPONENT) + + Truth.assertThat(component).isNotNull() + } + + @Test(expected = IllegalArgumentException::class) + fun componentAbsence_throws() { + kosmos.componentByKey = emptyMap() + initUnderTest() + + underTest.createComponent(TEST_COMPONENT) + } + + private companion object { + const val TEST_COMPONENT: VolumePanelComponentKey = "test_component" + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/ui/viewmodel/DefaultComponentsLayoutManagerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/ui/viewmodel/DefaultComponentsLayoutManagerTest.kt index e5fb9426e8e2..35d96989e35d 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/ui/viewmodel/DefaultComponentsLayoutManagerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/ui/viewmodel/DefaultComponentsLayoutManagerTest.kt @@ -16,7 +16,78 @@ package com.android.systemui.volume.panel.ui.viewmodel -class DefaultComponentsLayoutManagerTest { +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.testKosmos +import com.android.systemui.volume.panel.mockVolumePanelUiComponent +import com.android.systemui.volume.panel.shared.model.VolumePanelComponentKey +import com.android.systemui.volume.panel.ui.layout.ComponentsLayoutManager +import com.android.systemui.volume.panel.ui.layout.DefaultComponentsLayoutManager +import com.google.common.truth.Truth +import org.junit.Test +import org.junit.runner.RunWith - // TODO(b/318080198) Write tests +@SmallTest +@RunWith(AndroidJUnit4::class) +class DefaultComponentsLayoutManagerTest : SysuiTestCase() { + + private val kosmos = testKosmos() + private val underTest: ComponentsLayoutManager = DefaultComponentsLayoutManager() + + @Test + fun bottomBar_isSet() { + val bottomBarComponentState = + ComponentState(BOTTOM_BAR, kosmos.mockVolumePanelUiComponent, false) + val layout = + underTest.layout( + VolumePanelState(0, false), + setOf( + bottomBarComponentState, + ComponentState(COMPONENT_1, kosmos.mockVolumePanelUiComponent, false), + ComponentState(COMPONENT_2, kosmos.mockVolumePanelUiComponent, false), + ) + ) + + Truth.assertThat(layout.bottomBarComponent).isEqualTo(bottomBarComponentState) + } + + @Test + fun componentsAreInOrder() { + val bottomBarComponentState = + ComponentState(BOTTOM_BAR, kosmos.mockVolumePanelUiComponent, false) + val component1State = ComponentState(COMPONENT_1, kosmos.mockVolumePanelUiComponent, false) + val component2State = ComponentState(COMPONENT_2, kosmos.mockVolumePanelUiComponent, false) + val layout = + underTest.layout( + VolumePanelState(0, false), + setOf( + bottomBarComponentState, + component1State, + component2State, + ) + ) + + Truth.assertThat(layout.contentComponents[0]).isEqualTo(component1State) + Truth.assertThat(layout.contentComponents[1]).isEqualTo(component2State) + } + + @Test(expected = IllegalStateException::class) + fun bottomBarAbsence_throwsException() { + val component1State = ComponentState(COMPONENT_1, kosmos.mockVolumePanelUiComponent, false) + val component2State = ComponentState(COMPONENT_2, kosmos.mockVolumePanelUiComponent, false) + underTest.layout( + VolumePanelState(0, false), + setOf( + component1State, + component2State, + ) + ) + } + + private companion object { + const val BOTTOM_BAR: VolumePanelComponentKey = "bottom_bar" + const val COMPONENT_1: VolumePanelComponentKey = "test_component:1" + const val COMPONENT_2: VolumePanelComponentKey = "test_component:2" + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/ui/viewmodel/VolumePanelViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/ui/viewmodel/VolumePanelViewModelTest.kt index 9795237a3e7e..c4c9cc608afa 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/ui/viewmodel/VolumePanelViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/ui/viewmodel/VolumePanelViewModelTest.kt @@ -16,7 +16,120 @@ package com.android.systemui.volume.panel.ui.viewmodel -class VolumePanelViewModelTest { +import android.content.res.Configuration +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.kosmos.testScope +import com.android.systemui.statusbar.policy.fakeConfigurationController +import com.android.systemui.testKosmos +import com.android.systemui.volume.panel.componentByKey +import com.android.systemui.volume.panel.componentsLayoutManager +import com.android.systemui.volume.panel.criteriaByKey +import com.android.systemui.volume.panel.dagger.factory.KosmosVolumePanelComponentFactory +import com.android.systemui.volume.panel.mockVolumePanelUiComponentProvider +import com.android.systemui.volume.panel.shared.model.VolumePanelComponentKey +import com.android.systemui.volume.panel.ui.layout.FakeComponentsLayoutManager +import com.android.systemui.volume.panel.unavailableCriteria +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith - // TODO(b/318080198) Write tests +@SmallTest +@RunWith(AndroidJUnit4::class) +@OptIn(ExperimentalCoroutinesApi::class) +class VolumePanelViewModelTest : SysuiTestCase() { + + private val kosmos = + testKosmos().apply { + componentsLayoutManager = FakeComponentsLayoutManager { it.key == BOTTOM_BAR } + } + + private val testableResources = context.orCreateTestableResources + + private lateinit var underTest: VolumePanelViewModel + + private fun initUnderTest() { + underTest = + VolumePanelViewModel( + testableResources.resources, + KosmosVolumePanelComponentFactory(kosmos), + kosmos.fakeConfigurationController, + ) + } + + @Test + fun dismissingPanel_changesVisibility() { + with(kosmos) { + testScope.runTest { + initUnderTest() + assertThat(underTest.volumePanelState.value.isVisible).isTrue() + + underTest.dismissPanel() + runCurrent() + + assertThat(underTest.volumePanelState.value.isVisible).isFalse() + } + } + } + + @Test + fun orientationChanges_panelOrientationChanges() { + with(kosmos) { + testScope.runTest { + initUnderTest() + val volumePanelState by collectLastValue(underTest.volumePanelState) + testableResources.overrideConfiguration( + Configuration().apply { orientation = Configuration.ORIENTATION_PORTRAIT } + ) + assertThat(volumePanelState!!.orientation) + .isEqualTo(Configuration.ORIENTATION_PORTRAIT) + + fakeConfigurationController.onConfigurationChanged( + Configuration().apply { orientation = Configuration.ORIENTATION_LANDSCAPE } + ) + runCurrent() + + assertThat(volumePanelState!!.orientation) + .isEqualTo(Configuration.ORIENTATION_LANDSCAPE) + } + } + } + + @Test + fun components_areReturned() { + with(kosmos) { + testScope.runTest { + componentByKey = + mapOf( + COMPONENT_1 to mockVolumePanelUiComponentProvider, + COMPONENT_2 to mockVolumePanelUiComponentProvider, + BOTTOM_BAR to mockVolumePanelUiComponentProvider, + ) + criteriaByKey = mapOf(COMPONENT_2 to unavailableCriteria) + initUnderTest() + + val componentsLayout by collectLastValue(underTest.componentsLayout) + runCurrent() + + assertThat(componentsLayout!!.contentComponents).hasSize(2) + assertThat(componentsLayout!!.contentComponents[0].key).isEqualTo(COMPONENT_1) + assertThat(componentsLayout!!.contentComponents[0].isVisible).isTrue() + assertThat(componentsLayout!!.contentComponents[1].key).isEqualTo(COMPONENT_2) + assertThat(componentsLayout!!.contentComponents[1].isVisible).isFalse() + assertThat(componentsLayout!!.bottomBarComponent.key).isEqualTo(BOTTOM_BAR) + assertThat(componentsLayout!!.bottomBarComponent.isVisible).isTrue() + } + } + } + + private companion object { + const val BOTTOM_BAR: VolumePanelComponentKey = "test_bottom_bar" + const val COMPONENT_1: VolumePanelComponentKey = "test_component:1" + const val COMPONENT_2: VolumePanelComponentKey = "test_component:2" + } } diff --git a/packages/SystemUI/res/values/colors.xml b/packages/SystemUI/res/values/colors.xml index 3839dd98cdd3..307a6192a570 100644 --- a/packages/SystemUI/res/values/colors.xml +++ b/packages/SystemUI/res/values/colors.xml @@ -29,6 +29,9 @@ <color name="status_bar_icons_hover_color_light">#38FFFFFF</color> <!-- 22% white --> <color name="status_bar_icons_hover_color_dark">#38000000</color> <!-- 22% black --> + <!-- The dark background color behind the shade --> + <color name="shade_scrim_background_dark">@*android:color/black</color> + <!-- The color of the background in the separated list of the Global Actions menu --> <color name="global_actions_separated_background">#F5F5F5</color> diff --git a/packages/SystemUI/res/values/ids.xml b/packages/SystemUI/res/values/ids.xml index 8ec5ccd7a080..2ab0813300e3 100644 --- a/packages/SystemUI/res/values/ids.xml +++ b/packages/SystemUI/res/values/ids.xml @@ -184,6 +184,7 @@ <item type="id" name="action_move_to_edge_and_hide"/> <item type="id" name="action_move_out_edge_and_show"/> <item type="id" name="action_remove_menu"/> + <item type="id" name="action_edit"/> <!-- rounded corner view id --> <item type="id" name="rounded_corner_top_left"/> diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index 2b43360f0689..8fa4a9e3932f 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -1077,8 +1077,6 @@ <!-- Indicator on keyguard to start the communal tutorial. [CHAR LIMIT=100] --> <string name="communal_tutorial_indicator_text">Swipe left to start the communal tutorial</string> - <!-- Description for the button that opens the widget editor on click. [CHAR LIMIT=50] --> - <string name="button_to_open_widget_editor">Open the widget editor</string> <!-- Text for CTA button that launches the hub mode widget editor on click. [CHAR LIMIT=50] --> <string name="cta_tile_button_to_open_widget_editor">Customize</string> <!-- Text for CTA button that dismisses the tile on click. [CHAR LIMIT=50] --> @@ -2560,6 +2558,8 @@ <string name="accessibility_floating_button_action_remove_menu">Remove</string> <!-- Action in accessibility menu to toggle on/off the accessibility feature. [CHAR LIMIT=30]--> <string name="accessibility_floating_button_action_double_tap_to_toggle">toggle</string> + <!-- Action in accessibility menu to open the shortcut edit menu" [CHAR LIMIT=30]--> + <string name="accessibility_floating_button_action_edit">Edit</string> <!-- Device Controls strings --> <!-- Device Controls, Quick Settings tile title [CHAR LIMIT=30] --> diff --git a/packages/SystemUI/shared/biometrics/src/com/android/systemui/biometrics/shared/model/FingerprintSensorType.kt b/packages/SystemUI/shared/biometrics/src/com/android/systemui/biometrics/shared/model/FingerprintSensorType.kt index a00cdfa05726..7c674c8ca9e8 100644 --- a/packages/SystemUI/shared/biometrics/src/com/android/systemui/biometrics/shared/model/FingerprintSensorType.kt +++ b/packages/SystemUI/shared/biometrics/src/com/android/systemui/biometrics/shared/model/FingerprintSensorType.kt @@ -47,3 +47,14 @@ fun Int.toSensorType(): FingerprintSensorType = FingerprintSensorProperties.TYPE_HOME_BUTTON -> FingerprintSensorType.HOME_BUTTON else -> throw IllegalArgumentException("Invalid SensorType value: $this") } + +/** Convert [this] to corresponding [Int] */ +fun FingerprintSensorType.toInt(): Int = + when (this) { + FingerprintSensorType.UNKNOWN -> FingerprintSensorProperties.TYPE_UNKNOWN + FingerprintSensorType.REAR -> FingerprintSensorProperties.TYPE_REAR + FingerprintSensorType.UDFPS_ULTRASONIC -> FingerprintSensorProperties.TYPE_UDFPS_ULTRASONIC + FingerprintSensorType.UDFPS_OPTICAL -> FingerprintSensorProperties.TYPE_UDFPS_OPTICAL + FingerprintSensorType.POWER_BUTTON -> FingerprintSensorProperties.TYPE_POWER_BUTTON + FingerprintSensorType.HOME_BUTTON -> FingerprintSensorProperties.TYPE_HOME_BUTTON + } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java index ecce22315c50..25d771308aea 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java @@ -19,9 +19,9 @@ package com.android.keyguard; import static android.app.StatusBarManager.SESSION_KEYGUARD; import static android.content.res.Configuration.ORIENTATION_LANDSCAPE; +import static com.android.keyguard.KeyguardSecurityContainer.BOUNCER_DISMISSIBLE_KEYGUARD; import static com.android.keyguard.KeyguardSecurityContainer.BOUNCER_DISMISS_BIOMETRIC; import static com.android.keyguard.KeyguardSecurityContainer.BOUNCER_DISMISS_EXTENDED_ACCESS; -import static com.android.keyguard.KeyguardSecurityContainer.BOUNCER_DISMISSIBLE_KEYGUARD; import static com.android.keyguard.KeyguardSecurityContainer.BOUNCER_DISMISS_NONE_SECURITY; import static com.android.keyguard.KeyguardSecurityContainer.BOUNCER_DISMISS_PASSWORD; import static com.android.keyguard.KeyguardSecurityContainer.BOUNCER_DISMISS_SIM; @@ -84,6 +84,7 @@ import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFaceAuthInt import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor; 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.KeyguardTransitionInteractor; import com.android.systemui.log.SessionTracker; import com.android.systemui.plugins.ActivityStarter; @@ -100,8 +101,6 @@ import com.android.systemui.util.ViewController; import com.android.systemui.util.kotlin.JavaAdapter; import com.android.systemui.util.settings.GlobalSettings; -import dagger.Lazy; - import java.io.File; import java.util.Arrays; import java.util.Optional; @@ -109,6 +108,7 @@ import java.util.Optional; import javax.inject.Inject; import javax.inject.Provider; +import dagger.Lazy; import kotlinx.coroutines.Job; /** Controller for {@link KeyguardSecurityContainer} */ @@ -330,7 +330,7 @@ public class KeyguardSecurityContainerController extends ViewController<Keyguard } } - if (mFeatureFlags.isEnabled(Flags.KEYGUARD_WM_STATE_REFACTOR)) { + if (KeyguardWmStateRefactor.isEnabled()) { mKeyguardTransitionInteractor.startDismissKeyguardTransition(); } } diff --git a/packages/SystemUI/src/com/android/keyguard/LockIconViewController.java b/packages/SystemUI/src/com/android/keyguard/LockIconViewController.java index d5dc85cd8715..8e9815085e31 100644 --- a/packages/SystemUI/src/com/android/keyguard/LockIconViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/LockIconViewController.java @@ -63,6 +63,7 @@ import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor; +import com.android.systemui.deviceentry.shared.DeviceEntryUdfpsRefactor; import com.android.systemui.dump.DumpManager; import com.android.systemui.flags.FeatureFlags; import com.android.systemui.flags.Flags; @@ -87,6 +88,8 @@ import java.util.function.Consumer; import javax.inject.Inject; +import kotlinx.coroutines.ExperimentalCoroutinesApi; + /** * Controls when to show the LockIcon affordance (lock/unlocked icon or circle) on lock screen. * @@ -717,6 +720,7 @@ public class LockIconViewController implements Dumpable { return mDownDetected; } + @ExperimentalCoroutinesApi @VisibleForTesting protected void onLongPress() { cancelTouches(); @@ -727,7 +731,8 @@ public class LockIconViewController implements Dumpable { // pre-emptively set to true to hide view mIsBouncerShowing = true; - if (mUdfpsSupported && mShowUnlockIcon && mAuthRippleController != null) { + if (!DeviceEntryUdfpsRefactor.isEnabled() + && mUdfpsSupported && mShowUnlockIcon && mAuthRippleController != null) { mAuthRippleController.showUnlockRipple(FINGERPRINT); } updateVisibility(); diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/AccessibilityModule.kt b/packages/SystemUI/src/com/android/systemui/accessibility/AccessibilityModule.kt index 8c2d221e3f97..35f9344ae897 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/AccessibilityModule.kt +++ b/packages/SystemUI/src/com/android/systemui/accessibility/AccessibilityModule.kt @@ -16,6 +16,8 @@ package com.android.systemui.accessibility +import com.android.systemui.accessibility.data.repository.AccessibilityQsShortcutsRepository +import com.android.systemui.accessibility.data.repository.AccessibilityQsShortcutsRepositoryImpl import com.android.systemui.accessibility.data.repository.ColorCorrectionRepository import com.android.systemui.accessibility.data.repository.ColorCorrectionRepositoryImpl import com.android.systemui.accessibility.data.repository.ColorInversionRepository @@ -31,4 +33,9 @@ interface AccessibilityModule { @Binds fun colorInversionRepository(impl: ColorInversionRepositoryImpl): ColorInversionRepository + + @Binds + fun accessibilityQsShortcutsRepository( + impl: AccessibilityQsShortcutsRepositoryImpl + ): AccessibilityQsShortcutsRepository } diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/data/repository/AccessibilityQsShortcutsRepository.kt b/packages/SystemUI/src/com/android/systemui/accessibility/data/repository/AccessibilityQsShortcutsRepository.kt new file mode 100644 index 000000000000..401ac0f9337b --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/accessibility/data/repository/AccessibilityQsShortcutsRepository.kt @@ -0,0 +1,54 @@ +/* + * 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.accessibility.data.repository + +import android.util.SparseArray +import androidx.annotation.GuardedBy +import com.android.systemui.dagger.SysUISingleton +import javax.inject.Inject +import kotlinx.coroutines.flow.SharedFlow + +/** Provides data related to accessibility quick setting shortcut option. */ +interface AccessibilityQsShortcutsRepository { + /** + * Observable for the a11y features the user chooses in the Settings app to use the quick + * setting option. + */ + fun a11yQsShortcutTargets(userId: Int): SharedFlow<Set<String>> +} + +@SysUISingleton +class AccessibilityQsShortcutsRepositoryImpl +@Inject +constructor( + private val userA11yQsShortcutsRepositoryFactory: UserA11yQsShortcutsRepository.Factory, +) : AccessibilityQsShortcutsRepository { + + @GuardedBy("userA11yQsShortcutsRepositories") + private val userA11yQsShortcutsRepositories = SparseArray<UserA11yQsShortcutsRepository>() + + override fun a11yQsShortcutTargets(userId: Int): SharedFlow<Set<String>> { + return synchronized(userA11yQsShortcutsRepositories) { + if (userId !in userA11yQsShortcutsRepositories) { + val userA11yQsShortcutsRepository = + userA11yQsShortcutsRepositoryFactory.create(userId) + userA11yQsShortcutsRepositories.put(userId, userA11yQsShortcutsRepository) + } + userA11yQsShortcutsRepositories.get(userId).targets + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/data/repository/UserA11yQsShortcutsRepository.kt b/packages/SystemUI/src/com/android/systemui/accessibility/data/repository/UserA11yQsShortcutsRepository.kt new file mode 100644 index 000000000000..ed91f03cc56e --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/accessibility/data/repository/UserA11yQsShortcutsRepository.kt @@ -0,0 +1,78 @@ +/* + * 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.accessibility.data.repository + +import android.provider.Settings +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.qs.pipeline.data.repository.TileSpecRepository +import com.android.systemui.util.settings.SecureSettings +import com.android.systemui.util.settings.SettingsProxyExt.observerFlow +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.flow.shareIn + +/** + * Single user version of [AccessibilityQsShortcutsRepository]. It provides a similar interface as + * [TileSpecRepository], but focusing solely on the user it was created for. It observes the changes + * on the [Settings.Secure.ACCESSIBILITY_QS_TARGETS] for a given user + */ +class UserA11yQsShortcutsRepository +@AssistedInject +constructor( + @Assisted private val userId: Int, + private val secureSettings: SecureSettings, + @Application private val applicationScope: CoroutineScope, + @Background private val backgroundDispatcher: CoroutineDispatcher, +) { + val targets = + secureSettings + .observerFlow(userId, SETTING_NAME) + // Force an update + .onStart { emit(Unit) } + .map { getA11yQsShortcutTargets(userId) } + .flowOn(backgroundDispatcher) + .shareIn( + scope = applicationScope, + started = SharingStarted.WhileSubscribed(), + replay = 1 + ) + + private fun getA11yQsShortcutTargets(userId: Int): Set<String> { + val settingValue = secureSettings.getStringForUser(SETTING_NAME, userId) ?: "" + return settingValue.split(SETTING_SEPARATOR).filterNot { it.isEmpty() }.toSet() + } + + companion object { + const val SETTING_NAME = Settings.Secure.ACCESSIBILITY_QS_TARGETS + const val SETTING_SEPARATOR = ":" + } + + @AssistedFactory + interface Factory { + fun create( + userId: Int, + ): UserA11yQsShortcutsRepository + } +} diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/DragToInteractAnimationController.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/DragToInteractAnimationController.java index 568b24dbd4f3..7fd72ec8ce93 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/DragToInteractAnimationController.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/DragToInteractAnimationController.java @@ -16,127 +16,138 @@ package com.android.systemui.accessibility.floatingmenu; +import static android.R.id.empty; + import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.ValueAnimator; +import android.util.ArrayMap; +import android.util.Pair; import android.view.MotionEvent; import androidx.annotation.NonNull; import androidx.dynamicanimation.animation.DynamicAnimation; import com.android.internal.annotations.VisibleForTesting; +import com.android.systemui.Flags; +import com.android.wm.shell.common.bubbles.DismissCircleView; import com.android.wm.shell.common.bubbles.DismissView; import com.android.wm.shell.common.magnetictarget.MagnetizedObject; +import java.util.Map; +import java.util.Objects; + /** * Controls the interaction between {@link MagnetizedObject} and * {@link MagnetizedObject.MagneticTarget}. */ class DragToInteractAnimationController { - private static final boolean ENABLE_FLING_TO_DISMISS_MENU = false; private static final float COMPLETELY_OPAQUE = 1.0f; private static final float COMPLETELY_TRANSPARENT = 0.0f; private static final float CIRCLE_VIEW_DEFAULT_SCALE = 1.0f; private static final float ANIMATING_MAX_ALPHA = 0.7f; + private final DragToInteractView mInteractView; private final DismissView mDismissView; private final MenuView mMenuView; - private final ValueAnimator mDismissAnimator; - private final MagnetizedObject<?> mMagnetizedObject; - private float mMinDismissSize; + + /** + * MagnetizedObject cannot differentiate between its MagnetizedTargets, + * so we need an object & an animator for every interactable. + */ + private final ArrayMap<Integer, Pair<MagnetizedObject<MenuView>, ValueAnimator>> mInteractMap; + + private float mMinInteractSize; private float mSizePercent; - DragToInteractAnimationController(DismissView dismissView, MenuView menuView) { - mDismissView = dismissView; - mDismissView.setPivotX(dismissView.getWidth() / 2.0f); - mDismissView.setPivotY(dismissView.getHeight() / 2.0f); + DragToInteractAnimationController(DragToInteractView interactView, MenuView menuView) { + mDismissView = null; + mInteractView = interactView; + mInteractView.setPivotX(interactView.getWidth() / 2.0f); + mInteractView.setPivotY(interactView.getHeight() / 2.0f); mMenuView = menuView; updateResources(); - mDismissAnimator = ValueAnimator.ofFloat(COMPLETELY_OPAQUE, COMPLETELY_TRANSPARENT); - mDismissAnimator.addUpdateListener(dismissAnimation -> { - final float animatedValue = (float) dismissAnimation.getAnimatedValue(); - final float scaleValue = Math.max(animatedValue, mSizePercent); - dismissView.getCircle().setScaleX(scaleValue); - dismissView.getCircle().setScaleY(scaleValue); - - menuView.setAlpha(Math.max(animatedValue, ANIMATING_MAX_ALPHA)); + mInteractMap = new ArrayMap<>(); + interactView.getInteractMap().forEach((viewId, pair) -> { + DismissCircleView circleView = pair.getFirst(); + createMagnetizedObjectAndAnimator(circleView); }); + } - mDismissAnimator.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(@NonNull Animator animation, boolean isReverse) { - super.onAnimationEnd(animation, isReverse); + DragToInteractAnimationController(DismissView dismissView, MenuView menuView) { + mDismissView = dismissView; + mInteractView = null; + mDismissView.setPivotX(dismissView.getWidth() / 2.0f); + mDismissView.setPivotY(dismissView.getHeight() / 2.0f); + mMenuView = menuView; - if (isReverse) { - mDismissView.getCircle().setScaleX(CIRCLE_VIEW_DEFAULT_SCALE); - mDismissView.getCircle().setScaleY(CIRCLE_VIEW_DEFAULT_SCALE); - mMenuView.setAlpha(COMPLETELY_OPAQUE); - } - } - }); + updateResources(); - mMagnetizedObject = - new MagnetizedObject<MenuView>(mMenuView.getContext(), mMenuView, - new MenuAnimationController.MenuPositionProperty( - DynamicAnimation.TRANSLATION_X), - new MenuAnimationController.MenuPositionProperty( - DynamicAnimation.TRANSLATION_Y)) { - @Override - public void getLocationOnScreen(MenuView underlyingObject, int[] loc) { - underlyingObject.getLocationOnScreen(loc); - } - - @Override - public float getHeight(MenuView underlyingObject) { - return underlyingObject.getHeight(); - } - - @Override - public float getWidth(MenuView underlyingObject) { - return underlyingObject.getWidth(); - } - }; - - final MagnetizedObject.MagneticTarget magneticTarget = new MagnetizedObject.MagneticTarget( - dismissView.getCircle(), (int) mMinDismissSize); - mMagnetizedObject.addTarget(magneticTarget); - mMagnetizedObject.setFlingToTargetEnabled(ENABLE_FLING_TO_DISMISS_MENU); + mInteractMap = new ArrayMap<>(); + createMagnetizedObjectAndAnimator(dismissView.getCircle()); } - void showDismissView(boolean show) { - if (show) { - mDismissView.show(); - } else { - mDismissView.hide(); + void showInteractView(boolean show) { + if (Flags.floatingMenuDragToEdit() && mInteractView != null) { + if (show) { + mInteractView.show(); + } else { + mInteractView.hide(); + } + } else if (mDismissView != null) { + if (show) { + mDismissView.show(); + } else { + mDismissView.hide(); + } } } void setMagnetListener(MagnetizedObject.MagnetListener magnetListener) { - mMagnetizedObject.setMagnetListener(magnetListener); + mInteractMap.forEach((viewId, pair) -> { + MagnetizedObject<?> magnetizedObject = pair.first; + magnetizedObject.setMagnetListener(magnetListener); + }); } @VisibleForTesting - MagnetizedObject.MagnetListener getMagnetListener() { - return mMagnetizedObject.getMagnetListener(); + MagnetizedObject.MagnetListener getMagnetListener(int id) { + return Objects.requireNonNull(mInteractMap.get(id)).first.getMagnetListener(); } void maybeConsumeDownMotionEvent(MotionEvent event) { - mMagnetizedObject.maybeConsumeMotionEvent(event); + mInteractMap.forEach((viewId, pair) -> { + MagnetizedObject<?> magnetizedObject = pair.first; + magnetizedObject.maybeConsumeMotionEvent(event); + }); + } + + private int maybeConsumeMotionEvent(MotionEvent event) { + for (Map.Entry<Integer, Pair<MagnetizedObject<MenuView>, ValueAnimator>> set: + mInteractMap.entrySet()) { + MagnetizedObject<MenuView> magnetizedObject = set.getValue().first; + if (magnetizedObject.maybeConsumeMotionEvent(event)) { + return set.getKey(); + } + } + return empty; } /** - * This used to pass {@link MotionEvent#ACTION_DOWN} to the magnetized object to check if it was - * within the magnetic field. It should be used in the {@link MenuListViewTouchHandler}. + * This used to pass {@link MotionEvent#ACTION_DOWN} to the magnetized objects + * to check if it was within a magnetic field. + * It should be used in the {@link MenuListViewTouchHandler}. * * @param event that move the magnetized object which is also the menu list view. - * @return true if the location of the motion events moves within the magnetic field of a - * target, but false if didn't set + * @return id of a target if the location of the motion events moves + * within the field of the target, otherwise it returns{@link android.R.id#empty}. + * <p> * {@link DragToInteractAnimationController#setMagnetListener(MagnetizedObject.MagnetListener)}. */ - boolean maybeConsumeMoveMotionEvent(MotionEvent event) { - return mMagnetizedObject.maybeConsumeMotionEvent(event); + int maybeConsumeMoveMotionEvent(MotionEvent event) { + return maybeConsumeMotionEvent(event); } /** @@ -144,31 +155,93 @@ class DragToInteractAnimationController { * within the magnetic field. It should be used in the {@link MenuListViewTouchHandler}. * * @param event that move the magnetized object which is also the menu list view. - * @return true if the location of the motion events moves within the magnetic field of a - * target, but false if didn't set + * @return id of a target if the location of the motion events moves + * within the field of the target, otherwise it returns{@link android.R.id#empty}. * {@link DragToInteractAnimationController#setMagnetListener(MagnetizedObject.MagnetListener)}. */ - boolean maybeConsumeUpMotionEvent(MotionEvent event) { - return mMagnetizedObject.maybeConsumeMotionEvent(event); + int maybeConsumeUpMotionEvent(MotionEvent event) { + return maybeConsumeMotionEvent(event); } - void animateDismissMenu(boolean scaleUp) { + void animateInteractMenu(int targetViewId, boolean scaleUp) { + Pair<MagnetizedObject<MenuView>, ValueAnimator> value = mInteractMap.get(targetViewId); + if (value == null) { + return; + } + ValueAnimator animator = value.second; if (scaleUp) { - mDismissAnimator.start(); + animator.start(); } else { - mDismissAnimator.reverse(); + animator.reverse(); } } void updateResources() { - final float maxDismissSize = mDismissView.getResources().getDimensionPixelSize( + final float maxInteractSize = mMenuView.getResources().getDimensionPixelSize( com.android.wm.shell.R.dimen.dismiss_circle_size); - mMinDismissSize = mDismissView.getResources().getDimensionPixelSize( + mMinInteractSize = mMenuView.getResources().getDimensionPixelSize( com.android.wm.shell.R.dimen.dismiss_circle_small); - mSizePercent = mMinDismissSize / maxDismissSize; + mSizePercent = mMinInteractSize / maxInteractSize; } - interface DismissCallback { - void onDismiss(); + /** + * Creates a magnetizedObject & valueAnimator pair for the provided circleView, + * and adds them to the interactMap. + * + * @param circleView circleView to create objects for. + */ + private void createMagnetizedObjectAndAnimator(DismissCircleView circleView) { + MagnetizedObject<MenuView> magnetizedObject = new MagnetizedObject<MenuView>( + mMenuView.getContext(), mMenuView, + new MenuAnimationController.MenuPositionProperty( + DynamicAnimation.TRANSLATION_X), + new MenuAnimationController.MenuPositionProperty( + DynamicAnimation.TRANSLATION_Y)) { + @Override + public void getLocationOnScreen(MenuView underlyingObject, @NonNull int[] loc) { + underlyingObject.getLocationOnScreen(loc); + } + + @Override + public float getHeight(MenuView underlyingObject) { + return underlyingObject.getHeight(); + } + + @Override + public float getWidth(MenuView underlyingObject) { + return underlyingObject.getWidth(); + } + }; + // Avoid unintended selection of an object / option + magnetizedObject.setFlingToTargetEnabled(false); + magnetizedObject.addTarget(new MagnetizedObject.MagneticTarget( + circleView, (int) mMinInteractSize)); + + final ValueAnimator animator = + ValueAnimator.ofFloat(COMPLETELY_OPAQUE, COMPLETELY_TRANSPARENT); + + animator.addUpdateListener(dismissAnimation -> { + final float animatedValue = (float) dismissAnimation.getAnimatedValue(); + final float scaleValue = Math.max(animatedValue, mSizePercent); + circleView.setScaleX(scaleValue); + circleView.setScaleY(scaleValue); + + mMenuView.setAlpha(Math.max(animatedValue, ANIMATING_MAX_ALPHA)); + }); + + animator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(@NonNull Animator animation, boolean isReverse) { + super.onAnimationEnd(animation, isReverse); + + if (isReverse) { + circleView.setScaleX(CIRCLE_VIEW_DEFAULT_SCALE); + circleView.setScaleY(CIRCLE_VIEW_DEFAULT_SCALE); + mMenuView.setAlpha(COMPLETELY_OPAQUE); + } + } + }); + + mInteractMap.put(circleView.getId(), new Pair<>(magnetizedObject, animator)); } } diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/DragToInteractView.kt b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/DragToInteractView.kt new file mode 100644 index 000000000000..0ef3d200d1fa --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/DragToInteractView.kt @@ -0,0 +1,322 @@ +/* + * 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.accessibility.floatingmenu + +import android.animation.ObjectAnimator +import android.content.Context +import android.graphics.Color +import android.graphics.drawable.GradientDrawable +import android.util.ArrayMap +import android.util.IntProperty +import android.util.Log +import android.view.Gravity +import android.view.View +import android.view.ViewGroup +import android.view.WindowInsets +import android.view.WindowManager +import android.widget.FrameLayout +import android.widget.LinearLayout +import android.widget.Space +import androidx.annotation.ColorRes +import androidx.annotation.DimenRes +import androidx.annotation.DrawableRes +import androidx.core.content.ContextCompat +import androidx.dynamicanimation.animation.DynamicAnimation +import androidx.dynamicanimation.animation.SpringForce.DAMPING_RATIO_LOW_BOUNCY +import androidx.dynamicanimation.animation.SpringForce.STIFFNESS_LOW +import com.android.wm.shell.R +import com.android.wm.shell.animation.PhysicsAnimator +import com.android.wm.shell.common.bubbles.DismissCircleView +import com.android.wm.shell.common.bubbles.DismissView + +/** + * View that handles interactions between DismissCircleView and BubbleStackView. + * + * @note [setup] method should be called after initialisation + */ +class DragToInteractView(context: Context) : FrameLayout(context) { + /** + * The configuration is used to provide module specific resource ids + * + * @see [setup] method + */ + data class Config( + /** dimen resource id of the dismiss target circle view size */ + @DimenRes val targetSizeResId: Int, + /** dimen resource id of the icon size in the dismiss target */ + @DimenRes val iconSizeResId: Int, + /** dimen resource id of the bottom margin for the dismiss target */ + @DimenRes var bottomMarginResId: Int, + /** dimen resource id of the height for dismiss area gradient */ + @DimenRes val floatingGradientHeightResId: Int, + /** color resource id of the dismiss area gradient color */ + @ColorRes val floatingGradientColorResId: Int, + /** drawable resource id of the dismiss target background */ + @DrawableRes val backgroundResId: Int, + /** drawable resource id of the icon for the dismiss target */ + @DrawableRes val iconResId: Int + ) + + companion object { + private const val SHOULD_SETUP = "The view isn't ready. Should be called after `setup`" + private val TAG = DragToInteractView::class.simpleName + } + + // START DragToInteractView modification + // We could technically access each DismissCircleView from their Animator, + // but the animators only store a weak reference to their targets. This is safer. + var interactMap = ArrayMap<Int, Pair<DismissCircleView, PhysicsAnimator<DismissCircleView>>>() + // END DragToInteractView modification + var isShowing = false + var config: Config? = null + + private val spring = PhysicsAnimator.SpringConfig(STIFFNESS_LOW, DAMPING_RATIO_LOW_BOUNCY) + private val INTERACT_SCRIM_FADE_MS = 200L + private var wm: WindowManager = + context.getSystemService(Context.WINDOW_SERVICE) as WindowManager + private var gradientDrawable: GradientDrawable? = null + + private val GRADIENT_ALPHA: IntProperty<GradientDrawable> = + object : IntProperty<GradientDrawable>("alpha") { + override fun setValue(d: GradientDrawable, percent: Int) { + d.alpha = percent + } + override fun get(d: GradientDrawable): Int { + return d.alpha + } + } + + init { + clipToPadding = false + clipChildren = false + visibility = View.INVISIBLE + + // START DragToInteractView modification + // Resources included within implementation as we aren't concerned with decoupling them. + setup( + Config( + targetSizeResId = R.dimen.dismiss_circle_size, + iconSizeResId = R.dimen.dismiss_target_x_size, + bottomMarginResId = R.dimen.floating_dismiss_bottom_margin, + floatingGradientHeightResId = R.dimen.floating_dismiss_gradient_height, + floatingGradientColorResId = android.R.color.system_neutral1_900, + backgroundResId = R.drawable.dismiss_circle_background, + iconResId = R.drawable.pip_ic_close_white + ) + ) + // END DragToInteractView modification + } + + /** + * Sets up view with the provided resource ids. + * + * Decouples resource dependency in order to be used externally (e.g. Launcher). Usually called + * with default params in module specific extension: + * + * @see [DismissView.setup] in DismissViewExt.kt + */ + fun setup(config: Config) { + this.config = config + + // Setup layout + layoutParams = + LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + resources.getDimensionPixelSize(config.floatingGradientHeightResId), + Gravity.BOTTOM + ) + updatePadding() + + // Setup gradient + gradientDrawable = createGradient(color = config.floatingGradientColorResId) + background = gradientDrawable + + // START DragToInteractView modification + + // Setup LinearLayout. Added to organize multiple circles. + val linearLayout = LinearLayout(context) + linearLayout.layoutParams = + LinearLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT + ) + linearLayout.weightSum = 0f + addView(linearLayout) + + // Setup DismissCircleView. Code block replaced with repeatable functions + addSpace(linearLayout) + addCircle( + config, + com.android.systemui.res.R.id.action_remove_menu, + R.drawable.pip_ic_close_white, + linearLayout + ) + addCircle( + config, + com.android.systemui.res.R.id.action_edit, + com.android.systemui.res.R.drawable.ic_screenshot_edit, + linearLayout + ) + // END DragToInteractView modification + } + + /** Animates this view in. */ + fun show() { + if (isShowing) return + val gradientDrawable = checkExists(gradientDrawable) ?: return + isShowing = true + visibility = View.VISIBLE + val alphaAnim = + ObjectAnimator.ofInt(gradientDrawable, GRADIENT_ALPHA, gradientDrawable.alpha, 255) + alphaAnim.duration = INTERACT_SCRIM_FADE_MS + alphaAnim.start() + + // START DragToInteractView modification + interactMap.forEach { + val animator = it.value.second + animator.cancel() + animator.spring(DynamicAnimation.TRANSLATION_Y, 0f, spring).start() + } + // END DragToInteractView modification + } + + /** + * Animates this view out, as well as the circle that encircles the bubbles, if they were + * dragged into the target and encircled. + */ + fun hide() { + if (!isShowing) return + val gradientDrawable = checkExists(gradientDrawable) ?: return + isShowing = false + val alphaAnim = + ObjectAnimator.ofInt(gradientDrawable, GRADIENT_ALPHA, gradientDrawable.alpha, 0) + alphaAnim.duration = INTERACT_SCRIM_FADE_MS + alphaAnim.start() + + // START DragToInteractView modification + interactMap.forEach { + val animator = it.value.second + animator + .spring(DynamicAnimation.TRANSLATION_Y, height.toFloat(), spring) + .withEndActions({ visibility = View.INVISIBLE }) + .start() + } + // END DragToInteractView modification + } + + /** Cancels the animator for the dismiss target. */ + fun cancelAnimators() { + // START DragToInteractView modification + interactMap.forEach { + val animator = it.value.second + animator.cancel() + } + // END DragToInteractView modification + } + + fun updateResources() { + val config = checkExists(config) ?: return + updatePadding() + layoutParams.height = resources.getDimensionPixelSize(config.floatingGradientHeightResId) + val targetSize = resources.getDimensionPixelSize(config.targetSizeResId) + + // START DragToInteractView modification + interactMap.forEach { + val circle = it.value.first + circle.layoutParams.width = targetSize + circle.layoutParams.height = targetSize + circle.requestLayout() + } + // END DragToInteractView modification + } + + private fun createGradient(@ColorRes color: Int): GradientDrawable { + val gradientColor = ContextCompat.getColor(context, color) + val alpha = 0.7f * 255 + val gradientColorWithAlpha = + Color.argb( + alpha.toInt(), + Color.red(gradientColor), + Color.green(gradientColor), + Color.blue(gradientColor) + ) + val gd = + GradientDrawable( + GradientDrawable.Orientation.BOTTOM_TOP, + intArrayOf(gradientColorWithAlpha, Color.TRANSPARENT) + ) + gd.setDither(true) + gd.alpha = 0 + return gd + } + + private fun updatePadding() { + val config = checkExists(config) ?: return + val insets: WindowInsets = wm.currentWindowMetrics.windowInsets + val navInset = insets.getInsetsIgnoringVisibility(WindowInsets.Type.navigationBars()) + setPadding( + 0, + 0, + 0, + navInset.bottom + resources.getDimensionPixelSize(config.bottomMarginResId) + ) + } + + /** + * Checks if the value is set up and exists, if not logs an exception. Used for convenient + * logging in case `setup` wasn't called before + * + * @return value provided as argument + */ + private fun <T> checkExists(value: T?): T? { + if (value == null) Log.e(TAG, SHOULD_SETUP) + return value + } + + // START DragToInteractView modification + private fun addSpace(parent: LinearLayout) { + val space = Space(context) + // Spaces are weighted equally to space out circles evenly + space.layoutParams = + LinearLayout.LayoutParams( + ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT, + 1f + ) + parent.addView(space) + parent.weightSum = parent.weightSum + 1f + } + + private fun addCircle(config: Config, id: Int, iconResId: Int, parent: LinearLayout) { + val targetSize = resources.getDimensionPixelSize(config.targetSizeResId) + val circleLayoutParams = LinearLayout.LayoutParams(targetSize, targetSize, 0f) + circleLayoutParams.gravity = Gravity.BOTTOM or Gravity.CENTER_HORIZONTAL + val circle = DismissCircleView(context) + circle.id = id + circle.setup(config.backgroundResId, iconResId, config.iconSizeResId) + circle.layoutParams = circleLayoutParams + + // Initial position with circle offscreen so it's animated up + circle.translationY = + resources.getDimensionPixelSize(config.floatingGradientHeightResId).toFloat() + + interactMap[circle.id] = Pair(circle, PhysicsAnimator.getInstance(circle)) + parent.addView(circle) + addSpace(parent) + } + // END DragToInteractView modification +} diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationController.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationController.java index a2705584d76a..d3e85e092b3a 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationController.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationController.java @@ -37,7 +37,6 @@ import androidx.dynamicanimation.animation.SpringForce; import androidx.recyclerview.widget.RecyclerView; import com.android.internal.annotations.VisibleForTesting; -import com.android.internal.util.Preconditions; import com.android.systemui.Flags; import java.util.HashMap; @@ -73,7 +72,6 @@ class MenuAnimationController { private final ValueAnimator mFadeOutAnimator; private final Handler mHandler; private boolean mIsFadeEffectEnabled; - private DragToInteractAnimationController.DismissCallback mDismissCallback; private Runnable mSpringAnimationsEndAction; // Cache the animations state of {@link DynamicAnimation.TRANSLATION_X} and {@link @@ -170,11 +168,6 @@ class MenuAnimationController { mSpringAnimationsEndAction = runnable; } - void setDismissCallback( - DragToInteractAnimationController.DismissCallback dismissCallback) { - mDismissCallback = dismissCallback; - } - void moveToTopLeftPosition() { mMenuView.updateMenuMoveToTucked(/* isMoveToTucked= */ false); final Rect draggableBounds = mMenuView.getMenuDraggableBounds(); @@ -205,13 +198,6 @@ class MenuAnimationController { constrainPositionAndUpdate(position, /* writeToPosition = */ true); } - void removeMenu() { - Preconditions.checkArgument(mDismissCallback != null, - "The dismiss callback should be initialized first."); - - mDismissCallback.onDismiss(); - } - void flingMenuThenSpringToEdge(float x, float velocityX, float velocityY) { final boolean shouldMenuFlingLeft = isOnLeftSide() ? velocityX < ESCAPE_VELOCITY @@ -334,8 +320,6 @@ class MenuAnimationController { moveToEdgeAndHide(); return true; } - - fadeOutIfEnabled(); return false; } @@ -453,8 +437,6 @@ class MenuAnimationController { mMenuView.onBoundsInParentChanged((int) position.x, (int) position.y); constrainPositionAndUpdate(position, writeToPosition); - fadeOutIfEnabled(); - if (mSpringAnimationsEndAction != null) { mSpringAnimationsEndAction.run(); } 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 0538e7d55bfd..4a06ae9523fa 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuInfoRepository.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuInfoRepository.java @@ -21,7 +21,6 @@ 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; @@ -47,6 +46,7 @@ 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, ACCESSIBILITY_BUTTON)); + callback.onReady(getTargets(mContext, ShortcutConstants.UserShortcutType.SOFTWARE)); } void loadMenuSizeType(OnInfoReady<Integer> callback) { @@ -223,7 +223,7 @@ class MenuInfoRepository { private void onTargetFeaturesChanged() { mSettingsContentsCallback.onTargetFeaturesChanged( - getTargets(mContext, ACCESSIBILITY_BUTTON)); + getTargets(mContext, ShortcutConstants.UserShortcutType.SOFTWARE)); } private Position getStartPosition() { diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuItemAccessibilityDelegate.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuItemAccessibilityDelegate.java index 9c22a7738ad6..975a6020430d 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuItemAccessibilityDelegate.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuItemAccessibilityDelegate.java @@ -27,6 +27,7 @@ import androidx.annotation.NonNull; import androidx.core.view.accessibility.AccessibilityNodeInfoCompat; import androidx.recyclerview.widget.RecyclerViewAccessibilityDelegate; +import com.android.systemui.Flags; import com.android.systemui.res.R; /** @@ -35,15 +36,18 @@ import com.android.systemui.res.R; */ class MenuItemAccessibilityDelegate extends RecyclerViewAccessibilityDelegate.ItemDelegate { private final MenuAnimationController mAnimationController; + private final MenuViewLayer mMenuViewLayer; MenuItemAccessibilityDelegate(@NonNull RecyclerViewAccessibilityDelegate recyclerViewDelegate, - MenuAnimationController animationController) { + MenuAnimationController animationController, MenuViewLayer menuViewLayer) { super(recyclerViewDelegate); mAnimationController = animationController; + mMenuViewLayer = menuViewLayer; } @Override - public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) { + public void onInitializeAccessibilityNodeInfo( + @NonNull View host, @NonNull AccessibilityNodeInfoCompat info) { super.onInitializeAccessibilityNodeInfo(host, info); final Resources res = host.getResources(); @@ -90,6 +94,15 @@ class MenuItemAccessibilityDelegate extends RecyclerViewAccessibilityDelegate.It R.id.action_remove_menu, res.getString(R.string.accessibility_floating_button_action_remove_menu)); info.addAction(removeMenu); + + if (Flags.floatingMenuDragToEdit()) { + final AccessibilityNodeInfoCompat.AccessibilityActionCompat edit = + new AccessibilityNodeInfoCompat.AccessibilityActionCompat( + R.id.action_edit, + res.getString( + R.string.accessibility_floating_button_action_remove_menu)); + info.addAction(edit); + } } @Override @@ -132,8 +145,8 @@ class MenuItemAccessibilityDelegate extends RecyclerViewAccessibilityDelegate.It return true; } - if (action == R.id.action_remove_menu) { - mAnimationController.removeMenu(); + if (action == R.id.action_remove_menu || action == R.id.action_edit) { + mMenuViewLayer.dispatchAccessibilityAction(action); return true; } diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuListViewTouchHandler.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuListViewTouchHandler.java index 52e7b91d373e..75191685b119 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuListViewTouchHandler.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuListViewTouchHandler.java @@ -16,6 +16,8 @@ package com.android.systemui.accessibility.floatingmenu; +import static android.R.id.empty; + import android.graphics.PointF; import android.view.MotionEvent; import android.view.VelocityTracker; @@ -78,10 +80,9 @@ class MenuListViewTouchHandler implements RecyclerView.OnItemTouchListener { mMenuAnimationController.onDraggingStart(); } - mDragToInteractAnimationController.showDismissView(/* show= */ true); - - if (!mDragToInteractAnimationController.maybeConsumeMoveMotionEvent( - motionEvent)) { + mDragToInteractAnimationController.showInteractView(/* show= */ true); + if (mDragToInteractAnimationController.maybeConsumeMoveMotionEvent(motionEvent) + == empty) { mMenuAnimationController.moveToPositionX(mMenuTranslationDown.x + dx); mMenuAnimationController.moveToPositionYIfNeeded( mMenuTranslationDown.y + dy); @@ -94,21 +95,19 @@ class MenuListViewTouchHandler implements RecyclerView.OnItemTouchListener { final float endX = mMenuTranslationDown.x + dx; mIsDragging = false; + mDragToInteractAnimationController.showInteractView(/* show= */ false); + if (mMenuAnimationController.maybeMoveToEdgeAndHide(endX)) { - mDragToInteractAnimationController.showDismissView(/* show= */ false); mMenuAnimationController.fadeOutIfEnabled(); - return true; } - if (!mDragToInteractAnimationController.maybeConsumeUpMotionEvent( - motionEvent)) { + if (mDragToInteractAnimationController.maybeConsumeUpMotionEvent(motionEvent) + == empty) { mVelocityTracker.computeCurrentVelocity(VELOCITY_UNIT_SECONDS); mMenuAnimationController.flingMenuThenSpringToEdge(endX, mVelocityTracker.getXVelocity(), mVelocityTracker.getYVelocity()); - mDragToInteractAnimationController.showDismissView(/* show= */ false); } - // Avoid triggering the listener of the item. return true; } diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuView.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuView.java index 76808cb7ab7b..334cc87144f9 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuView.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuView.java @@ -21,24 +21,28 @@ import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; import android.annotation.SuppressLint; import android.content.ComponentCallbacks; import android.content.Context; +import android.content.Intent; import android.content.res.Configuration; import android.graphics.PointF; import android.graphics.Rect; import android.graphics.drawable.GradientDrawable; +import android.os.Bundle; +import android.os.UserHandle; +import android.provider.Settings; +import android.provider.SettingsStringUtil; import android.view.ViewGroup; import android.view.ViewTreeObserver; import android.widget.FrameLayout; import androidx.annotation.NonNull; -import androidx.core.view.AccessibilityDelegateCompat; import androidx.lifecycle.Observer; import androidx.recyclerview.widget.DiffUtil; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; -import androidx.recyclerview.widget.RecyclerViewAccessibilityDelegate; import com.android.internal.accessibility.dialog.AccessibilityTarget; import com.android.systemui.Flags; +import com.android.systemui.util.settings.SecureSettings; import java.util.ArrayList; import java.util.Collections; @@ -72,26 +76,20 @@ class MenuView extends FrameLayout implements private final MenuAnimationController mMenuAnimationController; private OnTargetFeaturesChangeListener mFeaturesChangeListener; private OnMoveToTuckedListener mMoveToTuckedListener; + private SecureSettings mSecureSettings; - MenuView(Context context, MenuViewModel menuViewModel, MenuViewAppearance menuViewAppearance) { + MenuView(Context context, MenuViewModel menuViewModel, MenuViewAppearance menuViewAppearance, + SecureSettings secureSettings) { super(context); mMenuViewModel = menuViewModel; mMenuViewAppearance = menuViewAppearance; + mSecureSettings = secureSettings; mMenuAnimationController = new MenuAnimationController(this, menuViewAppearance); mAdapter = new AccessibilityTargetAdapter(mTargetFeatures); mTargetFeaturesView = new RecyclerView(context); mTargetFeaturesView.setAdapter(mAdapter); mTargetFeaturesView.setLayoutManager(new LinearLayoutManager(context)); - mTargetFeaturesView.setAccessibilityDelegateCompat( - new RecyclerViewAccessibilityDelegate(mTargetFeaturesView) { - @NonNull - @Override - public AccessibilityDelegateCompat getItemDelegate() { - return new MenuItemAccessibilityDelegate(/* recyclerViewDelegate= */ this, - mMenuAnimationController); - } - }); setLayoutParams(new FrameLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT)); // Avoid drawing out of bounds of the parent view setClipToOutline(true); @@ -278,6 +276,7 @@ class MenuView extends FrameLayout implements if (mFeaturesChangeListener != null) { mFeaturesChangeListener.onChange(newTargetFeatures); } + mMenuAnimationController.fadeOutIfEnabled(); } @@ -306,6 +305,10 @@ class MenuView extends FrameLayout implements return mMenuViewAppearance.getMenuPosition(); } + RecyclerView getTargetFeaturesView() { + return mTargetFeaturesView; + } + void persistPositionAndUpdateEdge(Position percentagePosition) { mMenuViewModel.updateMenuSavingPosition(percentagePosition); mMenuViewAppearance.setPercentagePosition(percentagePosition); @@ -424,6 +427,35 @@ class MenuView extends FrameLayout implements onPositionChanged(); } + void gotoEditScreen() { + if (!Flags.floatingMenuDragToEdit()) { + return; + } + mMenuAnimationController.flingMenuThenSpringToEdge( + getMenuPosition().x, 100f, 0f); + mContext.startActivity(getIntentForEditScreen()); + } + + Intent getIntentForEditScreen() { + List<String> targets = new SettingsStringUtil.ColonDelimitedSet.OfStrings( + mSecureSettings.getStringForUser( + Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS, + UserHandle.USER_CURRENT)).stream().toList(); + + Intent intent = new Intent( + Settings.ACTION_ACCESSIBILITY_SHORTCUT_SETTINGS); + Bundle args = new Bundle(); + Bundle fragmentArgs = new Bundle(); + fragmentArgs.putStringArray("targets", targets.toArray(new String[0])); + args.putBundle(":settings:show_fragment_args", fragmentArgs); + // TODO: b/318748373 - The fragment should set its own title using the targets + args.putString( + ":settings:show_fragment_title", "Accessibility Shortcut"); + intent.replaceExtras(args); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP); + return intent; + } + private InstantInsetLayerDrawable getContainerViewInsetLayer() { return (InstantInsetLayerDrawable) getBackground(); } 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 97999cc19dc8..3acf80dbbe5f 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayer.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayer.java @@ -17,7 +17,6 @@ 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; @@ -59,8 +58,12 @@ import android.widget.FrameLayout; import android.widget.TextView; import androidx.annotation.NonNull; +import androidx.core.view.AccessibilityDelegateCompat; 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; @@ -94,6 +97,8 @@ class MenuViewLayer extends FrameLayout implements private final MenuListViewTouchHandler mMenuListViewTouchHandler; private final MenuMessageView mMessageView; private final DismissView mDismissView; + private final DragToInteractView mDragToInteractView; + private final MenuViewAppearance mMenuViewAppearance; private final MenuAnimationController mMenuAnimationController; private final AccessibilityManager mAccessibilityManager; @@ -149,7 +154,7 @@ class MenuViewLayer extends FrameLayout implements final List<ComponentName> hardwareKeyShortcutComponents = mAccessibilityManager.getAccessibilityShortcutTargets( - ACCESSIBILITY_SHORTCUT_KEY) + ShortcutConstants.UserShortcutType.HARDWARE) .stream() .map(ComponentName::unflattenFromString) .toList(); @@ -178,7 +183,10 @@ class MenuViewLayer extends FrameLayout implements }; MenuViewLayer(@NonNull Context context, WindowManager windowManager, - AccessibilityManager accessibilityManager, IAccessibilityFloatingMenu floatingMenu, + AccessibilityManager accessibilityManager, + MenuViewModel menuViewModel, + MenuViewAppearance menuViewAppearance, MenuView menuView, + IAccessibilityFloatingMenu floatingMenu, SecureSettings secureSettings) { super(context); @@ -190,43 +198,52 @@ class MenuViewLayer extends FrameLayout implements mFloatingMenu = floatingMenu; mSecureSettings = secureSettings; - mMenuViewModel = new MenuViewModel(context, accessibilityManager, secureSettings); - mMenuViewAppearance = new MenuViewAppearance(context, windowManager); - mMenuView = new MenuView(context, mMenuViewModel, mMenuViewAppearance); + mMenuViewModel = menuViewModel; + mMenuViewAppearance = menuViewAppearance; + mMenuView = menuView; + RecyclerView targetFeaturesView = mMenuView.getTargetFeaturesView(); + targetFeaturesView.setAccessibilityDelegateCompat( + new RecyclerViewAccessibilityDelegate(targetFeaturesView) { + @NonNull + @Override + public AccessibilityDelegateCompat getItemDelegate() { + return new MenuItemAccessibilityDelegate(/* recyclerViewDelegate= */ this, + mMenuAnimationController, MenuViewLayer.this); + } + }); mMenuAnimationController = mMenuView.getMenuAnimationController(); - if (Flags.floatingMenuDragToHide()) { - mMenuAnimationController.setDismissCallback(this::hideMenuAndShowNotification); - } else { - mMenuAnimationController.setDismissCallback(this::hideMenuAndShowMessage); - } mMenuAnimationController.setSpringAnimationsEndAction(this::onSpringAnimationsEndAction); mDismissView = new DismissView(context); + mDragToInteractView = new DragToInteractView(context); DismissViewUtils.setup(mDismissView); + mDismissView.getCircle().setId(R.id.action_remove_menu); mNotificationFactory = new MenuNotificationFactory(context); mNotificationManager = context.getSystemService(NotificationManager.class); - mDragToInteractAnimationController = new DragToInteractAnimationController( - mDismissView, mMenuView); + + if (Flags.floatingMenuDragToEdit()) { + mDragToInteractAnimationController = new DragToInteractAnimationController( + mDragToInteractView, mMenuView); + } else { + mDragToInteractAnimationController = new DragToInteractAnimationController( + mDismissView, mMenuView); + } mDragToInteractAnimationController.setMagnetListener(new MagnetizedObject.MagnetListener() { @Override public void onStuckToTarget(@NonNull MagnetizedObject.MagneticTarget target) { - mDragToInteractAnimationController.animateDismissMenu(/* scaleUp= */ true); + mDragToInteractAnimationController.animateInteractMenu( + target.getTargetView().getId(), /* scaleUp= */ true); } @Override public void onUnstuckFromTarget(@NonNull MagnetizedObject.MagneticTarget target, float velocityX, float velocityY, boolean wasFlungOut) { - mDragToInteractAnimationController.animateDismissMenu(/* scaleUp= */ false); + mDragToInteractAnimationController.animateInteractMenu( + target.getTargetView().getId(), /* scaleUp= */ false); } @Override public void onReleasedInTarget(@NonNull MagnetizedObject.MagneticTarget target) { - if (Flags.floatingMenuDragToHide()) { - hideMenuAndShowNotification(); - } else { - hideMenuAndShowMessage(); - } - mDismissView.hide(); - mDragToInteractAnimationController.animateDismissMenu(/* scaleUp= */ false); + dispatchAccessibilityAction(target.getTargetView().getId()); } }); @@ -262,7 +279,11 @@ class MenuViewLayer extends FrameLayout implements }); addView(mMenuView, LayerIndex.MENU_VIEW); - addView(mDismissView, LayerIndex.DISMISS_VIEW); + if (Flags.floatingMenuDragToEdit()) { + addView(mDragToInteractView, LayerIndex.DISMISS_VIEW); + } else { + addView(mDismissView, LayerIndex.DISMISS_VIEW); + } addView(mMessageView, LayerIndex.MESSAGE_VIEW); if (Flags.floatingMenuAnimatedTuck()) { @@ -272,6 +293,7 @@ class MenuViewLayer extends FrameLayout implements @Override public void onConfigurationChanged(@NonNull Configuration newConfig) { + mDragToInteractView.updateResources(); mDismissView.updateResources(); mDragToInteractAnimationController.updateResources(); } @@ -428,6 +450,23 @@ class MenuViewLayer extends FrameLayout implements } } + void dispatchAccessibilityAction(int id) { + if (id == R.id.action_remove_menu) { + if (Flags.floatingMenuDragToHide()) { + hideMenuAndShowNotification(); + } else { + hideMenuAndShowMessage(); + } + } else if (id == R.id.action_edit + && Flags.floatingMenuDragToEdit()) { + mMenuView.gotoEditScreen(); + } + mDismissView.hide(); + mDragToInteractView.hide(); + mDragToInteractAnimationController.animateInteractMenu( + id, /* scaleUp= */ false); + } + private CharSequence getMigrationMessage() { final Intent intent = new Intent(Settings.ACTION_ACCESSIBILITY_DETAILS_SETTINGS); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); @@ -475,7 +514,8 @@ class MenuViewLayer extends FrameLayout implements mEduTooltipView = Optional.empty(); } - private void hideMenuAndShowMessage() { + @VisibleForTesting + void hideMenuAndShowMessage() { final int delayTime = mAccessibilityManager.getRecommendedTimeoutMillis( SHOW_MESSAGE_DELAY_MS, AccessibilityManager.FLAG_CONTENT_TEXT @@ -485,7 +525,8 @@ class MenuViewLayer extends FrameLayout implements mMenuAnimationController.startShrinkAnimation(() -> mMenuView.setVisibility(GONE)); } - private void hideMenuAndShowNotification() { + @VisibleForTesting + void hideMenuAndShowNotification() { mMenuAnimationController.startShrinkAnimation(() -> mMenuView.setVisibility(GONE)); showNotification(); } diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerController.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerController.java index 1f549525256b..bc9d1ffd259b 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerController.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerController.java @@ -39,7 +39,16 @@ class MenuViewLayerController implements IAccessibilityFloatingMenu { MenuViewLayerController(Context context, WindowManager windowManager, AccessibilityManager accessibilityManager, SecureSettings secureSettings) { mWindowManager = windowManager; - mMenuViewLayer = new MenuViewLayer(context, windowManager, accessibilityManager, this, + + MenuViewModel menuViewModel = new MenuViewModel( + context, accessibilityManager, secureSettings); + MenuViewAppearance menuViewAppearance = new MenuViewAppearance(context, windowManager); + + mMenuViewLayer = new MenuViewLayer(context, windowManager, accessibilityManager, + menuViewModel, + menuViewAppearance, + new MenuView(context, menuViewModel, menuViewAppearance, secureSettings), + this, secureSettings); } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleController.kt b/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleController.kt index 71d0e7d6081a..d2c62272e2ec 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleController.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleController.kt @@ -25,6 +25,7 @@ import android.hardware.biometrics.BiometricFingerprintConstants import android.hardware.biometrics.BiometricSourceType import android.util.DisplayMetrics import androidx.annotation.VisibleForTesting +import androidx.lifecycle.repeatOnLifecycle import com.android.app.animation.Interpolators import com.android.keyguard.KeyguardUpdateMonitor import com.android.keyguard.KeyguardUpdateMonitorCallback @@ -35,7 +36,11 @@ import com.android.systemui.Flags.lightRevealMigration import com.android.systemui.biometrics.data.repository.FacePropertyRepository import com.android.systemui.biometrics.shared.model.UdfpsOverlayParams import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.deviceentry.domain.interactor.AuthRippleInteractor +import com.android.systemui.deviceentry.shared.DeviceEntryUdfpsRefactor import com.android.systemui.keyguard.WakefulnessLifecycle +import com.android.systemui.keyguard.shared.model.BiometricUnlockSource +import com.android.systemui.lifecycle.repeatWhenAttached import com.android.systemui.log.core.LogLevel import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.res.R @@ -78,6 +83,7 @@ class AuthRippleController @Inject constructor( private val logger: KeyguardLogger, private val biometricUnlockController: BiometricUnlockController, private val lightRevealScrim: LightRevealScrim, + private val authRippleInteractor: AuthRippleInteractor, private val facePropertyRepository: FacePropertyRepository, rippleView: AuthRippleView? ) : @@ -100,6 +106,22 @@ class AuthRippleController @Inject constructor( init() } + init { + if (DeviceEntryUdfpsRefactor.isEnabled) { + rippleView?.repeatWhenAttached { + repeatOnLifecycle(androidx.lifecycle.Lifecycle.State.CREATED) { + authRippleInteractor.showUnlockRipple.collect { biometricUnlockSource -> + if (biometricUnlockSource == BiometricUnlockSource.FINGERPRINT_SENSOR) { + showUnlockRippleInternal(BiometricSourceType.FINGERPRINT) + } else { + showUnlockRippleInternal(BiometricSourceType.FACE) + } + } + } + } + } + } + @VisibleForTesting public override fun onViewAttached() { authController.addCallback(authControllerCallback) @@ -111,7 +133,9 @@ class AuthRippleController @Inject constructor( keyguardStateController.addCallback(this) wakefulnessLifecycle.addObserver(this) commandRegistry.registerCommand("auth-ripple") { AuthRippleCommand() } - biometricUnlockController.addListener(biometricModeListener) + if (!DeviceEntryUdfpsRefactor.isEnabled) { + biometricUnlockController.addListener(biometricModeListener) + } } private val biometricModeListener = @@ -119,8 +143,9 @@ class AuthRippleController @Inject constructor( override fun onBiometricUnlockedWithKeyguardDismissal( biometricSourceType: BiometricSourceType? ) { + DeviceEntryUdfpsRefactor.assertInLegacyMode() if (biometricSourceType != null) { - showUnlockRipple(biometricSourceType) + showUnlockRippleInternal(biometricSourceType) } else { logger.log(TAG, LogLevel.ERROR, @@ -143,7 +168,13 @@ class AuthRippleController @Inject constructor( notificationShadeWindowController.setForcePluginOpen(false, this) } - fun showUnlockRipple(biometricSourceType: BiometricSourceType) { + @Deprecated("Update authRippleInteractor.showUnlockRipple instead of calling this.") + fun showUnlockRipple(biometricSourceType: BiometricSourceType) { + DeviceEntryUdfpsRefactor.assertInLegacyMode() + showUnlockRippleInternal(biometricSourceType) + } + + private fun showUnlockRippleInternal(biometricSourceType: BiometricSourceType) { val keyguardNotShowing = !keyguardStateController.isShowing val unlockNotAllowed = !keyguardUpdateMonitor .isUnlockingWithBiometricAllowed(biometricSourceType) @@ -377,12 +408,12 @@ class AuthRippleController @Inject constructor( } "fingerprint" -> { pw.println("fingerprint ripple sensorLocation=$fingerprintSensorLocation") - showUnlockRipple(BiometricSourceType.FINGERPRINT) + showUnlockRippleInternal(BiometricSourceType.FINGERPRINT) } "face" -> { // note: only shows when about to proceed to the home screen pw.println("face ripple sensorLocation=$faceSensorLocation") - showUnlockRipple(BiometricSourceType.FACE) + showUnlockRippleInternal(BiometricSourceType.FACE) } "custom" -> { if (args.size != 3 || @@ -409,7 +440,7 @@ class AuthRippleController @Inject constructor( pw.println(" custom <x-location: int> <y-location: int>") } - fun invalidCommand(pw: PrintWriter) { + private fun invalidCommand(pw: PrintWriter) { pw.println("invalid command") help(pw) } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricCustomizedViewBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricCustomizedViewBinder.kt index 16e7f05fae37..96582cb56dd7 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricCustomizedViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricCustomizedViewBinder.kt @@ -114,7 +114,7 @@ private fun createNewRowLayout(inflater: LayoutInflater): LinearLayout { private fun PromptContentItem.doesExceedMaxLinesIfTwoColumn( resources: Resources, ): Boolean { - val passedInText: CharSequence = + val passedInText: String = when (this) { is PromptContentItemPlainText -> text is PromptContentItemBulletedText -> text 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 c36e0e21d021..80d37b4741e4 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 @@ -121,11 +121,13 @@ constructor( if (it.isAttachedToWindow) { lottie = it.requireViewById<LottieAnimationView>(R.id.sidefps_animation) lottie?.pauseAnimation() + lottie?.removeAllLottieOnCompositionLoadedListener() windowManager.get().removeView(it) } } overlayView = layoutInflater.get().inflate(R.layout.sidefps_view, null, false) + val overlayViewModel = SideFpsOverlayViewModel( applicationContext, @@ -163,8 +165,10 @@ constructor( val lottie = it.requireViewById<LottieAnimationView>(R.id.sidefps_animation) lottie.addLottieOnCompositionLoadedListener { composition: LottieComposition -> - viewModel.setLottieBounds(composition.bounds) - overlayView.visibility = View.VISIBLE + if (overlayView.visibility != View.VISIBLE) { + viewModel.setLottieBounds(composition.bounds) + overlayView.visibility = View.VISIBLE + } } it.alpha = 0f val overlayShowAnimator = 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 e78a7a942993..0f1340a63032 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 @@ -259,7 +259,9 @@ constructor( /** Custom content view for the prompt. */ val contentView: Flow<PromptContentView?> = - promptSelectorInteractor.prompt.map { it?.contentView }.distinctUntilChanged() + promptSelectorInteractor.prompt + .map { if (customBiometricPrompt()) it?.contentView else null } + .distinctUntilChanged() private val originalDescription = promptSelectorInteractor.prompt.map { it?.description ?: "" }.distinctUntilChanged() 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 3287ed4d4991..f36547b01802 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 @@ -27,21 +27,17 @@ import com.android.systemui.communal.shared.model.CommunalWidgetContentModel import com.android.systemui.communal.widgets.CommunalAppWidgetHost import com.android.systemui.communal.widgets.WidgetConfigurator import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.dagger.qualifiers.Application 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.util.kotlin.getValue import java.util.Optional import javax.inject.Inject import kotlin.coroutines.cancellation.CancellationException import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.flatMapLatest -import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch @@ -67,18 +63,15 @@ interface CommunalWidgetRepository { * @param widgetIdToPriorityMap mapping of the widget ids to the priority of the widget. */ fun updateWidgetOrder(widgetIdToPriorityMap: Map<Int, Int>) {} - - /** Update whether the app widget host should be active. */ - fun updateAppWidgetHostActive(active: Boolean) } @SysUISingleton class CommunalWidgetRepositoryImpl @Inject constructor( - private val appWidgetManager: Optional<AppWidgetManager>, + appWidgetManagerOptional: Optional<AppWidgetManager>, private val appWidgetHost: CommunalAppWidgetHost, - @Application private val applicationScope: CoroutineScope, + @Background private val bgScope: CoroutineScope, @Background private val bgDispatcher: CoroutineDispatcher, private val communalWidgetHost: CommunalWidgetHost, private val communalWidgetDao: CommunalWidgetDao, @@ -90,41 +83,22 @@ constructor( private val logger = Logger(logBuffer, TAG) - override fun updateAppWidgetHostActive(active: Boolean) { - if (active == isHostActive.value) { - return - } - - if (active) { - appWidgetHost.startListening() - } else { - appWidgetHost.stopListening() - } - isHostActive.value = active - } - - private val isHostActive = MutableStateFlow(false) + private val appWidgetManager by appWidgetManagerOptional - @OptIn(ExperimentalCoroutinesApi::class) override val communalWidgets: Flow<List<CommunalWidgetContentModel>> = - isHostActive.flatMapLatest { isHostActive -> - if (!isHostActive || !appWidgetManager.isPresent) { - return@flatMapLatest flowOf(emptyList()) - } - communalWidgetDao - .getWidgets() - .map { it.map(::mapToContentModel) } - // As this reads from a database and triggers IPCs to AppWidgetManager, - // it should be executed in the background. - .flowOn(bgDispatcher) - } + communalWidgetDao + .getWidgets() + .map { it.mapNotNull(::mapToContentModel) } + // As this reads from a database and triggers IPCs to AppWidgetManager, + // it should be executed in the background. + .flowOn(bgDispatcher) override fun addWidget( provider: ComponentName, priority: Int, configurator: WidgetConfigurator? ) { - applicationScope.launch(bgDispatcher) { + bgScope.launch { val id = communalWidgetHost.allocateIdAndBindWidget(provider) if (id == null) { logger.e("Failed to allocate widget id to ${provider.flattenToString()}") @@ -170,7 +144,7 @@ constructor( } override fun deleteWidget(widgetId: Int) { - applicationScope.launch(bgDispatcher) { + bgScope.launch { communalWidgetDao.deleteWidgetById(widgetId) appWidgetHost.deleteAppWidgetId(widgetId) logger.i("Deleted widget with id $widgetId.") @@ -178,7 +152,7 @@ constructor( } override fun updateWidgetOrder(widgetIdToPriorityMap: Map<Int, Int>) { - applicationScope.launch(bgDispatcher) { + bgScope.launch { communalWidgetDao.updateWidgetOrder(widgetIdToPriorityMap) logger.i({ "Updated the order of widget list with ids: $str1." }) { str1 = widgetIdToPriorityMap.toString() @@ -189,11 +163,12 @@ constructor( @WorkerThread private fun mapToContentModel( entry: Map.Entry<CommunalItemRank, CommunalWidgetItem> - ): CommunalWidgetContentModel { + ): CommunalWidgetContentModel? { val (_, widgetId) = entry.value + val providerInfo = appWidgetManager?.getAppWidgetInfo(widgetId) ?: return null return CommunalWidgetContentModel( appWidgetId = widgetId, - providerInfo = appWidgetManager.get().getAppWidgetInfo(widgetId), + providerInfo = providerInfo, priority = entry.key.rank, ) } 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 c36f7fa22c82..80fee640974d 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,23 +33,23 @@ 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.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.SharingStarted +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow 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 -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.flow.stateIn /** Encapsulates business-logic related to communal mode. */ @OptIn(ExperimentalCoroutinesApi::class) @@ -57,7 +57,6 @@ import kotlinx.coroutines.flow.stateIn class CommunalInteractor @Inject constructor( - @Application applicationScope: CoroutineScope, private val communalRepository: CommunalRepository, private val widgetRepository: CommunalWidgetRepository, private val communalPrefsRepository: CommunalPrefsRepository, @@ -68,38 +67,24 @@ constructor( private val appWidgetHost: CommunalAppWidgetHost, private val editWidgetsActivityStarter: EditWidgetsActivityStarter ) { + private val _editModeOpen = MutableStateFlow(false) + + /** Whether edit mode is currently open. */ + val editModeOpen: StateFlow<Boolean> = _editModeOpen.asStateFlow() + /** Whether communal features are enabled. */ val isCommunalEnabled: Boolean get() = communalRepository.isCommunalEnabled - /** A {@link StateFlow} that tracks whether communal features are enabled. */ - val communalEnabledState: StateFlow<Boolean> - get() = communalRepository.communalEnabledState - /** Whether communal features are enabled and available. */ - val isCommunalAvailable: StateFlow<Boolean> = - flowOf(isCommunalEnabled) - .flatMapLatest { enabled -> - if (enabled) - combine( - keyguardInteractor.isEncryptedOrLockdown, - userRepository.selectedUserInfo, - keyguardInteractor.isKeyguardVisible, - keyguardInteractor.isDreaming, - ) { isEncryptedOrLockdown, selectedUserInfo, isKeyguardVisible, isDreaming -> - !isEncryptedOrLockdown && - selectedUserInfo.isMain && - (isKeyguardVisible || isDreaming) - } - else flowOf(false) - } - .distinctUntilChanged() - .onEach { available -> widgetRepository.updateAppWidgetHostActive(available) } - .stateIn( - scope = applicationScope, - started = SharingStarted.WhileSubscribed(), - initialValue = false, + val isCommunalAvailable: Flow<Boolean> = + and( + communalRepository.communalEnabledState, + userRepository.selectedUserInfo.map { it.isMain }, + not(keyguardInteractor.isEncryptedOrLockdown), + or(keyguardInteractor.isKeyguardVisible, keyguardInteractor.isDreaming) ) + .distinctUntilChanged() /** * Target scene as requested by the underlying [SceneTransitionLayout] or through @@ -166,9 +151,13 @@ constructor( communalRepository.setDesiredScene(newScene) } + fun setEditModeOpen(isOpen: Boolean) { + _editModeOpen.value = isOpen + } + /** Show the widget editor Activity. */ - fun showWidgetEditor() { - editWidgetsActivityStarter.startActivity() + fun showWidgetEditor(preselectedKey: String? = null) { + editWidgetsActivityStarter.startActivity(preselectedKey) } /** Dismiss the CTA tile from the hub in view mode. */ diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/model/CommunalContentModel.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/model/CommunalContentModel.kt index acd6cb09e241..ae019a187bae 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/domain/model/CommunalContentModel.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/domain/model/CommunalContentModel.kt @@ -128,4 +128,6 @@ sealed interface CommunalContentModel { } } } + + fun isWidget() = this is Widget } diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt index 1e64d3f9cedc..a87ebd7e8e9f 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt @@ -33,18 +33,16 @@ abstract class BaseCommunalViewModel( private val communalInteractor: CommunalInteractor, val mediaHost: MediaHost, ) { - val isCommunalAvailable: StateFlow<Boolean> = communalInteractor.isCommunalAvailable - val currentScene: StateFlow<CommunalSceneKey> = communalInteractor.desiredScene /** Whether widgets are currently being re-ordered. */ open val reorderingWidgets: StateFlow<Boolean> = MutableStateFlow(false) - private val _selectedIndex: MutableStateFlow<Int?> = MutableStateFlow(null) + private val _selectedKey: MutableStateFlow<String?> = MutableStateFlow(null) - /** The index of the currently selected item, or null if no item selected. */ - val selectedIndex: StateFlow<Int?> - get() = _selectedIndex + /** The key of the currently selected item, or null if no item selected. */ + val selectedKey: StateFlow<String?> + get() = _selectedKey fun onSceneChanged(scene: CommunalSceneKey) { communalInteractor.onSceneChanged(scene) @@ -94,8 +92,8 @@ abstract class BaseCommunalViewModel( */ open fun onReorderWidgets(widgetIdToPriorityMap: Map<Int, Int>) {} - /** Called as the UI requests opening the widget editor. */ - open fun onOpenWidgetEditor() {} + /** Called as the UI requests opening the widget editor with an optional preselected widget. */ + open fun onOpenWidgetEditor(preselectedKey: String? = null) {} /** Called as the UI requests to dismiss the CTA tile. */ open fun onDismissCtaTile() {} @@ -109,8 +107,8 @@ abstract class BaseCommunalViewModel( /** Called as the user cancels dragging a widget to reorder. */ open fun onReorderWidgetCancel() {} - /** Set the index of the currently selected item */ - fun setSelectedIndex(index: Int?) { - _selectedIndex.value = index + /** Set the key of the currently selected item */ + fun setSelectedKey(key: String?) { + _selectedKey.value = key } } 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 237a0c005af5..ebcfb8bb8209 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 @@ -29,7 +29,6 @@ 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 @@ -44,10 +43,9 @@ constructor( // Only widgets are editable. The CTA tile comes last in the list and remains visible. override val communalContent: Flow<List<CommunalContentModel>> = - communalInteractor.widgetContent - // Clear the selected index when the list is updated. - .onEach { setSelectedIndex(null) } - .map { widgets -> widgets + listOf(CommunalContentModel.CtaTileInEditMode()) } + communalInteractor.widgetContent.map { widgets -> + widgets + listOf(CommunalContentModel.CtaTileInEditMode()) + } private val _reorderingWidgets = MutableStateFlow(false) @@ -61,7 +59,7 @@ constructor( override fun onReorderWidgetStart() { // Clear selection status - setSelectedIndex(null) + setSelectedKey(null) _reorderingWidgets.value = true uiEventLogger.log(CommunalUiEvent.COMMUNAL_HUB_REORDER_WIDGET_START) } @@ -75,4 +73,7 @@ constructor( _reorderingWidgets.value = false uiEventLogger.log(CommunalUiEvent.COMMUNAL_HUB_REORDER_WIDGET_CANCEL) } + + /** Sets whether edit mode is currently open */ + fun setEditModeOpen(isOpen: Boolean) = communalInteractor.setEditModeOpen(isOpen) } 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 a909383f4a2c..d7a3705248ff 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 @@ -81,7 +81,8 @@ constructor( } } - override fun onOpenWidgetEditor() = communalInteractor.showWidgetEditor() + override fun onOpenWidgetEditor(preselectedKey: String?) = + communalInteractor.showWidgetEditor(preselectedKey) override fun onDismissCtaTile() { scope.launch { diff --git a/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartable.kt b/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartable.kt new file mode 100644 index 000000000000..586df32e6561 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartable.kt @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.communal.widgets + +import com.android.systemui.CoreStartable +import com.android.systemui.communal.domain.interactor.CommunalInteractor +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.util.kotlin.BooleanFlowOperators.or +import com.android.systemui.util.kotlin.pairwise +import javax.inject.Inject +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.withContext + +@SysUISingleton +class CommunalAppWidgetHostStartable +@Inject +constructor( + private val appWidgetHost: CommunalAppWidgetHost, + private val communalInteractor: CommunalInteractor, + @Background private val bgScope: CoroutineScope, + @Main private val uiDispatcher: CoroutineDispatcher +) : CoreStartable { + override fun start() { + or(communalInteractor.isCommunalAvailable, communalInteractor.editModeOpen) + // Only trigger updates on state changes, ignoring the initial false value. + .pairwise(false) + .filter { (previous, new) -> previous != new } + .onEach { (_, shouldListen) -> updateAppWidgetHostActive(shouldListen) } + .launchIn(bgScope) + } + + private suspend fun updateAppWidgetHostActive(active: Boolean) = + // Always ensure this is called on the main/ui thread. + withContext(uiDispatcher) { + if (active) { + appWidgetHost.startListening() + } else { + appWidgetHost.stopListening() + } + } +} 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 c7a14f9eefe1..ad1327e90710 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt @@ -47,6 +47,7 @@ constructor( private const val EXTRA_FILTER_STRATEGY = "filter_strategy" private const val FILTER_STRATEGY_GLANCEABLE_HUB = 1 private const val TAG = "EditWidgetsActivity" + const val EXTRA_PRESELECTED_KEY = "preselected_key" } private val widgetConfigurator by lazy { widgetConfiguratorFactory.create(this) } @@ -86,10 +87,15 @@ constructor( override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + communalViewModel.setEditModeOpen(true) + val windowInsetsController = window.decorView.windowInsetsController windowInsetsController?.hide(WindowInsets.Type.systemBars()) window.setDecorFitsSystemWindows(false) + val preselectedKey = intent.getStringExtra(EXTRA_PRESELECTED_KEY) + communalViewModel.setSelectedKey(preselectedKey) + setCommunalEditWidgetActivityContent( activity = this, viewModel = communalViewModel, @@ -138,13 +144,16 @@ constructor( override fun onStart() { super.onStart() - uiEventLogger.log(CommunalUiEvent.COMMUNAL_HUB_EDIT_MODE_SHOWN) } override fun onStop() { super.onStop() - uiEventLogger.log(CommunalUiEvent.COMMUNAL_HUB_EDIT_MODE_GONE) } + + override fun onDestroy() { + super.onDestroy() + communalViewModel.setEditModeOpen(false) + } } diff --git a/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivityStarter.kt b/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivityStarter.kt index 55acad0e9ed5..d1843afbc1c2 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivityStarter.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivityStarter.kt @@ -18,12 +18,13 @@ package com.android.systemui.communal.widgets import android.content.Context import android.content.Intent +import com.android.systemui.communal.widgets.EditWidgetsActivity.Companion.EXTRA_PRESELECTED_KEY import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.plugins.ActivityStarter import javax.inject.Inject interface EditWidgetsActivityStarter { - fun startActivity() + fun startActivity(preselectedKey: String? = null) } class EditWidgetsActivityStarterImpl @@ -33,10 +34,11 @@ constructor( private val activityStarter: ActivityStarter, ) : EditWidgetsActivityStarter { - override fun startActivity() { + override fun startActivity(preselectedKey: String?) { activityStarter.startActivityDismissingKeyguard( Intent(applicationContext, EditWidgetsActivity::class.java) - .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK), + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK) + .apply { preselectedKey?.let { putExtra(EXTRA_PRESELECTED_KEY, preselectedKey) } }, /* onlyProvisioned = */ true, /* dismissShade = */ true, ) diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt index 8d82b552fc1e..95233f701bbb 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt +++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt @@ -25,6 +25,7 @@ import com.android.systemui.back.domain.interactor.BackActionInteractor import com.android.systemui.biometrics.BiometricNotificationService import com.android.systemui.clipboardoverlay.ClipboardListener import com.android.systemui.communal.log.CommunalLoggerStartable +import com.android.systemui.communal.widgets.CommunalAppWidgetHostStartable import com.android.systemui.controls.dagger.StartControlsStartableModule import com.android.systemui.dagger.qualifiers.PerUser import com.android.systemui.dreams.AssistantAttentionMonitor @@ -324,4 +325,11 @@ abstract class SystemUICoreStartableModule { @IntoMap @ClassKey(CommunalLoggerStartable::class) abstract fun bindCommunalLoggerStartable(impl: CommunalLoggerStartable): CoreStartable + + @Binds + @IntoMap + @ClassKey(CommunalAppWidgetHostStartable::class) + abstract fun bindCommunalAppWidgetHostStartable( + impl: CommunalAppWidgetHostStartable + ): CoreStartable } diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/data/repository/DeviceEntryFaceAuthRepository.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/data/repository/DeviceEntryFaceAuthRepository.kt index 7a70c4ac9fab..cf7d60140aee 100644 --- a/packages/SystemUI/src/com/android/systemui/deviceentry/data/repository/DeviceEntryFaceAuthRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/deviceentry/data/repository/DeviceEntryFaceAuthRepository.kt @@ -40,8 +40,7 @@ import com.android.systemui.deviceentry.shared.model.FailedFaceAuthenticationSta import com.android.systemui.deviceentry.shared.model.HelpFaceAuthenticationStatus import com.android.systemui.deviceentry.shared.model.SuccessFaceAuthenticationStatus import com.android.systemui.dump.DumpManager -import com.android.systemui.flags.FeatureFlags -import com.android.systemui.flags.Flags +import com.android.systemui.keyguard.KeyguardWmStateRefactor import com.android.systemui.keyguard.data.repository.BiometricSettingsRepository import com.android.systemui.keyguard.data.repository.BiometricType import com.android.systemui.keyguard.data.repository.DeviceEntryFingerprintAuthRepository @@ -63,10 +62,6 @@ import com.android.systemui.statusbar.phone.KeyguardBypassController import com.android.systemui.user.data.model.SelectionStatus import com.android.systemui.user.data.repository.UserRepository import com.google.errorprone.annotations.CompileTimeConstant -import java.io.PrintWriter -import java.util.Arrays -import java.util.stream.Collectors -import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job @@ -88,6 +83,10 @@ import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import java.io.PrintWriter +import java.util.Arrays +import java.util.stream.Collectors +import javax.inject.Inject /** * API to run face authentication and detection for device entry / on keyguard (as opposed to the @@ -165,7 +164,6 @@ constructor( @FaceAuthTableLog private val faceAuthLog: TableLogBuffer, private val keyguardTransitionInteractor: KeyguardTransitionInteractor, private val displayStateInteractor: DisplayStateInteractor, - private val featureFlags: FeatureFlags, dumpManager: DumpManager, ) : DeviceEntryFaceAuthRepository, Dumpable { private var authCancellationSignal: CancellationSignal? = null @@ -315,7 +313,7 @@ constructor( // or device starts going to sleep. merge( powerInteractor.isAsleep, - if (featureFlags.isEnabled(Flags.KEYGUARD_WM_STATE_REFACTOR)) { + if (KeyguardWmStateRefactor.isEnabled) { keyguardTransitionInteractor.isInTransitionToState(KeyguardState.GONE) } else { keyguardRepository.keyguardDoneAnimationsFinished.map { true } diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/data/repository/DeviceEntryRepository.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/data/repository/DeviceEntryRepository.kt index 08e8c2d8271f..82834387f7b8 100644 --- a/packages/SystemUI/src/com/android/systemui/deviceentry/data/repository/DeviceEntryRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/deviceentry/data/repository/DeviceEntryRepository.kt @@ -8,35 +8,26 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.keyguard.data.repository.KeyguardRepository -import com.android.systemui.keyguard.shared.model.BiometricUnlockModel -import com.android.systemui.keyguard.shared.model.BiometricUnlockSource import com.android.systemui.statusbar.phone.KeyguardBypassController import com.android.systemui.statusbar.policy.KeyguardStateController import com.android.systemui.user.data.repository.UserRepository -import com.android.systemui.util.kotlin.sample import dagger.Binds import dagger.Module 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.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.filter -import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.withContext /** Interface for classes that can access device-entry-related application state. */ interface DeviceEntryRepository { - /** Whether the device is immediately entering the device after a biometric unlock. */ - val enteringDeviceFromBiometricUnlock: Flow<BiometricUnlockSource> - /** * Whether the device is unlocked. * @@ -85,12 +76,6 @@ constructor( keyguardStateController: KeyguardStateController, keyguardRepository: KeyguardRepository, ) : DeviceEntryRepository { - override val enteringDeviceFromBiometricUnlock = - keyguardRepository.biometricUnlockState - .filter { BiometricUnlockModel.dismissesKeyguard(it) } - .sample( - keyguardRepository.biometricUnlockSource.filterNotNull(), - ) private val _isUnlocked = MutableStateFlow(false) diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/AuthRippleInteractor.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/AuthRippleInteractor.kt new file mode 100644 index 000000000000..337fe1ea7d98 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/AuthRippleInteractor.kt @@ -0,0 +1,55 @@ +/* + * 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.deviceentry.domain.interactor + +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.keyguard.shared.model.BiometricUnlockSource +import javax.inject.Inject +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.merge + +/** Business logic for device entry auth ripple interactions. */ +@ExperimentalCoroutinesApi +@SysUISingleton +class AuthRippleInteractor +@Inject +constructor( + deviceEntrySourceInteractor: DeviceEntrySourceInteractor, + deviceEntryUdfpsInteractor: DeviceEntryUdfpsInteractor, +) { + private val showUnlockRippleFromDeviceEntryIcon: Flow<BiometricUnlockSource> = + deviceEntryUdfpsInteractor.isUdfpsSupported.flatMapLatest { isUdfpsSupported -> + if (isUdfpsSupported) { + deviceEntrySourceInteractor.deviceEntryFromDeviceEntryIcon.map { + BiometricUnlockSource.FINGERPRINT_SENSOR + } + } else { + emptyFlow() + } + } + + private val showUnlockRippleFromBiometricUnlock: Flow<BiometricUnlockSource> = + deviceEntrySourceInteractor.deviceEntryFromBiometricSource + val showUnlockRipple: Flow<BiometricUnlockSource> = + merge( + showUnlockRippleFromDeviceEntryIcon, + showUnlockRippleFromBiometricUnlock, + ) +} diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractor.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractor.kt index 649a9715ffea..782bce494d11 100644 --- a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractor.kt @@ -46,7 +46,7 @@ import kotlinx.coroutines.flow.onStart class DeviceEntryHapticsInteractor @Inject constructor( - deviceEntryInteractor: DeviceEntryInteractor, + deviceEntrySourceInteractor: DeviceEntrySourceInteractor, deviceEntryFingerprintAuthInteractor: DeviceEntryFingerprintAuthInteractor, deviceEntryBiometricAuthInteractor: DeviceEntryBiometricAuthInteractor, fingerprintPropertyRepository: FingerprintPropertyRepository, @@ -80,7 +80,7 @@ constructor( } val playSuccessHaptic: Flow<Unit> = - deviceEntryInteractor.enteringDeviceFromBiometricUnlock + deviceEntrySourceInteractor.deviceEntryFromBiometricSource .sample( combine( powerButtonSideFpsEnrolled, diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractor.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractor.kt index 09853578d3f1..73389cb1bdce 100644 --- a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractor.kt @@ -23,7 +23,6 @@ import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.deviceentry.data.repository.DeviceEntryFaceAuthRepository import com.android.systemui.deviceentry.data.repository.DeviceEntryRepository import com.android.systemui.keyguard.data.repository.TrustRepository -import com.android.systemui.keyguard.shared.model.BiometricUnlockSource import com.android.systemui.scene.domain.interactor.SceneInteractor import com.android.systemui.scene.shared.flag.SceneContainerFlags import com.android.systemui.scene.shared.model.SceneKey @@ -31,7 +30,6 @@ import com.android.systemui.scene.shared.model.SceneModel import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.collectLatest @@ -55,7 +53,7 @@ class DeviceEntryInteractor @Inject constructor( @Application private val applicationScope: CoroutineScope, - repository: DeviceEntryRepository, + private val repository: DeviceEntryRepository, private val authenticationInteractor: AuthenticationInteractor, private val sceneInteractor: SceneInteractor, deviceEntryFaceAuthRepository: DeviceEntryFaceAuthRepository, @@ -63,9 +61,6 @@ constructor( flags: SceneContainerFlags, deviceUnlockedInteractor: DeviceUnlockedInteractor, ) { - val enteringDeviceFromBiometricUnlock: Flow<BiometricUnlockSource> = - repository.enteringDeviceFromBiometricUnlock - /** * Whether the device is unlocked. * diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntrySourceInteractor.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntrySourceInteractor.kt new file mode 100644 index 000000000000..d4f76a84c016 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntrySourceInteractor.kt @@ -0,0 +1,64 @@ +/* + * 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.deviceentry.domain.interactor + +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor +import com.android.systemui.keyguard.shared.model.BiometricUnlockModel +import com.android.systemui.keyguard.shared.model.BiometricUnlockSource +import com.android.systemui.util.kotlin.sample +import javax.inject.Inject +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.map + +/** + * Hosts application business logic related to the source of the user entering the device. Note: The + * source of the user entering the device isn't equivalent to the reason the device is unlocked. + * + * For example, the user successfully enters the device when they dismiss the lockscreen via a + * bypass biometric or, if the device is already unlocked, by triggering an affordance that + * dismisses the lockscreen. + */ +@ExperimentalCoroutinesApi +@SysUISingleton +class DeviceEntrySourceInteractor +@Inject +constructor( + keyguardInteractor: KeyguardInteractor, +) { + val deviceEntryFromBiometricSource: Flow<BiometricUnlockSource> = + keyguardInteractor.biometricUnlockState + .filter { BiometricUnlockModel.dismissesKeyguard(it) } + .sample( + keyguardInteractor.biometricUnlockSource.filterNotNull(), + ) + + private val attemptEnterDeviceFromDeviceEntryIcon: MutableSharedFlow<Unit> = MutableSharedFlow() + val deviceEntryFromDeviceEntryIcon: Flow<Unit> = + attemptEnterDeviceFromDeviceEntryIcon + .sample(keyguardInteractor.isKeyguardDismissible) + .filter { it } // only send events if the keyguard is dismissible + .map {} // map to Unit + + suspend fun attemptEnterDeviceFromDeviceEntryIcon() { + attemptEnterDeviceFromDeviceEntryIcon.emit(Unit) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt index d2883cce06c2..c69c9ef93761 100644 --- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt +++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt @@ -220,19 +220,6 @@ object Flags { @JvmField val WALLPAPER_PICKER_PREVIEW_ANIMATION = releasedFlag("wallpaper_picker_preview_animation") - /** - * TODO(b/278086361): Tracking bug - * Complete rewrite of the interactions between System UI and Window Manager involving keyguard - * state. When enabled, calls to ActivityTaskManagerService from System UI will exclusively - * occur from [WmLockscreenVisibilityManager] rather than the legacy KeyguardViewMediator. - * - * This flag is under development; some types of unlock may not animate properly if you enable - * it. - */ - @JvmField - val KEYGUARD_WM_STATE_REFACTOR: UnreleasedFlag = - unreleasedFlag("keyguard_wm_state_refactor") - // 300 - power menu // TODO(b/254512600): Tracking Bug @JvmField val POWER_MENU_LITE = releasedFlag("power_menu_lite") diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/BluetoothDialog.java b/packages/SystemUI/src/com/android/systemui/keyboard/BluetoothDialogDelegate.java index 5deea9bc737a..98642d75f907 100644 --- a/packages/SystemUI/src/com/android/systemui/keyboard/BluetoothDialog.java +++ b/packages/SystemUI/src/com/android/systemui/keyboard/BluetoothDialogDelegate.java @@ -16,17 +16,25 @@ package com.android.systemui.keyboard; -import android.content.Context; import android.view.WindowManager; import com.android.systemui.statusbar.phone.SystemUIDialog; -public class BluetoothDialog extends SystemUIDialog { +import javax.inject.Inject; - public BluetoothDialog(Context context) { - super(context); +public class BluetoothDialogDelegate implements SystemUIDialog.Delegate{ - getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_DIALOG); - setShowForAllUsers(true); + private final SystemUIDialog.Factory mSystemUIDialogFactory; + @Inject + public BluetoothDialogDelegate(SystemUIDialog.Factory systemUIDialogFactory) { + mSystemUIDialogFactory = systemUIDialogFactory; + } + + @Override + public SystemUIDialog createDialog() { + SystemUIDialog dialog = mSystemUIDialogFactory.create(this); + dialog.getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_DIALOG); + dialog.setShowForAllUsers(true); + return dialog; } } diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/KeyboardUI.java b/packages/SystemUI/src/com/android/systemui/keyboard/KeyboardUI.java index 1cdbe6fed58a..17e3ca6f1a8c 100644 --- a/packages/SystemUI/src/com/android/systemui/keyboard/KeyboardUI.java +++ b/packages/SystemUI/src/com/android/systemui/keyboard/KeyboardUI.java @@ -52,6 +52,7 @@ import com.android.settingslib.bluetooth.LocalBluetoothProfileManager; import com.android.systemui.CoreStartable; import com.android.systemui.res.R; import com.android.systemui.dagger.SysUISingleton; +import com.android.systemui.statusbar.phone.SystemUIDialog; import com.android.systemui.util.settings.SecureSettings; import java.io.PrintWriter; @@ -109,6 +110,7 @@ public class KeyboardUI implements CoreStartable, InputManager.OnTabletModeChang private final Provider<LocalBluetoothManager> mBluetoothManagerProvider; private final SecureSettings mSecureSettings; + private final BluetoothDialogDelegate mBluetoothDialogDelegate; private boolean mEnabled; private String mKeyboardName; @@ -121,16 +123,20 @@ public class KeyboardUI implements CoreStartable, InputManager.OnTabletModeChang private int mInTabletMode = InputManager.SWITCH_STATE_UNKNOWN; private int mScanAttempt = 0; private ScanCallback mScanCallback; - private BluetoothDialog mDialog; + private SystemUIDialog mDialog; private int mState; @Inject - public KeyboardUI(Context context, Provider<LocalBluetoothManager> bluetoothManagerProvider, - SecureSettings secureSettings) { + public KeyboardUI( + Context context, + Provider<LocalBluetoothManager> bluetoothManagerProvider, + SecureSettings secureSettings, + BluetoothDialogDelegate bluetoothDialogDelegate) { mContext = context; this.mBluetoothManagerProvider = bluetoothManagerProvider; mSecureSettings = secureSettings; + mBluetoothDialogDelegate = bluetoothDialogDelegate; } @Override @@ -437,7 +443,7 @@ public class KeyboardUI implements CoreStartable, InputManager.OnTabletModeChang new BluetoothDialogClickListener(); DialogInterface.OnDismissListener dismissListener = new BluetoothDialogDismissListener(); - mDialog = new BluetoothDialog(mContext); + mDialog = mBluetoothDialogDelegate.createDialog(); mDialog.setTitle(R.string.enable_bluetooth_title); mDialog.setMessage(R.string.enable_bluetooth_message); mDialog.setPositiveButton( diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/stickykeys/StickyKeysLogger.kt b/packages/SystemUI/src/com/android/systemui/keyboard/stickykeys/StickyKeysLogger.kt index 4ed812010100..5ef5ef19c809 100644 --- a/packages/SystemUI/src/com/android/systemui/keyboard/stickykeys/StickyKeysLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/keyboard/stickykeys/StickyKeysLogger.kt @@ -43,4 +43,13 @@ class StickyKeysLogger @Inject constructor(@KeyboardLog private val buffer: LogB { "new sticky keys state received: $str1" } ) } -}
\ No newline at end of file + + fun logNewSettingValue(enabled: Boolean) { + buffer.log( + TAG, + LogLevel.INFO, + { bool1 = enabled }, + { "sticky key setting changed, new state: ${if (bool1) "enabled" else "disabled"}" } + ) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/stickykeys/data/repository/StickyKeysRepository.kt b/packages/SystemUI/src/com/android/systemui/keyboard/stickykeys/data/repository/StickyKeysRepository.kt index 34d288815570..ec29bd6014ef 100644 --- a/packages/SystemUI/src/com/android/systemui/keyboard/stickykeys/data/repository/StickyKeysRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyboard/stickykeys/data/repository/StickyKeysRepository.kt @@ -19,8 +19,10 @@ package com.android.systemui.keyboard.stickykeys.data.repository import android.hardware.input.InputManager import android.hardware.input.InputManager.StickyModifierStateListener import android.hardware.input.StickyModifierState +import android.provider.Settings 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.Background import com.android.systemui.keyboard.stickykeys.StickyKeysLogger import com.android.systemui.keyboard.stickykeys.shared.model.Locked @@ -30,14 +32,19 @@ import com.android.systemui.keyboard.stickykeys.shared.model.ModifierKey.ALT_GR import com.android.systemui.keyboard.stickykeys.shared.model.ModifierKey.CTRL import com.android.systemui.keyboard.stickykeys.shared.model.ModifierKey.META import com.android.systemui.keyboard.stickykeys.shared.model.ModifierKey.SHIFT +import com.android.systemui.user.data.repository.UserRepository +import com.android.systemui.util.settings.SecureSettings +import com.android.systemui.util.settings.SettingsProxyExt.observerFlow import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.onStart import javax.inject.Inject interface StickyKeysRepository { @@ -45,11 +52,15 @@ interface StickyKeysRepository { val settingEnabled: Flow<Boolean> } +@SysUISingleton +@OptIn(ExperimentalCoroutinesApi::class) class StickyKeysRepositoryImpl @Inject constructor( private val inputManager: InputManager, @Background private val backgroundDispatcher: CoroutineDispatcher, + private val secureSettings: SecureSettings, + userRepository: UserRepository, private val stickyKeysLogger: StickyKeysLogger, ) : StickyKeysRepository { @@ -66,8 +77,26 @@ constructor( .onEach { stickyKeysLogger.logNewStickyKeysReceived(it) } .flowOn(backgroundDispatcher) - // TODO(b/319837892): Implement reading actual setting - override val settingEnabled: StateFlow<Boolean> = MutableStateFlow(true) + override val settingEnabled: Flow<Boolean> = + userRepository.selectedUserInfo + .flatMapLatest { stickyKeySettingObserver(it.id) } + .flowOn(backgroundDispatcher) + + private fun stickyKeySettingObserver(userId: Int): Flow<Boolean> { + return secureSettings + .observerFlow(userId, SETTING_KEY) + .onStart { emit(Unit) } + .map { isSettingEnabledForCurrentUser(userId) } + .distinctUntilChanged() + .onEach { stickyKeysLogger.logNewSettingValue(it) } + } + + private fun isSettingEnabledForCurrentUser(userId: Int) = + secureSettings.getIntForUser( + /* name= */ SETTING_KEY, + /* default= */ 0, + /* userHandle= */ userId + ) != 0 private fun toStickyKeysMap(state: StickyModifierState): LinkedHashMap<ModifierKey, Locked> { val keys = linkedMapOf<ModifierKey, Locked>() @@ -88,5 +117,6 @@ constructor( companion object { const val TAG = "StickyKeysRepositoryImpl" + const val SETTING_KEY = Settings.Secure.ACCESSIBILITY_STICKY_KEYS } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java index e2ab20e29e2d..f10b87ee1cc5 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java @@ -77,7 +77,6 @@ import com.android.keyguard.mediator.ScreenOnCoordinator; import com.android.systemui.SystemUIApplication; import com.android.systemui.dagger.qualifiers.Application; import com.android.systemui.flags.FeatureFlags; -import com.android.systemui.flags.Flags; import com.android.systemui.keyguard.ui.binder.KeyguardSurfaceBehindParamsApplier; import com.android.systemui.keyguard.ui.binder.KeyguardSurfaceBehindViewBinder; import com.android.systemui.keyguard.ui.binder.WindowManagerLockscreenVisibilityViewBinder; @@ -329,7 +328,7 @@ public class KeyguardService extends Service { mFlags = featureFlags; mPowerInteractor = powerInteractor; - if (mFlags.isEnabled(Flags.KEYGUARD_WM_STATE_REFACTOR)) { + if (KeyguardWmStateRefactor.isEnabled()) { WindowManagerLockscreenVisibilityViewBinder.bind( wmLockscreenVisibilityViewModel, wmLockscreenVisibilityManager, diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt index 01ba0d214a97..53c81e537708 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt @@ -419,7 +419,7 @@ class KeyguardUnlockAnimationController @Inject constructor( */ fun canPerformInWindowLauncherAnimations(): Boolean { // TODO(b/278086361): Refactor in-window animations. - return !featureFlags.isEnabled(Flags.KEYGUARD_WM_STATE_REFACTOR) && + return !KeyguardWmStateRefactor.isEnabled && isSupportedLauncherUnderneath() && // If the launcher is underneath, but we're about to launch an activity, don't do // the animations since they won't be visible. @@ -866,7 +866,7 @@ class KeyguardUnlockAnimationController @Inject constructor( } surfaceBehindRemoteAnimationTargets?.forEach { surfaceBehindRemoteAnimationTarget -> - if (!featureFlags.isEnabled(Flags.KEYGUARD_WM_STATE_REFACTOR)) { + if (!KeyguardWmStateRefactor.isEnabled) { val surfaceHeight: Int = surfaceBehindRemoteAnimationTarget.screenSpaceBounds.height() @@ -1005,7 +1005,7 @@ class KeyguardUnlockAnimationController @Inject constructor( if (keyguardStateController.isShowing) { // Hide the keyguard, with no fade out since we animated it away during the unlock. - if (!featureFlags.isEnabled(Flags.KEYGUARD_WM_STATE_REFACTOR)) { + if (!KeyguardWmStateRefactor.isEnabled) { keyguardViewController.hide( surfaceBehindRemoteAnimationStartTime, 0 /* fadeOutDuration */ diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java index 50caf17f71dd..794befa3725d 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java @@ -139,7 +139,6 @@ import com.android.systemui.dagger.qualifiers.UiBackground; import com.android.systemui.dreams.DreamOverlayStateController; import com.android.systemui.dump.DumpManager; import com.android.systemui.flags.FeatureFlags; -import com.android.systemui.flags.Flags; import com.android.systemui.flags.SystemPropertiesHelper; import com.android.systemui.keyguard.dagger.KeyguardModule; import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor; @@ -175,8 +174,6 @@ 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; @@ -186,6 +183,7 @@ import java.util.Objects; import java.util.concurrent.Executor; import java.util.function.Consumer; +import dagger.Lazy; import kotlinx.coroutines.CoroutineDispatcher; /** @@ -1051,7 +1049,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, IRemoteAnimationFinishedCallback finishedCallback) { Trace.beginSection("mExitAnimationRunner.onAnimationStart#startKeyguardExitAnimation"); startKeyguardExitAnimation(transit, apps, wallpapers, nonApps, finishedCallback); - if (mFeatureFlags.isEnabled(Flags.KEYGUARD_WM_STATE_REFACTOR)) { + if (KeyguardWmStateRefactor.isEnabled()) { mWmLockscreenVisibilityManager.get().onKeyguardGoingAwayRemoteAnimationStart( transit, apps, wallpapers, nonApps, finishedCallback); } @@ -1061,7 +1059,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, @Override // Binder interface public void onAnimationCancelled() { cancelKeyguardExitAnimation(); - if (mFeatureFlags.isEnabled(Flags.KEYGUARD_WM_STATE_REFACTOR)) { + if (KeyguardWmStateRefactor.isEnabled()) { mWmLockscreenVisibilityManager.get().onKeyguardGoingAwayRemoteAnimationCancelled(); } } @@ -2186,6 +2184,10 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, */ public void showDismissibleKeyguard() { if (mFoldGracePeriodProvider.isEnabled()) { + if (!mUpdateMonitor.isDeviceProvisioned()) { + Log.d(TAG, "Device not provisioned, so ignore request to show keyguard."); + return; + } Bundle showKeyguardUnlocked = new Bundle(); showKeyguardUnlocked.putBoolean(OPTION_SHOW_DISMISSIBLE, true); showKeyguard(showKeyguardUnlocked); @@ -2757,7 +2759,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, mUiBgExecutor.execute(() -> { Log.d(TAG, "updateActivityLockScreenState(" + showing + ", " + aodShowing + ")"); - if (mFeatureFlags.isEnabled(Flags.KEYGUARD_WM_STATE_REFACTOR)) { + if (KeyguardWmStateRefactor.isEnabled()) { // Handled in WmLockscreenVisibilityManager if flag is enabled. return; } @@ -2811,7 +2813,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, setShowingLocked(true, hidingOrGoingAway /* force */); mHiding = false; - if (!mFeatureFlags.isEnabled(Flags.KEYGUARD_WM_STATE_REFACTOR)) { + if (!KeyguardWmStateRefactor.isEnabled()) { // Handled directly in StatusBarKeyguardViewManager if enabled. mKeyguardViewControllerLazy.get().show(options); } @@ -2888,7 +2890,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, mKeyguardViewControllerLazy.get().setKeyguardGoingAwayState(true); // Handled in WmLockscreenVisibilityManager if flag is enabled. - if (!mFeatureFlags.isEnabled(Flags.KEYGUARD_WM_STATE_REFACTOR)) { + if (!KeyguardWmStateRefactor.isEnabled()) { // Don't actually hide the Keyguard at the moment, wait for window manager // until it tells us it's safe to do so with startKeyguardExitAnimation. // Posting to mUiOffloadThread to ensure that calls to ActivityTaskManager @@ -2994,7 +2996,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, } else { Log.d(TAG, "Hiding keyguard while occluded. Just hide the keyguard view and exit."); - if (!mFeatureFlags.isEnabled(Flags.KEYGUARD_WM_STATE_REFACTOR)) { + if (!KeyguardWmStateRefactor.isEnabled()) { mKeyguardViewControllerLazy.get().hide( mSystemClock.uptimeMillis() + mHideAnimation.getStartOffset(), mHideAnimation.getDuration()); @@ -3030,7 +3032,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, // If the flag is enabled, remote animation state is handled in // WmLockscreenVisibilityManager. if (finishedCallback != null - && !mFeatureFlags.isEnabled(Flags.KEYGUARD_WM_STATE_REFACTOR)) { + && !KeyguardWmStateRefactor.isEnabled()) { // There will not execute animation, send a finish callback to ensure the remote // animation won't hang there. try { @@ -3056,7 +3058,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, new IRemoteAnimationFinishedCallback() { @Override public void onAnimationFinished() throws RemoteException { - if (!mFeatureFlags.isEnabled(Flags.KEYGUARD_WM_STATE_REFACTOR)) { + if (!KeyguardWmStateRefactor.isEnabled()) { try { finishedCallback.onAnimationFinished(); } catch (RemoteException e) { @@ -3088,7 +3090,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, // it will dismiss the panel in that case. } else if (!mStatusBarStateController.leaveOpenOnKeyguardHide() && apps != null && apps.length > 0) { - if (!mFeatureFlags.isEnabled(Flags.KEYGUARD_WM_STATE_REFACTOR)) { + if (!KeyguardWmStateRefactor.isEnabled()) { // Handled in WmLockscreenVisibilityManager. Other logic in this class will // short circuit when this is null. mSurfaceBehindRemoteAnimationFinishedCallback = finishedCallback; @@ -3112,7 +3114,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, createInteractionJankMonitorConf( CUJ_LOCKSCREEN_UNLOCK_ANIMATION, "RemoteAnimationDisabled")); - if (!mFeatureFlags.isEnabled(Flags.KEYGUARD_WM_STATE_REFACTOR)) { + if (!KeyguardWmStateRefactor.isEnabled()) { // Handled directly in StatusBarKeyguardViewManager if enabled. mKeyguardViewControllerLazy.get().hide(startTime, fadeoutDuration); } @@ -3131,7 +3133,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, Slog.e(TAG, "Keyguard exit without a corresponding app to show."); try { - if (!mFeatureFlags.isEnabled(Flags.KEYGUARD_WM_STATE_REFACTOR)) { + if (!KeyguardWmStateRefactor.isEnabled()) { finishedCallback.onAnimationFinished(); } } catch (RemoteException e) { @@ -3163,7 +3165,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, @Override public void onAnimationEnd(Animator animation) { try { - if (!mFeatureFlags.isEnabled(Flags.KEYGUARD_WM_STATE_REFACTOR)) { + if (!KeyguardWmStateRefactor.isEnabled()) { finishedCallback.onAnimationFinished(); } } catch (RemoteException e) { @@ -3176,7 +3178,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, @Override public void onAnimationCancel(Animator animation) { try { - if (!mFeatureFlags.isEnabled(Flags.KEYGUARD_WM_STATE_REFACTOR)) { + if (!KeyguardWmStateRefactor.isEnabled()) { finishedCallback.onAnimationFinished(); } } catch (RemoteException e) { @@ -3341,7 +3343,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, flags |= KEYGUARD_GOING_AWAY_FLAG_TO_LAUNCHER_CLEAR_SNAPSHOT; } - if (!mFeatureFlags.isEnabled(Flags.KEYGUARD_WM_STATE_REFACTOR)) { + if (!KeyguardWmStateRefactor.isEnabled()) { // Handled in WmLockscreenVisibilityManager. mActivityTaskManagerService.keyguardGoingAway(flags); } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardWmStateRefactor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardWmStateRefactor.kt new file mode 100644 index 000000000000..ddccc5d9e96d --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardWmStateRefactor.kt @@ -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.systemui.keyguard + +import com.android.systemui.Flags +import com.android.systemui.flags.FlagToken +import com.android.systemui.flags.RefactorFlagUtils + +/** Helper for reading or using the keyguard wm state refactor flag state. */ +@Suppress("NOTHING_TO_INLINE") +object KeyguardWmStateRefactor { + /** The aconfig flag name */ + const val FLAG_NAME = Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR + + /** A token used for dependency declaration */ + val token: FlagToken + get() = FlagToken(FLAG_NAME, isEnabled) + + /** Is the refactor enabled */ + @JvmStatic + inline val isEnabled + get() = Flags.keyguardWmStateRefactor() + + /** + * 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/data/repository/KeyguardRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt index 14371949c9c8..64e28700aa74 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt @@ -101,7 +101,7 @@ interface KeyguardRepository { * Whether the device is locked or unlocked right now. This is true when keyguard has been * dismissed or can be dismissed by a swipe */ - val isKeyguardUnlocked: StateFlow<Boolean> + val isKeyguardDismissible: StateFlow<Boolean> /** * Observable for the signal that keyguard is about to go away. @@ -388,7 +388,7 @@ constructor( } .distinctUntilChanged() - override val isKeyguardUnlocked: StateFlow<Boolean> = + override val isKeyguardDismissible: StateFlow<Boolean> = conflatedCallbackFlow { val callback = object : KeyguardStateController.Callback { @@ -396,7 +396,7 @@ constructor( trySendWithFailureLogging( keyguardStateController.isUnlocked, TAG, - "updated isKeyguardUnlocked due to onUnlockedChanged" + "updated isKeyguardDismissible due to onUnlockedChanged" ) } @@ -404,7 +404,7 @@ constructor( trySendWithFailureLogging( keyguardStateController.isUnlocked, TAG, - "updated isKeyguardUnlocked due to onKeyguardShowingChanged" + "updated isKeyguardDismissible due to onKeyguardShowingChanged" ) } } @@ -567,17 +567,17 @@ constructor( val callback = object : KeyguardUpdateMonitorCallback() { override fun onStrongAuthStateChanged(userId: Int) { - trySend(userId) + trySendWithFailureLogging(userId, TAG, "strong auth state change") } } - keyguardUpdateMonitor.registerCallback(callback) - awaitClose { keyguardUpdateMonitor.removeCallback(callback) } } .filter { userId -> userId == userTracker.userId } .onStart { emit(userTracker.userId) } .mapLatest { userId -> keyguardUpdateMonitor.isEncryptedOrLockdown(userId) } + // KeyguardUpdateMonitor#registerCallback needs to be called on the main thread. + .flowOn(mainDispatcher) override fun isKeyguardShowing(): Boolean { return keyguardStateController.isShowing 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 8b2b45f1bf57..3965648bd224 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 @@ -23,7 +23,7 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.flags.FeatureFlags -import com.android.systemui.flags.Flags +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 @@ -230,7 +230,7 @@ constructor( combine( startedKeyguardTransitionStep, keyguardInteractor.statusBarState, - keyguardInteractor.isKeyguardUnlocked, + keyguardInteractor.isKeyguardDismissible, ::Triple ), ::toQuad @@ -307,7 +307,7 @@ constructor( } private fun listenForLockscreenToGone() { - if (flags.isEnabled(Flags.KEYGUARD_WM_STATE_REFACTOR)) { + if (KeyguardWmStateRefactor.isEnabled) { return } @@ -324,7 +324,7 @@ constructor( } private fun listenForLockscreenToGoneDragging() { - if (flags.isEnabled(Flags.KEYGUARD_WM_STATE_REFACTOR)) { + if (KeyguardWmStateRefactor.isEnabled) { return } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractor.kt index 33b6373d5876..acbd9fb4c407 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractor.kt @@ -23,7 +23,7 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.flags.FeatureFlags -import com.android.systemui.flags.Flags +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 @@ -217,7 +217,7 @@ constructor( } private fun listenForPrimaryBouncerToGone() { - if (flags.isEnabled(Flags.KEYGUARD_WM_STATE_REFACTOR)) { + if (KeyguardWmStateRefactor.isEnabled) { // This is handled in KeyguardSecurityContainerController and // StatusBarKeyguardViewManager, which calls the transition interactor to kick off a // transition vs. listening to legacy state flags. diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt index 36bd905d23ac..22d11d08054e 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt @@ -32,6 +32,7 @@ import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor import com.android.systemui.dagger.SysUISingleton import com.android.systemui.keyguard.data.repository.KeyguardRepository import com.android.systemui.keyguard.shared.model.BiometricUnlockModel +import com.android.systemui.keyguard.shared.model.BiometricUnlockSource import com.android.systemui.keyguard.shared.model.CameraLaunchSourceModel import com.android.systemui.keyguard.shared.model.DozeStateModel import com.android.systemui.keyguard.shared.model.DozeStateModel.Companion.isDozeOff @@ -162,8 +163,8 @@ constructor( /** Whether the keyguard is showing or not. */ val isKeyguardShowing: Flow<Boolean> = repository.isKeyguardShowing - /** Whether the keyguard is unlocked or not. */ - val isKeyguardUnlocked: Flow<Boolean> = repository.isKeyguardUnlocked + /** Whether the keyguard is dismissible or not. */ + val isKeyguardDismissible: Flow<Boolean> = repository.isKeyguardDismissible /** Whether the keyguard is occluded (covered by an activity). */ val isKeyguardOccluded: Flow<Boolean> = repository.isKeyguardOccluded @@ -194,6 +195,9 @@ constructor( /** Observable for the [StatusBarState] */ val statusBarState: Flow<StatusBarState> = repository.statusBarState + /** Source of the most recent biometric unlock, such as fingerprint or face. */ + val biometricUnlockSource: Flow<BiometricUnlockSource?> = repository.biometricUnlockSource + /** * Observable for [BiometricUnlockModel] when biometrics like face or any fingerprint (rear, * side, under display) is used to unlock the device. diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/DeviceEntryIconViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/DeviceEntryIconViewBinder.kt index a02e8ac84a75..703bb879533c 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/DeviceEntryIconViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/DeviceEntryIconViewBinder.kt @@ -32,6 +32,7 @@ import com.android.systemui.keyguard.ui.viewmodel.DeviceEntryIconViewModel import com.android.systemui.lifecycle.repeatWhenAttached import com.android.systemui.plugins.FalsingManager import com.android.systemui.statusbar.VibratorHelper +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.launch @@ -48,6 +49,7 @@ object DeviceEntryIconViewBinder { @SuppressLint("ClickableViewAccessibility") @JvmStatic fun bind( + applicationScope: CoroutineScope, view: DeviceEntryIconView, viewModel: DeviceEntryIconViewModel, fgViewModel: DeviceEntryForegroundViewModel, @@ -69,7 +71,7 @@ object DeviceEntryIconViewBinder { view, HapticFeedbackConstants.CONFIRM, ) - viewModel.onLongPress() + applicationScope.launch { viewModel.onLongPress() } } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodNotificationIconsSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodNotificationIconsSection.kt index bc6c7cbf35fb..ad589dfcff9e 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodNotificationIconsSection.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodNotificationIconsSection.kt @@ -120,8 +120,20 @@ constructor( } else { connect(nicId, TOP, R.id.keyguard_status_view, topAlignment, bottomMargin) } - connect(nicId, START, PARENT_ID, START) - connect(nicId, END, PARENT_ID, END) + connect( + nicId, + START, + PARENT_ID, + START, + context.resources.getDimensionPixelSize(R.dimen.status_view_margin_horizontal) + ) + connect( + nicId, + END, + PARENT_ID, + END, + context.resources.getDimensionPixelSize(R.dimen.status_view_margin_horizontal) + ) constrainHeight( nicId, context.resources.getDimensionPixelSize(R.dimen.notification_shelf_height) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt index a1b3f270f642..fe4f07d022dd 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt @@ -31,6 +31,7 @@ import androidx.constraintlayout.widget.ConstraintSet.TOP import androidx.constraintlayout.widget.ConstraintSet.VISIBLE import androidx.constraintlayout.widget.ConstraintSet.WRAP_CONTENT import com.android.systemui.Flags +import com.android.systemui.customization.R as customizationR import com.android.systemui.keyguard.domain.interactor.KeyguardBlueprintInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor import com.android.systemui.keyguard.shared.model.KeyguardSection @@ -160,16 +161,14 @@ constructor( var largeClockTopMargin = context.resources.getDimensionPixelSize(R.dimen.status_bar_height) + context.resources.getDimensionPixelSize( - com.android.systemui.customization.R.dimen.small_clock_padding_top + customizationR.dimen.small_clock_padding_top ) + context.resources.getDimensionPixelSize(R.dimen.keyguard_smartspace_top_offset) largeClockTopMargin += getDimen(DATE_WEATHER_VIEW_HEIGHT) largeClockTopMargin += getDimen(ENHANCED_SMARTSPACE_HEIGHT) if (!keyguardClockViewModel.useLargeClock) { largeClockTopMargin -= - context.resources.getDimensionPixelSize( - com.android.systemui.customization.R.dimen.small_clock_height - ) + context.resources.getDimensionPixelSize(customizationR.dimen.small_clock_height) } connect(R.id.lockscreen_clock_view_large, TOP, PARENT_ID, TOP, largeClockTopMargin) constrainHeight(R.id.lockscreen_clock_view_large, WRAP_CONTENT) @@ -177,18 +176,15 @@ constructor( constrainWidth(R.id.lockscreen_clock_view, WRAP_CONTENT) constrainHeight( R.id.lockscreen_clock_view, - context.resources.getDimensionPixelSize( - com.android.systemui.customization.R.dimen.small_clock_height - ) + context.resources.getDimensionPixelSize(customizationR.dimen.small_clock_height) ) connect( R.id.lockscreen_clock_view, START, PARENT_ID, START, - context.resources.getDimensionPixelSize( - com.android.systemui.customization.R.dimen.clock_padding_start - ) + context.resources.getDimensionPixelSize(customizationR.dimen.clock_padding_start) + + context.resources.getDimensionPixelSize(R.dimen.status_view_margin_horizontal) ) var smallClockTopMargin = if (splitShadeStateController.shouldUseSplitNotificationShade(context.resources)) { @@ -199,9 +195,7 @@ constructor( } if (keyguardClockViewModel.useLargeClock) { smallClockTopMargin -= - context.resources.getDimensionPixelSize( - com.android.systemui.customization.R.dimen.small_clock_height - ) + context.resources.getDimensionPixelSize(customizationR.dimen.small_clock_height) } connect(R.id.lockscreen_clock_view, TOP, PARENT_ID, TOP, smallClockTopMargin) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntrySection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntrySection.kt index 0bf9ad02eb58..3fc9b4200f35 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntrySection.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntrySection.kt @@ -31,6 +31,7 @@ import com.android.keyguard.LockIconView import com.android.keyguard.LockIconViewController import com.android.systemui.Flags.keyguardBottomAreaRefactor import com.android.systemui.biometrics.AuthController +import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.deviceentry.shared.DeviceEntryUdfpsRefactor import com.android.systemui.flags.FeatureFlags import com.android.systemui.flags.Flags @@ -46,6 +47,7 @@ import com.android.systemui.shade.NotificationPanelView import com.android.systemui.statusbar.VibratorHelper import dagger.Lazy import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi /** Includes the device entry icon. */ @@ -53,6 +55,7 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi class DefaultDeviceEntrySection @Inject constructor( + @Application private val applicationScope: CoroutineScope, private val keyguardUpdateMonitor: KeyguardUpdateMonitor, private val authController: AuthController, private val windowManager: WindowManager, @@ -91,6 +94,7 @@ constructor( if (DeviceEntryUdfpsRefactor.isEnabled) { constraintLayout.findViewById<DeviceEntryIconView?>(deviceEntryIconViewId)?.let { DeviceEntryIconViewBinder.bind( + applicationScope, it, deviceEntryIconViewModel.get(), deviceEntryForegroundViewModel.get(), diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultNotificationStackScrollLayoutSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultNotificationStackScrollLayoutSection.kt index 8c5e9b4c6817..d75a72f91061 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultNotificationStackScrollLayoutSection.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultNotificationStackScrollLayoutSection.kt @@ -18,7 +18,6 @@ package com.android.systemui.keyguard.ui.view.layout.sections import android.content.Context -import android.view.View import androidx.constraintlayout.widget.ConstraintSet import androidx.constraintlayout.widget.ConstraintSet.BOTTOM import androidx.constraintlayout.widget.ConstraintSet.END @@ -67,7 +66,6 @@ constructor( notificationStackSizeCalculator, mainDispatcher, ) { - private val smartSpaceBarrier = View.generateViewId() override fun applyConstraints(constraintSet: ConstraintSet) { if (!KeyguardShadeMigrationNssl.isEnabled) { return diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SmartspaceSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SmartspaceSection.kt index 37842a84c3d3..2f99719df36c 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SmartspaceSection.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SmartspaceSection.kt @@ -31,7 +31,8 @@ import com.android.systemui.keyguard.shared.model.KeyguardSection import com.android.systemui.keyguard.ui.binder.KeyguardSmartspaceViewBinder import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel import com.android.systemui.keyguard.ui.viewmodel.KeyguardSmartspaceViewModel -import com.android.systemui.shared.R +import com.android.systemui.res.R as R +import com.android.systemui.shared.R as sharedR import com.android.systemui.statusbar.lockscreen.LockscreenSmartspaceController import dagger.Lazy import javax.inject.Inject @@ -100,94 +101,94 @@ constructor( if (!migrateClocksToBlueprint()) { return } + val horizontalPaddingStart = + context.resources.getDimensionPixelSize(R.dimen.below_clock_padding_start) + + context.resources.getDimensionPixelSize(R.dimen.status_view_margin_horizontal) + val horizontalPaddingEnd = + context.resources.getDimensionPixelSize(R.dimen.below_clock_padding_end) + + context.resources.getDimensionPixelSize(R.dimen.status_view_margin_horizontal) constraintSet.apply { // migrate addDateWeatherView, addWeatherView from KeyguardClockSwitchController - constrainHeight(R.id.date_smartspace_view, ConstraintSet.WRAP_CONTENT) - constrainWidth(R.id.date_smartspace_view, ConstraintSet.WRAP_CONTENT) + constrainHeight(sharedR.id.date_smartspace_view, ConstraintSet.WRAP_CONTENT) + constrainWidth(sharedR.id.date_smartspace_view, ConstraintSet.WRAP_CONTENT) connect( - R.id.date_smartspace_view, + sharedR.id.date_smartspace_view, ConstraintSet.START, ConstraintSet.PARENT_ID, ConstraintSet.START, - context.resources.getDimensionPixelSize( - com.android.systemui.res.R.dimen.below_clock_padding_start - ) + horizontalPaddingStart ) - constrainWidth(R.id.weather_smartspace_view, ConstraintSet.WRAP_CONTENT) + constrainWidth(sharedR.id.weather_smartspace_view, ConstraintSet.WRAP_CONTENT) connect( - R.id.weather_smartspace_view, + sharedR.id.weather_smartspace_view, ConstraintSet.TOP, - R.id.date_smartspace_view, + sharedR.id.date_smartspace_view, ConstraintSet.TOP ) connect( - R.id.weather_smartspace_view, + sharedR.id.weather_smartspace_view, ConstraintSet.BOTTOM, - R.id.date_smartspace_view, + sharedR.id.date_smartspace_view, ConstraintSet.BOTTOM ) connect( - R.id.weather_smartspace_view, + sharedR.id.weather_smartspace_view, ConstraintSet.START, - R.id.date_smartspace_view, + sharedR.id.date_smartspace_view, ConstraintSet.END, 4 ) // migrate addSmartspaceView from KeyguardClockSwitchController - constrainHeight(R.id.bc_smartspace_view, ConstraintSet.WRAP_CONTENT) + constrainHeight(sharedR.id.bc_smartspace_view, ConstraintSet.WRAP_CONTENT) connect( - R.id.bc_smartspace_view, + sharedR.id.bc_smartspace_view, ConstraintSet.START, ConstraintSet.PARENT_ID, ConstraintSet.START, - context.resources.getDimensionPixelSize( - com.android.systemui.res.R.dimen.below_clock_padding_start - ) + horizontalPaddingStart ) connect( - R.id.bc_smartspace_view, + sharedR.id.bc_smartspace_view, ConstraintSet.END, if (keyguardClockViewModel.clockShouldBeCentered.value) ConstraintSet.PARENT_ID - else com.android.systemui.res.R.id.split_shade_guideline, + else R.id.split_shade_guideline, ConstraintSet.END, - context.resources.getDimensionPixelSize( - com.android.systemui.res.R.dimen.below_clock_padding_end - ) + horizontalPaddingEnd ) if (keyguardClockViewModel.hasCustomWeatherDataDisplay.value) { - clear(R.id.date_smartspace_view, ConstraintSet.TOP) + clear(sharedR.id.date_smartspace_view, ConstraintSet.TOP) connect( - R.id.date_smartspace_view, + sharedR.id.date_smartspace_view, ConstraintSet.BOTTOM, - R.id.bc_smartspace_view, + sharedR.id.bc_smartspace_view, ConstraintSet.TOP ) } else { - clear(R.id.date_smartspace_view, ConstraintSet.BOTTOM) + clear(sharedR.id.date_smartspace_view, ConstraintSet.BOTTOM) connect( - R.id.date_smartspace_view, + sharedR.id.date_smartspace_view, ConstraintSet.TOP, - com.android.systemui.res.R.id.lockscreen_clock_view, + R.id.lockscreen_clock_view, ConstraintSet.BOTTOM ) connect( - R.id.bc_smartspace_view, + sharedR.id.bc_smartspace_view, ConstraintSet.TOP, - R.id.date_smartspace_view, + sharedR.id.date_smartspace_view, ConstraintSet.BOTTOM ) } createBarrier( - com.android.systemui.res.R.id.smart_space_barrier_bottom, + R.id.smart_space_barrier_bottom, Barrier.BOTTOM, 0, *intArrayOf( - R.id.bc_smartspace_view, - R.id.date_smartspace_view, - R.id.weather_smartspace_view, + sharedR.id.bc_smartspace_view, + sharedR.id.date_smartspace_view, + sharedR.id.weather_smartspace_view, ) ) } @@ -212,7 +213,7 @@ constructor( private fun updateVisibility(constraintSet: ConstraintSet) { constraintSet.apply { setVisibility( - R.id.weather_smartspace_view, + sharedR.id.weather_smartspace_view, when (keyguardClockViewModel.hasCustomWeatherDataDisplay.value) { true -> ConstraintSet.GONE false -> @@ -223,7 +224,7 @@ constructor( } ) setVisibility( - R.id.date_smartspace_view, + sharedR.id.date_smartspace_view, if (keyguardClockViewModel.hasCustomWeatherDataDisplay.value) ConstraintSet.GONE else ConstraintSet.VISIBLE ) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModel.kt index eacaa40de821..a3d54532411c 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModel.kt @@ -20,6 +20,7 @@ import android.animation.FloatEvaluator import android.animation.IntEvaluator import com.android.keyguard.KeyguardViewController import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor +import com.android.systemui.deviceentry.domain.interactor.DeviceEntrySourceInteractor import com.android.systemui.deviceentry.domain.interactor.DeviceEntryUdfpsInteractor import com.android.systemui.keyguard.domain.interactor.BurnInInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor @@ -56,6 +57,7 @@ constructor( private val sceneContainerFlags: SceneContainerFlags, private val keyguardViewController: Lazy<KeyguardViewController>, private val deviceEntryInteractor: DeviceEntryInteractor, + private val deviceEntrySourceInteractor: DeviceEntrySourceInteractor, ) { private val intEvaluator = IntEvaluator() private val floatEvaluator = FloatEvaluator() @@ -208,14 +210,13 @@ constructor( } } - fun onLongPress() { - // TODO (b/309804148): play auth ripple via an interactor - + suspend fun onLongPress() { if (sceneContainerFlags.isEnabled()) { deviceEntryInteractor.attemptDeviceEntry() } else { keyguardViewController.get().showPrimaryBouncer(/* scrim */ true) } + deviceEntrySourceInteractor.attemptEnterDeviceFromDeviceEntryIcon() } private fun DeviceEntryIconView.IconType.toAccessibilityHintType(): diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/NavBarHelper.java b/packages/SystemUI/src/com/android/systemui/navigationbar/NavBarHelper.java index d7e062fb926f..7d13397b374c 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/NavBarHelper.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/NavBarHelper.java @@ -41,6 +41,7 @@ import android.net.Uri; import android.os.Bundle; import android.os.Handler; import android.os.Looper; +import android.os.Process; import android.os.RemoteException; import android.os.UserHandle; import android.provider.Settings; @@ -55,12 +56,14 @@ 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; import com.android.systemui.accessibility.SystemActions; import com.android.systemui.assist.AssistManager; import com.android.systemui.dagger.SysUISingleton; +import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dump.DumpManager; import com.android.systemui.navigationbar.gestural.EdgeBackGestureHandler; import com.android.systemui.recents.OverviewProxyService; @@ -79,6 +82,7 @@ import java.io.PrintWriter; import java.util.ArrayList; import java.util.List; import java.util.Optional; +import java.util.concurrent.Executor; import javax.inject.Inject; @@ -101,6 +105,7 @@ public final class NavBarHelper implements private static final String TAG = NavBarHelper.class.getSimpleName(); private final Handler mHandler = new Handler(Looper.getMainLooper()); + private final Executor mMainExecutor; private final AccessibilityManager mAccessibilityManager; private final Lazy<AssistManager> mAssistManagerLazy; private final Lazy<Optional<CentralSurfaces>> mCentralSurfacesOptionalLazy; @@ -185,7 +190,12 @@ public final class NavBarHelper implements DisplayTracker displayTracker, NotificationShadeWindowController notificationShadeWindowController, DumpManager dumpManager, - CommandQueue commandQueue) { + CommandQueue commandQueue, + @Main Executor mainExecutor) { + // b/319489709: This component shouldn't be running for a non-primary user + if (!Process.myUserHandle().equals(UserHandle.SYSTEM)) { + Log.wtf(TAG, "Unexpected initialization for non-primary user", new Throwable()); + } mContext = context; mNotificationShadeWindowController = notificationShadeWindowController; mCommandQueue = commandQueue; @@ -201,6 +211,7 @@ public final class NavBarHelper implements mWm = wm; mDefaultDisplayId = displayTracker.getDefaultDisplayId(); mEdgeBackGestureHandler = edgeBackGestureHandlerFactory.create(context); + mMainExecutor = mainExecutor; mNavBarMode = navigationModeController.addListener(this); mCommandQueue.addCallback(this); @@ -370,7 +381,7 @@ public final class NavBarHelper implements // permission final List<String> a11yButtonTargets = mAccessibilityManager.getAccessibilityShortcutTargets( - AccessibilityManager.ACCESSIBILITY_BUTTON); + ShortcutConstants.UserShortcutType.SOFTWARE); final int requestingServices = a11yButtonTargets.size(); clickable = requestingServices >= 1; @@ -418,7 +429,11 @@ public final class NavBarHelper implements @Override public void onConnectionChanged(boolean isConnected) { if (isConnected) { - updateAssistantAvailability(); + // We add the OPS callback during construction, so if the service is already connected + // then we will try to get the AssistManager dependency which itself has an indirect + // dependency on NavBarHelper leading to a cycle. For now, we can defer updating the + // assistant availability. + mMainExecutor.execute(this::updateAssistantAvailability); } } diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanelController.kt b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanelController.kt index fa03dc245745..b3d848c2d23b 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanelController.kt +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanelController.kt @@ -34,6 +34,8 @@ import androidx.annotation.VisibleForTesting import androidx.core.os.postDelayed import androidx.core.view.isVisible import androidx.dynamicanimation.animation.DynamicAnimation +import com.android.internal.jank.Cuj.CUJ_BACK_PANEL_ARROW +import com.android.internal.jank.InteractionJankMonitor import com.android.internal.util.LatencyTracker import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.plugins.NavigationEdgeBackPlugin @@ -86,6 +88,7 @@ internal constructor( private val vibratorHelper: VibratorHelper, private val configurationController: ConfigurationController, private val latencyTracker: LatencyTracker, + private val interactionJankMonitor: InteractionJankMonitor, ) : ViewController<BackPanel>(BackPanel(context, latencyTracker)), NavigationEdgeBackPlugin { /** @@ -103,6 +106,7 @@ internal constructor( private val vibratorHelper: VibratorHelper, private val configurationController: ConfigurationController, private val latencyTracker: LatencyTracker, + private val interactionJankMonitor: InteractionJankMonitor, ) { /** Construct a [BackPanelController]. */ fun create(context: Context): BackPanelController { @@ -115,6 +119,7 @@ internal constructor( vibratorHelper, configurationController, latencyTracker, + interactionJankMonitor ) backPanelController.init() return backPanelController @@ -183,7 +188,7 @@ internal constructor( /* Arrow is animating in */ ENTRY, - /* could be entry, neutral, or stretched, releasing will commit back */ + /* releasing will commit back */ ACTIVE, /* releasing will cancel back */ @@ -813,7 +818,7 @@ internal constructor( scale = when (currentState) { GestureState.ACTIVE, - GestureState.FLUNG, -> params.activeIndicator.scale + GestureState.FLUNG -> params.activeIndicator.scale GestureState.COMMITTED -> params.committedIndicator.scale else -> params.preThresholdIndicator.scale }, diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/dagger/BaseAutoAddableModule.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/dagger/BaseAutoAddableModule.kt index adea26e5aa26..e1ec338cec6f 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/pipeline/dagger/BaseAutoAddableModule.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/dagger/BaseAutoAddableModule.kt @@ -18,6 +18,8 @@ package com.android.systemui.qs.pipeline.dagger import android.content.res.Resources import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.qs.pipeline.domain.autoaddable.A11yShortcutAutoAddable +import com.android.systemui.qs.pipeline.domain.autoaddable.A11yShortcutAutoAddableList import com.android.systemui.qs.pipeline.domain.autoaddable.AutoAddableSetting import com.android.systemui.qs.pipeline.domain.autoaddable.AutoAddableSettingList import com.android.systemui.qs.pipeline.domain.autoaddable.CastAutoAddable @@ -51,6 +53,16 @@ interface BaseAutoAddableModule { ) .toSet() } + + @Provides + @ElementsIntoSet + fun providesA11yShortcutAutoAddable( + a11yShortcutAutoAddableFactory: A11yShortcutAutoAddable.Factory + ): Set<AutoAddable> { + return A11yShortcutAutoAddableList.getA11yShortcutAutoAddables( + a11yShortcutAutoAddableFactory + ) + } } @Binds @IntoSet fun bindCastAutoAddable(impl: CastAutoAddable): AutoAddable 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 bcd09bd877fd..dc39c97bc9ab 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 @@ -25,6 +25,7 @@ import android.content.pm.PackageManager import android.content.pm.PackageManager.ResolveInfoFlags 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.dagger.SysUISingleton @@ -32,12 +33,13 @@ import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.util.kotlin.isComponentActuallyEnabled import javax.inject.Inject -import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.flow.shareIn interface InstalledTilesComponentRepository { @@ -49,33 +51,39 @@ class InstalledTilesComponentRepositoryImpl @Inject constructor( @Application private val applicationContext: Context, - @Background private val backgroundDispatcher: CoroutineDispatcher, + @Background private val backgroundScope: CoroutineScope, private val packageChangeRepository: PackageChangeRepository ) : InstalledTilesComponentRepository { - override fun getInstalledTilesComponents(userId: Int): Flow<Set<ComponentName>> { - /* - * In order to query [PackageManager] for different users, this implementation will call - * [Context.createContextAsUser] and retrieve the [PackageManager] from that context. - */ - val packageManager = - if (applicationContext.userId == userId) { - applicationContext.packageManager - } else { - applicationContext - .createContextAsUser( - UserHandle.of(userId), - /* flags */ 0, - ) - .packageManager + @GuardedBy("userMap") private val userMap = mutableMapOf<Int, Flow<Set<ComponentName>>>() + + override fun getInstalledTilesComponents(userId: Int): Flow<Set<ComponentName>> = + synchronized(userMap) { + userMap.getOrPut(userId) { + /* + * In order to query [PackageManager] for different users, this implementation will + * call [Context.createContextAsUser] and retrieve the [PackageManager] from that + * context. + */ + val packageManager = + if (applicationContext.userId == userId) { + applicationContext.packageManager + } else { + applicationContext + .createContextAsUser( + UserHandle.of(userId), + /* flags */ 0, + ) + .packageManager + } + packageChangeRepository + .packageChanged(UserHandle.of(userId)) + .onStart { emit(PackageChangeModel.Empty) } + .map { reloadComponents(userId, packageManager) } + .distinctUntilChanged() + .shareIn(backgroundScope, SharingStarted.WhileSubscribed(), replay = 1) } - return packageChangeRepository - .packageChanged(UserHandle.of(userId)) - .onStart { emit(PackageChangeModel.Empty) } - .map { reloadComponents(userId, packageManager) } - .distinctUntilChanged() - .flowOn(backgroundDispatcher) - } + } @WorkerThread private fun reloadComponents(userId: Int, packageManager: PackageManager): Set<ComponentName> { diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/A11yShortcutAutoAddable.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/A11yShortcutAutoAddable.kt new file mode 100644 index 000000000000..2cebbe3f372c --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/A11yShortcutAutoAddable.kt @@ -0,0 +1,94 @@ +/* + * 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.qs.pipeline.domain.autoaddable + +import android.content.ComponentName +import android.provider.Settings +import com.android.systemui.accessibility.data.repository.AccessibilityQsShortcutsRepository +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.qs.pipeline.domain.model.AutoAddSignal +import com.android.systemui.qs.pipeline.domain.model.AutoAddTracking +import com.android.systemui.qs.pipeline.domain.model.AutoAddable +import com.android.systemui.qs.pipeline.shared.TileSpec +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import java.util.Objects +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map + +/** + * [A11yShortcutAutoAddable] will auto add/remove qs tile of the accessibility framework feature + * based on the user's choices in the Settings app. + * + * The a11y feature component is added to [Settings.Secure.ACCESSIBILITY_QS_TARGETS] when the user + * selects to use qs tile as a shortcut for the a11 feature in the Settings app. The accessibility + * feature component is removed from [Settings.Secure.ACCESSIBILITY_QS_TARGETS] when the user + * doesn't want to use qs tile as a shortcut for the a11y feature in the Settings app. + * + * [A11yShortcutAutoAddable] tracks a [Settings.Secure.ACCESSIBILITY_QS_TARGETS] and when its value + * changes, it will emit a [AutoAddSignal.Add] for the [spec] if the [componentName] is a substring + * of the value; it will emit a [AutoAddSignal.Remove] for the [spec] if the [componentName] is not + * a substring of the value. + */ +class A11yShortcutAutoAddable +@AssistedInject +constructor( + private val a11yQsShortcutsRepository: AccessibilityQsShortcutsRepository, + @Background private val bgDispatcher: CoroutineDispatcher, + @Assisted private val spec: TileSpec, + @Assisted private val componentName: ComponentName +) : AutoAddable { + + override fun autoAddSignal(userId: Int): Flow<AutoAddSignal> { + return a11yQsShortcutsRepository + .a11yQsShortcutTargets(userId) + .map { it.contains(componentName.flattenToString()) } + .filterNotNull() + .distinctUntilChanged() + .map { if (it) AutoAddSignal.Add(spec) else AutoAddSignal.Remove(spec) } + .flowOn(bgDispatcher) + } + + override val autoAddTracking = AutoAddTracking.Always + + override val description = + "A11yShortcutAutoAddableSetting: $spec:$componentName ($autoAddTracking)" + + override fun equals(other: Any?): Boolean { + return other is A11yShortcutAutoAddable && + spec == other.spec && + componentName == other.componentName + } + + override fun hashCode(): Int { + return Objects.hash(spec, componentName) + } + + override fun toString(): String { + return description + } + + @AssistedFactory + interface Factory { + fun create(spec: TileSpec, componentName: ComponentName): A11yShortcutAutoAddable + } +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/A11yShortcutAutoAddableList.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/A11yShortcutAutoAddableList.kt new file mode 100644 index 000000000000..08e39204386e --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/A11yShortcutAutoAddableList.kt @@ -0,0 +1,58 @@ +/* + * 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.qs.pipeline.domain.autoaddable + +import android.view.accessibility.Flags +import com.android.internal.accessibility.AccessibilityShortcutController +import com.android.systemui.qs.pipeline.domain.model.AutoAddable +import com.android.systemui.qs.pipeline.shared.TileSpec +import com.android.systemui.qs.tiles.ColorCorrectionTile +import com.android.systemui.qs.tiles.ColorInversionTile +import com.android.systemui.qs.tiles.OneHandedModeTile +import com.android.systemui.qs.tiles.ReduceBrightColorsTile + +object A11yShortcutAutoAddableList { + + /** + * Generate a collection of [A11yShortcutAutoAddable] for the framework tiles related to + * accessibility features with shortcut options + */ + fun getA11yShortcutAutoAddables(factory: A11yShortcutAutoAddable.Factory): Set<AutoAddable> { + return if (Flags.a11yQsShortcut()) { + setOf( + factory.create( + TileSpec.create(ColorCorrectionTile.TILE_SPEC), + AccessibilityShortcutController.DALTONIZER_COMPONENT_NAME + ), + factory.create( + TileSpec.create(ColorInversionTile.TILE_SPEC), + AccessibilityShortcutController.COLOR_INVERSION_COMPONENT_NAME + ), + factory.create( + TileSpec.create(OneHandedModeTile.TILE_SPEC), + AccessibilityShortcutController.ONE_HANDED_COMPONENT_NAME + ), + factory.create( + TileSpec.create(ReduceBrightColorsTile.TILE_SPEC), + AccessibilityShortcutController.REDUCE_BRIGHT_COLORS_COMPONENT_NAME + ), + ) + } else { + emptySet() + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSIconViewImpl.java b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSIconViewImpl.java index d04e4f52ac4d..53f287b81be9 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSIconViewImpl.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSIconViewImpl.java @@ -33,11 +33,13 @@ import android.view.View; import android.widget.ImageView; import android.widget.ImageView.ScaleType; +import androidx.annotation.VisibleForTesting; + import com.android.settingslib.Utils; -import com.android.systemui.res.R; import com.android.systemui.plugins.qs.QSIconView; import com.android.systemui.plugins.qs.QSTile; import com.android.systemui.plugins.qs.QSTile.State; +import com.android.systemui.res.R; import java.util.Objects; @@ -52,7 +54,10 @@ public class QSIconViewImpl extends QSIconView { private boolean mDisabledByPolicy = false; private int mTint; @Nullable - private QSTile.Icon mLastIcon; + @VisibleForTesting + QSTile.Icon mLastIcon; + + private boolean mIconChangeScheduled; private ValueAnimator mColorAnimator = new ValueAnimator(); @@ -112,6 +117,7 @@ public class QSIconViewImpl extends QSIconView { } protected void updateIcon(ImageView iv, State state, boolean allowAnimations) { + mIconChangeScheduled = false; final QSTile.Icon icon = state.iconSupplier != null ? state.iconSupplier.get() : state.icon; if (!Objects.equals(icon, iv.getTag(R.id.qs_icon_tag))) { boolean shouldAnimate = allowAnimations && shouldAnimate(iv); @@ -167,7 +173,12 @@ public class QSIconViewImpl extends QSIconView { mState = state.state; mDisabledByPolicy = state.disabledByPolicy; if (mTint != 0 && allowAnimations && shouldAnimate(iv)) { - animateGrayScale(mTint, color, iv, () -> updateIcon(iv, state, allowAnimations)); + mIconChangeScheduled = true; + animateGrayScale(mTint, color, iv, () -> { + if (mIconChangeScheduled) { + updateIcon(iv, state, allowAnimations); + } + }); } else { setTint(iv, color); updateIcon(iv, state, allowAnimations); diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/RecordIssueTile.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/RecordIssueTile.kt index 216d716b07a4..88863cbad1ee 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/RecordIssueTile.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/RecordIssueTile.kt @@ -17,6 +17,8 @@ package com.android.systemui.qs.tiles import android.app.AlertDialog +import android.app.BroadcastOptions +import android.app.PendingIntent import android.content.Intent import android.os.Handler import android.os.Looper @@ -42,6 +44,8 @@ import com.android.systemui.qs.logging.QSLogger import com.android.systemui.qs.tileimpl.QSTileImpl import com.android.systemui.recordissue.RecordIssueDialogDelegate import com.android.systemui.res.R +import com.android.systemui.screenrecord.RecordingService +import com.android.systemui.settings.UserContextProvider import com.android.systemui.statusbar.phone.KeyguardDismissUtil import com.android.systemui.statusbar.policy.KeyguardStateController import javax.inject.Inject @@ -61,6 +65,7 @@ constructor( private val keyguardDismissUtil: KeyguardDismissUtil, private val keyguardStateController: KeyguardStateController, private val dialogLaunchAnimator: DialogLaunchAnimator, + private val userContextProvider: UserContextProvider, private val delegateFactory: RecordIssueDialogDelegate.Factory, ) : QSTileImpl<QSTile.BooleanState>( @@ -91,12 +96,22 @@ constructor( public override fun handleClick(view: View?) { if (isRecording) { isRecording = false + stopScreenRecord() } else { mUiHandler.post { showPrompt(view) } } refreshState() } + private fun stopScreenRecord() = + PendingIntent.getService( + userContextProvider.userContext, + RecordingService.REQUEST_CODE, + RecordingService.getStopIntent(userContextProvider.userContext), + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + ) + .send(BroadcastOptions.makeBasic().apply { isInteractive = true }.toBundle()) + private fun showPrompt(view: View?) { val dialog: AlertDialog = delegateFactory diff --git a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java index cc53aabfcd25..4e89fbfeb6e3 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java +++ b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java @@ -16,6 +16,7 @@ package com.android.systemui.recents; +import static android.app.Flags.keyguardPrivateNotifications; import static android.content.Intent.ACTION_PACKAGE_ADDED; import static android.content.Intent.EXTRA_CHANGED_COMPONENT_NAME_LIST; import static android.content.pm.PackageManager.MATCH_SYSTEM_ONLY; @@ -81,12 +82,13 @@ import com.android.internal.logging.UiEventLogger; import com.android.internal.util.ScreenshotHelper; import com.android.internal.util.ScreenshotRequest; import com.android.systemui.Dumpable; +import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dump.DumpManager; import com.android.systemui.flags.FeatureFlags; -import com.android.systemui.flags.Flags; import com.android.systemui.keyguard.KeyguardUnlockAnimationController; +import com.android.systemui.keyguard.KeyguardWmStateRefactor; import com.android.systemui.keyguard.WakefulnessLifecycle; import com.android.systemui.keyguard.ui.view.InWindowLauncherUnlockAnimationManager; import com.android.systemui.model.SysUiState; @@ -167,13 +169,16 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis private final Optional<UnfoldTransitionProgressForwarder> mUnfoldTransitionProgressForwarder; private final UiEventLogger mUiEventLogger; private final DisplayTracker mDisplayTracker; - private Region mActiveNavBarRegion; + private final BroadcastDispatcher mBroadcastDispatcher; + private IOverviewProxy mOverviewProxy; private int mConnectionBackoffAttempts; private boolean mBound; private boolean mIsEnabled; + + private boolean mIsNonPrimaryUser; private int mCurrentBoundedUserId = -1; private boolean mInputFocusTransferStarted; private float mInputFocusTransferStartY; @@ -419,6 +424,21 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis retryConnectionWithBackoff(); }; + private final BroadcastReceiver mUserEventReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (Objects.equals(intent.getAction(), Intent.ACTION_USER_UNLOCKED)) { + if (keyguardPrivateNotifications()) { + // Start the overview connection to the launcher service + // Connect if user hasn't connected yet + if (getProxy() == null) { + startConnectionToCurrentUser(); + } + } + } + } + }; + private final BroadcastReceiver mLauncherStateChangedReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { @@ -586,11 +606,13 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis FeatureFlags featureFlags, SceneContainerFlags sceneContainerFlags, DumpManager dumpManager, - Optional<UnfoldTransitionProgressForwarder> unfoldTransitionProgressForwarder + Optional<UnfoldTransitionProgressForwarder> unfoldTransitionProgressForwarder, + BroadcastDispatcher broadcastDispatcher ) { // b/241601880: This component shouldn't be running for a non-primary user - if (!Process.myUserHandle().equals(UserHandle.SYSTEM)) { - Log.e(TAG_OPS, "Unexpected initialization for non-primary user", new Throwable()); + mIsNonPrimaryUser = !Process.myUserHandle().equals(UserHandle.SYSTEM); + if (mIsNonPrimaryUser) { + Log.wtf(TAG_OPS, "Unexpected initialization for non-primary user", new Throwable()); } mContext = context; @@ -615,8 +637,9 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis mUiEventLogger = uiEventLogger; mDisplayTracker = displayTracker; mUnfoldTransitionProgressForwarder = unfoldTransitionProgressForwarder; + mBroadcastDispatcher = broadcastDispatcher; - if (!featureFlags.isEnabled(Flags.KEYGUARD_WM_STATE_REFACTOR)) { + if (!KeyguardWmStateRefactor.isEnabled()) { mSysuiUnlockAnimationController = sysuiUnlockAnimationController; } else { mSysuiUnlockAnimationController = inWindowLauncherUnlockAnimationManager; @@ -635,6 +658,12 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis filter.addAction(Intent.ACTION_PACKAGE_CHANGED); mContext.registerReceiver(mLauncherStateChangedReceiver, filter); + if (keyguardPrivateNotifications()) { + mBroadcastDispatcher.registerReceiver(mUserEventReceiver, + new IntentFilter(Intent.ACTION_USER_UNLOCKED), + null /* executor */, UserHandle.ALL); + } + // Listen for status bar state changes statusBarWinController.registerCallback(mStatusBarWindowCallback); mScreenshotHelper = new ScreenshotHelper(context); @@ -772,6 +801,13 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis } private void internalConnectToCurrentUser(String reason) { + if (mIsNonPrimaryUser) { + // This should not happen, but if any per-user SysUI component has a dependency on OPS, + // then this could get triggered + Log.w(TAG_OPS, "Skipping connection to overview service due to non-primary user " + + "caller"); + return; + } disconnectFromLauncherService(reason); // If user has not setup yet or already connected, do not try to connect diff --git a/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt b/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt index 97ec3f98cf0c..1c7cc007cbc7 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt @@ -34,7 +34,7 @@ import com.android.systemui.res.R import com.android.systemui.shade.domain.interactor.ShadeInteractor import com.android.systemui.util.kotlin.collectFlow import javax.inject.Inject -import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.Flow /** * Controller that's responsible for the glanceable hub container view and its touch handling. @@ -110,10 +110,8 @@ constructor( return communalInteractor.isCommunalEnabled && isComposeAvailable() } - /** Returns a {@link StateFlow} that tracks whether communal hub is enabled. */ - fun enabledState(): StateFlow<Boolean> { - return communalInteractor.communalEnabledState - } + /** Returns a {@link StateFlow} that tracks whether communal hub is available. */ + fun communalAvailable(): Flow<Boolean> = communalInteractor.isCommunalAvailable /** * Creates the container view containing the glanceable hub UI. diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java index c0ceba3c1d4f..530c124f4f24 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java @@ -3576,11 +3576,6 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump } @Override - public NotificationStackScrollLayoutController getNotificationStackScrollLayoutController() { - return mNotificationStackScrollLayoutController; - } - - @Override public void disableHeader(int state1, int state2, boolean animated) { mShadeHeaderController.disable(state1, state2, animated); } diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java index 5ecc54b09806..19a58401cbec 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java @@ -607,7 +607,7 @@ public class NotificationShadeWindowViewController implements Dumpable { public void setupCommunalHubLayout() { collectFlow( mView, - mGlanceableHubContainerController.enabledState(), + mGlanceableHubContainerController.communalAvailable(), isEnabled -> { if (isEnabled) { View communalPlaceholder = mView.findViewById(R.id.communal_ui_stub); diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeSurface.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeSurface.kt index e54286fcf992..4f970b3923aa 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ShadeSurface.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeSurface.kt @@ -17,7 +17,6 @@ package com.android.systemui.shade import android.view.ViewPropertyAnimator import com.android.systemui.statusbar.GestureRecorder -import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController import com.android.systemui.statusbar.phone.CentralSurfaces import com.android.systemui.statusbar.policy.HeadsUpManager @@ -44,9 +43,6 @@ interface ShadeSurface : ShadeViewController { /** Animates the view from its current alpha to zero then runs the runnable. */ fun fadeOut(startDelayMs: Long, durationMs: Long, endAction: Runnable): ViewPropertyAnimator - /** Returns the NSSL controller. */ - val notificationStackScrollLayoutController: NotificationStackScrollLayoutController - /** Set whether the bouncer is showing. */ fun setBouncerShowing(bouncerShowing: Boolean) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java index 24ac70e63e46..2a4753def463 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java @@ -234,10 +234,12 @@ public class NotificationLockscreenUserManagerImpl implements } else if (profileAvailabilityActions(action)) { updateCurrentProfilesCache(); } else if (Objects.equals(action, Intent.ACTION_USER_UNLOCKED)) { - // Start the overview connection to the launcher service - // Connect if user hasn't connected yet - if (mOverviewProxyServiceLazy.get().getProxy() == null) { - mOverviewProxyServiceLazy.get().startConnectionToCurrentUser(); + if (!keyguardPrivateNotifications()) { + // Start the overview connection to the launcher service + // Connect if user hasn't connected yet + if (mOverviewProxyServiceLazy.get().getProxy() == null) { + mOverviewProxyServiceLazy.get().startConnectionToCurrentUser(); + } } } else if (Objects.equals(action, NOTIFICATION_UNLOCKED_BY_WORK_CHALLENGE_ACTION)) { final IntentSender intentSender = intent.getParcelableExtra( diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/ConversationNotifications.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/ConversationNotifications.kt index 6e3b15da4423..c643238b7e30 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/ConversationNotifications.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/ConversationNotifications.kt @@ -52,8 +52,8 @@ class ConversationNotificationProcessor @Inject constructor( entry: NotificationEntry, recoveredBuilder: Notification.Builder, logger: NotificationContentInflaterLogger - ) { - val messagingStyle = recoveredBuilder.style as? Notification.MessagingStyle ?: return + ): Notification.MessagingStyle? { + val messagingStyle = recoveredBuilder.style as? Notification.MessagingStyle ?: return null messagingStyle.conversationType = if (entry.ranking.channel.isImportantConversation) Notification.MessagingStyle.CONVERSATION_TYPE_IMPORTANT @@ -68,6 +68,7 @@ class ConversationNotificationProcessor @Inject constructor( } messagingStyle.unreadMessageCount = conversationNotificationManager.getUnreadCount(entry, recoveredBuilder) + return messagingStyle } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinator.java index 73decfc326a4..639e23ae0765 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinator.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinator.java @@ -362,8 +362,12 @@ public class PreparationCoordinator implements Coordinator { } NotifInflater.Params getInflaterParams(NotifUiAdjustment adjustment, String reason) { - return new NotifInflater.Params(adjustment.isMinimized(), reason, - adjustment.isSnoozeEnabled()); + return new NotifInflater.Params( + adjustment.isMinimized(), + reason, + adjustment.isSnoozeEnabled(), + adjustment.isChildInGroup() + ); } private void abortInflation(NotificationEntry entry, String reason) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotifInflater.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotifInflater.kt index 4483599d6857..c0b187be42f3 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotifInflater.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotifInflater.kt @@ -20,9 +20,9 @@ import com.android.systemui.statusbar.notification.collection.NotificationEntry import com.android.systemui.statusbar.notification.collection.render.NotifViewController /** - * Used by the [PreparationCoordinator]. When notifications are added or updated, the - * NotifInflater is asked to (re)inflated and prepare their views. This inflation occurs off the - * main thread. When the inflation is finished, NotifInflater will trigger its InflationCallback. + * Used by the [PreparationCoordinator]. When notifications are added or updated, the NotifInflater + * is asked to (re)inflated and prepare their views. This inflation occurs off the main thread. When + * the inflation is finished, NotifInflater will trigger its InflationCallback. */ interface NotifInflater { /** @@ -33,7 +33,7 @@ interface NotifInflater { fun rebindViews(entry: NotificationEntry, params: Params, callback: InflationCallback) /** - * Called to inflate the views of an entry. Views are not considered inflated until all of its + * Called to inflate the views of an entry. Views are not considered inflated until all of its * views are bound. Once all views are inflated, the InflationCallback is triggered. * * @param callback callback called after inflation finishes @@ -41,25 +41,24 @@ interface NotifInflater { fun inflateViews(entry: NotificationEntry, params: Params, callback: InflationCallback) /** - * Request to stop the inflation of an entry. For example, called when a notification is - * removed and no longer needs to be inflated. Returns whether anything may have been aborted. + * Request to stop the inflation of an entry. For example, called when a notification is removed + * and no longer needs to be inflated. Returns whether anything may have been aborted. */ fun abortInflation(entry: NotificationEntry): Boolean - /** - * Called to let the system remove the content views from the notification row. - */ + /** Called to let the system remove the content views from the notification row. */ fun releaseViews(entry: NotificationEntry) - /** - * Callback once all the views are inflated and bound for a given NotificationEntry. - */ + /** Callback once all the views are inflated and bound for a given NotificationEntry. */ interface InflationCallback { fun onInflationFinished(entry: NotificationEntry, controller: NotifViewController) } - /** - * A class holding parameters used when inflating the notification row - */ - class Params(val isLowPriority: Boolean, val reason: String, val showSnooze: Boolean) + /** A class holding parameters used when inflating the notification row */ + class Params( + val isLowPriority: Boolean, + val reason: String, + val showSnooze: Boolean, + val isChildInGroup: Boolean = false, + ) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotifUiAdjustment.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotifUiAdjustment.kt index ee0b00807e27..e1d2cdc65d5a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotifUiAdjustment.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotifUiAdjustment.kt @@ -20,6 +20,7 @@ import android.app.Notification import android.app.RemoteInput import android.graphics.drawable.Icon import android.text.TextUtils +import com.android.systemui.statusbar.notification.row.shared.AsyncHybridViewInflation /** * An immutable object which contains minimal state extracted from an entry that represents state @@ -34,6 +35,7 @@ class NotifUiAdjustment internal constructor( val isSnoozeEnabled: Boolean, val isMinimized: Boolean, val needsRedaction: Boolean, + val isChildInGroup: Boolean, ) { companion object { @JvmStatic @@ -48,6 +50,11 @@ class NotifUiAdjustment internal constructor( oldAdjustment.needsRedaction != newAdjustment.needsRedaction -> true areDifferent(oldAdjustment.smartActions, newAdjustment.smartActions) -> true newAdjustment.smartReplies != oldAdjustment.smartReplies -> true + // TODO(b/217799515): Here we decide whether to re-inflate the row on every group-status + // change if we want to keep the single-line view, the following line should be: + // !oldAdjustment.isChildInGroup && newAdjustment.isChildInGroup -> true + AsyncHybridViewInflation.isEnabled && + oldAdjustment.isChildInGroup != newAdjustment.isChildInGroup -> true else -> false } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotifUiAdjustmentProvider.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotifUiAdjustmentProvider.kt index 058545689c01..6f44c13a3e71 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotifUiAdjustmentProvider.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotifUiAdjustmentProvider.kt @@ -29,6 +29,7 @@ import com.android.systemui.statusbar.NotificationLockscreenUserManager import com.android.systemui.statusbar.notification.collection.GroupEntry import com.android.systemui.statusbar.notification.collection.NotificationEntry import com.android.systemui.statusbar.notification.collection.provider.SectionStyleProvider +import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManager import com.android.systemui.util.ListenerSet import com.android.systemui.util.settings.SecureSettings import javax.inject.Inject @@ -43,7 +44,8 @@ class NotifUiAdjustmentProvider @Inject constructor( private val secureSettings: SecureSettings, private val lockscreenUserManager: NotificationLockscreenUserManager, private val sectionStyleProvider: SectionStyleProvider, - private val userTracker: UserTracker + private val userTracker: UserTracker, + private val groupMembershipManager: GroupMembershipManager, ) { private val dirtyListeners = ListenerSet<Runnable>() private var isSnoozeSettingsEnabled = false @@ -121,5 +123,6 @@ class NotifUiAdjustmentProvider @Inject constructor( isSnoozeEnabled = isSnoozeSettingsEnabled && !entry.isCanceled, isMinimized = isEntryMinimized(entry), needsRedaction = lockscreenUserManager.needsRedaction(entry), + isChildInGroup = groupMembershipManager.isChildInGroup(entry), ) } 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 80ef14bb4673..cd816aea452b 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 @@ -20,6 +20,7 @@ import static com.android.systemui.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; +import static com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_SINGLE_LINE; import static java.util.Objects.requireNonNull; @@ -49,6 +50,7 @@ import com.android.systemui.statusbar.notification.row.RowContentBindParams; import com.android.systemui.statusbar.notification.row.RowContentBindStage; import com.android.systemui.statusbar.notification.row.RowInflaterTask; import com.android.systemui.statusbar.notification.row.dagger.ExpandableNotificationRowComponent; +import com.android.systemui.statusbar.notification.row.shared.AsyncHybridViewInflation; import com.android.systemui.statusbar.notification.stack.NotificationListContainer; import javax.inject.Inject; @@ -127,6 +129,8 @@ public class NotificationRowBinderImpl implements NotificationRowBinder { @NonNull NotifInflater.Params params, NotificationRowContentBinder.InflationCallback callback) throws InflationException { + //TODO(b/217799515): Remove the entry parameter from getViewParentForNotification(), this + // function returns the NotificationStackScrollLayout regardless of the entry. ViewGroup parent = mListContainer.getViewParentForNotification(entry); if (entry.rowExists()) { @@ -174,6 +178,9 @@ public class NotificationRowBinderImpl implements NotificationRowBinder { params.markContentViewsFreeable(FLAG_CONTENT_VIEW_CONTRACTED); params.markContentViewsFreeable(FLAG_CONTENT_VIEW_EXPANDED); params.markContentViewsFreeable(FLAG_CONTENT_VIEW_PUBLIC); + if (AsyncHybridViewInflation.isEnabled()) { + params.markContentViewsFreeable(FLAG_CONTENT_VIEW_SINGLE_LINE); + } mRowContentBindStage.requestRebind(entry, null); } @@ -254,6 +261,16 @@ public class NotificationRowBinderImpl implements NotificationRowBinder { params.markContentViewsFreeable(FLAG_CONTENT_VIEW_PUBLIC); } + if (AsyncHybridViewInflation.isEnabled()) { + if (inflaterParams.isChildInGroup()) { + params.requireContentViews(FLAG_CONTENT_VIEW_SINGLE_LINE); + } else { + // TODO(b/217799515): here we decide whether to free the single-line view + // when the group status changes + params.markContentViewsFreeable(FLAG_CONTENT_VIEW_SINGLE_LINE); + } + } + params.rebindAllContentViews(); mLogger.logRequestingRebind(entry, inflaterParams); mRowContentBindStage.requestRebind(entry, en -> { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/ShadeViewDiffer.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/ShadeViewDiffer.kt index 61e6f65b2bc2..8021d8f58ccc 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/ShadeViewDiffer.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/ShadeViewDiffer.kt @@ -127,6 +127,9 @@ class ShadeViewDiffer( } } + /** + * Attach the Child Nodes to the parentNode using the structure from specMap + */ private fun attachChildren( parentNode: ShadeNode, specMap: Map<NodeController, NodeSpec> diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/BindStage.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/BindStage.java index d626c18e46f5..8ae324fa4ef8 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/BindStage.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/BindStage.java @@ -44,6 +44,7 @@ public abstract class BindStage<Params> extends BindRequester { /** * Execute the stage asynchronously. * + * @param entry the NotificationEntry to bind * @param row notification top-level view to bind views to * @param callback callback after stage finishes */ diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/HybridConversationNotificationView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/HybridConversationNotificationView.java index 43d99a0e03f2..6bc2b2f9e250 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/HybridConversationNotificationView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/HybridConversationNotificationView.java @@ -16,19 +16,27 @@ package com.android.systemui.statusbar.notification.row; +import android.annotation.NonNull; import android.annotation.Nullable; import android.content.Context; +import android.content.res.ColorStateList; import android.graphics.drawable.Icon; import android.text.TextUtils; import android.util.AttributeSet; import android.view.View; +import android.view.ViewStub; import android.widget.FrameLayout; import android.widget.ImageView; import android.widget.TextView; +import com.android.internal.annotations.VisibleForTesting; import com.android.internal.widget.ConversationLayout; import com.android.systemui.res.R; import com.android.systemui.statusbar.notification.NotificationFadeAware; +import com.android.systemui.statusbar.notification.row.shared.AsyncHybridViewInflation; +import com.android.systemui.statusbar.notification.row.ui.viewmodel.ConversationAvatar; +import com.android.systemui.statusbar.notification.row.ui.viewmodel.FacePile; +import com.android.systemui.statusbar.notification.row.ui.viewmodel.SingleIcon; /** * A hybrid view which may contain information about one ore more conversations. @@ -37,6 +45,7 @@ public class HybridConversationNotificationView extends HybridNotificationView { private ImageView mConversationIconView; private TextView mConversationSenderName; + private ViewStub mConversationFacePileStub; private View mConversationFacePile; private int mSingleAvatarSize; private int mFacePileSize; @@ -65,7 +74,16 @@ public class HybridConversationNotificationView extends HybridNotificationView { protected void onFinishInflate() { super.onFinishInflate(); mConversationIconView = requireViewById(com.android.internal.R.id.conversation_icon); - mConversationFacePile = requireViewById(com.android.internal.R.id.conversation_face_pile); + if (AsyncHybridViewInflation.isEnabled()) { + mConversationFacePileStub = + requireViewById(com.android.internal.R.id.conversation_face_pile); + } else { + // TODO(b/217799515): This usage is vague because mConversationFacePile represents both + // View and ViewStub at different stages of View inflation, should be removed when + // AsyncHybridViewInflation flag is removed + mConversationFacePile = + requireViewById(com.android.internal.R.id.conversation_face_pile); + } mConversationSenderName = requireViewById(R.id.conversation_notification_sender); applyTextColor(mConversationSenderName, mSecondaryTextColor); mFacePileSize = getResources() @@ -85,7 +103,8 @@ public class HybridConversationNotificationView extends HybridNotificationView { @Override public void bind(@Nullable CharSequence title, @Nullable CharSequence text, - @Nullable View contentView) { + @Nullable View contentView) { + AsyncHybridViewInflation.assertInLegacyMode(); if (!(contentView instanceof ConversationLayout)) { super.bind(title, text, contentView); return; @@ -137,6 +156,77 @@ public class HybridConversationNotificationView extends HybridNotificationView { super.bind(conversationTitle, conversationText, conversationLayout); } + /** + * Set the avatar using ConversationAvatar from SingleLineViewModel + * + * @param conversationAvatar the icon needed for a single-line conversation view, it should be + * either an instance of SingleIcon or FacePile + */ + public void setAvatar(@NonNull ConversationAvatar conversationAvatar) { + if (AsyncHybridViewInflation.isUnexpectedlyInLegacyMode()) return; + if (conversationAvatar instanceof SingleIcon) { + SingleIcon avatar = (SingleIcon) conversationAvatar; + if (mConversationFacePile != null) mConversationFacePile.setVisibility(GONE); + mConversationIconView.setVisibility(VISIBLE); + mConversationIconView.setImageDrawable(avatar.getIconDrawable()); + setSize(mConversationIconView, mSingleAvatarSize); + return; + } + + // If conversationAvatar is not a SingleIcon, it should be a FacePile. + // Bind the face pile with it. + FacePile facePileModel = (FacePile) conversationAvatar; + mConversationIconView.setVisibility(GONE); + // Inflate mConversationFacePile from ViewStub + if (mConversationFacePile == null) { + mConversationFacePile = mConversationFacePileStub.inflate(); + } + mConversationFacePile.setVisibility(VISIBLE); + + ImageView facePileBottomBg = mConversationFacePile.requireViewById( + com.android.internal.R.id.conversation_face_pile_bottom_background); + ImageView facePileBottom = mConversationFacePile.requireViewById( + com.android.internal.R.id.conversation_face_pile_bottom); + ImageView facePileTop = mConversationFacePile.requireViewById( + com.android.internal.R.id.conversation_face_pile_top); + + int bottomBackgroundColor = facePileModel.getBottomBackgroundColor(); + facePileBottomBg.setImageTintList(ColorStateList.valueOf(bottomBackgroundColor)); + + facePileBottom.setImageDrawable(facePileModel.getBottomIconDrawable()); + facePileTop.setImageDrawable(facePileModel.getTopIconDrawable()); + + setSize(mConversationFacePile, mFacePileSize); + setSize(facePileBottom, mFacePileAvatarSize); + setSize(facePileTop, mFacePileAvatarSize); + setSize(facePileBottomBg, mFacePileAvatarSize + 2 * mFacePileProtectionWidth); + + mTransformationHelper.addViewTransformingToSimilar(facePileTop); + mTransformationHelper.addViewTransformingToSimilar(facePileBottom); + mTransformationHelper.addViewTransformingToSimilar(facePileBottomBg); + + } + + /** + * bind the text views + */ + public void setText( + CharSequence titleText, + CharSequence contentText, + CharSequence conversationSenderName + ) { + if (AsyncHybridViewInflation.isUnexpectedlyInLegacyMode()) return; + if (conversationSenderName == null) { + mConversationSenderName.setVisibility(GONE); + } else { + mConversationSenderName.setVisibility(VISIBLE); + mConversationSenderName.setText(conversationSenderName); + } + // TODO (b/217799515): super.bind() doesn't use contentView, remove the contentView + // argument when the flag is removed + super.bind(/* title = */ titleText, /* text = */ contentText, /* contentView = */ null); + } + private static void setSize(View view, int size) { FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) view.getLayoutParams(); lp.width = size; @@ -153,4 +243,9 @@ public class HybridConversationNotificationView extends HybridNotificationView { super.setNotificationFaded(faded); NotificationFadeAware.setLayerTypeForFaded(mConversationFacePile, faded); } + + @VisibleForTesting + TextView getConversationSenderNameView() { + return mConversationSenderName; + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/HybridGroupManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/HybridGroupManager.java index ddd9bddc7375..09c034978977 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/HybridGroupManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/HybridGroupManager.java @@ -32,6 +32,7 @@ import android.widget.TextView; import com.android.internal.widget.ConversationLayout; import com.android.systemui.res.R; +import com.android.systemui.statusbar.notification.row.shared.AsyncHybridViewInflation; /** * A class managing hybrid groups that include {@link HybridNotificationView} and the notification @@ -41,6 +42,8 @@ public class HybridGroupManager { private final Context mContext; + private static final String TAG = "HybridGroupManager"; + private float mOverflowNumberSize; private int mOverflowNumberPadding; @@ -93,21 +96,34 @@ public class HybridGroupManager { public HybridNotificationView bindFromNotification(HybridNotificationView reusableView, View contentView, StatusBarNotification notification, ViewGroup parent) { + AsyncHybridViewInflation.assertInLegacyMode(); boolean isNewView = false; if (reusableView == null) { Trace.beginSection("HybridGroupManager#bindFromNotification"); reusableView = inflateHybridView(contentView, parent); isNewView = true; } - CharSequence titleText = resolveTitle(notification.getNotification()); - CharSequence contentText = resolveText(notification.getNotification()); - reusableView.bind(titleText, contentText, contentView); + + updateReusableView(reusableView, notification, contentView); if (isNewView) { Trace.endSection(); } return reusableView; } + /** + * Update the HybridNotificationView (single-line view)'s appearance + */ + public void updateReusableView(HybridNotificationView reusableView, + StatusBarNotification notification, View contentView) { + AsyncHybridViewInflation.assertInLegacyMode(); + final CharSequence titleText = resolveTitle(notification.getNotification()); + final CharSequence contentText = resolveText(notification.getNotification()); + if (reusableView != null) { + reusableView.bind(titleText, contentText, contentView); + } + } + @Nullable public static CharSequence resolveText(Notification notification) { CharSequence contentText = notification.extras.getCharSequence(Notification.EXTRA_TEXT); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java index f186e665f773..913d5f6d3848 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java @@ -20,6 +20,7 @@ import static com.android.internal.annotations.VisibleForTesting.Visibility.PACK import static com.android.systemui.statusbar.notification.row.NotificationContentView.VISIBLE_TYPE_CONTRACTED; import static com.android.systemui.statusbar.notification.row.NotificationContentView.VISIBLE_TYPE_EXPANDED; import static com.android.systemui.statusbar.notification.row.NotificationContentView.VISIBLE_TYPE_HEADSUP; +import static com.android.systemui.statusbar.notification.row.NotificationContentView.VISIBLE_TYPE_SINGLELINE; import android.annotation.NonNull; import android.annotation.Nullable; @@ -41,15 +42,19 @@ import android.widget.RemoteViews; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.widget.ImageMessageConsumer; -import com.android.systemui.res.R; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.media.controls.util.MediaFeatureFlag; +import com.android.systemui.res.R; import com.android.systemui.statusbar.InflationTask; import com.android.systemui.statusbar.NotificationRemoteInputManager; import com.android.systemui.statusbar.notification.ConversationNotificationProcessor; import com.android.systemui.statusbar.notification.InflationException; import com.android.systemui.statusbar.notification.collection.NotificationEntry; +import com.android.systemui.statusbar.notification.row.shared.AsyncHybridViewInflation; +import com.android.systemui.statusbar.notification.row.ui.viewbinder.SingleLineConversationViewBinder; +import com.android.systemui.statusbar.notification.row.ui.viewbinder.SingleLineViewBinder; +import com.android.systemui.statusbar.notification.row.ui.viewmodel.SingleLineViewModel; import com.android.systemui.statusbar.notification.row.wrapper.NotificationViewWrapper; import com.android.systemui.statusbar.phone.CentralSurfaces; import com.android.systemui.statusbar.policy.InflatedSmartReplyState; @@ -135,7 +140,7 @@ public class NotificationContentInflater implements NotificationRowContentBinder AsyncInflationTask task = new AsyncInflationTask( mBgExecutor, mInflateSynchronously, - contentToBind, + /* reInflateFlags = */ contentToBind, mRemoteViewCache, entry, mConversationProcessor, @@ -145,7 +150,7 @@ public class NotificationContentInflater implements NotificationRowContentBinder bindParams.usesIncreasedHeadsUpHeight, callback, mRemoteInputManager.getRemoteViewsOnClickHandler(), - mIsMediaInQS, + /* isMediaFlagEnabled = */ mIsMediaInQS, mSmartReplyStateInflater, mNotifLayoutInflaterFactoryProvider, mLogger); @@ -178,6 +183,29 @@ public class NotificationContentInflater implements NotificationRowContentBinder result = inflateSmartReplyViews(result, reInflateFlags, entry, row.getContext(), packageContext, row.getExistingSmartReplyState(), smartRepliesInflater, mLogger); + if (AsyncHybridViewInflation.isEnabled()) { + boolean isConversation = entry.getRanking().isConversation(); + Notification.MessagingStyle messagingStyle = null; + if (isConversation) { + messagingStyle = mConversationProcessor + .processNotification(entry, builder, mLogger); + } + result.mInflatedSingleLineViewModel = SingleLineViewInflater + .inflateSingleLineViewModel( + entry.getSbn().getNotification(), + messagingStyle, + builder, + row.getContext() + ); + result.mInflatedSingleLineViewHolder = + SingleLineViewInflater.inflateSingleLineViewHolder( + isConversation, + reInflateFlags, + entry, + row.getContext(), + mLogger + ); + } apply( mBgExecutor, @@ -255,6 +283,15 @@ public class NotificationContentInflater implements NotificationRowContentBinder mRemoteViewCache.removeCachedView(entry, FLAG_CONTENT_VIEW_PUBLIC); }); break; + case FLAG_CONTENT_VIEW_SINGLE_LINE: { + if (AsyncHybridViewInflation.isEnabled()) { + row.getPrivateLayout().performWhenContentInactive( + VISIBLE_TYPE_SINGLELINE, + () -> row.getPrivateLayout().setSingleLineView(null) + ); + } + break; + } default: break; } @@ -282,6 +319,10 @@ public class NotificationContentInflater implements NotificationRowContentBinder if ((contentViews & FLAG_CONTENT_VIEW_PUBLIC) != 0) { row.getPublicLayout().removeContentInactiveRunnable(VISIBLE_TYPE_CONTRACTED); } + if (AsyncHybridViewInflation.isEnabled() + && (contentViews & FLAG_CONTENT_VIEW_SINGLE_LINE) != 0) { + row.getPrivateLayout().removeContentInactiveRunnable(VISIBLE_TYPE_SINGLELINE); + } } private static InflationProgress inflateSmartReplyViews( @@ -772,6 +813,25 @@ public class NotificationContentInflater implements NotificationRowContentBinder } setRepliesAndActions = true; } + + if (AsyncHybridViewInflation.isEnabled() + && (reInflateFlags & FLAG_CONTENT_VIEW_SINGLE_LINE) != 0) { + HybridNotificationView viewHolder = result.mInflatedSingleLineViewHolder; + SingleLineViewModel viewModel = result.mInflatedSingleLineViewModel; + if (viewHolder != null && viewModel != null) { + if (viewModel.isConversation()) { + SingleLineConversationViewBinder.bind( + result.mInflatedSingleLineViewModel, + result.mInflatedSingleLineViewHolder + ); + } else { + SingleLineViewBinder.bind(result.mInflatedSingleLineViewModel, + result.mInflatedSingleLineViewHolder); + } + privateLayout.setSingleLineView(result.mInflatedSingleLineViewHolder); + } + } + if (setRepliesAndActions) { privateLayout.setInflatedSmartReplyState(result.inflatedSmartReplyState); } @@ -941,19 +1001,23 @@ public class NotificationContentInflater implements NotificationRowContentBinder // For all of our templates, we want it to be RTL packageContext = new RtlEnabledContext(packageContext); } - if (mEntry.getRanking().isConversation()) { - mConversationProcessor.processNotification(mEntry, recoveredBuilder, mLogger); + boolean isConversation = mEntry.getRanking().isConversation(); + Notification.MessagingStyle messagingStyle = null; + if (isConversation) { + messagingStyle = mConversationProcessor.processNotification( + mEntry, recoveredBuilder, mLogger); } InflationProgress inflationProgress = createRemoteViews(mReInflateFlags, recoveredBuilder, mIsLowPriority, mUsesIncreasedHeight, mUsesIncreasedHeadsUpHeight, packageContext, mRow, mNotifLayoutInflaterFactoryProvider, mLogger); + mLogger.logAsyncTaskProgress(mEntry, "getting existing smart reply state (on wrong thread!)"); InflatedSmartReplyState previousSmartReplyState = mRow.getExistingSmartReplyState(); mLogger.logAsyncTaskProgress(mEntry, "inflating smart reply views"); InflationProgress result = inflateSmartReplyViews( - inflationProgress, + /* result = */ inflationProgress, mReInflateFlags, mEntry, mContext, @@ -962,6 +1026,27 @@ public class NotificationContentInflater implements NotificationRowContentBinder mSmartRepliesInflater, mLogger); + if (AsyncHybridViewInflation.isEnabled()) { + // Inflate the single-line content view's ViewModel and ViewHolder from the + // background thread, the ViewHolder needs to be bind with ViewModel later from + // the main thread. + result.mInflatedSingleLineViewModel = SingleLineViewInflater + .inflateSingleLineViewModel( + mEntry.getSbn().getNotification(), + messagingStyle, + recoveredBuilder, + mContext + ); + result.mInflatedSingleLineViewHolder = + SingleLineViewInflater.inflateSingleLineViewHolder( + isConversation, + mReInflateFlags, + mEntry, + mContext, + mLogger + ); + } + mLogger.logAsyncTaskProgress(mEntry, "getting row image resolver (on wrong thread!)"); final NotificationInlineImageResolver imageResolver = mRow.getImageResolver(); @@ -1078,6 +1163,11 @@ public class NotificationContentInflater implements NotificationRowContentBinder private InflatedSmartReplyState inflatedSmartReplyState; private InflatedSmartReplyViewHolder expandedInflatedSmartReplies; private InflatedSmartReplyViewHolder headsUpInflatedSmartReplies; + + // ViewModel for SingleLineView, holds the UI State + SingleLineViewModel mInflatedSingleLineViewModel; + // Inflated SingleLineViewHolder, SingleLineView that lacks the UI State + HybridNotificationView mInflatedSingleLineViewHolder; } @VisibleForTesting diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterLogger.kt index 4f5455dc455f..ee9462c60674 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterLogger.kt @@ -26,6 +26,7 @@ import com.android.systemui.statusbar.notification.row.NotificationRowContentBin import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_EXPANDED import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_HEADS_UP import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_PUBLIC +import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_SINGLE_LINE import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.InflationFlag import javax.inject.Inject @@ -99,6 +100,26 @@ constructor(@NotifInflationLog private val buffer: LogBuffer) { ) } + fun logInflateSingleLine( + entry: NotificationEntry, + @InflationFlag inflationFlags: Int, + isConversation: Boolean + ) { + buffer.log( + TAG, + LogLevel.DEBUG, + { + str1 = entry.logKey + int1 = inflationFlags + bool1 = isConversation + }, + { + "inflateSingleLineView, inflationFlags: ${flagToString(int1)} for $str1, " + + "isConversation: $bool1" + } + ) + } + companion object { fun flagToString(@InflationFlag flag: Int): String { if (flag == 0) { @@ -121,6 +142,9 @@ constructor(@NotifInflationLog private val buffer: LogBuffer) { if (flag and FLAG_CONTENT_VIEW_PUBLIC != 0) { l.add("PUBLIC") } + if (flag and FLAG_CONTENT_VIEW_SINGLE_LINE != 0) { + l.add("SINGLE_LINE") + } return l.joinToString("|") } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java index a1718b9fbb02..402ea51bebb6 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java @@ -57,6 +57,7 @@ import com.android.systemui.statusbar.notification.NotificationUtils; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManager; import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier; +import com.android.systemui.statusbar.notification.row.shared.AsyncHybridViewInflation; import com.android.systemui.statusbar.notification.row.wrapper.NotificationCustomViewWrapper; import com.android.systemui.statusbar.notification.row.wrapper.NotificationViewWrapper; import com.android.systemui.statusbar.policy.InflatedSmartReplyState; @@ -86,7 +87,7 @@ public class NotificationContentView extends FrameLayout implements Notification public static final int VISIBLE_TYPE_CONTRACTED = 0; public static final int VISIBLE_TYPE_EXPANDED = 1; public static final int VISIBLE_TYPE_HEADSUP = 2; - private static final int VISIBLE_TYPE_SINGLELINE = 3; + public static final int VISIBLE_TYPE_SINGLELINE = 3; /** * Used when there is no content on the view such as when we're a public layout but don't * need to show. @@ -98,6 +99,7 @@ public class NotificationContentView extends FrameLayout implements Notification private final Rect mClipBounds = new Rect(); private int mMinContractedHeight; + private int mMinSingleLineHeight; private View mContractedChild; private View mExpandedChild; private View mHeadsUpChild; @@ -234,6 +236,11 @@ public class NotificationContentView extends FrameLayout implements Notification public void reinflate() { mMinContractedHeight = getResources().getDimensionPixelSize( R.dimen.min_notification_layout_height); + if (AsyncHybridViewInflation.isEnabled()) { + //TODO: set the height with a more reasonable min single-line height + mMinSingleLineHeight = getResources().getDimensionPixelSize( + R.dimen.conversation_single_line_face_pile_size); + } } public void setHeights(int smallHeight, int headsUpMaxHeight, int maxHeight) { @@ -540,6 +547,28 @@ public class NotificationContentView extends FrameLayout implements Notification updateShownWrapper(mVisibleType); } + /** + * Sets the single-line view. Child may be null to remove the view. + * @param child single-line content view to set + */ + public void setSingleLineView(@Nullable HybridNotificationView child) { + if (AsyncHybridViewInflation.isUnexpectedlyInLegacyMode()) return; + if (mSingleLineView != null) { + mOnContentViewInactiveListeners.remove(mSingleLineView); + mSingleLineView.animate().cancel(); + removeView(mSingleLineView); + } + if (child == null) { + mSingleLineView = null; + if (mTransformationStartVisibleType == VISIBLE_TYPE_SINGLELINE) { + mTransformationStartVisibleType = VISIBLE_TYPE_NONE; + } + return; + } + addView(child); + mSingleLineView = child; + } + @Override public void onViewAdded(View child) { super.onViewAdded(child); @@ -809,7 +838,17 @@ public class NotificationContentView extends FrameLayout implements Notification return mContractedChild != null ? getViewHeight(VISIBLE_TYPE_CONTRACTED) : mMinContractedHeight; } else { - return mSingleLineView.getHeight(); + if (AsyncHybridViewInflation.isEnabled()) { + if (mSingleLineView != null) { + return getViewHeight(VISIBLE_TYPE_SINGLELINE); + } else { + Log.wtf(TAG, "getMinHeight: mSingleLineView == null"); + return mMinSingleLineHeight; + } + } else { + AsyncHybridViewInflation.assertInLegacyMode(); + return mSingleLineView.getHeight(); + } } } @@ -1264,19 +1303,30 @@ public class NotificationContentView extends FrameLayout implements Notification } private void updateSingleLineView() { - if (mIsChildInGroup) { + try { Trace.beginSection("NotifContentView#updateSingleLineView"); - boolean isNewView = mSingleLineView == null; - mSingleLineView = mHybridGroupManager.bindFromNotification( - mSingleLineView, mContractedChild, mNotificationEntry.getSbn(), this); - if (isNewView) { - updateViewVisibility(mVisibleType, VISIBLE_TYPE_SINGLELINE, - mSingleLineView, mSingleLineView); + if (AsyncHybridViewInflation.isEnabled()) { + return; + } + AsyncHybridViewInflation.assertInLegacyMode(); + if (mIsChildInGroup) { + boolean isNewView = mSingleLineView == null; + mSingleLineView = mHybridGroupManager.bindFromNotification( + /* reusableView = */ mSingleLineView, + /* contentView = */ mContractedChild, + /* notification = */ mNotificationEntry.getSbn(), + /* parent = */ this + ); + if (isNewView && mSingleLineView != null) { + updateViewVisibility(mVisibleType, VISIBLE_TYPE_SINGLELINE, + mSingleLineView, mSingleLineView); + } + } else if (mSingleLineView != null) { + removeView(mSingleLineView); + mSingleLineView = null; } + } finally { Trace.endSection(); - } else if (mSingleLineView != null) { - removeView(mSingleLineView); - mSingleLineView = null; } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinder.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinder.java index d7b7aa210257..736140c44dfd 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinder.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinder.java @@ -80,6 +80,7 @@ public interface NotificationRowContentBinder { FLAG_CONTENT_VIEW_EXPANDED, FLAG_CONTENT_VIEW_HEADS_UP, FLAG_CONTENT_VIEW_PUBLIC, + FLAG_CONTENT_VIEW_SINGLE_LINE, FLAG_CONTENT_VIEW_ALL}) @interface InflationFlag {} /** @@ -102,7 +103,12 @@ public interface NotificationRowContentBinder { */ int FLAG_CONTENT_VIEW_PUBLIC = 1 << 3; - int FLAG_CONTENT_VIEW_ALL = (1 << 4) - 1; + /** + * The single line notification view. Show when the notification is shown as a child in group. + */ + int FLAG_CONTENT_VIEW_SINGLE_LINE = 1 << 4; + + int FLAG_CONTENT_VIEW_ALL = (1 << 5) - 1; /** * Parameters for content view binding diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindParams.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindParams.java index a52f638e7c26..1494c275d061 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindParams.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindParams.java @@ -102,9 +102,9 @@ public final class RowContentBindParams { * @see InflationFlag */ public void markContentViewsFreeable(@InflationFlag int contentViews) { - @InflationFlag int existingContentViews = contentViews &= mContentViews; + @InflationFlag int existingFreeableContentViews = contentViews &= mContentViews; mContentViews &= ~contentViews; - mDirtyContentViews |= existingContentViews; + mDirtyContentViews |= existingFreeableContentViews; } public @InflationFlag int getContentViews() { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindStage.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindStage.java index b70da00ad517..f4f8374d0a9f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindStage.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindStage.java @@ -63,7 +63,10 @@ public class RowContentBindStage extends BindStage<RowContentBindParams> { @InflationFlag int inflationFlags = params.getContentViews(); @InflationFlag int invalidatedFlags = params.getDirtyContentViews(); + // Rebind the content views which are needed now, and the corresponding old views are + // invalidated @InflationFlag int contentToBind = invalidatedFlags & inflationFlags; + // Unbind the content views that are not needed @InflationFlag int contentToUnbind = inflationFlags ^ FLAG_CONTENT_VIEW_ALL; // Bind/unbind with parameters diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/SingleLineViewInflater.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/SingleLineViewInflater.kt new file mode 100644 index 000000000000..d6118a0b3865 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/SingleLineViewInflater.kt @@ -0,0 +1,390 @@ +/* + * 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.app.Notification +import android.app.Notification.MessagingStyle +import android.app.Person +import android.content.Context +import android.graphics.drawable.Icon +import android.util.Log +import android.view.LayoutInflater +import com.android.app.tracing.traceSection +import com.android.internal.R +import com.android.internal.widget.MessagingMessage +import com.android.internal.widget.PeopleHelper +import com.android.systemui.statusbar.notification.collection.NotificationEntry +import com.android.systemui.statusbar.notification.logKey +import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_SINGLE_LINE +import com.android.systemui.statusbar.notification.row.shared.AsyncHybridViewInflation +import com.android.systemui.statusbar.notification.row.ui.viewmodel.ConversationAvatar +import com.android.systemui.statusbar.notification.row.ui.viewmodel.ConversationData +import com.android.systemui.statusbar.notification.row.ui.viewmodel.FacePile +import com.android.systemui.statusbar.notification.row.ui.viewmodel.SingleIcon +import com.android.systemui.statusbar.notification.row.ui.viewmodel.SingleLineViewModel + +/** The inflater of SingleLineViewModel and SingleLineViewHolder */ +internal object SingleLineViewInflater { + const val TAG = "SingleLineViewInflater" + + /** + * Inflate an instance of SingleLineViewModel. + * + * @param notification the notification to show + * @param messagingStyle the MessagingStyle information is only provided for conversation + * notification, not for legacy messaging notifications + * @param builder the recovered Notification Builder + * @param systemUiContext the context of Android System UI + * @return the inflated SingleLineViewModel + */ + @JvmStatic + fun inflateSingleLineViewModel( + notification: Notification, + messagingStyle: MessagingStyle?, + builder: Notification.Builder, + systemUiContext: Context, + ): SingleLineViewModel { + if (AsyncHybridViewInflation.isUnexpectedlyInLegacyMode()) { + return SingleLineViewModel(null, null, null) + } + peopleHelper.init(systemUiContext) + var titleText = HybridGroupManager.resolveTitle(notification) + var contentText = HybridGroupManager.resolveText(notification) + + if (messagingStyle == null) { + return SingleLineViewModel( + titleText = titleText, + contentText = contentText, + conversationData = null, + ) + } + + val isGroupConversation = messagingStyle.isGroupConversation + + val conversationTextData = messagingStyle.loadConversationTextData(systemUiContext) + if (conversationTextData?.conversationTitle?.isNotEmpty() == true) { + titleText = conversationTextData.conversationTitle + } + if (conversationTextData?.conversationText?.isNotEmpty() == true) { + contentText = conversationTextData.conversationText + } + + val conversationAvatar = + messagingStyle.loadConversationAvatar( + notification = notification, + isGroupConversation = isGroupConversation, + builder = builder, + systemUiContext = systemUiContext + ) + + val conversationData = + ConversationData( + // We don't show the sender's name for one-to-one conversation + conversationSenderName = + if (isGroupConversation) conversationTextData?.senderName else null, + avatar = conversationAvatar + ) + + return SingleLineViewModel( + titleText = titleText, + contentText = contentText, + conversationData = conversationData, + ) + } + + /** load conversation text data from the MessagingStyle of conversation notifications */ + private fun MessagingStyle.loadConversationTextData( + systemUiContext: Context + ): ConversationTextData? { + if (AsyncHybridViewInflation.isUnexpectedlyInLegacyMode()) { + return null + } + var conversationText: CharSequence? + + if (messages.isEmpty()) { + return null + } + + // load the conversation text + val lastMessage = messages[messages.lastIndex] + conversationText = lastMessage.text + if (conversationText == null && lastMessage.isImageMessage()) { + conversationText = findBackUpConversationText(lastMessage, systemUiContext) + } + + // load the sender's name to display + val name = lastMessage.senderPerson?.name + val senderName = + systemUiContext.resources.getString( + R.string.conversation_single_line_name_display, + name + ) + + // We need to find back-up values for those texts if they are needed and empty + return ConversationTextData( + conversationTitle = conversationTitle + ?: findBackUpConversationTitle(senderName, systemUiContext), + conversationText = conversationText, + senderName = senderName, + ) + } + + private fun MessagingStyle.Message.isImageMessage(): Boolean = MessagingMessage.hasImage(this) + + /** find a back-up conversation title when the conversation title is null. */ + private fun MessagingStyle.findBackUpConversationTitle( + senderName: CharSequence?, + systemUiContext: Context, + ): CharSequence { + if (AsyncHybridViewInflation.isUnexpectedlyInLegacyMode()) { + return "" + } + return if (isGroupConversation) { + systemUiContext.resources.getString(R.string.conversation_title_fallback_group_chat) + } else { + // Is one-to-one, let's try to use the last sender's name + // The last back-up is the value of resource: conversation_title_fallback_one_to_one + senderName + ?: systemUiContext.resources.getString( + R.string.conversation_title_fallback_one_to_one + ) + } + } + + /** + * find a back-up conversation text when the conversation has null text and is image message. + */ + private fun findBackUpConversationText( + message: MessagingStyle.Message, + context: Context, + ): CharSequence? { + if (AsyncHybridViewInflation.isUnexpectedlyInLegacyMode()) { + return null + } + // If the message is not an image message, just return empty, the back-up text for showing + // will be SingleLineViewModel.contentText + if (!message.isImageMessage()) return null + // If is image message, return a placeholder + return context.resources.getString(R.string.conversation_single_line_image_placeholder) + } + + /** + * The text data that we load from a conversation notification to show in the single-line views. + * + * Group conversation single-line view should be formatted as: + * [conversationTitle, senderName, conversationText] + * + * One-to-one single-line view should be formatted as: + * [conversationTitle (which is equal to the senderName), conversationText] + * + * @property conversationTitle the title of the conversation, not necessarily the title of the + * notification row. conversationTitle is non-null, though may be empty, in which case we need + * to show the notification title instead. + * @property conversationText the text content of the conversation, single-line will use the + * notification's text when conversationText is null + * @property senderName the sender's name to be shown in the row when needed. senderName can be + * null + */ + data class ConversationTextData( + val conversationTitle: CharSequence, + val conversationText: CharSequence?, + val senderName: CharSequence?, + ) + + private fun groupMessages( + messages: List<MessagingStyle.Message>, + historicMessages: List<MessagingStyle.Message>, + ): List<MutableList<MessagingStyle.Message>> { + if (AsyncHybridViewInflation.isUnexpectedlyInLegacyMode()) { + return listOf() + } + if (messages.isEmpty() && historicMessages.isEmpty()) return listOf() + var currentGroup: MutableList<MessagingStyle.Message>? = null + var currentSenderKey: CharSequence? = null + val groups = mutableListOf<MutableList<MessagingStyle.Message>>() + for (i in 0 until (historicMessages.size + messages.size)) { + val message = if (i < historicMessages.size) historicMessages[i] else messages[i] + + val sender = message.senderPerson + val senderKey = sender?.getKeyOrName() + val isNewGroup = (currentGroup == null) || senderKey != currentSenderKey + if (isNewGroup) { + currentGroup = mutableListOf() + groups.add(currentGroup) + currentSenderKey = senderKey + } + currentGroup?.add(message) + } + return groups + } + + private fun MessagingStyle.loadConversationAvatar( + builder: Notification.Builder, + notification: Notification, + isGroupConversation: Boolean, + systemUiContext: Context, + ): ConversationAvatar { + if (AsyncHybridViewInflation.isUnexpectedlyInLegacyMode()) { + return SingleIcon(null) + } + val userKey = user.getKeyOrName() + var conversationIcon: Icon? = null + var conversationText: CharSequence? = conversationTitle + + val groups = groupMessages(messages, historicMessages) + val uniqueNames = peopleHelper.mapUniqueNamesToPrefixWithGroupList(groups) + + if (!isGroupConversation) { + // Conversation is one-to-one, load the single icon + // Let's resolve the icon / text from the last sender + if (shortcutIcon != null) { + conversationIcon = shortcutIcon + } + + for (i in messages.lastIndex downTo 0) { + val message = messages[i] + val sender = message.senderPerson + val senderKey = sender?.getKeyOrName() + if ((sender != null && senderKey != userKey) || i == 0) { + if (conversationText.isNullOrEmpty()) { + // We use the senderName as header text if no conversation title is provided + // (This usually happens for most 1:1 conversations) + conversationText = sender?.name ?: "" + } + if (conversationIcon == null) { + var avatarIcon = sender?.icon + if (avatarIcon == null) { + avatarIcon = builder.getDefaultAvatar(name = conversationText) + } + conversationIcon = avatarIcon + } + break + } + } + } + + if (conversationIcon == null) { + conversationIcon = notification.getLargeIcon() + } + + // If is one-to-one or the conversation has an icon, return a single icon + if (!isGroupConversation || conversationIcon != null) { + return SingleIcon(conversationIcon?.loadDrawable(systemUiContext)) + } + + // Otherwise, let's find the two last conversations to build a face pile: + var secondLastIcon: Icon? = null + var lastIcon: Icon? = null + var lastKey: CharSequence? = null + + for (i in groups.lastIndex downTo 0) { + val message = groups[i][0] + val sender = message.senderPerson ?: user + val senderKey = sender.getKeyOrName() + val notUser = senderKey != userKey + val notIncluded = senderKey != lastKey + + if ((notUser && notIncluded) || (i == 0 && lastKey == null)) { + if (lastIcon == null) { + lastIcon = + sender.icon + ?: builder.getDefaultAvatar( + name = sender.name, + uniqueNames = uniqueNames + ) + lastKey = senderKey + } else { + secondLastIcon = + sender.icon + ?: builder.getDefaultAvatar( + name = sender.name, + uniqueNames = uniqueNames + ) + break + } + } + } + + if (lastIcon == null) { + lastIcon = builder.getDefaultAvatar(name = "") + } + + if (secondLastIcon == null) { + secondLastIcon = builder.getDefaultAvatar(name = "") + } + + return FacePile( + topIconDrawable = secondLastIcon.loadDrawable(systemUiContext), + bottomIconDrawable = lastIcon.loadDrawable(systemUiContext), + bottomBackgroundColor = builder.getBackgroundColor(/* isHeader = */ false), + ) + } + + @JvmStatic + fun inflateSingleLineViewHolder( + isConversation: Boolean, + reinflateFlags: Int, + entry: NotificationEntry, + context: Context, + logger: NotificationContentInflaterLogger, + ): HybridNotificationView? { + if (AsyncHybridViewInflation.isUnexpectedlyInLegacyMode()) return null + if (reinflateFlags and FLAG_CONTENT_VIEW_SINGLE_LINE == 0) { + return null + } + + logger.logInflateSingleLine(entry, reinflateFlags, isConversation) + logger.logAsyncTaskProgress(entry, "inflating single-line content view") + + var view: HybridNotificationView? = null + + traceSection("NotificationContentInflater#inflateSingleLineView") { + val inflater = LayoutInflater.from(context) + val layoutRes: Int = + if (isConversation) + com.android.systemui.res.R.layout.hybrid_conversation_notification + else com.android.systemui.res.R.layout.hybrid_notification + view = inflater.inflate(layoutRes, /* root = */ null) as HybridNotificationView + if (view == null) { + Log.wtf(TAG, "Single-line view inflation result is null for entry: ${entry.logKey}") + } + } + return view + } + + private fun Notification.Builder.getDefaultAvatar( + name: CharSequence?, + uniqueNames: PeopleHelper.NameToPrefixMap? = null + ): Icon { + val layoutColor = getSmallIconColor(/* isHeader = */ false) + if (!name.isNullOrEmpty()) { + val symbol = uniqueNames?.getPrefix(name) ?: "" + return peopleHelper.createAvatarSymbol( + /* name = */ name, + /* symbol = */ symbol, + /* layoutColor = */ layoutColor + ) + } + // If name is null, create default avatar with background color + // TODO(b/319829062): Investigate caching default icon for color + return peopleHelper.createAvatarSymbol(/* name = */ "", /* symbol = */ "", layoutColor) + } + + private fun Person.getKeyOrName(): CharSequence? = if (key == null) name else key + + private val peopleHelper = PeopleHelper() +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewbinder/SingleLineConversationViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewbinder/SingleLineConversationViewBinder.kt new file mode 100644 index 000000000000..69284bd7ef48 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewbinder/SingleLineConversationViewBinder.kt @@ -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.systemui.statusbar.notification.row.ui.viewbinder + +import com.android.systemui.statusbar.notification.row.HybridConversationNotificationView +import com.android.systemui.statusbar.notification.row.HybridNotificationView +import com.android.systemui.statusbar.notification.row.shared.AsyncHybridViewInflation +import com.android.systemui.statusbar.notification.row.ui.viewmodel.SingleLineViewModel + +object SingleLineConversationViewBinder { + @JvmStatic + fun bind(viewModel: SingleLineViewModel, view: HybridNotificationView?) { + if (AsyncHybridViewInflation.isUnexpectedlyInLegacyMode()) return + if (view !is HybridConversationNotificationView || !viewModel.isConversation()) { + SingleLineViewBinder.bind(viewModel, view) + return + } + + viewModel.conversationData?.avatar?.let { view.setAvatar(it) } + view.setText( + viewModel.titleText, + viewModel.contentText, + viewModel.conversationData?.conversationSenderName + ) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewbinder/SingleLineViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewbinder/SingleLineViewBinder.kt new file mode 100644 index 000000000000..22e10c165521 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewbinder/SingleLineViewBinder.kt @@ -0,0 +1,34 @@ +/* + * 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.ui.viewbinder + +import com.android.systemui.statusbar.notification.row.HybridNotificationView +import com.android.systemui.statusbar.notification.row.ui.viewmodel.SingleLineViewModel + +object SingleLineViewBinder { + @JvmStatic + fun bind(viewModel: SingleLineViewModel?, view: HybridNotificationView?) { + // bind the title and content text views + view?.apply { + bind( + /* title = */ viewModel?.titleText, + /* text = */ viewModel?.contentText, + /* contentView = */ null + ) + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/SingleLineViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/SingleLineViewModel.kt new file mode 100644 index 000000000000..d583fa5d97ed --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/SingleLineViewModel.kt @@ -0,0 +1,67 @@ +/* + * 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.ui.viewmodel + +import android.annotation.ColorInt +import android.graphics.drawable.Drawable + +/** + * ViewModel for SingleLine Notification View. + * + * @property titleText the text of notification view title + * @property contentText the text of view content + * @property conversationData the data that is needed specifically for conversation single-line + * views. Null conversationData shows that the notification is not conversation. Legacy + * MessagingStyle Notifications doesn't have this member. + */ +data class SingleLineViewModel( + var titleText: CharSequence?, + var contentText: CharSequence?, + var conversationData: ConversationData?, +) { + fun isConversation(): Boolean { + return conversationData != null + } +} + +/** + * @property conversationSenderName the name of sender to show in the single-line view. Only group + * conversation single-line views show the sender name. + * @property avatar the avatar to show for the conversation + */ +data class ConversationData( + val conversationSenderName: CharSequence?, + val avatar: ConversationAvatar, +) + +/** + * An avatar to show for a single-line conversation notification, it can be either a single icon or + * a face pile. + */ +sealed class ConversationAvatar + +data class SingleIcon(val iconDrawable: Drawable?) : ConversationAvatar() + +/** + * A kind of avatar to show for a group conversation notification view. It consists of two avatars + * of the last two senders. + */ +data class FacePile( + val topIconDrawable: Drawable?, + val bottomIconDrawable: Drawable?, + @ColorInt val bottomBackgroundColor: Int +) : ConversationAvatar() diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java index 45b9c269b61c..abf6c27c68ac 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java @@ -1295,8 +1295,8 @@ public class NotificationChildrenContainer extends ViewGroup if (singleLineView != null) { minExpandHeight += singleLineView.getHeight(); } else { - Log.e(TAG, "getMinHeight: child " + child + " single line view is null", - new Exception()); + Log.e(TAG, "getMinHeight: child " + child.getEntry().getKey() + + " single line view is null", new Exception()); } visibleChildren++; } 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 04db653282ac..dd04531b6b34 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 @@ -4917,30 +4917,12 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable public void removeContainerView(View v) { Assert.isMainThread(); removeView(v); - if (!FooterViewRefactor.isEnabled()) { - // A notification was removed, and we're not currently showing the empty shade view. - if (v instanceof ExpandableNotificationRow && !mController.isShowingEmptyShadeView()) { - mController.updateShowEmptyShadeView(); - updateFooter(); - mController.updateImportantForAccessibility(); - } - } - updateSpeedBumpIndex(); } public void addContainerView(View v) { Assert.isMainThread(); addView(v); - if (!FooterViewRefactor.isEnabled()) { - // A notification was added, and we're currently showing the empty shade view. - if (v instanceof ExpandableNotificationRow && mController.isShowingEmptyShadeView()) { - mController.updateShowEmptyShadeView(); - updateFooter(); - mController.updateImportantForAccessibility(); - } - } - updateSpeedBumpIndex(); } @@ -4948,14 +4930,6 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable Assert.isMainThread(); ensureRemovedFromTransientContainer(v); addView(v, index); - // A notification was added, and we're currently showing the empty shade view. - if (!FooterViewRefactor.isEnabled() && v instanceof ExpandableNotificationRow - && mController.isShowingEmptyShadeView()) { - mController.updateShowEmptyShadeView(); - updateFooter(); - mController.updateImportantForAccessibility(); - } - updateSpeedBumpIndex(); } 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 1143481863f5..49fde3984acc 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 @@ -2137,6 +2137,7 @@ public class NotificationStackScrollLayoutController implements Dumpable { if (!FooterViewRefactor.isEnabled()) { updateShowEmptyShadeView(); + updateImportantForAccessibility(); } } } 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 f842e304ffdf..fe5bdd41a94f 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 @@ -18,9 +18,12 @@ package com.android.systemui.statusbar.notification.stack.ui.viewbinder import android.animation.Animator import android.animation.AnimatorListenerAdapter +import android.view.View +import android.view.WindowInsets import androidx.lifecycle.Lifecycle import androidx.lifecycle.repeatOnLifecycle import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.keyguard.ui.viewmodel.BurnInParameters import com.android.systemui.lifecycle.repeatWhenAttached import com.android.systemui.scene.shared.flag.SceneContainerFlags import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController @@ -30,6 +33,9 @@ import com.android.systemui.statusbar.notification.stack.ui.view.SharedNotificat import com.android.systemui.statusbar.notification.stack.ui.viewmodel.SharedNotificationContainerViewModel import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.DisposableHandle +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch /** Binds the shared notification container to its view-model. */ @@ -65,6 +71,8 @@ object SharedNotificationContainerBinder { } } + val burnInParams = MutableStateFlow(BurnInParameters()) + /* * For animation sensitive coroutines, immediately run just like applicationScope does * instead of doing a post() to the main thread. This extra delay can cause visible jitter. @@ -122,7 +130,11 @@ object SharedNotificationContainerBinder { } } - launch { viewModel.translationY.collect { controller.setTranslationY(it) } } + launch { + burnInParams + .flatMapLatest { params -> viewModel.translationY(params) } + .collect { y -> controller.setTranslationY(y) } + } launch { viewModel.expansionAlpha.collect { controller.setMaxAlphaForExpansion(it) } @@ -137,11 +149,20 @@ object SharedNotificationContainerBinder { controller.setOnHeightChangedRunnable(Runnable { viewModel.notificationStackChanged() }) + view.setOnApplyWindowInsetsListener { v: View, insets: WindowInsets -> + val insetTypes = WindowInsets.Type.systemBars() or WindowInsets.Type.displayCutout() + burnInParams.update { current -> + current.copy(topInset = insets.getInsetsIgnoringVisibility(insetTypes).top) + } + insets + } + return object : DisposableHandle { override fun dispose() { disposableHandle.dispose() disposableHandleMainImmediate.dispose() controller.setOnHeightChangedRunnable(null) + view.setOnApplyWindowInsetsListener(null) } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt index 99cd89b84c14..4617ce49f44a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt @@ -28,6 +28,8 @@ import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.StatusBarState.SHADE_LOCKED import com.android.systemui.keyguard.shared.model.TransitionState.RUNNING import com.android.systemui.keyguard.shared.model.TransitionState.STARTED +import com.android.systemui.keyguard.ui.viewmodel.AodBurnInViewModel +import com.android.systemui.keyguard.ui.viewmodel.BurnInParameters import com.android.systemui.keyguard.ui.viewmodel.GlanceableHubToLockscreenTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.LockscreenToGlanceableHubTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.LockscreenToOccludedTransitionViewModel @@ -65,10 +67,11 @@ constructor( keyguardTransitionInteractor: KeyguardTransitionInteractor, private val shadeInteractor: ShadeInteractor, communalInteractor: CommunalInteractor, - occludedToLockscreenTransitionViewModel: OccludedToLockscreenTransitionViewModel, + private val occludedToLockscreenTransitionViewModel: OccludedToLockscreenTransitionViewModel, lockscreenToOccludedTransitionViewModel: LockscreenToOccludedTransitionViewModel, glanceableHubToLockscreenTransitionViewModel: GlanceableHubToLockscreenTransitionViewModel, - lockscreenToGlanceableHubTransitionViewModel: LockscreenToGlanceableHubTransitionViewModel + lockscreenToGlanceableHubTransitionViewModel: LockscreenToGlanceableHubTransitionViewModel, + private val aodBurnInViewModel: AodBurnInViewModel, ) { private val statesForConstrainedNotifications = setOf( @@ -313,20 +316,22 @@ constructor( * Under certain scenarios, such as swiping up on the lockscreen, the container will need to be * translated as the keyguard fades out. */ - val translationY: Flow<Float> = - combine( + fun translationY(params: BurnInParameters): Flow<Float> { + return combine( + aodBurnInViewModel.translationY(params).onStart { emit(0f) }, isOnLockscreenWithoutShade, merge( keyguardInteractor.keyguardTranslationY, occludedToLockscreenTransitionViewModel.lockscreenTranslationY, ) - ) { isOnLockscreenWithoutShade, translationY -> + ) { burnInY, isOnLockscreenWithoutShade, translationY -> if (isOnLockscreenWithoutShade) { - translationY + burnInY + translationY } else { 0f } } + } /** * When on keyguard, there is limited space to display notifications so calculate how many could 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 266c19c09941..7952511addfe 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java @@ -2587,8 +2587,7 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { // So if AOD is off or unsupported we need to trigger these updates at screen on // when the keyguard is occluded. mLockscreenUserManager.updatePublicMode(); - mShadeSurface.getNotificationStackScrollLayoutController() - .updateSensitivenessForOccludedWakeup(); + mStackScrollerController.updateSensitivenessForOccludedWakeup(); } if (mLaunchCameraWhenFinishedWaking) { mCameraLauncherLazy.get().launchCamera(mLastCameraLaunchSource, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java index aabe4a0d66f9..3f20eaf45260 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java @@ -763,10 +763,15 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, Dump // see: b/186644628 mNotificationsScrim.setDrawableBounds(left - 1, top, right + 1, bottom); mScrimBehind.setBottomEdgePosition((int) top); + } else { + mNotificationsScrim.setDrawableBounds(left, top, right, bottom); + } + + // Only clip if the notif scrim is visible + if (mNotificationsAlpha > 0f) { mKeyguardInteractor.setTopClippingBounds((int) top); } else { mKeyguardInteractor.setTopClippingBounds(null); - mNotificationsScrim.setDrawableBounds(left, top, right, bottom); } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimState.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimState.java index e3b65ab27f48..61bd112121bc 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimState.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimState.java @@ -20,6 +20,7 @@ import android.graphics.Color; import android.os.Trace; import com.android.systemui.dock.DockManager; +import com.android.systemui.res.R; import com.android.systemui.scrim.ScrimView; import com.android.systemui.statusbar.notification.stack.StackStateAnimator; @@ -39,8 +40,8 @@ public enum ScrimState { OFF { @Override public void prepare(ScrimState previousState) { - mFrontTint = Color.BLACK; - mBehindTint = Color.BLACK; + mFrontTint = mBackgroundColor; + mBehindTint = mBackgroundColor; mFrontAlpha = 1f; mBehindAlpha = 1f; @@ -74,15 +75,15 @@ public enum ScrimState { } else { mAnimationDuration = ScrimController.ANIMATION_DURATION; } - mFrontTint = Color.BLACK; - mBehindTint = Color.BLACK; - mNotifTint = mClipQsScrim ? Color.BLACK : Color.TRANSPARENT; + mFrontTint = mBackgroundColor; + mBehindTint = mBackgroundColor; + mNotifTint = mClipQsScrim ? mBackgroundColor : Color.TRANSPARENT; mFrontAlpha = 0; mBehindAlpha = mClipQsScrim ? 1 : mScrimBehindAlphaKeyguard; mNotifAlpha = mClipQsScrim ? mScrimBehindAlphaKeyguard : 0; if (mClipQsScrim) { - updateScrimColor(mScrimBehind, 1f /* alpha */, Color.BLACK); + updateScrimColor(mScrimBehind, 1f /* alpha */, mBackgroundColor); } } }, @@ -93,10 +94,10 @@ public enum ScrimState { // notif scrim alpha values are determined by ScrimController#applyState // based on the shade expansion - mFrontTint = Color.BLACK; + mFrontTint = mBackgroundColor; mFrontAlpha = .66f; - mBehindTint = Color.BLACK; + mBehindTint = mBackgroundColor; mBehindAlpha = 1f; } }, @@ -110,7 +111,7 @@ public enum ScrimState { mBehindTint = previousState.mBehindTint; mBehindAlpha = previousState.mBehindAlpha; - mFrontTint = Color.BLACK; + mFrontTint = mBackgroundColor; mFrontAlpha = .66f; } }, @@ -122,7 +123,7 @@ public enum ScrimState { @Override public void prepare(ScrimState previousState) { mBehindAlpha = mClipQsScrim ? 1 : mDefaultScrimAlpha; - mBehindTint = mClipQsScrim ? Color.BLACK : mSurfaceColor; + mBehindTint = mClipQsScrim ? mBackgroundColor : mSurfaceColor; mNotifAlpha = mClipQsScrim ? mDefaultScrimAlpha : 0; mNotifTint = Color.TRANSPARENT; mFrontAlpha = 0f; @@ -154,10 +155,10 @@ public enum ScrimState { mBehindAlpha = mClipQsScrim ? 1 : mDefaultScrimAlpha; mNotifAlpha = 1f; mFrontAlpha = 0f; - mBehindTint = mClipQsScrim ? Color.TRANSPARENT : Color.BLACK; + mBehindTint = mClipQsScrim ? Color.TRANSPARENT : mBackgroundColor; if (mClipQsScrim) { - updateScrimColor(mScrimBehind, 1f /* alpha */, Color.BLACK); + updateScrimColor(mScrimBehind, 1f /* alpha */, mBackgroundColor); } } }, @@ -184,11 +185,11 @@ public enum ScrimState { final boolean isDocked = mDockManager.isDocked(); mBlankScreen = mDisplayRequiresBlanking; - mFrontTint = Color.BLACK; + mFrontTint = mBackgroundColor; mFrontAlpha = (alwaysOnEnabled || isDocked || quickPickupEnabled) ? mAodFrontScrimAlpha : 1f; - mBehindTint = Color.BLACK; + mBehindTint = mBackgroundColor; mBehindAlpha = ScrimController.TRANSPARENT; mAnimationDuration = ScrimController.ANIMATION_DURATION_LONG; @@ -222,8 +223,8 @@ public enum ScrimState { @Override public void prepare(ScrimState previousState) { mFrontAlpha = mAodFrontScrimAlpha; - mBehindTint = Color.BLACK; - mFrontTint = Color.BLACK; + mBehindTint = mBackgroundColor; + mFrontTint = mBackgroundColor; mBlankScreen = mDisplayRequiresBlanking; mAnimationDuration = mWakeLockScreenSensorActive ? ScrimController.ANIMATION_DURATION_LONG : ScrimController.ANIMATION_DURATION; @@ -231,7 +232,7 @@ public enum ScrimState { @Override public float getMaxLightRevealScrimAlpha() { return mWakeLockScreenSensorActive ? ScrimController.WAKE_SENSOR_SCRIM_ALPHA - : AOD.getMaxLightRevealScrimAlpha(); + : AOD.getMaxLightRevealScrimAlpha(); } }, @@ -245,7 +246,6 @@ public enum ScrimState { mBehindAlpha = mClipQsScrim ? 1 : 0; mNotifAlpha = 0; mFrontAlpha = 0; - mAnimationDuration = mKeyguardFadingAway ? mKeyguardFadingAwayDuration : CentralSurfaces.FADE_KEYGUARD_DURATION; @@ -259,22 +259,22 @@ public enum ScrimState { && !fromAod; mFrontTint = Color.TRANSPARENT; - mBehindTint = Color.BLACK; + mBehindTint = mBackgroundColor; mBlankScreen = false; if (mDisplayRequiresBlanking && previousState == ScrimState.AOD) { // Set all scrims black, before they fade transparent. - updateScrimColor(mScrimInFront, 1f /* alpha */, Color.BLACK /* tint */); - updateScrimColor(mScrimBehind, 1f /* alpha */, Color.BLACK /* tint */); + updateScrimColor(mScrimInFront, 1f /* alpha */, mBackgroundColor /* tint */); + updateScrimColor(mScrimBehind, 1f /* alpha */, mBackgroundColor /* tint */); // Scrims should still be black at the end of the transition. - mFrontTint = Color.BLACK; - mBehindTint = Color.BLACK; + mFrontTint = mBackgroundColor; + mBehindTint = mBackgroundColor; mBlankScreen = true; } if (mClipQsScrim) { - updateScrimColor(mScrimBehind, 1f /* alpha */, Color.BLACK); + updateScrimColor(mScrimBehind, 1f /* alpha */, mBackgroundColor); } } }, @@ -283,8 +283,8 @@ public enum ScrimState { @Override public void prepare(ScrimState previousState) { mFrontTint = Color.TRANSPARENT; - mBehindTint = Color.BLACK; - mNotifTint = mClipQsScrim ? Color.BLACK : Color.TRANSPARENT; + mBehindTint = mBackgroundColor; + mNotifTint = mClipQsScrim ? mBackgroundColor : Color.TRANSPARENT; mFrontAlpha = 0; mBehindAlpha = mClipQsScrim ? 1 : 0; @@ -293,7 +293,7 @@ public enum ScrimState { mBlankScreen = false; if (mClipQsScrim) { - updateScrimColor(mScrimBehind, 1f /* alpha */, Color.BLACK); + updateScrimColor(mScrimBehind, 1f /* alpha */, mBackgroundColor); } } }; @@ -327,9 +327,11 @@ public enum ScrimState { boolean mKeyguardFadingAway; long mKeyguardFadingAwayDuration; boolean mClipQsScrim; + int mBackgroundColor; public void init(ScrimView scrimInFront, ScrimView scrimBehind, DozeParameters dozeParameters, DockManager dockManager) { + mBackgroundColor = scrimBehind.getContext().getColor(R.color.shade_scrim_background_dark); mScrimInFront = scrimInFront; mScrimBehind = scrimBehind; 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 88347ab90606..4c83ca28b3cb 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java @@ -69,6 +69,7 @@ import com.android.systemui.dock.DockManager; import com.android.systemui.dreams.DreamOverlayStateController; 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.KeyguardTransitionInteractor; import com.android.systemui.keyguard.domain.interactor.WindowManagerLockscreenVisibilityInteractor; @@ -474,7 +475,7 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb mIsDocked = mDockManager.isDocked(); } - if (mFlags.isEnabled(Flags.KEYGUARD_WM_STATE_REFACTOR)) { + if (KeyguardWmStateRefactor.isEnabled()) { // Show the keyguard views whenever we've told WM that the lockscreen is visible. mShadeViewController.postToView(() -> collectFlow( @@ -1428,7 +1429,7 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb executeAfterKeyguardGoneAction(); } - if (mFlags.isEnabled(Flags.KEYGUARD_WM_STATE_REFACTOR)) { + if (KeyguardWmStateRefactor.isEnabled()) { mKeyguardTransitionInteractor.startDismissKeyguardTransition(); } } diff --git a/packages/SystemUI/src/com/android/systemui/util/kotlin/BooleanFlowOperators.kt b/packages/SystemUI/src/com/android/systemui/util/kotlin/BooleanFlowOperators.kt new file mode 100644 index 000000000000..693a835e25d2 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/util/kotlin/BooleanFlowOperators.kt @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.util.kotlin + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.map + +object BooleanFlowOperators { + /** + * Logical AND operator for boolean flows. Will collect all flows and [combine] them to + * determine the result. + * + * Usage: + * ``` + * val result = and(flow1, flow2) + * ``` + */ + fun and(vararg flows: Flow<Boolean>): Flow<Boolean> = + combine(flows.asIterable()) { values -> values.all { it } } + + /** + * Logical NOT operator for a boolean flow. + * + * Usage: + * ``` + * val negatedFlow = not(flow) + * ``` + */ + fun not(flow: Flow<Boolean>) = flow.map { !it } + + /** + * Logical OR operator for a boolean flow. Will collect all flows and [combine] them to + * determine the result. + */ + fun or(vararg flows: Flow<Boolean>): Flow<Boolean> = + combine(flows.asIterable()) { values -> values.any { it } } +} diff --git a/packages/SystemUI/src/com/android/systemui/util/kotlin/Dagger.kt b/packages/SystemUI/src/com/android/systemui/util/kotlin/Dagger.kt index c587f2edd601..5150389930a9 100644 --- a/packages/SystemUI/src/com/android/systemui/util/kotlin/Dagger.kt +++ b/packages/SystemUI/src/com/android/systemui/util/kotlin/Dagger.kt @@ -17,6 +17,7 @@ package com.android.systemui.util.kotlin import dagger.Lazy +import java.util.Optional import kotlin.reflect.KProperty /** @@ -30,3 +31,16 @@ import kotlin.reflect.KProperty * ``` */ operator fun <T> Lazy<T>.getValue(thisRef: Any?, property: KProperty<*>): T = get() + +/** + * Extension operator that allows developers to use [java.util.Optional] as a nullable property + * delegate: + * ```kotlin + * class MyClass @Inject constructor( + * optionalDependency: Optional<Foo>, + * ) { + * val dependency: Foo? by optionalDependency + * } + * ``` + */ +operator fun <T> Optional<T>.getValue(thisRef: Any?, property: KProperty<*>): T? = getOrNull() diff --git a/packages/SystemUI/src/com/android/systemui/volume/dagger/AudioModule.kt b/packages/SystemUI/src/com/android/systemui/volume/dagger/AudioModule.kt new file mode 100644 index 000000000000..8d5e55a2917e --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/volume/dagger/AudioModule.kt @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.volume.dagger + +import android.media.AudioManager +import com.android.settingslib.volume.data.repository.AudioRepository +import com.android.settingslib.volume.data.repository.AudioRepositoryImpl +import com.android.settingslib.volume.domain.interactor.AudioModeInteractor +import com.android.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 + +/** Dagger module for audio code in the volume package */ +@Module +interface AudioModule { + + companion object { + + @Provides + fun provideAudioRepository( + audioManager: AudioManager, + @Background coroutineContext: CoroutineContext, + @Application coroutineScope: CoroutineScope, + ): AudioRepository = AudioRepositoryImpl(audioManager, coroutineContext, coroutineScope) + + @Provides + fun provideAudioModeInteractor(repository: AudioRepository): AudioModeInteractor = + AudioModeInteractor(repository) + } +} 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 b1bfbe0016e1..c842e5f295b9 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java +++ b/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java @@ -60,6 +60,9 @@ import kotlinx.coroutines.CoroutineScope; /** Dagger Module for code in the volume package. */ @Module( + includes = { + AudioModule.class, + }, subcomponents = { VolumePanelComponent.class } diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/DragToInteractAnimationControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/DragToInteractAnimationControllerTest.java index 9bcab57bec87..90878169c201 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/DragToInteractAnimationControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/DragToInteractAnimationControllerTest.java @@ -16,10 +16,12 @@ package com.android.systemui.accessibility.floatingmenu; -import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; +import android.annotation.NonNull; +import android.platform.test.annotations.DisableFlags; +import android.platform.test.annotations.EnableFlags; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; import android.view.WindowManager; @@ -27,10 +29,12 @@ import android.view.accessibility.AccessibilityManager; import androidx.test.filters.SmallTest; +import com.android.systemui.Flags; import com.android.systemui.SysuiTestCase; +import com.android.systemui.accessibility.utils.TestUtils; import com.android.systemui.util.settings.SecureSettings; -import com.android.wm.shell.bubbles.DismissViewUtils; import com.android.wm.shell.common.bubbles.DismissView; +import com.android.wm.shell.common.magnetictarget.MagnetizedObject; import org.junit.Before; import org.junit.Rule; @@ -46,6 +50,7 @@ import org.mockito.junit.MockitoRule; @TestableLooper.RunWithLooper public class DragToInteractAnimationControllerTest extends SysuiTestCase { private DragToInteractAnimationController mDragToInteractAnimationController; + private DragToInteractView mInteractView; private DismissView mDismissView; @Rule @@ -57,29 +62,72 @@ public class DragToInteractAnimationControllerTest extends SysuiTestCase { @Before public void setUp() throws Exception { final WindowManager stubWindowManager = mContext.getSystemService(WindowManager.class); + final SecureSettings mockSecureSettings = TestUtils.mockSecureSettings(); final MenuViewModel stubMenuViewModel = new MenuViewModel(mContext, mAccessibilityManager, - mock(SecureSettings.class)); + mockSecureSettings); final MenuViewAppearance stubMenuViewAppearance = new MenuViewAppearance(mContext, stubWindowManager); - final MenuView stubMenuView = new MenuView(mContext, stubMenuViewModel, - stubMenuViewAppearance); + final MenuView stubMenuView = spy(new MenuView(mContext, stubMenuViewModel, + stubMenuViewAppearance, mockSecureSettings)); + mInteractView = spy(new DragToInteractView(mContext)); mDismissView = spy(new DismissView(mContext)); - DismissViewUtils.setup(mDismissView); - mDragToInteractAnimationController = new DragToInteractAnimationController( - mDismissView, stubMenuView); + + if (Flags.floatingMenuDragToEdit()) { + mDragToInteractAnimationController = new DragToInteractAnimationController( + mInteractView, stubMenuView); + } else { + mDragToInteractAnimationController = new DragToInteractAnimationController( + mDismissView, stubMenuView); + } + + mDragToInteractAnimationController.setMagnetListener(new MagnetizedObject.MagnetListener() { + @Override + public void onStuckToTarget(@NonNull MagnetizedObject.MagneticTarget target) { + + } + + @Override + public void onUnstuckFromTarget(@NonNull MagnetizedObject.MagneticTarget target, + float velX, float velY, boolean wasFlungOut) { + + } + + @Override + public void onReleasedInTarget(@NonNull MagnetizedObject.MagneticTarget target) { + + } + }); } @Test - public void showDismissView_success() { - mDragToInteractAnimationController.showDismissView(true); + @DisableFlags(Flags.FLAG_FLOATING_MENU_DRAG_TO_EDIT) + public void showDismissView_success_old() { + mDragToInteractAnimationController.showInteractView(true); verify(mDismissView).show(); } @Test - public void hideDismissView_success() { - mDragToInteractAnimationController.showDismissView(false); + @DisableFlags(Flags.FLAG_FLOATING_MENU_DRAG_TO_EDIT) + public void hideDismissView_success_old() { + mDragToInteractAnimationController.showInteractView(false); verify(mDismissView).hide(); } + + @Test + @EnableFlags(Flags.FLAG_FLOATING_MENU_DRAG_TO_EDIT) + public void showDismissView_success() { + mDragToInteractAnimationController.showInteractView(true); + + verify(mInteractView).show(); + } + + @Test + @EnableFlags(Flags.FLAG_FLOATING_MENU_DRAG_TO_EDIT) + public void hideDismissView_success() { + mDragToInteractAnimationController.showInteractView(false); + + verify(mInteractView).hide(); + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationControllerTest.java index 215f93d1e163..e0df1e0e5586 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationControllerTest.java @@ -17,6 +17,7 @@ package com.android.systemui.accessibility.floatingmenu; import static com.google.common.truth.Truth.assertThat; + import static org.mockito.Mockito.any; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; @@ -42,6 +43,7 @@ import androidx.test.filters.SmallTest; import com.android.systemui.Flags; import com.android.systemui.Prefs; import com.android.systemui.SysuiTestCase; +import com.android.systemui.accessibility.utils.TestUtils; import com.android.systemui.util.settings.SecureSettings; import org.junit.After; @@ -79,10 +81,12 @@ public class MenuAnimationControllerTest extends SysuiTestCase { final WindowManager stubWindowManager = mContext.getSystemService(WindowManager.class); final MenuViewAppearance stubMenuViewAppearance = new MenuViewAppearance(mContext, stubWindowManager); + final SecureSettings secureSettings = TestUtils.mockSecureSettings(); final MenuViewModel stubMenuViewModel = new MenuViewModel(mContext, mAccessibilityManager, - mock(SecureSettings.class)); + secureSettings); - mMenuView = spy(new MenuView(mContext, stubMenuViewModel, stubMenuViewAppearance)); + mMenuView = spy(new MenuView(mContext, stubMenuViewModel, stubMenuViewAppearance, + secureSettings)); mViewPropertyAnimator = spy(mMenuView.animate()); doReturn(mViewPropertyAnimator).when(mMenuView).animate(); diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuItemAccessibilityDelegateTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuItemAccessibilityDelegateTest.java index 9c8de302c5e1..c2ed7d4a9d3c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuItemAccessibilityDelegateTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuItemAccessibilityDelegateTest.java @@ -22,10 +22,13 @@ import static androidx.core.view.accessibility.AccessibilityNodeInfoCompat.ACTIO import static com.google.common.truth.Truth.assertThat; import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import android.graphics.Rect; +import android.platform.test.annotations.DisableFlags; +import android.platform.test.annotations.EnableFlags; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; import android.view.WindowManager; @@ -37,7 +40,9 @@ import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerViewAccessibilityDelegate; import androidx.test.filters.SmallTest; +import com.android.systemui.Flags; import com.android.systemui.SysuiTestCase; +import com.android.systemui.accessibility.utils.TestUtils; import com.android.systemui.res.R; import com.android.systemui.util.settings.SecureSettings; @@ -49,6 +54,8 @@ import org.mockito.Mock; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; +import java.util.concurrent.atomic.AtomicBoolean; + /** Tests for {@link MenuItemAccessibilityDelegate}. */ @SmallTest @TestableLooper.RunWithLooper @@ -59,17 +66,16 @@ public class MenuItemAccessibilityDelegateTest extends SysuiTestCase { @Mock private AccessibilityManager mAccessibilityManager; - @Mock - private SecureSettings mSecureSettings; - @Mock - private DragToInteractAnimationController.DismissCallback mStubDismissCallback; - + private final SecureSettings mSecureSettings = TestUtils.mockSecureSettings(); private RecyclerView mStubListView; private MenuView mMenuView; + private MenuViewLayer mMenuViewLayer; private MenuItemAccessibilityDelegate mMenuItemAccessibilityDelegate; private MenuAnimationController mMenuAnimationController; private final Rect mDraggableBounds = new Rect(100, 200, 300, 400); + private final AtomicBoolean mEditReceived = new AtomicBoolean(false); + @Before public void setUp() { final WindowManager stubWindowManager = mContext.getSystemService(WindowManager.class); @@ -80,20 +86,28 @@ public class MenuItemAccessibilityDelegateTest extends SysuiTestCase { final int halfScreenHeight = stubWindowManager.getCurrentWindowMetrics().getBounds().height() / 2; - mMenuView = spy(new MenuView(mContext, stubMenuViewModel, stubMenuViewAppearance)); + mMenuView = spy(new MenuView(mContext, stubMenuViewModel, stubMenuViewAppearance, + mSecureSettings)); mMenuView.setTranslationY(halfScreenHeight); + mMenuViewLayer = spy(new MenuViewLayer( + mContext, stubWindowManager, mAccessibilityManager, + stubMenuViewModel, stubMenuViewAppearance, mMenuView, + mock(IAccessibilityFloatingMenu.class), mSecureSettings)); + doReturn(mDraggableBounds).when(mMenuView).getMenuDraggableBounds(); mStubListView = new RecyclerView(mContext); mMenuAnimationController = spy(new MenuAnimationController(mMenuView, stubMenuViewAppearance)); mMenuItemAccessibilityDelegate = new MenuItemAccessibilityDelegate(new RecyclerViewAccessibilityDelegate( - mStubListView), mMenuAnimationController); + mStubListView), mMenuAnimationController, mMenuViewLayer); + mEditReceived.set(false); } @Test - public void getAccessibilityActionList_matchSize() { + @DisableFlags(Flags.FLAG_FLOATING_MENU_DRAG_TO_EDIT) + public void getAccessibilityActionList_matchSize_withoutEdit() { final AccessibilityNodeInfoCompat info = new AccessibilityNodeInfoCompat(new AccessibilityNodeInfo()); @@ -103,6 +117,17 @@ public class MenuItemAccessibilityDelegateTest extends SysuiTestCase { } @Test + @EnableFlags(Flags.FLAG_FLOATING_MENU_DRAG_TO_EDIT) + public void getAccessibilityActionList_matchSize() { + final AccessibilityNodeInfoCompat info = + new AccessibilityNodeInfoCompat(new AccessibilityNodeInfo()); + + mMenuItemAccessibilityDelegate.onInitializeAccessibilityNodeInfo(mStubListView, info); + + assertThat(info.getActionList().size()).isEqualTo(7); + } + + @Test public void performMoveTopLeftAction_matchPosition() { final boolean moveTopLeftAction = mMenuItemAccessibilityDelegate.performAccessibilityAction(mStubListView, @@ -169,13 +194,22 @@ public class MenuItemAccessibilityDelegateTest extends SysuiTestCase { @Test public void performRemoveMenuAction_success() { - mMenuAnimationController.setDismissCallback(mStubDismissCallback); final boolean removeMenuAction = mMenuItemAccessibilityDelegate.performAccessibilityAction(mStubListView, R.id.action_remove_menu, null); assertThat(removeMenuAction).isTrue(); - verify(mMenuAnimationController).removeMenu(); + verify(mMenuViewLayer).dispatchAccessibilityAction(R.id.action_remove_menu); + } + + @Test + public void performEditAction_success() { + final boolean editAction = + mMenuItemAccessibilityDelegate.performAccessibilityAction(mStubListView, + R.id.action_edit, null); + + assertThat(editAction).isTrue(); + verify(mMenuViewLayer).dispatchAccessibilityAction(R.id.action_edit); } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuListViewTouchHandlerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuListViewTouchHandlerTest.java index e1522f5f6751..9e8c6b3395e2 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuListViewTouchHandlerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuListViewTouchHandlerTest.java @@ -16,6 +16,7 @@ package com.android.systemui.accessibility.floatingmenu; +import static android.R.id.empty; import static android.view.View.OVER_SCROLL_NEVER; import static com.google.common.truth.Truth.assertThat; @@ -27,6 +28,8 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; +import android.platform.test.annotations.DisableFlags; +import android.platform.test.annotations.EnableFlags; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; import android.view.MotionEvent; @@ -38,10 +41,11 @@ import androidx.recyclerview.widget.RecyclerView; import androidx.test.filters.SmallTest; import com.android.internal.accessibility.dialog.AccessibilityTarget; +import com.android.systemui.Flags; import com.android.systemui.SysuiTestCase; import com.android.systemui.accessibility.MotionEventHelper; +import com.android.systemui.accessibility.utils.TestUtils; import com.android.systemui.util.settings.SecureSettings; -import com.android.wm.shell.bubbles.DismissViewUtils; import com.android.wm.shell.common.bubbles.DismissView; import org.junit.After; @@ -71,6 +75,7 @@ public class MenuListViewTouchHandlerTest extends SysuiTestCase { private DragToInteractAnimationController mDragToInteractAnimationController; private RecyclerView mStubListView; private DismissView mDismissView; + private DragToInteractView mInteractView; @Rule public MockitoRule mockito = MockitoJUnit.rule(); @@ -81,19 +86,28 @@ public class MenuListViewTouchHandlerTest extends SysuiTestCase { @Before public void setUp() throws Exception { final WindowManager windowManager = mContext.getSystemService(WindowManager.class); + final SecureSettings secureSettings = TestUtils.mockSecureSettings(); final MenuViewModel stubMenuViewModel = new MenuViewModel(mContext, mAccessibilityManager, - mock(SecureSettings.class)); + secureSettings); final MenuViewAppearance stubMenuViewAppearance = new MenuViewAppearance(mContext, windowManager); - mStubMenuView = new MenuView(mContext, stubMenuViewModel, stubMenuViewAppearance); + mStubMenuView = new MenuView(mContext, stubMenuViewModel, stubMenuViewAppearance, + secureSettings); mStubMenuView.setTranslationX(0); mStubMenuView.setTranslationY(0); mMenuAnimationController = spy(new MenuAnimationController( mStubMenuView, stubMenuViewAppearance)); + mInteractView = spy(new DragToInteractView(mContext)); mDismissView = spy(new DismissView(mContext)); - DismissViewUtils.setup(mDismissView); - mDragToInteractAnimationController = - spy(new DragToInteractAnimationController(mDismissView, mStubMenuView)); + + if (Flags.floatingMenuDragToEdit()) { + mDragToInteractAnimationController = spy(new DragToInteractAnimationController( + mInteractView, mStubMenuView)); + } else { + mDragToInteractAnimationController = spy(new DragToInteractAnimationController( + mDismissView, mStubMenuView)); + } + mTouchHandler = new MenuListViewTouchHandler(mMenuAnimationController, mDragToInteractAnimationController); final AccessibilityTargetAdapter stubAdapter = new AccessibilityTargetAdapter(mStubTargets); @@ -115,7 +129,7 @@ public class MenuListViewTouchHandlerTest extends SysuiTestCase { @Test public void onActionMoveEvent_notConsumedEvent_shouldMoveToPosition() { - doReturn(false).when(mDragToInteractAnimationController).maybeConsumeMoveMotionEvent( + doReturn(empty).when(mDragToInteractAnimationController).maybeConsumeMoveMotionEvent( any(MotionEvent.class)); final int offset = 100; final MotionEvent stubDownEvent = @@ -136,6 +150,7 @@ public class MenuListViewTouchHandlerTest extends SysuiTestCase { } @Test + @DisableFlags(Flags.FLAG_FLOATING_MENU_DRAG_TO_EDIT) public void onActionMoveEvent_shouldShowDismissView() { final int offset = 100; final MotionEvent stubDownEvent = @@ -154,6 +169,25 @@ public class MenuListViewTouchHandlerTest extends SysuiTestCase { } @Test + @EnableFlags(Flags.FLAG_FLOATING_MENU_DRAG_TO_EDIT) + public void onActionMoveEvent_shouldShowInteractView() { + final int offset = 100; + final MotionEvent stubDownEvent = + mMotionEventHelper.obtainMotionEvent(/* downTime= */ 0, /* eventTime= */ 1, + MotionEvent.ACTION_DOWN, mStubMenuView.getTranslationX(), + mStubMenuView.getTranslationY()); + final MotionEvent stubMoveEvent = + mMotionEventHelper.obtainMotionEvent(/* downTime= */ 0, /* eventTime= */ 3, + MotionEvent.ACTION_MOVE, mStubMenuView.getTranslationX() + offset, + mStubMenuView.getTranslationY() + offset); + + mTouchHandler.onInterceptTouchEvent(mStubListView, stubDownEvent); + mTouchHandler.onInterceptTouchEvent(mStubListView, stubMoveEvent); + + verify(mInteractView).show(); + } + + @Test public void dragAndDrop_shouldFlingMenuThenSpringToEdge() { final int offset = 100; final MotionEvent stubDownEvent = 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 bc9a0a5484ac..ca3eb3e077d4 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 @@ -30,6 +30,7 @@ import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.eq; import static org.mockito.Mockito.mock; @@ -68,10 +69,13 @@ 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; import com.android.systemui.SysuiTestableContext; +import com.android.systemui.accessibility.utils.TestUtils; +import com.android.systemui.res.R; import com.android.systemui.util.settings.SecureSettings; import com.android.wm.shell.common.magnetictarget.MagnetizedObject; @@ -81,6 +85,7 @@ import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; +import org.mockito.ArgumentMatcher; import org.mockito.Mock; import org.mockito.Spy; import org.mockito.junit.MockitoJUnit; @@ -122,18 +127,17 @@ public class MenuViewLayerTest extends SysuiTestCase { private SysuiTestableContext mSpyContext = getContext(); @Mock private IAccessibilityFloatingMenu mFloatingMenu; - - @Mock - private SecureSettings mSecureSettings; - @Mock private WindowManager mStubWindowManager; - @Mock private AccessibilityManager mStubAccessibilityManager; + private final SecureSettings mSecureSettings = TestUtils.mockSecureSettings(); private final NotificationManager mMockNotificationManager = mock(NotificationManager.class); + private final ArgumentMatcher<IntentFilter> mNotificationMatcher = + (arg) -> arg.hasAction(ACTION_UNDO) && arg.hasAction(ACTION_DELETE); + @Before public void setUp() throws Exception { mSpyContext.addMockSystemService(Context.NOTIFICATION_SERVICE, mMockNotificationManager); @@ -145,8 +149,16 @@ public class MenuViewLayerTest extends SysuiTestCase { new WindowMetrics(mDisplayBounds, fakeDisplayInsets(), /* density = */ 0.0f)); doReturn(mWindowMetrics).when(mStubWindowManager).getCurrentWindowMetrics(); - mMenuViewLayer = new MenuViewLayer(mSpyContext, mStubWindowManager, - mStubAccessibilityManager, mFloatingMenu, mSecureSettings); + MenuViewModel menuViewModel = new MenuViewModel( + mSpyContext, mStubAccessibilityManager, mSecureSettings); + MenuViewAppearance menuViewAppearance = new MenuViewAppearance( + mSpyContext, mStubWindowManager); + mMenuView = spy( + new MenuView(mSpyContext, menuViewModel, menuViewAppearance, mSecureSettings)); + + mMenuViewLayer = spy(new MenuViewLayer(mSpyContext, mStubWindowManager, + mStubAccessibilityManager, menuViewModel, menuViewAppearance, mMenuView, + mFloatingMenu, mSecureSettings)); mMenuView = (MenuView) mMenuViewLayer.getChildAt(LayerIndex.MENU_VIEW); mMenuAnimationController = mMenuView.getMenuAnimationController(); @@ -226,7 +238,7 @@ public class MenuViewLayerTest extends SysuiTestCase { final List<String> stubShortcutTargets = new ArrayList<>(); stubShortcutTargets.add(TEST_SELECT_TO_SPEAK_COMPONENT_NAME.flattenToString()); when(mStubAccessibilityManager.getAccessibilityShortcutTargets( - AccessibilityManager.ACCESSIBILITY_SHORTCUT_KEY)).thenReturn(stubShortcutTargets); + ShortcutConstants.UserShortcutType.HARDWARE)).thenReturn(stubShortcutTargets); mMenuViewLayer.mDismissMenuAction.run(); final String value = Settings.Secure.getString(mSpyContext.getContentResolver(), @@ -236,6 +248,27 @@ public class MenuViewLayerTest extends SysuiTestCase { } @Test + @EnableFlags(Flags.FLAG_FLOATING_MENU_DRAG_TO_EDIT) + public void onEditAction_gotoEditScreen_isCalled() { + mMenuViewLayer.dispatchAccessibilityAction(R.id.action_edit); + verify(mMenuView).gotoEditScreen(); + } + + @Test + @EnableFlags(Flags.FLAG_FLOATING_MENU_DRAG_TO_HIDE) + public void onDismissAction_hideMenuAndShowNotification() { + mMenuViewLayer.dispatchAccessibilityAction(R.id.action_remove_menu); + verify(mMenuViewLayer).hideMenuAndShowNotification(); + } + + @Test + @DisableFlags(Flags.FLAG_FLOATING_MENU_DRAG_TO_HIDE) + public void onDismissAction_hideMenuAndShowMessage() { + mMenuViewLayer.dispatchAccessibilityAction(R.id.action_remove_menu); + verify(mMenuViewLayer).hideMenuAndShowMessage(); + } + + @Test public void showingImeInsetsChange_notOverlapOnIme_menuKeepOriginalPosition() { final float menuTop = STATUS_BAR_HEIGHT + 100; mMenuAnimationController.moveAndPersistPosition(new PointF(0, menuTop)); @@ -307,19 +340,13 @@ public class MenuViewLayerTest extends SysuiTestCase { @Test @EnableFlags(Flags.FLAG_FLOATING_MENU_DRAG_TO_HIDE) public void onReleasedInTarget_hideMenuAndShowNotificationWithExpectedActions() { - dragMenuThenReleasedInTarget(); + dragMenuThenReleasedInTarget(R.id.action_remove_menu); verify(mMockNotificationManager).notify( eq(SystemMessageProto.SystemMessage.NOTE_A11Y_FLOATING_MENU_HIDDEN), any(Notification.class)); - ArgumentCaptor<IntentFilter> intentFilterCaptor = ArgumentCaptor.forClass( - IntentFilter.class); verify(mSpyContext).registerReceiver( - any(BroadcastReceiver.class), - intentFilterCaptor.capture(), - anyInt()); - assertThat(intentFilterCaptor.getValue().matchAction(ACTION_UNDO)).isTrue(); - assertThat(intentFilterCaptor.getValue().matchAction(ACTION_DELETE)).isTrue(); + any(BroadcastReceiver.class), argThat(mNotificationMatcher), anyInt()); } @Test @@ -327,10 +354,10 @@ public class MenuViewLayerTest extends SysuiTestCase { public void receiveActionUndo_dismissNotificationAndMenuVisible() { ArgumentCaptor<BroadcastReceiver> broadcastReceiverCaptor = ArgumentCaptor.forClass( BroadcastReceiver.class); - dragMenuThenReleasedInTarget(); + dragMenuThenReleasedInTarget(R.id.action_remove_menu); verify(mSpyContext).registerReceiver(broadcastReceiverCaptor.capture(), - any(IntentFilter.class), anyInt()); + argThat(mNotificationMatcher), anyInt()); broadcastReceiverCaptor.getValue().onReceive(mSpyContext, new Intent(ACTION_UNDO)); verify(mSpyContext).unregisterReceiver(broadcastReceiverCaptor.getValue()); @@ -344,10 +371,10 @@ public class MenuViewLayerTest extends SysuiTestCase { public void receiveActionDelete_dismissNotificationAndHideMenu() { ArgumentCaptor<BroadcastReceiver> broadcastReceiverCaptor = ArgumentCaptor.forClass( BroadcastReceiver.class); - dragMenuThenReleasedInTarget(); + dragMenuThenReleasedInTarget(R.id.action_remove_menu); verify(mSpyContext).registerReceiver(broadcastReceiverCaptor.capture(), - any(IntentFilter.class), anyInt()); + argThat(mNotificationMatcher), anyInt()); broadcastReceiverCaptor.getValue().onReceive(mSpyContext, new Intent(ACTION_DELETE)); verify(mSpyContext).unregisterReceiver(broadcastReceiverCaptor.getValue()); @@ -423,10 +450,12 @@ public class MenuViewLayerTest extends SysuiTestCase { }); } - private void dragMenuThenReleasedInTarget() { + private void dragMenuThenReleasedInTarget(int id) { MagnetizedObject.MagnetListener magnetListener = - mMenuViewLayer.getDragToInteractAnimationController().getMagnetListener(); + mMenuViewLayer.getDragToInteractAnimationController().getMagnetListener(id); + View view = mock(View.class); + when(view.getId()).thenReturn(id); magnetListener.onReleasedInTarget( - new MagnetizedObject.MagneticTarget(mock(View.class), 200)); + new MagnetizedObject.MagneticTarget(view, 200)); } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewTest.java index 8da6cf98d76f..7c97f53d539d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewTest.java @@ -17,15 +17,19 @@ package com.android.systemui.accessibility.floatingmenu; import static android.app.UiModeManager.MODE_NIGHT_YES; + import static com.google.common.truth.Truth.assertThat; -import static org.mockito.Mockito.mock; + +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import android.app.UiModeManager; +import android.content.Intent; import android.graphics.Rect; import android.graphics.drawable.GradientDrawable; import android.platform.test.annotations.EnableFlags; +import android.provider.Settings; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; import android.view.WindowManager; @@ -36,6 +40,8 @@ import androidx.test.filters.SmallTest; import com.android.systemui.Flags; import com.android.systemui.Prefs; import com.android.systemui.SysuiTestCase; +import com.android.systemui.SysuiTestableContext; +import com.android.systemui.accessibility.utils.TestUtils; import com.android.systemui.util.settings.SecureSettings; import org.junit.After; @@ -65,17 +71,23 @@ public class MenuViewTest extends SysuiTestCase { @Mock private AccessibilityManager mAccessibilityManager; + private SysuiTestableContext mSpyContext; + @Before public void setUp() throws Exception { mUiModeManager = mContext.getSystemService(UiModeManager.class); mNightMode = mUiModeManager.getNightMode(); mUiModeManager.setNightMode(MODE_NIGHT_YES); + + mSpyContext = spy(mContext); + final SecureSettings secureSettings = TestUtils.mockSecureSettings(); final MenuViewModel stubMenuViewModel = new MenuViewModel(mContext, mAccessibilityManager, - mock(SecureSettings.class)); + secureSettings); final WindowManager stubWindowManager = mContext.getSystemService(WindowManager.class); - mStubMenuViewAppearance = new MenuViewAppearance(mContext, stubWindowManager); - mMenuView = spy(new MenuView(mContext, stubMenuViewModel, mStubMenuViewAppearance)); - mLastPosition = Prefs.getString(mContext, + mStubMenuViewAppearance = new MenuViewAppearance(mSpyContext, stubWindowManager); + mMenuView = spy(new MenuView(mSpyContext, stubMenuViewModel, mStubMenuViewAppearance, + secureSettings)); + mLastPosition = Prefs.getString(mSpyContext, Prefs.Key.ACCESSIBILITY_FLOATING_MENU_POSITION, /* defaultValue= */ null); } @@ -154,6 +166,25 @@ public class MenuViewTest extends SysuiTestCase { assertThat(radiiAnimator.isStarted()).isTrue(); } + @Test + public void getIntentForEditScreen_validate() { + Intent intent = mMenuView.getIntentForEditScreen(); + String[] targets = intent.getBundleExtra( + ":settings:show_fragment_args").getStringArray("targets"); + + assertThat(intent.getAction()).isEqualTo(Settings.ACTION_ACCESSIBILITY_SHORTCUT_SETTINGS); + assertThat(targets).asList().containsExactlyElementsIn(TestUtils.TEST_BUTTON_TARGETS); + } + + @Test + @EnableFlags(Flags.FLAG_FLOATING_MENU_DRAG_TO_EDIT) + public void gotoEditScreen_sendsIntent() { + // Notably, this shouldn't crash the settings app, + // because the button target args are configured. + mMenuView.gotoEditScreen(); + verify(mSpyContext).startActivity(any()); + } + private InstantInsetLayerDrawable getMenuViewInsetLayer() { return (InstantInsetLayerDrawable) mMenuView.getBackground(); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/utils/TestUtils.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/utils/TestUtils.java index 10c8caa4fd27..8399fa85bfb1 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/utils/TestUtils.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/utils/TestUtils.java @@ -16,11 +16,27 @@ package com.android.systemui.accessibility.utils; +import static com.android.internal.accessibility.common.ShortcutConstants.SERVICES_SEPARATOR; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import android.content.ComponentName; import android.os.SystemClock; +import android.os.UserHandle; +import android.provider.Settings; + +import com.android.systemui.util.settings.SecureSettings; +import java.util.Set; +import java.util.StringJoiner; import java.util.function.BooleanSupplier; public class TestUtils { + private static final ComponentName TEST_COMPONENT_A = new ComponentName("pkg", "A"); + private static final ComponentName TEST_COMPONENT_B = new ComponentName("pkg", "B"); + public static final String[] TEST_BUTTON_TARGETS = { + TEST_COMPONENT_A.flattenToString(), TEST_COMPONENT_B.flattenToString()}; public static long DEFAULT_CONDITION_DURATION = 5_000; /** @@ -55,4 +71,28 @@ public class TestUtils { SystemClock.sleep(sleepMs); } } + + /** + * Returns a mock secure settings configured to return information needed for tests. + * Currently, this only includes button targets. + */ + public static SecureSettings mockSecureSettings() { + SecureSettings secureSettings = mock(SecureSettings.class); + + final String targets = getShortcutTargets( + Set.of(TEST_COMPONENT_A, TEST_COMPONENT_B)); + when(secureSettings.getStringForUser( + Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS, + UserHandle.USER_CURRENT)).thenReturn(targets); + + return secureSettings; + } + + private static String getShortcutTargets(Set<ComponentName> components) { + final StringJoiner stringJoiner = new StringJoiner(String.valueOf(SERVICES_SEPARATOR)); + for (ComponentName target : components) { + stringJoiner.add(target.flattenToString()); + } + return stringJoiner.toString(); + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthRippleControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthRippleControllerTest.kt index a47e28801709..7c03d7899398 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthRippleControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthRippleControllerTest.kt @@ -27,12 +27,13 @@ import com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession import com.android.keyguard.KeyguardUpdateMonitor import com.android.keyguard.KeyguardUpdateMonitorCallback import com.android.keyguard.logging.KeyguardLogger +import com.android.systemui.Flags import com.android.systemui.Flags.FLAG_LIGHT_REVEAL_MIGRATION import com.android.systemui.SysuiTestCase import com.android.systemui.biometrics.data.repository.FakeFacePropertyRepository -import com.android.systemui.log.logcatLogBuffer -import com.android.systemui.flags.FeatureFlags +import com.android.systemui.deviceentry.domain.interactor.AuthRippleInteractor import com.android.systemui.keyguard.WakefulnessLifecycle +import com.android.systemui.log.logcatLogBuffer import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.statusbar.LightRevealScrim import com.android.systemui.statusbar.NotificationShadeWindowController @@ -42,7 +43,7 @@ import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.statusbar.policy.KeyguardStateController import com.android.systemui.util.leak.RotationUtils import com.android.systemui.util.mockito.any -import javax.inject.Provider +import kotlinx.coroutines.ExperimentalCoroutinesApi import org.junit.After import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue @@ -61,8 +62,10 @@ import org.mockito.Mockito.`when` import org.mockito.MockitoAnnotations import org.mockito.MockitoSession import org.mockito.quality.Strictness +import javax.inject.Provider +@ExperimentalCoroutinesApi @SmallTest @RunWith(AndroidTestingRunner::class) class AuthRippleControllerTest : SysuiTestCase() { @@ -74,6 +77,7 @@ class AuthRippleControllerTest : SysuiTestCase() { @Mock private lateinit var configurationController: ConfigurationController @Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor @Mock private lateinit var authController: AuthController + @Mock private lateinit var authRippleInteractor: AuthRippleInteractor @Mock private lateinit var keyguardStateController: KeyguardStateController @Mock private lateinit var wakefulnessLifecycle: WakefulnessLifecycle @@ -88,8 +92,6 @@ class AuthRippleControllerTest : SysuiTestCase() { @Mock private lateinit var statusBarStateController: StatusBarStateController @Mock - private lateinit var featureFlags: FeatureFlags - @Mock private lateinit var lightRevealScrim: LightRevealScrim @Mock private lateinit var fpSensorProp: FingerprintSensorPropertiesInternal @@ -103,6 +105,7 @@ class AuthRippleControllerTest : SysuiTestCase() { @Before fun setUp() { + mSetFlagsRule.disableFlags(Flags.FLAG_DEVICE_ENTRY_UDFPS_REFACTOR) MockitoAnnotations.initMocks(this) staticMockSession = mockitoSession() .mockStatic(RotationUtils::class.java) @@ -128,6 +131,7 @@ class AuthRippleControllerTest : SysuiTestCase() { KeyguardLogger(logcatLogBuffer(AuthRippleController.TAG)), biometricUnlockController, lightRevealScrim, + authRippleInteractor, facePropertyRepository, rippleView, ) 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 8127bb1f0465..6a9c88151dd0 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 @@ -1226,6 +1226,7 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa @Test fun descriptionOverriddenByContentView() = runGenericTest(contentView = promptContentView, description = "test description") { + mSetFlagsRule.enableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT) val contentView by collectLastValue(viewModel.contentView) val description by collectLastValue(viewModel.description) @@ -1236,6 +1237,7 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa @Test fun descriptionWithoutContentView() = runGenericTest(description = "test description") { + mSetFlagsRule.enableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT) val contentView by collectLastValue(viewModel.contentView) val description by collectLastValue(viewModel.description) diff --git a/packages/SystemUI/tests/src/com/android/systemui/deviceentry/data/repository/DeviceEntryHapticsInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractorTest.kt index 0dfdeca60fcd..bdf0e06ce410 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/deviceentry/data/repository/DeviceEntryHapticsInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractorTest.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.deviceentry.data.repository +package com.android.systemui.deviceentry.domain.interactor import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest @@ -23,12 +23,13 @@ import com.android.systemui.biometrics.data.repository.fingerprintPropertyReposi import com.android.systemui.biometrics.shared.model.FingerprintSensorType import com.android.systemui.biometrics.shared.model.SensorStrength import com.android.systemui.coroutines.collectLastValue -import com.android.systemui.deviceentry.domain.interactor.deviceEntryHapticsInteractor import com.android.systemui.deviceentry.shared.model.FailedFaceAuthenticationStatus import com.android.systemui.keyevent.data.repository.fakeKeyEventRepository import com.android.systemui.keyguard.data.repository.biometricSettingsRepository import com.android.systemui.keyguard.data.repository.deviceEntryFingerprintAuthRepository import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFaceAuthRepository +import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository +import com.android.systemui.keyguard.shared.model.BiometricUnlockModel import com.android.systemui.keyguard.shared.model.BiometricUnlockSource import com.android.systemui.keyguard.shared.model.FailFingerprintAuthenticationStatus import com.android.systemui.kosmos.testScope @@ -158,9 +159,10 @@ class DeviceEntryHapticsInteractorTest : SysuiTestCase() { } private suspend fun enterDeviceFromBiometricUnlock() { - kosmos.fakeDeviceEntryRepository.enteringDeviceFromBiometricUnlock( + kosmos.fakeKeyguardRepository.setBiometricUnlockSource( BiometricUnlockSource.FINGERPRINT_SENSOR ) + kosmos.fakeKeyguardRepository.setBiometricUnlockState(BiometricUnlockModel.WAKE_AND_UNLOCK) } private fun fingerprintFailure() { diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyboard/stickykeys/data/repository/StickyKeysRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyboard/stickykeys/data/repository/StickyKeysRepositoryImplTest.kt new file mode 100644 index 000000000000..ed80a869dda8 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/keyboard/stickykeys/data/repository/StickyKeysRepositoryImplTest.kt @@ -0,0 +1,98 @@ +package com.android.systemui.keyboard.stickykeys.data.repository + +import android.content.pm.UserInfo +import android.hardware.input.InputManager +import android.provider.Settings.Secure.ACCESSIBILITY_STICKY_KEYS +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.coroutines.collectValues +import com.android.systemui.keyboard.stickykeys.StickyKeysLogger +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.user.data.repository.fakeUserRepository +import com.android.systemui.util.mockito.mock +import com.android.systemui.util.settings.FakeSettings +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@RunWith(JUnit4::class) +class StickyKeysRepositoryImplTest : SysuiTestCase() { + + private val dispatcher = StandardTestDispatcher() + private val testScope = TestScope(dispatcher) + private val secureSettings = FakeSettings() + private val userRepository = Kosmos().fakeUserRepository + private lateinit var stickyKeysRepository: StickyKeysRepositoryImpl + + @Before + fun setup() { + stickyKeysRepository = StickyKeysRepositoryImpl( + mock<InputManager>(), + dispatcher, + secureSettings, + userRepository, + mock<StickyKeysLogger>() + ) + userRepository.setUserInfos(USER_INFOS) + setStickyKeySettingForUser(enabled = true, userInfo = SETTING_ENABLED_USER) + setStickyKeySettingForUser(enabled = false, userInfo = SETTING_DISABLED_USER) + } + + @Test + fun settingEnabledEmitsValueForCurrentUser() { + testScope.runTest { + userRepository.setSelectedUserInfo(SETTING_ENABLED_USER) + + val enabled by collectLastValue(stickyKeysRepository.settingEnabled) + + assertThat(enabled).isTrue() + } + } + + @Test + fun settingEnabledEmitsNewValueWhenSettingChanges() { + testScope.runTest { + userRepository.setSelectedUserInfo(SETTING_ENABLED_USER) + val enabled by collectValues(stickyKeysRepository.settingEnabled) + runCurrent() + + setStickyKeySettingForUser(enabled = false, userInfo = SETTING_ENABLED_USER) + + assertThat(enabled).containsExactly(true, false).inOrder() + } + } + + @Test + fun settingEnabledEmitsValueForNewUserWhenUserChanges() { + testScope.runTest { + userRepository.setSelectedUserInfo(SETTING_ENABLED_USER) + val enabled by collectLastValue(stickyKeysRepository.settingEnabled) + runCurrent() + + userRepository.setSelectedUserInfo(SETTING_DISABLED_USER) + + assertThat(enabled).isFalse() + } + } + + private fun setStickyKeySettingForUser(enabled: Boolean, userInfo: UserInfo) { + val newValue = if (enabled) "1" else "0" + secureSettings.putStringForUser(ACCESSIBILITY_STICKY_KEYS, newValue, userInfo.id) + } + + private companion object { + val SETTING_ENABLED_USER = UserInfo(/* id= */ 0, "user1", /* flags= */ 0) + val SETTING_DISABLED_USER = UserInfo(/* id= */ 1, "user2", /* flags= */ 0) + val USER_INFOS = listOf(SETTING_ENABLED_USER, SETTING_DISABLED_USER) + } +}
\ No newline at end of file diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyboard/stickykeys/ui/viewmodel/StickyKeysIndicatorViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyboard/stickykeys/ui/viewmodel/StickyKeysIndicatorViewModelTest.kt index 8a713688acf9..6eebb6d19e5e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyboard/stickykeys/ui/viewmodel/StickyKeysIndicatorViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyboard/stickykeys/ui/viewmodel/StickyKeysIndicatorViewModelTest.kt @@ -18,6 +18,7 @@ package com.android.systemui.keyboard.stickykeys.ui.viewmodel import android.hardware.input.InputManager import android.hardware.input.StickyModifierState +import android.provider.Settings.Secure.ACCESSIBILITY_STICKY_KEYS import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue @@ -31,8 +32,11 @@ import com.android.systemui.keyboard.stickykeys.shared.model.ModifierKey.ALT_GR import com.android.systemui.keyboard.stickykeys.shared.model.ModifierKey.CTRL import com.android.systemui.keyboard.stickykeys.shared.model.ModifierKey.META import com.android.systemui.keyboard.stickykeys.shared.model.ModifierKey.SHIFT +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.user.data.repository.fakeUserRepository import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.mock +import com.android.systemui.util.settings.FakeSettings import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.StandardTestDispatcher @@ -57,6 +61,8 @@ class StickyKeysIndicatorViewModelTest : SysuiTestCase() { private lateinit var viewModel: StickyKeysIndicatorViewModel private val inputManager = mock<InputManager>() private val keyboardRepository = FakeKeyboardRepository() + private val secureSettings = FakeSettings() + private val userRepository = Kosmos().fakeUserRepository private val captor = ArgumentCaptor.forClass(InputManager.StickyModifierStateListener::class.java) @@ -65,8 +71,11 @@ class StickyKeysIndicatorViewModelTest : SysuiTestCase() { val stickyKeysRepository = StickyKeysRepositoryImpl( inputManager, dispatcher, + secureSettings, + userRepository, mock<StickyKeysLogger>() ) + setStickyKeySetting(enabled = false) viewModel = StickyKeysIndicatorViewModel( stickyKeysRepository = stickyKeysRepository, @@ -76,13 +85,24 @@ class StickyKeysIndicatorViewModelTest : SysuiTestCase() { } @Test - fun startsListeningToStickyKeysOnlyWhenKeyboardIsConnected() { + fun doesntListenToStickyKeysOnlyWhenKeyboardIsConnected() { testScope.runTest { collectLastValue(viewModel.indicatorContent) + + keyboardRepository.setIsAnyKeyboardConnected(true) runCurrent() + verifyZeroInteractions(inputManager) + } + } + @Test + fun startsListeningToStickyKeysOnlyWhenKeyboardIsConnectedAndSettingIsOn() { + testScope.runTest { + collectLastValue(viewModel.indicatorContent) keyboardRepository.setIsAnyKeyboardConnected(true) + + setStickyKeySetting(enabled = true) runCurrent() verify(inputManager) @@ -93,11 +113,31 @@ class StickyKeysIndicatorViewModelTest : SysuiTestCase() { } } + private fun setStickyKeySetting(enabled: Boolean) { + val newValue = if (enabled) "1" else "0" + val defaultUser = userRepository.getSelectedUserInfo().id + secureSettings.putStringForUser(ACCESSIBILITY_STICKY_KEYS, newValue, defaultUser) + } + + @Test + fun stopsListeningToStickyKeysWhenStickyKeySettingsIsTurnedOff() { + testScope.runTest { + collectLastValue(viewModel.indicatorContent) + setStickyKeysActive() + runCurrent() + + setStickyKeySetting(enabled = false) + runCurrent() + + verify(inputManager).unregisterStickyModifierStateListener(any()) + } + } + @Test fun stopsListeningToStickyKeysWhenKeyboardDisconnects() { testScope.runTest { collectLastValue(viewModel.indicatorContent) - keyboardRepository.setIsAnyKeyboardConnected(true) + setStickyKeysActive() runCurrent() keyboardRepository.setIsAnyKeyboardConnected(false) @@ -111,7 +151,7 @@ class StickyKeysIndicatorViewModelTest : SysuiTestCase() { fun emitsStickyKeysListWhenStickyKeyIsPressed() { testScope.runTest { val stickyKeys by collectLastValue(viewModel.indicatorContent) - keyboardRepository.setIsAnyKeyboardConnected(true) + setStickyKeysActive() setStickyKeys(mapOf(ALT to false)) @@ -123,7 +163,7 @@ class StickyKeysIndicatorViewModelTest : SysuiTestCase() { fun emitsEmptyListWhenNoStickyKeysAreActive() { testScope.runTest { val stickyKeys by collectLastValue(viewModel.indicatorContent) - keyboardRepository.setIsAnyKeyboardConnected(true) + setStickyKeysActive() setStickyKeys(emptyMap()) @@ -135,7 +175,7 @@ class StickyKeysIndicatorViewModelTest : SysuiTestCase() { fun passesAllStickyKeysToDialog() { testScope.runTest { val stickyKeys by collectLastValue(viewModel.indicatorContent) - keyboardRepository.setIsAnyKeyboardConnected(true) + setStickyKeysActive() setStickyKeys(mapOf( ALT to false, @@ -154,7 +194,7 @@ class StickyKeysIndicatorViewModelTest : SysuiTestCase() { fun showsOnlyLockedStateIfKeyIsStickyAndLocked() { testScope.runTest { val stickyKeys by collectLastValue(viewModel.indicatorContent) - keyboardRepository.setIsAnyKeyboardConnected(true) + setStickyKeysActive() setStickyKeys(mapOf( ALT to false, @@ -168,7 +208,7 @@ class StickyKeysIndicatorViewModelTest : SysuiTestCase() { fun doesNotChangeOrderOfKeysIfTheyBecomeLocked() { testScope.runTest { val stickyKeys by collectLastValue(viewModel.indicatorContent) - keyboardRepository.setIsAnyKeyboardConnected(true) + setStickyKeysActive() setStickyKeys(mapOf( META to false, @@ -186,6 +226,11 @@ class StickyKeysIndicatorViewModelTest : SysuiTestCase() { } } + private fun setStickyKeysActive() { + keyboardRepository.setIsAnyKeyboardConnected(true) + setStickyKeySetting(enabled = true) + } + private fun TestScope.setStickyKeys(keys: Map<ModifierKey, Boolean>) { runCurrent() verify(inputManager).registerStickyModifierStateListener(any(), captor.capture()) diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java index 8a3a4342915b..14cae0b89a49 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java @@ -25,6 +25,7 @@ import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STR import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_NON_STRONG_BIOMETRICS_TIMEOUT; import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN; import static com.android.systemui.Flags.FLAG_REFACTOR_GET_CURRENT_USER; +import static com.android.systemui.Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR; import static com.android.systemui.keyguard.KeyguardViewMediator.DELAYED_KEYGUARD_ACTION; import static com.android.systemui.keyguard.KeyguardViewMediator.KEYGUARD_LOCK_AFTER_DELAY_DEFAULT; import static com.android.systemui.keyguard.KeyguardViewMediator.REBOOT_MAINLINE_UPDATE; @@ -270,8 +271,8 @@ public class KeyguardViewMediatorTest extends SysuiTestCase { mSceneContainerFlags, mKosmos::getCommunalInteractor); mFeatureFlags = new FakeFeatureFlags(); - mFeatureFlags.set(Flags.KEYGUARD_WM_STATE_REFACTOR, false); mSetFlagsRule.enableFlags(FLAG_REFACTOR_GET_CURRENT_USER); + mSetFlagsRule.disableFlags(FLAG_KEYGUARD_WM_STATE_REFACTOR); DejankUtils.setImmediate(true); @@ -356,6 +357,7 @@ public class KeyguardViewMediatorTest extends SysuiTestCase { mock(FoldGracePeriodProvider.class); mViewMediator.mFoldGracePeriodProvider = mockedFoldGracePeriodProvider; when(mockedFoldGracePeriodProvider.isEnabled()).thenReturn(true); + when(mUpdateMonitor.isDeviceProvisioned()).thenReturn(true); // GIVEN keyguard is not enabled and isn't showing mViewMediator.onSystemReady(); @@ -374,12 +376,40 @@ public class KeyguardViewMediatorTest extends SysuiTestCase { @Test @TestableLooper.RunWithLooper(setAsMainLooper = true) + public void doNotShowKeyguard_deviceNotProvisioned() { + // GIVEN feature is enabled + final FoldGracePeriodProvider mockedFoldGracePeriodProvider = + mock(FoldGracePeriodProvider.class); + mViewMediator.mFoldGracePeriodProvider = mockedFoldGracePeriodProvider; + when(mockedFoldGracePeriodProvider.isEnabled()).thenReturn(true); + + // GIVEN keyguard is not enabled and isn't showing + mViewMediator.onSystemReady(); + mViewMediator.setKeyguardEnabled(false); + TestableLooper.get(this).processAllMessages(); + captureKeyguardUpdateMonitorCallback(); + assertFalse(mViewMediator.isShowingAndNotOccluded()); + + // WHEN device is NOT provisioned + when(mUpdateMonitor.isDeviceProvisioned()).thenReturn(false); + + // WHEN showKeyguard is requested + mViewMediator.showDismissibleKeyguard(); + + // THEN keyguard is NOT shown + TestableLooper.get(this).processAllMessages(); + assertFalse(mViewMediator.isShowingAndNotOccluded()); + } + + @Test + @TestableLooper.RunWithLooper(setAsMainLooper = true) public void showKeyguardAfterKeyguardNotEnabled_featureNotEnabled() { // GIVEN feature is NOT enabled final FoldGracePeriodProvider mockedFoldGracePeriodProvider = mock(FoldGracePeriodProvider.class); mViewMediator.mFoldGracePeriodProvider = mockedFoldGracePeriodProvider; when(mockedFoldGracePeriodProvider.isEnabled()).thenReturn(false); + when(mUpdateMonitor.isDeviceProvisioned()).thenReturn(true); // GIVEN keyguard is not enabled and isn't showing mViewMediator.onSystemReady(); 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 4f3a63dd2829..e93ad0be3e85 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 @@ -21,6 +21,7 @@ import androidx.test.filters.SmallTest import com.android.keyguard.KeyguardSecurityModel 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 @@ -29,7 +30,6 @@ 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.flags.Flags import com.android.systemui.keyguard.data.repository.FakeCommandQueue import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository import com.android.systemui.keyguard.data.repository.FakeKeyguardSurfaceBehindRepository @@ -137,8 +137,8 @@ class KeyguardTransitionScenariosTest : SysuiTestCase() { whenever(keyguardSecurityModel.getSecurityMode(anyInt())).thenReturn(PIN) - featureFlags = FakeFeatureFlags().apply { set(Flags.KEYGUARD_WM_STATE_REFACTOR, false) } mSetFlagsRule.enableFlags(FLAG_COMMUNAL_HUB) + featureFlags = FakeFeatureFlags() keyguardInteractor = createKeyguardInteractor() @@ -299,6 +299,10 @@ class KeyguardTransitionScenariosTest : SysuiTestCase() { powerInteractor = powerInteractor, ) .apply { start() } + + mSetFlagsRule.disableFlags( + FLAG_KEYGUARD_WM_STATE_REFACTOR, + ) } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntrySectionTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntrySectionTest.kt index c864704f6997..699284e29ce3 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntrySectionTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntrySectionTest.kt @@ -39,6 +39,7 @@ import com.android.systemui.shade.NotificationPanelView import com.android.systemui.statusbar.VibratorHelper import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestScope import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -71,6 +72,7 @@ class DefaultDeviceEntrySectionTest : SysuiTestCase() { FakeFeatureFlagsClassic().apply { set(Flags.LOCKSCREEN_ENABLE_LANDSCAPE, false) } underTest = DefaultDeviceEntrySection( + TestScope().backgroundScope, keyguardUpdateMonitor, authController, windowManager, 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 8d306cceeaa7..28d35ce24f4b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavBarHelperTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavBarHelperTest.java @@ -41,6 +41,7 @@ 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; @@ -56,6 +57,8 @@ 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; @@ -65,8 +68,7 @@ import org.mockito.MockitoAnnotations; import java.util.ArrayList; import java.util.List; import java.util.Optional; - -import dagger.Lazy; +import java.util.concurrent.Executor; /** * Tests for {@link NavBarHelper}. @@ -123,6 +125,8 @@ public class NavBarHelperTest extends SysuiTestCase { SYSUI_STATE_A11Y_BUTTON_CLICKABLE | SYSUI_STATE_A11Y_BUTTON_LONG_CLICKABLE; private NavBarHelper mNavBarHelper; + private final Executor mSynchronousExecutor = runnable -> runnable.run(); + @Before public void setup() { MockitoAnnotations.initMocks(this); @@ -140,7 +144,8 @@ public class NavBarHelperTest extends SysuiTestCase { mSystemActions, mOverviewProxyService, mAssistManagerLazy, () -> Optional.of(mock(CentralSurfaces.class)), mock(KeyguardStateController.class), mNavigationModeController, mEdgeBackGestureHandlerFactory, mWm, mUserTracker, - mDisplayTracker, mNotificationShadeWindowController, mDumpManager, mCommandQueue); + mDisplayTracker, mNotificationShadeWindowController, mDumpManager, mCommandQueue, + mSynchronousExecutor); } @@ -266,7 +271,8 @@ public class NavBarHelperTest extends SysuiTestCase { @Test public void initNavBarHelper_buttonModeNavBar_a11yButtonClickableState() { when(mAccessibilityManager.getAccessibilityShortcutTargets( - AccessibilityManager.ACCESSIBILITY_BUTTON)).thenReturn(createFakeShortcutTargets()); + ShortcutConstants.UserShortcutType.SOFTWARE)).thenReturn( + createFakeShortcutTargets()); mNavBarHelper.registerNavTaskStateUpdater(mNavbarTaskbarStateUpdater); @@ -291,7 +297,8 @@ public class NavBarHelperTest extends SysuiTestCase { ACCESSIBILITY_BUTTON_MODE_NAVIGATION_BAR); when(mAccessibilityManager.getAccessibilityShortcutTargets( - AccessibilityManager.ACCESSIBILITY_BUTTON)).thenReturn(createFakeShortcutTargets()); + ShortcutConstants.UserShortcutType.SOFTWARE)).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 ddceed62fdeb..db5bd9b13dd0 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarTest.java @@ -228,6 +228,8 @@ public class NavigationBarTest extends SysuiTestCase { @Rule public final LeakCheckedTest.SysuiLeakCheck mLeakCheck = new LeakCheckedTest.SysuiLeakCheck(); + private final Executor mSynchronousExecutor = runnable -> runnable.run(); + @Before public void setup() throws Exception { MockitoAnnotations.initMocks(this); @@ -269,7 +271,7 @@ public class NavigationBarTest extends SysuiTestCase { mEdgeBackGestureHandlerFactory, mock(IWindowManager.class), mock(UserTracker.class), mock(DisplayTracker.class), mNotificationShadeWindowController, mock(DumpManager.class), - mock(CommandQueue.class))); + mock(CommandQueue.class), mSynchronousExecutor)); mNavigationBar = createNavBar(mContext); mExternalDisplayNavigationBar = createNavBar(mSysuiTestableContextExternal); }); diff --git a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/gestural/BackPanelControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/gestural/BackPanelControllerTest.kt index f93d52b2c35c..aa54565c2aa0 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/gestural/BackPanelControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/gestural/BackPanelControllerTest.kt @@ -28,6 +28,7 @@ import android.view.MotionEvent.ACTION_UP import android.view.ViewConfiguration import android.view.WindowManager import androidx.test.filters.SmallTest +import com.android.internal.jank.InteractionJankMonitor import com.android.internal.util.LatencyTracker import com.android.systemui.SysuiTestCase import com.android.systemui.plugins.NavigationEdgeBackPlugin @@ -40,8 +41,10 @@ import org.junit.runner.RunWith import org.mockito.ArgumentMatchers.any import org.mockito.ArgumentMatchers.eq import org.mockito.Mock +import org.mockito.Mockito.anyInt import org.mockito.Mockito.clearInvocations import org.mockito.Mockito.verify +import org.mockito.Mockito.`when` import org.mockito.MockitoAnnotations @SmallTest @@ -59,12 +62,16 @@ class BackPanelControllerTest : SysuiTestCase() { @Mock private lateinit var windowManager: WindowManager @Mock private lateinit var configurationController: ConfigurationController @Mock private lateinit var latencyTracker: LatencyTracker + @Mock private lateinit var interactionJankMonitor: InteractionJankMonitor @Mock private lateinit var layoutParams: WindowManager.LayoutParams @Mock private lateinit var backCallback: NavigationEdgeBackPlugin.BackCallback @Before fun setup() { MockitoAnnotations.initMocks(this) + `when`(interactionJankMonitor.begin(any(), anyInt())).thenReturn(true) + `when`(interactionJankMonitor.end(anyInt())).thenReturn(true) + `when`(interactionJankMonitor.cancel(anyInt())).thenReturn(true) mBackPanelController = BackPanelController( context, @@ -74,6 +81,7 @@ class BackPanelControllerTest : SysuiTestCase() { vibratorHelper, configurationController, latencyTracker, + interactionJankMonitor, ) mBackPanelController.setLayoutParams(layoutParams) mBackPanelController.setBackCallback(backCallback) diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/LeftRightArrowPressedListenerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/LeftRightArrowPressedListenerTest.kt index 60eb3aec190f..40eccad94340 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/LeftRightArrowPressedListenerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/LeftRightArrowPressedListenerTest.kt @@ -37,7 +37,7 @@ class LeftRightArrowPressedListenerTest : SysuiTestCase() { object : Consumer<Int> { var lastValue: Int? = null - override fun accept(keyCode: Int?) { + override fun accept(keyCode: Int) { lastValue = keyCode } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSIconViewImplTest_311121830.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSIconViewImplTest_311121830.kt new file mode 100644 index 000000000000..e8aa8f0bdc5d --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSIconViewImplTest_311121830.kt @@ -0,0 +1,94 @@ +/* + * 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.tileimpl + +import android.animation.AnimatorTestRule +import android.content.Context +import android.service.quicksettings.Tile +import android.testing.AndroidTestingRunner +import android.testing.UiThreadTest +import android.view.ContextThemeWrapper +import android.view.View +import android.widget.ImageView +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.plugins.qs.QSTile +import com.android.systemui.res.R +import com.android.systemui.statusbar.connectivity.WifiIcons +import com.google.common.truth.Truth.assertThat +import kotlin.test.Test +import org.junit.Rule +import org.junit.runner.RunWith + +/** Test for regression b/311121830 */ +@RunWith(AndroidTestingRunner::class) +@UiThreadTest +@SmallTest +class QSIconViewImplTest_311121830 : SysuiTestCase() { + + @get:Rule val animatorRule = AnimatorTestRule() + + @Test + fun alwaysLastIcon() { + // Need to inflate with the correct theme so the colors can be retrieved and the animations + // are run + val iconView = + AnimateQSIconViewImpl( + ContextThemeWrapper(context, R.style.Theme_SystemUI_QuickSettings) + ) + + val initialState = + QSTile.State().apply { + state = Tile.STATE_INACTIVE + icon = QSTileImpl.ResourceIcon.get(R.drawable.ic_qs_no_internet_available) + } + val firstState = + QSTile.State().apply { + state = Tile.STATE_ACTIVE + icon = QSTileImpl.ResourceIcon.get(WifiIcons.WIFI_NO_INTERNET_ICONS[4]) + } + val secondState = + QSTile.State().apply { + state = Tile.STATE_ACTIVE + icon = QSTileImpl.ResourceIcon.get(WifiIcons.WIFI_FULL_ICONS[4]) + } + + // Start with the initial state + iconView.setIcon(initialState, /* allowAnimations= */ false) + + // Set the first state to animate, and advance time to half the time of the animation + iconView.setIcon(firstState, /* allowAnimations= */ true) + animatorRule.advanceTimeBy(QSIconViewImpl.QS_ANIM_LENGTH / 2) + + // Set the second state to animate (it shouldn't, because `State.state` is the same) and + // advance time to 2 animations length + iconView.setIcon(secondState, /* allowAnimations= */ true) + animatorRule.advanceTimeBy(QSIconViewImpl.QS_ANIM_LENGTH * 2) + + assertThat(iconView.mLastIcon).isEqualTo(secondState.icon) + } + + private class AnimateQSIconViewImpl(context: Context) : QSIconViewImpl(context) { + override fun createIcon(): View { + return object : ImageView(context) { + override fun isShown(): Boolean { + return true + } + } + } + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/RecordIssueTileTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/RecordIssueTileTest.kt index c7479fd50db1..1ed8c3cdf0ba 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/RecordIssueTileTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/RecordIssueTileTest.kt @@ -32,6 +32,7 @@ import com.android.systemui.qs.QsEventLogger import com.android.systemui.qs.logging.QSLogger import com.android.systemui.recordissue.RecordIssueDialogDelegate import com.android.systemui.res.R +import com.android.systemui.settings.UserContextProvider import com.android.systemui.statusbar.phone.KeyguardDismissUtil import com.android.systemui.statusbar.phone.SystemUIDialog import com.android.systemui.statusbar.policy.KeyguardStateController @@ -65,6 +66,7 @@ class RecordIssueTileTest : SysuiTestCase() { @Mock private lateinit var keyguardDismissUtil: KeyguardDismissUtil @Mock private lateinit var keyguardStateController: KeyguardStateController @Mock private lateinit var dialogLauncherAnimator: DialogLaunchAnimator + @Mock private lateinit var userContextProvider: UserContextProvider @Mock private lateinit var delegateFactory: RecordIssueDialogDelegate.Factory @Mock private lateinit var dialogDelegate: RecordIssueDialogDelegate @Mock private lateinit var dialog: SystemUIDialog @@ -94,6 +96,7 @@ class RecordIssueTileTest : SysuiTestCase() { keyguardDismissUtil, keyguardStateController, dialogLauncherAnimator, + userContextProvider, delegateFactory, ) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/recents/OverviewProxyServiceTest.kt b/packages/SystemUI/tests/src/com/android/systemui/recents/OverviewProxyServiceTest.kt index 70a48f574949..bcb64aaca3b3 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/recents/OverviewProxyServiceTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/recents/OverviewProxyServiceTest.kt @@ -17,16 +17,21 @@ package com.android.systemui.recents import android.content.ComponentName +import android.content.Context import android.content.pm.PackageManager import android.content.pm.ResolveInfo import android.os.PowerManager +import android.os.Process; +import android.os.UserHandle import android.testing.AndroidTestingRunner import android.testing.TestableContext import android.testing.TestableLooper import androidx.test.filters.SmallTest +import com.android.dx.mockito.inline.extended.ExtendedMockito import com.android.internal.app.AssistUtils import com.android.internal.logging.UiEventLogger import com.android.systemui.SysuiTestCase +import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.dump.DumpManager import com.android.systemui.flags.FakeFeatureFlags @@ -66,10 +71,14 @@ import org.mockito.ArgumentMatchers import org.mockito.Mock import org.mockito.Mockito.any import org.mockito.Mockito.anyInt +import org.mockito.Mockito.atLeast import org.mockito.Mockito.clearInvocations import org.mockito.Mockito.intThat import org.mockito.Mockito.mock +import org.mockito.Mockito.spy +import org.mockito.Mockito.times import org.mockito.Mockito.verify +import org.mockito.Mockito.`when` import org.mockito.MockitoAnnotations @SmallTest @@ -80,7 +89,7 @@ class OverviewProxyServiceTest : SysuiTestCase() { @Main private val executor: Executor = MoreExecutors.directExecutor() private lateinit var subject: OverviewProxyService - private val dumpManager = DumpManager() + @Mock private val dumpManager = DumpManager() private val displayTracker = FakeDisplayTracker(mContext) private val fakeSystemClock = FakeSystemClock() private val sysUiState = SysUiState(displayTracker) @@ -109,6 +118,8 @@ class OverviewProxyServiceTest : SysuiTestCase() { @Mock private lateinit var unfoldTransitionProgressForwarder: Optional<UnfoldTransitionProgressForwarder> + @Mock + private lateinit var broadcastDispatcher: BroadcastDispatcher @Before fun setUp() { @@ -131,32 +142,11 @@ class OverviewProxyServiceTest : SysuiTestCase() { whenever(packageManager.resolveServiceAsUser(any(), anyInt(), anyInt())) .thenReturn(mock(ResolveInfo::class.java)) - featureFlags.set(Flags.KEYGUARD_WM_STATE_REFACTOR, false) - subject = - OverviewProxyService( - context, - executor, - commandQueue, - shellInterface, - { navBarController }, - { shadeViewController }, - screenPinningRequest, - navModeController, - statusBarWinController, - sysUiState, - mock(), - userTracker, - wakefulnessLifecycle, - uiEventLogger, - displayTracker, - sysuiUnlockAnimationController, - inWindowLauncherUnlockAnimationManager, - assistUtils, - featureFlags, - FakeSceneContainerFlags(), - dumpManager, - unfoldTransitionProgressForwarder - ) + mSetFlagsRule.disableFlags( + com.android.systemui.Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR, + ) + + subject = createOverviewProxyService(context) } @After @@ -209,4 +199,66 @@ class OverviewProxyServiceTest : SysuiTestCase() { intThat { it and SYSUI_STATE_WAKEFULNESS_MASK == WAKEFULNESS_GOING_TO_SLEEP } ) } + + @Test + fun connectToOverviewService_primaryUser_expectBindService() { + val mockitoSession = ExtendedMockito.mockitoSession() + .spyStatic(Process::class.java) + .startMocking() + try { + `when`(Process.myUserHandle()).thenReturn(UserHandle.SYSTEM) + val spyContext = spy(context) + val ops = createOverviewProxyService(spyContext) + ops.startConnectionToCurrentUser() + verify(spyContext, atLeast(1)).bindServiceAsUser(any(), any(), + anyInt(), any()) + } finally { + mockitoSession.finishMocking() + } + } + + @Test + fun connectToOverviewService_nonPrimaryUser_expectNoBindService() { + val mockitoSession = ExtendedMockito.mockitoSession() + .spyStatic(Process::class.java) + .startMocking() + try { + `when`(Process.myUserHandle()).thenReturn(UserHandle.of(12345)) + val spyContext = spy(context) + val ops = createOverviewProxyService(spyContext) + ops.startConnectionToCurrentUser() + verify(spyContext, times(0)).bindServiceAsUser(any(), any(), + anyInt(), any()) + } finally { + mockitoSession.finishMocking() + } + } + + private fun createOverviewProxyService(ctx: Context) : OverviewProxyService { + return OverviewProxyService( + ctx, + executor, + commandQueue, + shellInterface, + { navBarController }, + { shadeViewController }, + screenPinningRequest, + navModeController, + statusBarWinController, + sysUiState, + mock(), + userTracker, + wakefulnessLifecycle, + uiEventLogger, + displayTracker, + sysuiUnlockAnimationController, + inWindowLauncherUnlockAnimationManager, + assistUtils, + featureFlags, + FakeSceneContainerFlags(), + dumpManager, + unfoldTransitionProgressForwarder, + broadcastDispatcher + ) + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt index 6681ceee3d09..22b05be507c6 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt @@ -514,7 +514,7 @@ class NotificationShadeWindowViewControllerTest : SysuiTestCase() { } whenever(mGlanceableHubContainerController.isEnabled()).thenReturn(false) - whenever(mGlanceableHubContainerController.enabledState()) + whenever(mGlanceableHubContainerController.communalAvailable()) .thenReturn(MutableStateFlow(false)) val mockCommunalPlaceholder = mock(View::class.java) diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorTest.java index 58eec2e71d32..4519ba6d3590 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorTest.java @@ -65,6 +65,7 @@ import com.android.systemui.statusbar.notification.collection.listbuilder.plugga import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifSectioner; import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener; import com.android.systemui.statusbar.notification.collection.provider.SectionStyleProvider; +import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManager; import com.android.systemui.statusbar.notification.collection.render.NotifViewBarn; import com.android.systemui.statusbar.notification.row.NotifInflationErrorManager; import com.android.systemui.util.settings.SecureSettings; @@ -111,6 +112,7 @@ public class PreparationCoordinatorTest extends SysuiTestCase { @Spy private FakeNotifInflater mNotifInflater = new FakeNotifInflater(); private final SectionStyleProvider mSectionStyleProvider = new SectionStyleProvider(); @Mock private UserTracker mUserTracker; + @Mock private GroupMembershipManager mGroupMembershipManager; private NotifUiAdjustmentProvider mAdjustmentProvider; @@ -127,7 +129,9 @@ public class PreparationCoordinatorTest extends SysuiTestCase { mSecureSettings, mLockscreenUserManager, mSectionStyleProvider, - mUserTracker); + mUserTracker, + mGroupMembershipManager + ); mEntry = getNotificationEntryBuilder().setParent(ROOT_ENTRY).build(); mInflationError = new Exception(TEST_MESSAGE); mErrorManager = new NotifInflationErrorManager(); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/inflation/NotifUiAdjustmentProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/inflation/NotifUiAdjustmentProviderTest.kt index f9f8d8a2cfc6..73c49c023dd5 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/inflation/NotifUiAdjustmentProviderTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/inflation/NotifUiAdjustmentProviderTest.kt @@ -17,6 +17,8 @@ package com.android.systemui.statusbar.notification.collection.inflation import android.database.ContentObserver import android.os.Handler +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags import android.provider.Settings.Secure.SHOW_NOTIFICATION_SNOOZE import android.testing.AndroidTestingRunner import android.testing.TestableLooper.RunWithLooper @@ -28,6 +30,8 @@ import com.android.systemui.statusbar.notification.collection.GroupEntry import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder import com.android.systemui.statusbar.notification.collection.listbuilder.NotifSection import com.android.systemui.statusbar.notification.collection.provider.SectionStyleProvider +import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManager +import com.android.systemui.statusbar.notification.row.shared.AsyncHybridViewInflation import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.eq import com.android.systemui.util.mockito.mock @@ -35,6 +39,8 @@ import com.android.systemui.util.mockito.withArgCaptor import com.android.systemui.util.settings.FakeSettings import com.android.systemui.util.settings.SecureSettings import com.google.common.truth.Truth.assertThat +import kotlin.test.assertFalse +import kotlin.test.assertTrue import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -55,6 +61,7 @@ class NotifUiAdjustmentProviderTest : SysuiTestCase() { private val uri = FakeSettings().getUriFor(SHOW_NOTIFICATION_SNOOZE) private val dirtyListener: Runnable = mock() private val userTracker: UserTracker = mock() + private val groupMembershipManager: GroupMembershipManager = mock() private val section = NotifSection(mock(), 0) private val entry = NotificationEntryBuilder() @@ -69,7 +76,8 @@ class NotifUiAdjustmentProviderTest : SysuiTestCase() { secureSettings, lockscreenUserManager, sectionStyleProvider, - userTracker + userTracker, + groupMembershipManager, ) @Before @@ -127,4 +135,42 @@ class NotifUiAdjustmentProviderTest : SysuiTestCase() { assertThat(withSnoozing.isSnoozeEnabled).isTrue() assertThat(withSnoozing).isNotEqualTo(original) } + + @Test + @EnableFlags(AsyncHybridViewInflation.FLAG_NAME) + fun changeIsChildInGroup_asyncHybirdFlagEnabled_needReInflation() { + // Given: an Entry that is not child in group + // AsyncHybridViewInflation flag is enabled + whenever(groupMembershipManager.isChildInGroup(entry)).thenReturn(false) + val oldAdjustment = adjustmentProvider.calculateAdjustment(entry) + assertThat(oldAdjustment.isChildInGroup).isFalse() + + // When: the Entry becomes a group child + whenever(groupMembershipManager.isChildInGroup(entry)).thenReturn(true) + val newAdjustment = adjustmentProvider.calculateAdjustment(entry) + assertThat(newAdjustment.isChildInGroup).isTrue() + assertThat(newAdjustment).isNotEqualTo(oldAdjustment) + + // Then: need re-inflation + assertTrue(NotifUiAdjustment.needReinflate(oldAdjustment, newAdjustment)) + } + + @Test + @DisableFlags(AsyncHybridViewInflation.FLAG_NAME) + fun changeIsChildInGroup_asyncHybirdFlagDisabled_noNeedForReInflation() { + // Given: an Entry that is not child in group + // AsyncHybridViewInflation flag is disabled + whenever(groupMembershipManager.isChildInGroup(entry)).thenReturn(false) + val oldAdjustment = adjustmentProvider.calculateAdjustment(entry) + assertThat(oldAdjustment.isChildInGroup).isFalse() + + // When: the Entry becomes a group child + whenever(groupMembershipManager.isChildInGroup(entry)).thenReturn(true) + val newAdjustment = adjustmentProvider.calculateAdjustment(entry) + assertThat(newAdjustment.isChildInGroup).isTrue() + assertThat(newAdjustment).isNotEqualTo(oldAdjustment) + + // Then: need no re-inflation + assertFalse(NotifUiAdjustment.needReinflate(oldAdjustment, newAdjustment)) + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterTest.java index b0996ad48d0a..a0d10759ba56 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterTest.java @@ -88,6 +88,8 @@ public class NotificationContentInflaterTest extends SysuiTestCase { private Notification.Builder mBuilder; private ExpandableNotificationRow mRow; + private NotificationTestHelper mHelper; + @Mock private NotifRemoteViewCache mCache; @Mock private ConversationNotificationProcessor mConversationNotificationProcessor; @Mock private InflatedSmartReplyState mInflatedSmartReplyState; @@ -119,11 +121,11 @@ public class NotificationContentInflaterTest extends SysuiTestCase { .setContentTitle("Title") .setContentText("Text") .setStyle(new Notification.BigTextStyle().bigText("big text")); - NotificationTestHelper helper = new NotificationTestHelper( + mHelper = new NotificationTestHelper( mContext, mDependency, TestableLooper.get(this)); - ExpandableNotificationRow row = helper.createRow(mBuilder.build()); + ExpandableNotificationRow row = mHelper.createRow(mBuilder.build()); mRow = spy(row); when(mNotifLayoutInflaterFactoryProvider.provide(any(), any())) .thenReturn(mNotifLayoutInflaterFactory); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/SingleLineConversationViewBinderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/SingleLineConversationViewBinderTest.kt new file mode 100644 index 000000000000..1c959af6ec3f --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/SingleLineConversationViewBinderTest.kt @@ -0,0 +1,117 @@ +/* + * 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.app.Notification +import android.app.Person +import android.platform.test.annotations.EnableFlags +import android.testing.AndroidTestingRunner +import android.testing.TestableLooper +import androidx.test.filters.SmallTest +import com.android.internal.R +import com.android.systemui.SysuiTestCase +import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_SINGLE_LINE +import com.android.systemui.statusbar.notification.row.SingleLineViewInflater.inflateSingleLineViewHolder +import com.android.systemui.statusbar.notification.row.shared.AsyncHybridViewInflation +import com.android.systemui.statusbar.notification.row.ui.viewbinder.SingleLineConversationViewBinder +import com.android.systemui.util.mockito.mock +import kotlin.test.assertEquals +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidTestingRunner::class) +@TestableLooper.RunWithLooper +class SingleLineConversationViewBinderTest : SysuiTestCase() { + private lateinit var notificationBuilder: Notification.Builder + private lateinit var helper: NotificationTestHelper + + @Before + fun setUp() { + allowTestableLooperAsMainThread() + helper = NotificationTestHelper(context, mDependency, TestableLooper.get(this)) + notificationBuilder = Notification.Builder(context, CHANNEL_ID) + notificationBuilder + .setSmallIcon(R.drawable.ic_corp_icon) + .setContentTitle(CONTENT_TITLE) + .setContentText(CONTENT_TEXT) + } + + @Test + @EnableFlags(AsyncHybridViewInflation.FLAG_NAME) + fun bindGroupConversationSingleLineView() { + // GIVEN a row with a group conversation notification + val user = + Person.Builder() + // .setIcon(Icon.createWithResource(mContext, + // R.drawable.ic_account_circle)) + .setName(USER_NAME) + .build() + val style = + Notification.MessagingStyle(user) + .addMessage(MESSAGE_TEXT, System.currentTimeMillis(), user) + .addMessage( + "How about lunch?", + System.currentTimeMillis(), + Person.Builder().setName("user2").build() + ) + .setGroupConversation(true) + notificationBuilder.setStyle(style).setShortcutId(SHORTCUT_ID) + val notification = notificationBuilder.build() + val row = helper.createRow(notification) + + val viewHolder = + inflateSingleLineViewHolder( + isConversation = true, + reinflateFlags = FLAG_CONTENT_VIEW_SINGLE_LINE, + entry = row.entry, + context = context, + logger = mock() + ) + as HybridConversationNotificationView + val viewModel = + SingleLineViewInflater.inflateSingleLineViewModel( + notification = notification, + messagingStyle = style, + builder = notificationBuilder, + systemUiContext = context, + ) + // WHEN: binds the viewHolder + SingleLineConversationViewBinder.bind( + viewModel, + viewHolder, + ) + + // THEN: the single-line conversation view should be bind with view model's corresponding + // fields + assertEquals(viewModel.titleText, viewHolder.titleView.text) + assertEquals(viewModel.contentText, viewHolder.textView.text) + assertEquals( + viewModel.conversationData?.conversationSenderName, + viewHolder.conversationSenderNameView.text + ) + } + + private companion object { + const val CHANNEL_ID = "CHANNEL_ID" + const val CONTENT_TITLE = "CONTENT_TITLE" + const val CONTENT_TEXT = "CONTENT_TEXT" + const val USER_NAME = "USER_NAME" + const val MESSAGE_TEXT = "MESSAGE_TEXT" + const val SHORTCUT_ID = "Shortcut" + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/SingleLineViewBinderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/SingleLineViewBinderTest.kt new file mode 100644 index 000000000000..f0fc349777b2 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/SingleLineViewBinderTest.kt @@ -0,0 +1,91 @@ +/* + * 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.app.Notification +import android.platform.test.annotations.EnableFlags +import android.testing.AndroidTestingRunner +import android.testing.TestableLooper +import androidx.test.filters.SmallTest +import com.android.internal.R +import com.android.systemui.SysuiTestCase +import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_SINGLE_LINE +import com.android.systemui.statusbar.notification.row.SingleLineViewInflater.inflateSingleLineViewHolder +import com.android.systemui.statusbar.notification.row.shared.AsyncHybridViewInflation +import com.android.systemui.statusbar.notification.row.ui.viewbinder.SingleLineViewBinder +import com.android.systemui.util.mockito.mock +import org.junit.Assert +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidTestingRunner::class) +@TestableLooper.RunWithLooper +class SingleLineViewBinderTest : SysuiTestCase() { + private lateinit var notificationBuilder: Notification.Builder + private lateinit var helper: NotificationTestHelper + + @Before + fun setUp() { + allowTestableLooperAsMainThread() + helper = NotificationTestHelper(mContext, mDependency, TestableLooper.get(this)) + notificationBuilder = Notification.Builder(mContext, CHANNEL_ID) + notificationBuilder + .setSmallIcon(R.drawable.ic_corp_icon) + .setContentTitle(CONTENT_TITLE) + .setContentText(CONTENT_TEXT) + } + + @Test + @EnableFlags(AsyncHybridViewInflation.FLAG_NAME) + fun bindNonConversationSingleLineView() { + // GIVEN: a row with bigText style notification + val style = Notification.BigTextStyle().bigText(CONTENT_TEXT) + notificationBuilder.setStyle(style) + val notification = notificationBuilder.build() + val row: ExpandableNotificationRow = helper.createRow(notification) + + val viewHolder = + inflateSingleLineViewHolder( + isConversation = false, + reinflateFlags = FLAG_CONTENT_VIEW_SINGLE_LINE, + entry = row.entry, + context = context, + logger = mock() + ) + val viewModel = + SingleLineViewInflater.inflateSingleLineViewModel( + notification = notification, + messagingStyle = null, + builder = notificationBuilder, + systemUiContext = context, + ) + + // WHEN: binds the viewHolder + SingleLineViewBinder.bind(viewModel, viewHolder) + + // THEN: the single-line view should be bind with viewModel's title and content text + Assert.assertEquals(viewModel.titleText, viewHolder?.titleView?.text) + Assert.assertEquals(viewModel.contentText, viewHolder?.textView?.text) + } + + private companion object { + const val CHANNEL_ID = "CHANNEL_ID" + const val CONTENT_TITLE = "A Cool New Feature" + const val CONTENT_TEXT = "Checkout out new feature!" + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/SingleLineViewInflaterTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/SingleLineViewInflaterTest.kt new file mode 100644 index 000000000000..b67153a842ac --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/SingleLineViewInflaterTest.kt @@ -0,0 +1,463 @@ +/* + * 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.app.Notification +import android.app.Person +import android.content.Context +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.Paint +import android.graphics.PorterDuff +import android.graphics.PorterDuffXfermode +import android.graphics.drawable.Drawable +import android.graphics.drawable.Icon +import android.platform.test.annotations.EnableFlags +import android.testing.AndroidTestingRunner +import android.testing.TestableLooper +import androidx.core.graphics.drawable.toBitmap +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.res.R +import com.android.systemui.statusbar.notification.row.shared.AsyncHybridViewInflation +import com.android.systemui.statusbar.notification.row.ui.viewmodel.ConversationAvatar +import com.android.systemui.statusbar.notification.row.ui.viewmodel.FacePile +import com.android.systemui.statusbar.notification.row.ui.viewmodel.SingleIcon +import com.android.systemui.statusbar.notification.row.ui.viewmodel.SingleLineViewModel +import kotlin.test.assertEquals +import kotlin.test.assertIs +import kotlin.test.assertIsNot +import kotlin.test.assertNull +import kotlin.test.assertTrue +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidTestingRunner::class) +@TestableLooper.RunWithLooper +@EnableFlags(AsyncHybridViewInflation.FLAG_NAME) +class SingleLineViewInflaterTest : SysuiTestCase() { + private lateinit var helper: NotificationTestHelper + // Non-group MessagingStyles only have firstSender + private lateinit var firstSender: Person + private lateinit var lastSender: Person + private lateinit var firstSenderIcon: Icon + private lateinit var lastSenderIcon: Icon + private var firstSenderIconDrawable: Drawable? = null + private var lastSenderIconDrawable: Drawable? = null + private val currentUser: Person? = null + + private companion object { + const val FIRST_SENDER_NAME = "First Sender" + const val LAST_SENDER_NAME = "Second Sender" + const val LAST_MESSAGE = "How about lunch?" + + const val CONVERSATION_TITLE = "The Sender Family" + const val CONTENT_TITLE = "A Cool Group" + const val CONTENT_TEXT = "This is an amazing group chat" + + const val SHORTCUT_ID = "Shortcut" + } + + @Before + fun setUp() { + helper = NotificationTestHelper(mContext, mDependency, TestableLooper.get(this)) + firstSenderIcon = Icon.createWithBitmap(getBitmap(context, R.drawable.ic_person)) + firstSenderIconDrawable = firstSenderIcon.loadDrawable(context) + lastSenderIcon = + Icon.createWithBitmap( + getBitmap(context, com.android.internal.R.drawable.ic_account_circle) + ) + lastSenderIconDrawable = lastSenderIcon.loadDrawable(context) + firstSender = Person.Builder().setName(FIRST_SENDER_NAME).setIcon(firstSenderIcon).build() + lastSender = Person.Builder().setName(LAST_SENDER_NAME).setIcon(lastSenderIcon).build() + } + + @Test + fun createViewModelForNonConversationSingleLineView() { + // Given: a non-conversation notification + val notificationType = NonMessaging() + val notification = getNotification(NonMessaging()) + + // When: inflate the SingleLineViewModel + val singleLineViewModel = notification.makeSingleLineViewModel(notificationType) + + // Then: the inflated SingleLineViewModel should be as expected + // conversationData: null, because it's not a conversation notification + assertEquals(SingleLineViewModel(CONTENT_TITLE, CONTENT_TEXT, null), singleLineViewModel) + } + + @Test + fun createViewModelForNonGroupConversationNotification() { + // Given: a non-group conversation notification + val notificationType = OneToOneConversation() + val notification = getNotification(notificationType) + + // When: inflate the SingleLineViewModel + val singleLineViewModel = notification.makeSingleLineViewModel(notificationType) + + // Then: the inflated SingleLineViewModel should be as expected + // titleText: Notification.ConversationTitle + // contentText: the last message text + // conversationSenderName: null, because it's not a group conversation + // conversationData.avatar: a single icon of the last sender + assertEquals(CONVERSATION_TITLE, singleLineViewModel.titleText) + assertEquals(LAST_MESSAGE, singleLineViewModel.contentText) + assertNull( + singleLineViewModel.conversationData?.conversationSenderName, + "Sender name should be null for one-on-one conversation" + ) + assertTrue { + singleLineViewModel.conversationData + ?.avatar + ?.equalsTo(SingleIcon(firstSenderIcon.loadDrawable(context))) == true + } + } + + @Test + fun createViewModelForNonGroupLegacyMessagingStyleNotification() { + // Given: a non-group legacy messaging style notification + val notificationType = LegacyMessaging() + val notification = getNotification(notificationType) + + // When: inflate the SingleLineViewModel + val singleLineViewModel = notification.makeSingleLineViewModel(notificationType) + + // Then: the inflated SingleLineViewModel should be as expected + // titleText: CONVERSATION_TITLE: SENDER_NAME + // contentText: the last message text + // conversationData: null, because it's not a conversation notification + assertEquals("$CONVERSATION_TITLE: $FIRST_SENDER_NAME", singleLineViewModel.titleText) + assertEquals(LAST_MESSAGE, singleLineViewModel.contentText) + assertNull( + singleLineViewModel.conversationData, + "conversationData should be null for legacy messaging conversation" + ) + } + + @Test + fun createViewModelForGroupLegacyMessagingStyleNotification() { + // Given: a non-group legacy messaging style notification + val notificationType = LegacyMessagingGroup() + val notification = getNotification(notificationType) + + // When: inflate the SingleLineViewModel + val singleLineViewModel = notification.makeSingleLineViewModel(notificationType) + + // Then: the inflated SingleLineViewModel should be as expected + // titleText: CONVERSATION_TITLE: LAST_SENDER_NAME + // contentText: the last message text + // conversationData: null, because it's not a conversation notification + assertEquals("$CONVERSATION_TITLE: $LAST_SENDER_NAME", singleLineViewModel.titleText) + assertEquals(LAST_MESSAGE, singleLineViewModel.contentText) + assertNull( + singleLineViewModel.conversationData, + "conversationData should be null for legacy messaging conversation" + ) + } + + @Test + fun createViewModelForNonGroupConversationNotificationWithShortcutIcon() { + // Given: a non-group conversation notification with a shortcut icon + val shortcutIcon = + Icon.createWithResource(context, com.android.internal.R.drawable.ic_account_circle) + val notificationType = OneToOneConversation(shortcutIcon = shortcutIcon) + val notification = getNotification(notificationType) + + // When: inflate the SingleLineViewModel + val singleLineViewModel = notification.makeSingleLineViewModel(notificationType) + + // Then: the inflated SingleLineViewModel should be expected + // titleText: Notification.ConversationTitle + // contentText: the last message text + // conversationSenderName: null, because it's not a group conversation + // conversationData.avatar: a single icon of the shortcut icon + assertEquals(CONVERSATION_TITLE, singleLineViewModel.titleText) + assertEquals(LAST_MESSAGE, singleLineViewModel.contentText) + assertNull( + singleLineViewModel.conversationData?.conversationSenderName, + "Sender name should be null for one-on-one conversation" + ) + assertTrue { + singleLineViewModel.conversationData + ?.avatar + ?.equalsTo(SingleIcon(shortcutIcon.loadDrawable(context))) == true + } + } + + @Test + fun createViewModelForGroupConversationNotificationWithLargeIcon() { + // Given: a group conversation notification with a large icon + val largeIcon = + Icon.createWithResource(context, com.android.internal.R.drawable.ic_account_circle) + val notificationType = GroupConversation(largeIcon = largeIcon) + val notification = getNotification(notificationType) + + // When: inflate the SingleLineViewModel + val singleLineViewModel = notification.makeSingleLineViewModel(notificationType) + + // Then: the inflated SingleLineViewModel should be expected + // titleText: Notification.ConversationTitle + // contentText: the last message text + // conversationSenderName: the last non-user sender's name + // conversationData.avatar: a single icon + assertEquals(CONVERSATION_TITLE, singleLineViewModel.titleText) + assertEquals(LAST_MESSAGE, singleLineViewModel.contentText) + assertEquals( + context.resources.getString( + com.android.internal.R.string.conversation_single_line_name_display, + LAST_SENDER_NAME + ), + singleLineViewModel.conversationData?.conversationSenderName + ) + assertTrue { + singleLineViewModel.conversationData + ?.avatar + ?.equalsTo(SingleIcon(largeIcon.loadDrawable(context))) == true + } + } + + @Test + fun createViewModelForGroupConversationWithNoIcon() { + // Given: a group conversation notification + val notificationType = GroupConversation() + val notification = getNotification(notificationType) + + // When: inflate the SingleLineViewModel + val singleLineViewModel = notification.makeSingleLineViewModel(notificationType) + + // Then: the inflated SingleLineViewModel should be expected + // titleText: Notification.ConversationTitle + // contentText: the last message text + // conversationSenderName: the last non-user sender's name + // conversationData.avatar: a face-pile consists the last sender's icon + assertEquals(CONVERSATION_TITLE, singleLineViewModel.titleText) + assertEquals(LAST_MESSAGE, singleLineViewModel.contentText) + assertEquals( + context.resources.getString( + com.android.internal.R.string.conversation_single_line_name_display, + LAST_SENDER_NAME + ), + singleLineViewModel.conversationData?.conversationSenderName + ) + + val backgroundColor = + Notification.Builder.recoverBuilder(context, notification) + .getBackgroundColor(/* isHeader = */ false) + assertTrue { + singleLineViewModel.conversationData + ?.avatar + ?.equalsTo( + FacePile( + firstSenderIconDrawable, + lastSenderIconDrawable, + backgroundColor, + ) + ) == true + } + } + + sealed class NotificationType(val largeIcon: Icon? = null) + + class NonMessaging(largeIcon: Icon? = null) : NotificationType(largeIcon) + + class LegacyMessaging(largeIcon: Icon? = null) : NotificationType(largeIcon) + + class LegacyMessagingGroup(largeIcon: Icon? = null) : NotificationType(largeIcon) + + class OneToOneConversation(largeIcon: Icon? = null, val shortcutIcon: Icon? = null) : + NotificationType(largeIcon) + + class GroupConversation(largeIcon: Icon? = null) : NotificationType(largeIcon) + + private fun getNotification(type: NotificationType): Notification { + val notificationBuilder: Notification.Builder = + Notification.Builder(mContext, "channelId") + .setSmallIcon(R.drawable.ic_person) + .setContentTitle(CONTENT_TITLE) + .setContentText(CONTENT_TEXT) + .setLargeIcon(type.largeIcon) + + val user = Person.Builder().setName("User").build() + + val buildMessagingStyle = + Notification.MessagingStyle(user) + .setConversationTitle(CONVERSATION_TITLE) + .addMessage("Hi", 0, currentUser) + + return when (type) { + is NonMessaging -> + notificationBuilder + .setStyle(Notification.BigTextStyle().bigText("Big Text")) + .build() + is LegacyMessaging -> { + buildMessagingStyle + .addMessage("What's up?", 0, firstSender) + .addMessage("Not much", 0, currentUser) + .addMessage(LAST_MESSAGE, 0, firstSender) + + val notification = notificationBuilder.setStyle(buildMessagingStyle).build() + + assertNull(notification.shortcutId) + notification + } + is LegacyMessagingGroup -> { + buildMessagingStyle + .addMessage("What's up?", 0, firstSender) + .addMessage("Check out my new hover board!", 0, lastSender) + .setGroupConversation(true) + .addMessage(LAST_MESSAGE, 0, lastSender) + + val notification = notificationBuilder.setStyle(buildMessagingStyle).build() + + assertNull(notification.shortcutId) + notification + } + is OneToOneConversation -> { + buildMessagingStyle + .addMessage("What's up?", 0, firstSender) + .addMessage("Not much", 0, currentUser) + .addMessage(LAST_MESSAGE, 0, firstSender) + .setShortcutIcon(type.shortcutIcon) + notificationBuilder.setShortcutId(SHORTCUT_ID).setStyle(buildMessagingStyle).build() + } + is GroupConversation -> { + buildMessagingStyle + .addMessage("What's up?", 0, firstSender) + .addMessage("Check out my new hover board!", 0, lastSender) + .setGroupConversation(true) + .addMessage(LAST_MESSAGE, 0, lastSender) + notificationBuilder.setShortcutId(SHORTCUT_ID).setStyle(buildMessagingStyle).build() + } + } + } + + private fun Notification.makeSingleLineViewModel(type: NotificationType): SingleLineViewModel { + val builder = Notification.Builder.recoverBuilder(context, this) + + // Validate the recovered builder has the right type of style + val expectMessagingStyle = + when (type) { + is LegacyMessaging, + is LegacyMessagingGroup, + is OneToOneConversation, + is GroupConversation -> true + else -> false + } + if (expectMessagingStyle) { + assertIs<Notification.MessagingStyle>( + builder.style, + "Notification style should be MessagingStyle" + ) + } else { + assertIsNot<Notification.MessagingStyle>( + builder.style, + message = "Notification style should not be MessagingStyle" + ) + } + + // Inflate the SingleLineViewModel + // Mock the behavior of NotificationContentInflater.doInBackground + val messagingStyle = builder.getMessagingStyle() + val isConversation = type is OneToOneConversation || type is GroupConversation + return SingleLineViewInflater.inflateSingleLineViewModel( + this, + if (isConversation) messagingStyle else null, + builder, + context + ) + } + + private fun Notification.Builder.getMessagingStyle(): Notification.MessagingStyle? { + return style as? Notification.MessagingStyle + } + + private fun getBitmap(context: Context, resId: Int): Bitmap { + val largeIconDimension = + context.resources.getDimension(R.dimen.conversation_single_line_avatar_size) + val d = context.resources.getDrawable(resId) + val b = + Bitmap.createBitmap( + largeIconDimension.toInt(), + largeIconDimension.toInt(), + Bitmap.Config.ARGB_8888 + ) + val c = Canvas(b) + val paint = Paint() + c.drawCircle( + largeIconDimension / 2, + largeIconDimension / 2, + largeIconDimension.coerceAtMost(largeIconDimension) / 2, + paint + ) + d.setBounds(0, 0, largeIconDimension.toInt(), largeIconDimension.toInt()) + paint.setXfermode(PorterDuffXfermode(PorterDuff.Mode.SRC_IN)) + c.saveLayer(0F, 0F, largeIconDimension, largeIconDimension, paint, Canvas.ALL_SAVE_FLAG) + d.draw(c) + c.restore() + return b + } + + fun ConversationAvatar.equalsTo(other: ConversationAvatar?): Boolean = + when { + this === other -> true + this is SingleIcon && other is SingleIcon -> equalsTo(other) + this is FacePile && other is FacePile -> equalsTo(other) + else -> false + } + + private fun SingleIcon.equalsTo(other: SingleIcon): Boolean = + iconDrawable?.equalsTo(other.iconDrawable) == true + + private fun FacePile.equalsTo(other: FacePile): Boolean = + when { + bottomBackgroundColor != other.bottomBackgroundColor -> false + topIconDrawable?.equalsTo(other.topIconDrawable) != true -> false + bottomIconDrawable?.equalsTo(other.bottomIconDrawable) != true -> false + else -> true + } + + fun Drawable.equalsTo(other: Drawable?): Boolean = + when { + this === other -> true + this.pixelsEqualTo(other) -> true + else -> false + } + + private fun <T : Drawable> T.pixelsEqualTo(t: T?) = + toBitmap().pixelsEqualTo(t?.toBitmap(), false) + + private fun Bitmap.pixelsEqualTo(otherBitmap: Bitmap?, shouldRecycle: Boolean = false) = + otherBitmap?.let { other -> + if (width == other.width && height == other.height) { + val res = toPixels().contentEquals(other.toPixels()) + if (shouldRecycle) { + doRecycle().also { otherBitmap.doRecycle() } + } + res + } else false + } + ?: kotlin.run { false } + + private fun Bitmap.toPixels() = + IntArray(width * height).apply { getPixels(this, 0, width, 0, 0, width, height) } + + fun Bitmap.doRecycle() { + if (!isRecycled) recycle() + } +} 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 1ab4c32c7d08..dbe63f290407 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 @@ -860,9 +860,6 @@ public class NotificationStackScrollLayoutControllerTest extends SysuiTestCase { when(mNotificationStackScrollLayout.onKeyguard()).thenReturn(true); mController.getNotifStackController().setNotifStats(NotifStats.getEmpty()); - // WHEN: call updateImportantForAccessibility - mController.updateImportantForAccessibility(); - // THEN: mNotificationStackScrollLayout should not be important for A11y verify(mNotificationStackScrollLayout) .setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO); @@ -884,9 +881,6 @@ public class NotificationStackScrollLayoutControllerTest extends SysuiTestCase { /* hasClearableSilentNotifs = */ false) ); - // WHEN: call updateImportantForAccessibility - mController.updateImportantForAccessibility(); - // THEN: mNotificationStackScrollLayout should be important for A11y verify(mNotificationStackScrollLayout) .setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES); @@ -908,9 +902,6 @@ public class NotificationStackScrollLayoutControllerTest extends SysuiTestCase { /* hasClearableSilentNotifs = */ false) ); - // WHEN: call updateImportantForAccessibility - mController.updateImportantForAccessibility(); - // THEN: mNotificationStackScrollLayout should be important for A11y verify(mNotificationStackScrollLayout) .setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES); @@ -925,9 +916,6 @@ public class NotificationStackScrollLayoutControllerTest extends SysuiTestCase { when(mNotificationStackScrollLayout.onKeyguard()).thenReturn(false); mController.getNotifStackController().setNotifStats(NotifStats.getEmpty()); - // WHEN: call updateImportantForAccessibility - mController.updateImportantForAccessibility(); - // THEN: mNotificationStackScrollLayout should be important for A11y verify(mNotificationStackScrollLayout) .setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt index 06298b78ae57..32c727c70172 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt @@ -38,6 +38,9 @@ import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.StatusBarState import com.android.systemui.keyguard.shared.model.TransitionState import com.android.systemui.keyguard.shared.model.TransitionStep +import com.android.systemui.keyguard.ui.viewmodel.AodBurnInViewModel +import com.android.systemui.keyguard.ui.viewmodel.BurnInParameters +import com.android.systemui.keyguard.ui.viewmodel.aodBurnInViewModel import com.android.systemui.keyguard.ui.viewmodel.keyguardRootViewModel import com.android.systemui.kosmos.testScope import com.android.systemui.res.R @@ -45,6 +48,7 @@ import com.android.systemui.shade.data.repository.shadeRepository import com.android.systemui.shade.mockLargeScreenHeaderHelper import com.android.systemui.statusbar.notification.stack.domain.interactor.sharedNotificationContainerInteractor 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 @@ -55,15 +59,22 @@ import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +import org.mockito.Mockito.mock @SmallTest @RunWith(AndroidJUnit4::class) class SharedNotificationContainerViewModelTest : SysuiTestCase() { + val aodBurnInViewModel = mock(AodBurnInViewModel::class.java) + lateinit var translationYFlow: MutableStateFlow<Float> val kosmos = testKosmos().apply { fakeFeatureFlagsClassic.apply { set(Flags.FULL_SCREEN_USER_SWITCHER, false) } } + + init { + kosmos.aodBurnInViewModel = aodBurnInViewModel + } val testScope = kosmos.testScope val configurationRepository = kosmos.fakeConfigurationRepository val keyguardRepository = kosmos.fakeKeyguardRepository @@ -75,11 +86,14 @@ class SharedNotificationContainerViewModelTest : SysuiTestCase() { val sharedNotificationContainerInteractor = kosmos.sharedNotificationContainerInteractor val largeScreenHeaderHelper = kosmos.mockLargeScreenHeaderHelper - val underTest = kosmos.sharedNotificationContainerViewModel + lateinit var underTest: SharedNotificationContainerViewModel @Before fun setUp() { overrideResource(R.bool.config_use_split_notification_shade, false) + translationYFlow = MutableStateFlow(0f) + whenever(aodBurnInViewModel.translationY(any())).thenReturn(translationYFlow) + underTest = kosmos.sharedNotificationContainerViewModel } @Test @@ -579,9 +593,21 @@ class SharedNotificationContainerViewModelTest : SysuiTestCase() { } @Test + fun translationYUpdatesOnKeyguardForBurnIn() = + testScope.runTest { + val translationY by collectLastValue(underTest.translationY(BurnInParameters())) + + showLockscreen() + assertThat(translationY).isEqualTo(0) + + translationYFlow.value = 150f + assertThat(translationY).isEqualTo(150f) + } + + @Test fun translationYUpdatesOnKeyguard() = testScope.runTest { - val translationY by collectLastValue(underTest.translationY) + val translationY by collectLastValue(underTest.translationY(BurnInParameters())) configurationRepository.setDimensionPixelSize( R.dimen.keyguard_translate_distance_on_swipe_up, @@ -601,7 +627,7 @@ class SharedNotificationContainerViewModelTest : SysuiTestCase() { @Test fun translationYDoesNotUpdateWhenShadeIsExpanded() = testScope.runTest { - val translationY by collectLastValue(underTest.translationY) + val translationY by collectLastValue(underTest.translationY(BurnInParameters())) configurationRepository.setDimensionPixelSize( R.dimen.keyguard_translate_distance_on_swipe_up, 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 b3a47d77a307..423cc8478dda 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 @@ -31,6 +31,7 @@ import static org.mockito.ArgumentMatchers.anyFloat; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; @@ -1683,6 +1684,26 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + public void notificationBoundsTopGetsPassedToKeyguard() { + mScrimController.transitionTo(SHADE_LOCKED); + mScrimController.setQsPosition(1f, 0); + finishAnimationsImmediately(); + + mScrimController.setNotificationsBounds(0f, 100f, 0f, 0f); + verify(mKeyguardInteractor).setTopClippingBounds(eq(100)); + } + + @Test + public void notificationBoundsTopDoesNotGetPassedToKeyguardWhenNotifScrimIsNotVisible() { + mScrimController.setKeyguardOccluded(true); + mScrimController.transitionTo(ScrimState.KEYGUARD); + finishAnimationsImmediately(); + + mScrimController.setNotificationsBounds(0f, 100f, 0f, 0f); + verify(mKeyguardInteractor).setTopClippingBounds(eq(null)); + } + + @Test public void transitionToDreaming() { mScrimController.setRawPanelExpansionFraction(0f); mScrimController.setBouncerHiddenFraction(KeyguardBouncerConstants.EXPANSION_HIDDEN); 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 8dde9359bdfc..cb4531567e86 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 @@ -182,8 +182,10 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase { when(mBouncerViewDelegate.getBackCallback()).thenReturn(mBouncerViewDelegateBackCallback); mFeatureFlags = new FakeFeatureFlags(); mFeatureFlags.set(Flags.REFACTOR_KEYGUARD_DISMISS_INTENT, false); - mFeatureFlags.set(Flags.KEYGUARD_WM_STATE_REFACTOR, false); - mSetFlagsRule.disableFlags(com.android.systemui.Flags.FLAG_DEVICE_ENTRY_UDFPS_REFACTOR); + mSetFlagsRule.disableFlags( + com.android.systemui.Flags.FLAG_DEVICE_ENTRY_UDFPS_REFACTOR, + com.android.systemui.Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR + ); when(mNotificationShadeWindowController.getWindowRootView()) .thenReturn(mNotificationShadeWindowView); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImplTest.kt index 0e0d4897d667..5b5819d649b4 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImplTest.kt @@ -125,22 +125,6 @@ class DeviceBasedSatelliteRepositoryImplTest : SysuiTestCase() { } @Test - fun satelliteManagerThrows_doesNotCrash() = - testScope.runTest { - setupDefaultRepo() - - whenever(satelliteManager.registerForNtnSignalStrengthChanged(any(), any())) - .thenThrow(SatelliteException(13)) - - val conn by collectLastValue(underTest.connectionState) - val strength by collectLastValue(underTest.signalStrength) - - // Flows have not emitted, we haven't crashed - assertThat(conn).isNull() - assertThat(strength).isNull() - } - - @Test fun connectionState_mapsFromSatelliteModemState() = testScope.runTest { setupDefaultRepo() diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/SysuiTestCase.java b/packages/SystemUI/tests/utils/src/com/android/systemui/SysuiTestCase.java index af7f4c894c7c..b62b3a211e58 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/SysuiTestCase.java +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/SysuiTestCase.java @@ -21,17 +21,25 @@ import static org.mockito.Mockito.spy; import static org.mockito.Mockito.when; import android.app.Instrumentation; +import android.content.Context; +import android.content.res.Resources; import android.os.Build; import android.os.Handler; +import android.os.HandlerExecutor; import android.os.Looper; import android.os.MessageQueue; import android.os.ParcelFileDescriptor; +import android.platform.test.annotations.DisabledOnRavenwood; import android.platform.test.flag.junit.SetFlagsRule; +import android.platform.test.ravenwood.RavenwoodClassRule; +import android.platform.test.ravenwood.RavenwoodRule; +import android.test.mock.MockContext; import android.testing.DexmakerShareClassLoaderRule; import android.testing.LeakCheck; import android.testing.TestWithLooperRule; import android.testing.TestableLooper; import android.util.Log; +import android.util.Singleton; import androidx.annotation.NonNull; import androidx.core.animation.AndroidXAnimatorIsolationRule; @@ -44,17 +52,24 @@ import com.android.systemui.flags.SceneContainerRule; import org.junit.After; import org.junit.AfterClass; import org.junit.Before; +import org.junit.ClassRule; import org.junit.Rule; import org.mockito.Mockito; import java.io.FileInputStream; import java.io.IOException; import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executor; import java.util.concurrent.Future; /** * Base class that does System UI specific setup. */ +// NOTE: This @DisabledOnRavenwood annotation is inherited to all subclasses (unless overridden +// via a more-specific @EnabledOnRavenwood annotation); this means that by default all +// subclasses will be "ignored" when executed on the Ravenwood testing environment; more +// background on Ravenwood is available at go/ravenwood-docs +@DisabledOnRavenwood public abstract class SysuiTestCase { private static final String TAG = "SysuiTestCase"; @@ -66,6 +81,23 @@ public abstract class SysuiTestCase { public AndroidXAnimatorIsolationRule mAndroidXAnimatorIsolationRule = new AndroidXAnimatorIsolationRule(); + /** + * Rule that respects class-level annotations such as {@code @DisabledOnRavenwood} when tests + * are running on Ravenwood; on all other test environments this rule is a no-op passthrough. + */ + @ClassRule(order = Integer.MIN_VALUE + 1) + public static final RavenwoodClassRule sRavenwood = new RavenwoodClassRule(); + + /** + * Rule that defines and prepares the Ravenwood environment when tests are running on + * Ravenwood; on all other test environments this rule is a no-op passthrough. + */ + @Rule(order = Integer.MIN_VALUE + 1) + public final RavenwoodRule mRavenwood = new RavenwoodRule.Builder() + .setProcessApp() + .setProvideMainThread(true) + .build(); + @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(DEVICE_DEFAULT); @@ -78,7 +110,7 @@ public abstract class SysuiTestCase { @NonNull private SysuiTestableContext createTestableContext() { SysuiTestableContext context = new SysuiTestableContext( - InstrumentationRegistry.getContext(), getLeakCheck()); + getTestableContextBase(), getLeakCheck()); if (isRobolectricTest()) { // Manually associate a Display to context for Robolectric test. Similar to b/214297409 return context.createDefaultDisplayContext(); @@ -87,6 +119,43 @@ public abstract class SysuiTestCase { } } + @NonNull + private Context getTestableContextBase() { + if (isRavenwoodTest()) { + // TODO(b/292141694): build out Ravenwood support for Context + // Ravenwood doesn't yet provide a Context, but many SysUI tests assume one exists; + // so here we construct just enough of a Context to be useful; this will be replaced + // as more of the Ravenwood environment is built out + return new MockContext() { + @Override + public void setTheme(int resid) { + // TODO(b/318393625): build out Ravenwood support for Resources + // until then, ignored as no-op + } + + @Override + public Resources getResources() { + // TODO(b/318393625): build out Ravenwood support for Resources + return Mockito.mock(Resources.class); + } + + private Singleton<Executor> mMainExecutor = new Singleton<>() { + @Override + protected Executor create() { + return new HandlerExecutor(new Handler(Looper.getMainLooper())); + } + }; + + @Override + public Executor getMainExecutor() { + return mMainExecutor.get(); + } + }; + } else { + return InstrumentationRegistry.getContext(); + } + } + @Rule public final DexmakerShareClassLoaderRule mDexmakerShareClassLoaderRule = new DexmakerShareClassLoaderRule(); @@ -103,17 +172,22 @@ public abstract class SysuiTestCase { public void SysuiSetup() throws Exception { mSysuiDependency = new SysuiTestDependency(mContext, shouldFailOnLeakedReceiver()); mDependency = mSysuiDependency.install(); - mRealInstrumentation = InstrumentationRegistry.getInstrumentation(); - Instrumentation inst = spy(mRealInstrumentation); - when(inst.getContext()).thenAnswer(invocation -> { - throw new RuntimeException( - "SysUI Tests should use SysuiTestCase#getContext or SysuiTestCase#mContext"); - }); - when(inst.getTargetContext()).thenAnswer(invocation -> { - throw new RuntimeException( - "SysUI Tests should use SysuiTestCase#getContext or SysuiTestCase#mContext"); - }); - InstrumentationRegistry.registerInstance(inst, InstrumentationRegistry.getArguments()); + // TODO(b/292141694): build out Ravenwood support for Instrumentation + // Ravenwood doesn't yet provide Instrumentation, so we sidestep this global configuration + // step; any tests that rely on it are already being excluded on Ravenwood + if (!isRavenwoodTest()) { + mRealInstrumentation = InstrumentationRegistry.getInstrumentation(); + Instrumentation inst = spy(mRealInstrumentation); + when(inst.getContext()).thenAnswer(invocation -> { + throw new RuntimeException( + "Tests should use SysuiTestCase#getContext or SysuiTestCase#mContext"); + }); + when(inst.getTargetContext()).thenAnswer(invocation -> { + throw new RuntimeException( + "Tests should use SysuiTestCase#getContext or SysuiTestCase#mContext"); + }); + InstrumentationRegistry.registerInstance(inst, InstrumentationRegistry.getArguments()); + } } protected boolean shouldFailOnLeakedReceiver() { @@ -209,7 +283,11 @@ public abstract class SysuiTestCase { } public static boolean isRobolectricTest() { - return Build.FINGERPRINT.contains("robolectric"); + return !isRavenwoodTest() && Build.FINGERPRINT.contains("robolectric"); + } + + public static boolean isRavenwoodTest() { + return RavenwoodRule.isOnRavenwood(); } private static final void validateThread(Looper l) { diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/SysuiTestDependency.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/SysuiTestDependency.kt index d89d7b00592e..364d3b236ef5 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/SysuiTestDependency.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/SysuiTestDependency.kt @@ -34,10 +34,16 @@ class SysuiTestDependency( // is missing (constructing the actual one would throw). // TODO(b/219008720): Remove this. dependency.injectMockDependency(SystemUIDialogManager::class.java) - dependency.injectTestDependency( - DialogLaunchAnimator::class.java, - fakeDialogLaunchAnimator() - ) + + // TODO(b/292141694): build out Ravenwood support for UI animations + // Ravenwood doesn't yet provide UI animations, so we sidestep this global configuration + // step; any tests that rely on it are already being excluded under Ravenwood + if (!SysuiTestCase.isRavenwoodTest()) { + dependency.injectTestDependency( + DialogLaunchAnimator::class.java, + fakeDialogLaunchAnimator() + ) + } // Many tests end up creating a BroadcastDispatcher. Instead, give them a fake that will // record receivers registered. They are not actually leaked as they are kept just as a weak diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/accessibility/data/repository/FakeAccessibilityQsShortcutsRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/accessibility/data/repository/FakeAccessibilityQsShortcutsRepository.kt new file mode 100644 index 000000000000..e547da1b92dd --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/accessibility/data/repository/FakeAccessibilityQsShortcutsRepository.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.accessibility.data.repository + +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.asSharedFlow + +class FakeAccessibilityQsShortcutsRepository : AccessibilityQsShortcutsRepository { + + private val targetsPerUser = mutableMapOf<Int, MutableSharedFlow<Set<String>>>() + + override fun a11yQsShortcutTargets(userId: Int): SharedFlow<Set<String>> { + return getFlow(userId).asSharedFlow() + } + + /** + * Set the a11y qs shortcut targets. In real world, the A11y QS Shortcut targets are set by the + * Settings app not in SysUi + */ + suspend fun setA11yQsShortcutTargets(userId: Int, targets: Set<String>) { + getFlow(userId).emit(targets) + } + + private fun getFlow(userId: Int): MutableSharedFlow<Set<String>> = + targetsPerUser.getOrPut(userId) { MutableSharedFlow(replay = 1) } +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/UdfpsUtilsKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/UdfpsUtilsKosmos.kt new file mode 100644 index 000000000000..4849fec14a88 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/UdfpsUtilsKosmos.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.biometrics + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.util.mockito.mock + +var Kosmos.udfpsUtils by Kosmos.Fixture { mock<UdfpsUtils>() } 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 new file mode 100644 index 000000000000..961022f0f426 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/BiometricStatusRepositoryKosmos.kt @@ -0,0 +1,23 @@ +/* + * 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.data.repository + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.Kosmos.Fixture + +val Kosmos.biometricStatusRepository by Fixture { FakeBiometricStatusRepository() } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/PromptRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/PromptRepositoryKosmos.kt new file mode 100644 index 000000000000..31fa5d2ed715 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/PromptRepositoryKosmos.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.biometrics.data.repository + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.Kosmos.Fixture + +var Kosmos.promptRepository by Fixture { FakePromptRepository() } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/domain/interactor/BiometricStatusInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/domain/interactor/BiometricStatusInteractorKosmos.kt new file mode 100644 index 000000000000..1493f14d9aa7 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/domain/interactor/BiometricStatusInteractorKosmos.kt @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.biometrics.domain.interactor + +import com.android.app.activityTaskManager +import com.android.systemui.biometrics.data.repository.biometricStatusRepository +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.Kosmos.Fixture + +val Kosmos.biometricStatusInteractor by Fixture { + BiometricStatusInteractorImpl( + activityTaskManager = activityTaskManager, + biometricStatusRepository = biometricStatusRepository + ) +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractorKosmos.kt new file mode 100644 index 000000000000..7f9a71cd149e --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractorKosmos.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.biometrics.domain.interactor + +import com.android.internal.widget.lockPatternUtils +import com.android.systemui.biometrics.data.repository.fingerprintPropertyRepository +import com.android.systemui.biometrics.data.repository.promptRepository +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.Kosmos.Fixture + +val Kosmos.promptSelectorInteractor by Fixture { + PromptSelectorInteractorImpl( + fingerprintPropertyRepository = fingerprintPropertyRepository, + promptRepository = promptRepository, + lockPatternUtils = lockPatternUtils + ) +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelKosmos.kt new file mode 100644 index 000000000000..9cbe6337befe --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelKosmos.kt @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.biometrics.ui.viewmodel + +import android.content.applicationContext +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.udfpsUtils +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.Kosmos.Fixture + +val Kosmos.promptViewModel by Fixture { + PromptViewModel( + displayStateInteractor = displayStateInteractor, + promptSelectorInteractor = promptSelectorInteractor, + context = applicationContext, + udfpsOverlayInteractor = udfpsOverlayInteractor, + udfpsUtils = udfpsUtils + ) +} 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 e25e8c099c21..bc7e7af245a6 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 @@ -39,13 +39,4 @@ class FakeCommunalWidgetRepository(private val coroutineScope: CoroutineScope) : private fun onConfigured(id: Int, providerInfo: AppWidgetProviderInfo, priority: Int) { _communalWidgets.value += listOf(CommunalWidgetContentModel(id, providerInfo, priority)) } - - private var isHostActive = false - override fun updateAppWidgetHostActive(active: Boolean) { - isHostActive = active - } - - fun isHostActive(): Boolean { - return isHostActive - } } 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 1abf71fde14c..c818e9c8971c 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,14 +24,12 @@ 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.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, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/data/repository/FakeDeviceEntryRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/data/repository/FakeDeviceEntryRepository.kt index 6436a382eb7f..77caeaa6da4d 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/data/repository/FakeDeviceEntryRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/data/repository/FakeDeviceEntryRepository.kt @@ -16,25 +16,16 @@ package com.android.systemui.deviceentry.data.repository import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.keyguard.shared.model.BiometricUnlockSource import dagger.Binds import dagger.Module import javax.inject.Inject -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow /** Fake implementation of [DeviceEntryRepository] */ @SysUISingleton class FakeDeviceEntryRepository @Inject constructor() : DeviceEntryRepository { - private val _enteringDeviceFromBiometricUnlock: MutableSharedFlow<BiometricUnlockSource> = - MutableSharedFlow() - override val enteringDeviceFromBiometricUnlock: Flow<BiometricUnlockSource> = - _enteringDeviceFromBiometricUnlock.asSharedFlow() - private var isLockscreenEnabled = true private val _isBypassEnabled = MutableStateFlow(false) @@ -62,10 +53,6 @@ class FakeDeviceEntryRepository @Inject constructor() : DeviceEntryRepository { fun setBypassEnabled(isBypassEnabled: Boolean) { _isBypassEnabled.value = isBypassEnabled } - - suspend fun enteringDeviceFromBiometricUnlock(sourceType: BiometricUnlockSource) { - _enteringDeviceFromBiometricUnlock.emit(sourceType) - } } @Module diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/AuthRippleInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/AuthRippleInteractorKosmos.kt new file mode 100644 index 000000000000..3070cf4c06ad --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/AuthRippleInteractorKosmos.kt @@ -0,0 +1,29 @@ +/* + * 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.deviceentry.domain.interactor + +import com.android.systemui.kosmos.Kosmos +import kotlinx.coroutines.ExperimentalCoroutinesApi + +@OptIn(ExperimentalCoroutinesApi::class) +val Kosmos.authRippleInteractor by + Kosmos.Fixture { + AuthRippleInteractor( + deviceEntrySourceInteractor = deviceEntrySourceInteractor, + deviceEntryUdfpsInteractor = deviceEntryUdfpsInteractor, + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractorKosmos.kt index de58ae5e9452..878e38594fe1 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractorKosmos.kt @@ -30,7 +30,7 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi val Kosmos.deviceEntryHapticsInteractor by Kosmos.Fixture { DeviceEntryHapticsInteractor( - deviceEntryInteractor = deviceEntryInteractor, + deviceEntrySourceInteractor = deviceEntrySourceInteractor, deviceEntryFingerprintAuthInteractor = deviceEntryFingerprintAuthInteractor, deviceEntryBiometricAuthInteractor = deviceEntryBiometricAuthInteractor, fingerprintPropertyRepository = fingerprintPropertyRepository, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractorKosmos.kt index 8dcdd3a9425c..0d1a31f9605e 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractorKosmos.kt @@ -14,8 +14,6 @@ * limitations under the License. */ -@file:OptIn(ExperimentalCoroutinesApi::class) - package com.android.systemui.deviceentry.domain.interactor import com.android.systemui.authentication.domain.interactor.authenticationInteractor @@ -28,6 +26,7 @@ import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.scene.shared.flag.sceneContainerFlags import kotlinx.coroutines.ExperimentalCoroutinesApi +@ExperimentalCoroutinesApi val Kosmos.deviceEntryInteractor by Kosmos.Fixture { DeviceEntryInteractor( diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntrySourceInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntrySourceInteractorKosmos.kt new file mode 100644 index 000000000000..0b9ec92af2b5 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntrySourceInteractorKosmos.kt @@ -0,0 +1,29 @@ +/* + * 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.deviceentry.domain.interactor + +import com.android.systemui.keyguard.domain.interactor.keyguardInteractor +import com.android.systemui.kosmos.Kosmos +import kotlinx.coroutines.ExperimentalCoroutinesApi + +@ExperimentalCoroutinesApi +val Kosmos.deviceEntrySourceInteractor by + Kosmos.Fixture { + DeviceEntrySourceInteractor( + keyguardInteractor = keyguardInteractor, + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt index 5766f7a9028c..793e2d7efcda 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt @@ -65,7 +65,7 @@ class FakeKeyguardRepository @Inject constructor() : KeyguardRepository { override val isKeyguardShowing: Flow<Boolean> = _isKeyguardShowing private val _isKeyguardUnlocked = MutableStateFlow(false) - override val isKeyguardUnlocked: StateFlow<Boolean> = _isKeyguardUnlocked.asStateFlow() + override val isKeyguardDismissible: StateFlow<Boolean> = _isKeyguardUnlocked.asStateFlow() private val _isKeyguardOccluded = MutableStateFlow(false) override val isKeyguardOccluded: Flow<Boolean> = _isKeyguardOccluded @@ -165,7 +165,7 @@ class FakeKeyguardRepository @Inject constructor() : KeyguardRepository { _isKeyguardOccluded.value = isOccluded } - fun setKeyguardUnlocked(isUnlocked: Boolean) { + fun setKeyguardDismissible(isUnlocked: Boolean) { _isKeyguardUnlocked.value = isUnlocked } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModelKosmos.kt index 35cfa89e56ed..a8f45b0974c4 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModelKosmos.kt @@ -26,7 +26,7 @@ import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.Kosmos.Fixture import kotlinx.coroutines.ExperimentalCoroutinesApi -val Kosmos.aodBurnInViewModel by Fixture { +var Kosmos.aodBurnInViewModel by Fixture { AodBurnInViewModel( burnInInteractor = burnInInteractor, configurationInteractor = configurationInteractor, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModelKosmos.kt index 5ceefde32d2a..73fd9991945c 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModelKosmos.kt @@ -17,6 +17,7 @@ package com.android.systemui.keyguard.ui.viewmodel import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor +import com.android.systemui.deviceentry.domain.interactor.deviceEntrySourceInteractor import com.android.systemui.deviceentry.domain.interactor.deviceEntryUdfpsInteractor import com.android.systemui.keyguard.domain.interactor.burnInInteractor import com.android.systemui.keyguard.domain.interactor.keyguardInteractor @@ -27,6 +28,7 @@ import com.android.systemui.kosmos.Kosmos.Fixture import com.android.systemui.scene.shared.flag.sceneContainerFlags import com.android.systemui.shade.domain.interactor.shadeInteractor import com.android.systemui.statusbar.phone.statusBarKeyguardViewManager +import kotlinx.coroutines.ExperimentalCoroutinesApi val Kosmos.fakeDeviceEntryIconViewModelTransition by Fixture { FakeDeviceEntryIconTransition() } @@ -34,6 +36,7 @@ val Kosmos.deviceEntryIconViewModelTransitionsMock by Fixture { setOf<DeviceEntryIconTransition>(fakeDeviceEntryIconViewModelTransition) } +@ExperimentalCoroutinesApi val Kosmos.deviceEntryIconViewModel by Fixture { DeviceEntryIconViewModel( transitions = deviceEntryIconViewModelTransitionsMock, @@ -46,5 +49,6 @@ val Kosmos.deviceEntryIconViewModel by Fixture { sceneContainerFlags = sceneContainerFlags, keyguardViewController = { statusBarKeyguardViewManager }, deviceEntryInteractor = deviceEntryInteractor, + deviceEntrySourceInteractor = deviceEntrySourceInteractor, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelKosmos.kt index db4050905200..7c398cd45f90 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelKosmos.kt @@ -19,6 +19,7 @@ package com.android.systemui.statusbar.notification.stack.ui.viewmodel import com.android.systemui.communal.domain.interactor.communalInteractor import com.android.systemui.keyguard.domain.interactor.keyguardInteractor import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor +import com.android.systemui.keyguard.ui.viewmodel.aodBurnInViewModel import com.android.systemui.keyguard.ui.viewmodel.glanceableHubToLockscreenTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.lockscreenToGlanceableHubTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.lockscreenToOccludedTransitionViewModel @@ -40,6 +41,7 @@ val Kosmos.sharedNotificationContainerViewModel by Fixture { occludedToLockscreenTransitionViewModel = occludedToLockscreenTransitionViewModel, lockscreenToOccludedTransitionViewModel = lockscreenToOccludedTransitionViewModel, glanceableHubToLockscreenTransitionViewModel = glanceableHubToLockscreenTransitionViewModel, - lockscreenToGlanceableHubTransitionViewModel = lockscreenToGlanceableHubTransitionViewModel + lockscreenToGlanceableHubTransitionViewModel = lockscreenToGlanceableHubTransitionViewModel, + aodBurnInViewModel = aodBurnInViewModel, ) } 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 18a2f9482df8..33ed7e61de4d 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 @@ -20,3 +20,5 @@ import com.android.systemui.kosmos.Kosmos import com.android.systemui.util.mockito.mock val Kosmos.configurationController by Kosmos.Fixture { mock<ConfigurationController>() } +val Kosmos.fakeConfigurationController: FakeConfigurationController by + Kosmos.Fixture { FakeConfigurationController() } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/VolumePanelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/VolumePanelKosmos.kt new file mode 100644 index 000000000000..d3410737a432 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/VolumePanelKosmos.kt @@ -0,0 +1,72 @@ +/* + * 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 + +import android.content.res.mainResources +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.testScope +import com.android.systemui.statusbar.policy.fakeConfigurationController +import com.android.systemui.util.mockito.mock +import com.android.systemui.volume.panel.dagger.factory.KosmosVolumePanelComponentFactory +import com.android.systemui.volume.panel.domain.ComponentAvailabilityCriteria +import com.android.systemui.volume.panel.domain.TestComponentAvailabilityCriteria +import com.android.systemui.volume.panel.domain.interactor.ComponentsInteractor +import com.android.systemui.volume.panel.domain.interactor.ComponentsInteractorImpl +import com.android.systemui.volume.panel.shared.model.VolumePanelComponentKey +import com.android.systemui.volume.panel.shared.model.VolumePanelUiComponent +import com.android.systemui.volume.panel.ui.composable.ComponentsFactory +import com.android.systemui.volume.panel.ui.layout.ComponentsLayoutManager +import com.android.systemui.volume.panel.ui.viewmodel.VolumePanelViewModel +import javax.inject.Provider + +val Kosmos.mockVolumePanelUiComponent: VolumePanelUiComponent by Kosmos.Fixture { mock {} } +val Kosmos.mockVolumePanelUiComponentProvider: Provider<VolumePanelUiComponent> by + Kosmos.Fixture { Provider { mockVolumePanelUiComponent } } +var Kosmos.componentByKey: Map<VolumePanelComponentKey, Provider<VolumePanelUiComponent>> by + Kosmos.Fixture { emptyMap() } +val Kosmos.componentsFactory: ComponentsFactory by + Kosmos.Fixture { ComponentsFactory(componentByKey) } + +var Kosmos.componentsLayoutManager: ComponentsLayoutManager by Kosmos.Fixture() +var Kosmos.enabledComponents: Collection<VolumePanelComponentKey> by + Kosmos.Fixture { componentByKey.keys } +val Kosmos.unavailableCriteria: Provider<ComponentAvailabilityCriteria> by + Kosmos.Fixture { Provider { TestComponentAvailabilityCriteria(false) } } +val Kosmos.availableCriteria: Provider<ComponentAvailabilityCriteria> by + Kosmos.Fixture { Provider { TestComponentAvailabilityCriteria(true) } } +var Kosmos.defaultCriteria: Provider<ComponentAvailabilityCriteria> by + Kosmos.Fixture { availableCriteria } +var Kosmos.criteriaByKey: Map<VolumePanelComponentKey, Provider<ComponentAvailabilityCriteria>> by + Kosmos.Fixture { emptyMap() } +var Kosmos.componentsInteractor: ComponentsInteractor by + Kosmos.Fixture { + ComponentsInteractorImpl( + enabledComponents, + defaultCriteria, + testScope.backgroundScope, + criteriaByKey, + ) + } + +var Kosmos.volumePanelViewModel: VolumePanelViewModel by + Kosmos.Fixture { + VolumePanelViewModel( + mainResources, + KosmosVolumePanelComponentFactory(this), + fakeConfigurationController, + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/dagger/factory/KosmosVolumePanelComponentFactory.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/dagger/factory/KosmosVolumePanelComponentFactory.kt new file mode 100644 index 000000000000..49041ed0d652 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/dagger/factory/KosmosVolumePanelComponentFactory.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.systemui.volume.panel.dagger.factory + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.testScope +import com.android.systemui.volume.panel.componentsFactory +import com.android.systemui.volume.panel.componentsInteractor +import com.android.systemui.volume.panel.componentsLayoutManager +import com.android.systemui.volume.panel.dagger.VolumePanelComponent +import com.android.systemui.volume.panel.domain.interactor.ComponentsInteractor +import com.android.systemui.volume.panel.ui.composable.ComponentsFactory +import com.android.systemui.volume.panel.ui.layout.ComponentsLayoutManager +import com.android.systemui.volume.panel.ui.viewmodel.VolumePanelViewModel +import kotlinx.coroutines.CoroutineScope + +class KosmosVolumePanelComponentFactory(private val kosmos: Kosmos) : VolumePanelComponentFactory { + + override fun create(viewModel: VolumePanelViewModel): VolumePanelComponent = + object : VolumePanelComponent { + + override fun coroutineScope(): CoroutineScope = kosmos.testScope.backgroundScope + + override fun componentsInteractor(): ComponentsInteractor = kosmos.componentsInteractor + + override fun componentsFactory(): ComponentsFactory = kosmos.componentsFactory + + override fun componentsLayoutManager(): ComponentsLayoutManager = + kosmos.componentsLayoutManager + } +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/domain/TestComponentAvailabilityCriteria.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/domain/TestComponentAvailabilityCriteria.kt new file mode 100644 index 000000000000..5ab92745ae69 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/domain/TestComponentAvailabilityCriteria.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.volume.panel.domain + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOf + +class TestComponentAvailabilityCriteria(val isAvailable: Boolean) : ComponentAvailabilityCriteria { + + override fun isAvailable(): Flow<Boolean> = flowOf(isAvailable) +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/ui/layout/FakeComponentsLayoutManager.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/ui/layout/FakeComponentsLayoutManager.kt new file mode 100644 index 000000000000..655d8f7ad952 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/ui/layout/FakeComponentsLayoutManager.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.volume.panel.ui.layout + +import com.android.systemui.volume.panel.ui.viewmodel.ComponentState +import com.android.systemui.volume.panel.ui.viewmodel.VolumePanelState + +class FakeComponentsLayoutManager( + private val isBottomBar: (components: ComponentState) -> Boolean +) : ComponentsLayoutManager { + + override fun layout( + volumePanelState: VolumePanelState, + components: Collection<ComponentState> + ): ComponentsLayout { + return ComponentsLayout( + components + .filter { componentState -> !isBottomBar(componentState) } + .sortedBy { it.key }, + components.find(isBottomBar)!!, + ) + } +} diff --git a/ravenwood/framework-minus-apex-ravenwood-policies.txt b/ravenwood/framework-minus-apex-ravenwood-policies.txt index e33fff117d41..16f99e9289db 100644 --- a/ravenwood/framework-minus-apex-ravenwood-policies.txt +++ b/ravenwood/framework-minus-apex-ravenwood-policies.txt @@ -138,6 +138,9 @@ class com.android.modules.utils.TypedXmlSerializer stubclass class android.net.Uri stubclass class android.net.UriCodec stubclass +# Telephony +class android.telephony.PinResult stubclass + # Just enough to support mocking, no further functionality class android.content.Context stub method <init> ()V stub 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 91c522e82cce..b3dbcde9d324 100644 --- a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java +++ b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java @@ -22,7 +22,7 @@ import android.os.Looper; public class RavenwoodRuleImpl { private static final String MAIN_THREAD_NAME = "RavenwoodMain"; - public static boolean isUnderRavenwood() { + public static boolean isOnRavenwood() { return true; } diff --git a/ravenwood/junit-src/android/platform/test/annotations/IncludeUnderRavenwood.java b/ravenwood/junit-src/android/platform/test/annotations/DisabledOnRavenwood.java index 0b2e32f67960..4bf09807c360 100644 --- a/ravenwood/junit-src/android/platform/test/annotations/IncludeUnderRavenwood.java +++ b/ravenwood/junit-src/android/platform/test/annotations/DisabledOnRavenwood.java @@ -23,15 +23,16 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** - * Tests marked with this annotation are included when running under a Ravenwood test environment. + * Tests marked with this annotation are disabled when running under a Ravenwood test environment. * * A more specific method-level annotation always takes precedence over any class-level - * annotation, and an {@link IncludeUnderRavenwood} annotation always takes precedence over - * an {@link ExcludeUnderRavenwood} annotation. + * annotation, and an {@link EnabledOnRavenwood} annotation always takes precedence over + * an {@link DisabledOnRavenwood} annotation. * * This annotation only takes effect when the containing class has a {@code - * RavenwoodRule} configured. Ignoring is accomplished by throwing an {@code org.junit - * .AssumptionViolatedException} which test infrastructure treats as being ignored. + * RavenwoodRule} or {@code RavenwoodClassRule} configured. Ignoring is accomplished by + * throwing an {@code org.junit.AssumptionViolatedException} which test infrastructure treats as + * being ignored. * * This annotation has no effect on any other non-Ravenwood test environments. * @@ -40,5 +41,5 @@ import java.lang.annotation.Target; @Inherited @Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) -public @interface IncludeUnderRavenwood { +public @interface DisabledOnRavenwood { } diff --git a/ravenwood/junit-src/android/platform/test/annotations/ExcludeUnderRavenwood.java b/ravenwood/junit-src/android/platform/test/annotations/EnabledOnRavenwood.java index 2ef381e8682c..9dd0a58a9db1 100644 --- a/ravenwood/junit-src/android/platform/test/annotations/ExcludeUnderRavenwood.java +++ b/ravenwood/junit-src/android/platform/test/annotations/EnabledOnRavenwood.java @@ -23,15 +23,16 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** - * Tests marked with this annotation are excluded when running under a Ravenwood test environment. + * Tests marked with this annotation are enabled when running under a Ravenwood test environment. * * A more specific method-level annotation always takes precedence over any class-level - * annotation, and an {@link IncludeUnderRavenwood} annotation always takes precedence over - * an {@link ExcludeUnderRavenwood} annotation. + * annotation, and an {@link EnabledOnRavenwood} annotation always takes precedence over + * an {@link DisabledOnRavenwood} annotation. * * This annotation only takes effect when the containing class has a {@code - * RavenwoodRule} configured. Ignoring is accomplished by throwing an {@code org.junit - * .AssumptionViolatedException} which test infrastructure treats as being ignored. + * RavenwoodRule} or {@code RavenwoodClassRule} configured. Ignoring is accomplished by + * throwing an {@code org.junit.AssumptionViolatedException} which test infrastructure treats as + * being ignored. * * This annotation has no effect on any other non-Ravenwood test environments. * @@ -40,5 +41,5 @@ import java.lang.annotation.Target; @Inherited @Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) -public @interface ExcludeUnderRavenwood { +public @interface EnabledOnRavenwood { } diff --git a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodClassRule.java b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodClassRule.java new file mode 100644 index 000000000000..68b163ede89c --- /dev/null +++ b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodClassRule.java @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.platform.test.ravenwood; + +import static android.platform.test.ravenwood.RavenwoodRule.ENABLE_PROBE_IGNORED; +import static android.platform.test.ravenwood.RavenwoodRule.IS_ON_RAVENWOOD; +import static android.platform.test.ravenwood.RavenwoodRule.shouldEnableOnRavenwood; + +import android.platform.test.annotations.DisabledOnRavenwood; +import android.platform.test.annotations.EnabledOnRavenwood; + +import org.junit.Assume; +import org.junit.rules.TestRule; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; + +/** + * {@code @ClassRule} that respects Ravenwood-specific class annotations. This rule has no effect + * when tests are run on non-Ravenwood test environments. + * + * By default, all tests are executed on Ravenwood, but annotations such as + * {@link DisabledOnRavenwood} and {@link EnabledOnRavenwood} can be used at both the method + * and class level to "ignore" tests that may not be ready. + */ +public class RavenwoodClassRule implements TestRule { + @Override + public Statement apply(Statement base, Description description) { + // No special treatment when running outside Ravenwood; run tests as-is + if (!IS_ON_RAVENWOOD) { + return base; + } + + if (ENABLE_PROBE_IGNORED) { + // Pass through to possible underlying RavenwoodRule for both environment + // configuration and handling method-level annotations + return base; + } else { + return new Statement() { + @Override + public void evaluate() throws Throwable { + Assume.assumeTrue(shouldEnableOnRavenwood(description)); + // Pass through to possible underlying RavenwoodRule for both environment + // configuration and handling method-level annotations + base.evaluate(); + } + }; + } + } +} diff --git a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java index dd442f08321f..fef77f9d5ed7 100644 --- a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java +++ b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java @@ -18,9 +18,9 @@ package android.platform.test.ravenwood; import static org.junit.Assert.fail; -import android.platform.test.annotations.ExcludeUnderRavenwood; +import android.platform.test.annotations.DisabledOnRavenwood; import android.platform.test.annotations.IgnoreUnderRavenwood; -import android.platform.test.annotations.IncludeUnderRavenwood; +import android.platform.test.annotations.EnabledOnRavenwood; import org.junit.Assume; import org.junit.rules.TestRule; @@ -30,29 +30,36 @@ import org.junit.runners.model.Statement; import java.util.concurrent.atomic.AtomicInteger; /** - * THIS RULE IS EXPERIMENTAL. REACH OUT TO g/ravenwood BEFORE USING IT, OR YOU HAVE ANY - * QUESTIONS ABOUT IT. + * {@code @Rule} that configures the Ravenwood test environment. This rule has no effect when + * tests are run on non-Ravenwood test environments. * - * @hide + * This rule initializes and resets the Ravenwood environment between each test method to offer a + * hermetic testing environment. + * + * By default, all tests are executed on Ravenwood, but annotations such as + * {@link DisabledOnRavenwood} and {@link EnabledOnRavenwood} can be used at both the method + * and class level to "ignore" tests that may not be ready. When needed, a + * {@link RavenwoodClassRule} can be used in addition to a {@link RavenwoodRule} to ignore tests + * before a test class is fully initialized. */ public class RavenwoodRule implements TestRule { - private static AtomicInteger sNextPid = new AtomicInteger(100); - - private static final boolean IS_UNDER_RAVENWOOD = RavenwoodRuleImpl.isUnderRavenwood(); + static final boolean IS_ON_RAVENWOOD = RavenwoodRuleImpl.isOnRavenwood(); /** - * When probing is enabled, all tests will be unconditionally run under Ravenwood to detect + * When probing is enabled, all tests will be unconditionally run on Ravenwood to detect * cases where a test is able to pass despite being marked as {@code IgnoreUnderRavenwood}. * * This is typically helpful for internal maintainers discovering tests that had previously * been ignored, but now have enough Ravenwood-supported functionality to be enabled. */ - private static final boolean ENABLE_PROBE_IGNORED = false; // DO NOT SUBMIT WITH TRUE + static final boolean ENABLE_PROBE_IGNORED = false; // DO NOT SUBMIT WITH TRUE 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); + /** * Unless the test author requests differently, run as "nobody", and give each collection of * tests its own unique PID. @@ -75,7 +82,7 @@ public class RavenwoodRule implements TestRule { /** * Configure the identity of this process to be the system UID for the duration of the - * test. Has no effect under non-Ravenwood environments. + * test. Has no effect on non-Ravenwood environments. */ public Builder setProcessSystem() { mRule.mUid = SYSTEM_UID; @@ -84,7 +91,7 @@ public class RavenwoodRule implements TestRule { /** * Configure the identity of this process to be an app UID for the duration of the - * test. Has no effect under non-Ravenwood environments. + * test. Has no effect on non-Ravenwood environments. */ public Builder setProcessApp() { mRule.mUid = FIRST_APPLICATION_UID; @@ -93,7 +100,7 @@ public class RavenwoodRule implements TestRule { /** * Configure a "main" thread to be available for the duration of the test, as defined - * by {@code Looper.getMainLooper()}. Has no effect under non-Ravenwood environments. + * by {@code Looper.getMainLooper()}. Has no effect on non-Ravenwood environments. */ public Builder setProvideMainThread(boolean provideMainThread) { mRule.mProvideMainThread = provideMainThread; @@ -108,7 +115,7 @@ public class RavenwoodRule implements TestRule { * All properties in the {@code debug.*} namespace are automatically mutable, with no * developer action required. * - * Has no effect under non-Ravenwood environments. + * Has no effect on non-Ravenwood environments. */ public Builder setSystemPropertyImmutable(/* @NonNull */ String key, /* @Nullable */ Object value) { @@ -125,7 +132,7 @@ public class RavenwoodRule implements TestRule { * All properties in the {@code debug.*} namespace are automatically mutable, with no * developer action required. * - * Has no effect under non-Ravenwood environments. + * Has no effect on non-Ravenwood environments. */ public Builder setSystemPropertyMutable(/* @NonNull */ String key, /* @Nullable */ Object value) { @@ -140,42 +147,51 @@ public class RavenwoodRule implements TestRule { } /** - * Return if the current process is running under a Ravenwood test environment. + * @deprecated replaced by {@link #isOnRavenwood()} */ + @Deprecated public static boolean isUnderRavenwood() { - return IS_UNDER_RAVENWOOD; + return IS_ON_RAVENWOOD; + } + + /** + * Return if the current process is running on a Ravenwood test environment. + */ + public static boolean isOnRavenwood() { + return IS_ON_RAVENWOOD; } /** - * Determine if the given {@link Description} should be included when running under the + * Determine if the given {@link Description} should be enabled when running on the * Ravenwood test environment. * * A more specific method-level annotation always takes precedence over any class-level - * annotation, and an {@link IncludeUnderRavenwood} annotation always takes precedence over - * an {@link ExcludeUnderRavenwood} annotation. + * annotation, and an {@link EnabledOnRavenwood} annotation always takes precedence over + * an {@link DisabledOnRavenwood} annotation. */ - private boolean shouldIncludeUnderRavenwood(Description description) { - // Stopgap for http://g/ravenwood/EPAD-N5ntxM - if (description.getMethodName().endsWith("$noRavenwood")) { - return false; - } - + static boolean shouldEnableOnRavenwood(Description description) { // First, consult any method-level annotations - if (description.getAnnotation(IncludeUnderRavenwood.class) != null) { - return true; - } - if (description.getAnnotation(ExcludeUnderRavenwood.class) != null) { - return false; - } - if (description.getAnnotation(IgnoreUnderRavenwood.class) != null) { - return false; + if (description.isTest()) { + // Stopgap for http://g/ravenwood/EPAD-N5ntxM + if (description.getMethodName().endsWith("$noRavenwood")) { + return false; + } + if (description.getAnnotation(EnabledOnRavenwood.class) != null) { + return true; + } + if (description.getAnnotation(DisabledOnRavenwood.class) != null) { + return false; + } + if (description.getAnnotation(IgnoreUnderRavenwood.class) != null) { + return false; + } } // Otherwise, consult any class-level annotations - if (description.getTestClass().getAnnotation(IncludeUnderRavenwood.class) != null) { + if (description.getTestClass().getAnnotation(EnabledOnRavenwood.class) != null) { return true; } - if (description.getTestClass().getAnnotation(ExcludeUnderRavenwood.class) != null) { + if (description.getTestClass().getAnnotation(DisabledOnRavenwood.class) != null) { return false; } if (description.getTestClass().getAnnotation(IgnoreUnderRavenwood.class) != null) { @@ -189,7 +205,7 @@ public class RavenwoodRule implements TestRule { @Override public Statement apply(Statement base, Description description) { // No special treatment when running outside Ravenwood; run tests as-is - if (!IS_UNDER_RAVENWOOD) { + if (!IS_ON_RAVENWOOD) { return base; } @@ -207,7 +223,7 @@ public class RavenwoodRule implements TestRule { return new Statement() { @Override public void evaluate() throws Throwable { - Assume.assumeTrue(shouldIncludeUnderRavenwood(description)); + Assume.assumeTrue(shouldEnableOnRavenwood(description)); RavenwoodRuleImpl.init(RavenwoodRule.this); try { @@ -221,7 +237,7 @@ public class RavenwoodRule implements TestRule { /** * Run the given {@link Statement} with probing enabled. All tests will be unconditionally - * run under Ravenwood to detect cases where a test is able to pass despite being marked as + * run on Ravenwood to detect cases where a test is able to pass despite being marked as * {@code IgnoreUnderRavenwood}. */ private Statement applyProbeIgnored(Statement base, Description description) { @@ -234,13 +250,13 @@ public class RavenwoodRule implements TestRule { } catch (Throwable t) { // If the test isn't included, eat the exception and report the // assumption failure that test authors expect; otherwise throw - Assume.assumeTrue(shouldIncludeUnderRavenwood(description)); + Assume.assumeTrue(shouldEnableOnRavenwood(description)); throw t; } finally { RavenwoodRuleImpl.reset(RavenwoodRule.this); } - if (!shouldIncludeUnderRavenwood(description)) { + if (!shouldEnableOnRavenwood(description)) { fail("Test wasn't included under Ravenwood, but it actually " + "passed under Ravenwood; consider updating annotations"); } 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 0ff6a1ad846b..d0c2e18be8df 100644 --- a/ravenwood/junit-stub-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java +++ b/ravenwood/junit-stub-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java @@ -17,7 +17,7 @@ package android.platform.test.ravenwood; public class RavenwoodRuleImpl { - public static boolean isUnderRavenwood() { + public static boolean isOnRavenwood() { return false; } diff --git a/ravenwood/ravenwood-annotation-allowed-classes.txt b/ravenwood/ravenwood-annotation-allowed-classes.txt index 5700f000cbc5..eaf01a32592e 100644 --- a/ravenwood/ravenwood-annotation-allowed-classes.txt +++ b/ravenwood/ravenwood-annotation-allowed-classes.txt @@ -1,5 +1,6 @@ # Only classes listed here can use the Ravenwood annotations. +com.android.internal.display.BrightnessSynchronizer com.android.internal.util.ArrayUtils com.android.internal.logging.MetricsLogger com.android.internal.logging.testing.FakeMetricsLogger @@ -31,6 +32,7 @@ android.util.LruCache android.util.MonthDisplayHelper android.util.RecurrenceRule android.util.RotationUtils +android.util.Singleton android.util.Slog android.util.SparseDoubleArray android.util.SparseSetArray @@ -76,6 +78,7 @@ android.os.Trace android.os.UidBatteryConsumer android.os.UidBatteryConsumer$Builder android.os.UserHandle +android.os.UserManager android.os.WorkSource android.content.ClipData @@ -89,16 +92,22 @@ android.content.Intent android.content.IntentFilter android.content.UriMatcher -android.content.pm.PackageInfo +android.content.pm.ActivityInfo android.content.pm.ApplicationInfo -android.content.pm.PackageItemInfo android.content.pm.ComponentInfo -android.content.pm.ActivityInfo -android.content.pm.ServiceInfo +android.content.pm.PackageInfo +android.content.pm.PackageItemInfo +android.content.pm.PackageManager$Flags +android.content.pm.PackageManager$PackageInfoFlags +android.content.pm.PackageManager$ApplicationInfoFlags +android.content.pm.PackageManager$ComponentInfoFlags +android.content.pm.PackageManager$ResolveInfoFlags android.content.pm.PathPermission android.content.pm.ProviderInfo android.content.pm.ResolveInfo +android.content.pm.ServiceInfo android.content.pm.Signature +android.content.pm.UserInfo android.database.AbstractCursor android.database.CharArrayBuffer @@ -134,6 +143,10 @@ android.content.ContentProvider android.metrics.LogMaker +android.view.Display$HdrCapabilities +android.view.Display$Mode +android.view.DisplayInfo + com.android.server.LocalServices com.android.server.power.stats.BatteryStatsImpl diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java index d656892062d1..57c05396992e 100644 --- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java +++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java @@ -30,10 +30,7 @@ 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; @@ -142,6 +139,7 @@ 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; @@ -1721,7 +1719,7 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub } mMainHandler.sendMessage(obtainMessage( AccessibilityManagerService::performAccessibilityShortcutInternal, this, - displayId, ACCESSIBILITY_BUTTON, targetName)); + displayId, ShortcutConstants.UserShortcutType.SOFTWARE, targetName)); } /** @@ -2200,11 +2198,12 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub } private void showAccessibilityTargetsSelection(int displayId, - @ShortcutType int shortcutType) { + @ShortcutConstants.UserShortcutType int shortcutType) { final Intent intent = new Intent(AccessibilityManager.ACTION_CHOOSE_ACCESSIBILITY_BUTTON); - final String chooserClassName = (shortcutType == ACCESSIBILITY_SHORTCUT_KEY) - ? AccessibilityShortcutChooserActivity.class.getName() - : AccessibilityButtonChooserActivity.class.getName(); + final String chooserClassName = + (shortcutType == ShortcutConstants.UserShortcutType.HARDWARE) + ? 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 +3274,7 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub } final Set<String> currentTargets = - userState.getShortcutTargetsLocked(ACCESSIBILITY_SHORTCUT_KEY); + userState.getShortcutTargetsLocked(ShortcutConstants.UserShortcutType.HARDWARE); if (targetsFromSetting.equals(currentTargets)) { return false; } @@ -3291,7 +3290,7 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub userState.mUserId, str -> str, targetsFromSetting); final Set<String> currentTargets = - userState.getShortcutTargetsLocked(ACCESSIBILITY_BUTTON); + userState.getShortcutTargetsLocked(ShortcutConstants.UserShortcutType.SOFTWARE); if (targetsFromSetting.equals(currentTargets)) { return false; } @@ -3346,7 +3345,7 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub */ private void updateAccessibilityShortcutKeyTargetsLocked(AccessibilityUserState userState) { final Set<String> currentTargets = - userState.getShortcutTargetsLocked(ACCESSIBILITY_SHORTCUT_KEY); + userState.getShortcutTargetsLocked(ShortcutConstants.UserShortcutType.HARDWARE); final int lastSize = currentTargets.size(); if (lastSize == 0) { return; @@ -3531,7 +3530,7 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub } final Set<String> currentTargets = - userState.getShortcutTargetsLocked(ACCESSIBILITY_BUTTON); + userState.getShortcutTargetsLocked(ShortcutConstants.UserShortcutType.SOFTWARE); final int lastSize = currentTargets.size(); if (lastSize == 0) { return; @@ -3571,7 +3570,7 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub return; } final Set<String> buttonTargets = - userState.getShortcutTargetsLocked(ACCESSIBILITY_BUTTON); + userState.getShortcutTargetsLocked(ShortcutConstants.UserShortcutType.SOFTWARE); int lastSize = buttonTargets.size(); buttonTargets.removeIf(name -> { if (packageName != null && name != null && !name.contains(packageName)) { @@ -3608,7 +3607,7 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub lastSize = buttonTargets.size(); final Set<String> shortcutKeyTargets = - userState.getShortcutTargetsLocked(ACCESSIBILITY_SHORTCUT_KEY); + userState.getShortcutTargetsLocked(ShortcutConstants.UserShortcutType.HARDWARE); userState.mEnabledServices.forEach(componentName -> { if (packageName != null && componentName != null && !packageName.equals(componentName.getPackageName())) { @@ -3665,16 +3664,18 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub return; } final ComponentName serviceName = service.getComponentName(); - if (userState.removeShortcutTargetLocked(ACCESSIBILITY_SHORTCUT_KEY, serviceName)) { + if (userState.removeShortcutTargetLocked( + ShortcutConstants.UserShortcutType.HARDWARE, serviceName)) { final Set<String> currentTargets = userState.getShortcutTargetsLocked( - ACCESSIBILITY_SHORTCUT_KEY); + ShortcutConstants.UserShortcutType.HARDWARE); persistColonDelimitedSetToSettingLocked( Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE, userState.mUserId, currentTargets, str -> str); } - if (userState.removeShortcutTargetLocked(ACCESSIBILITY_BUTTON, serviceName)) { + if (userState.removeShortcutTargetLocked( + ShortcutConstants.UserShortcutType.SOFTWARE, serviceName)) { final Set<String> currentTargets = userState.getShortcutTargetsLocked( - ACCESSIBILITY_BUTTON); + ShortcutConstants.UserShortcutType.SOFTWARE); persistColonDelimitedSetToSettingLocked(Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS, userState.mUserId, currentTargets, str -> str); } @@ -3750,7 +3751,7 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub } mMainHandler.sendMessage(obtainMessage( AccessibilityManagerService::performAccessibilityShortcutInternal, this, - Display.DEFAULT_DISPLAY, ACCESSIBILITY_SHORTCUT_KEY, targetName)); + Display.DEFAULT_DISPLAY, ShortcutConstants.UserShortcutType.HARDWARE, targetName)); } /** @@ -3763,7 +3764,7 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub * specified target. */ private void performAccessibilityShortcutInternal(int displayId, - @ShortcutType int shortcutType, @Nullable String targetName) { + @ShortcutConstants.UserShortcutType 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); @@ -3814,7 +3815,7 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub } private boolean performAccessibilityFrameworkFeature(int displayId, - ComponentName assignedTarget, @ShortcutType int shortcutType) { + ComponentName assignedTarget, @ShortcutConstants.UserShortcutType int shortcutType) { final Map<ComponentName, FrameworkFeatureInfo> frameworkFeatureMap = AccessibilityShortcutController.getFrameworkShortcutFeaturesMap(); if (!frameworkFeatureMap.containsKey(assignedTarget)) { @@ -3863,12 +3864,12 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub /** * Perform accessibility service shortcut action. * - * 1) For {@link AccessibilityManager#ACCESSIBILITY_BUTTON} type and services targeting sdk + * 1) For {@link ShortcutConstants.UserShortcutType.SOFTWARE} type and services targeting sdk * version <= Q: callbacks to accessibility service if service is bounded and requests * accessibility button. - * 2) For {@link AccessibilityManager#ACCESSIBILITY_SHORTCUT_KEY} type and service targeting sdk + * 2) For {@link ShortcutConstants.UserShortcutType.HARDWARE} type and service targeting sdk * version <= Q: turns on / off the accessibility service. - * 3) For {@link AccessibilityManager#ACCESSIBILITY_SHORTCUT_KEY} type and service targeting sdk + * 3) For {@link ShortcutConstants.UserShortcutType.HARDWARE} 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.) @@ -3879,7 +3880,7 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub * button. */ private boolean performAccessibilityShortcutTargetService(int displayId, - @ShortcutType int shortcutType, ComponentName assignedTarget) { + @ShortcutConstants.UserShortcutType int shortcutType, ComponentName assignedTarget) { synchronized (mLock) { final AccessibilityUserState userState = getCurrentUserStateLocked(); final AccessibilityServiceInfo installedServiceInfo = @@ -3897,7 +3898,8 @@ 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 == ACCESSIBILITY_SHORTCUT_KEY) + if ((targetSdk <= Build.VERSION_CODES.Q + && shortcutType == ShortcutConstants.UserShortcutType.HARDWARE) || (targetSdk > Build.VERSION_CODES.Q && !requestA11yButton)) { if (serviceConnection == null) { logAccessibilityShortcutActivated(mContext, assignedTarget, shortcutType, @@ -3911,7 +3913,8 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub } return true; } - if (shortcutType == ACCESSIBILITY_SHORTCUT_KEY && targetSdk > Build.VERSION_CODES.Q + if (shortcutType == ShortcutConstants.UserShortcutType.HARDWARE + && targetSdk > Build.VERSION_CODES.Q && requestA11yButton) { if (!userState.getEnabledServicesLocked().contains(assignedTarget)) { enableAccessibilityServiceLocked(assignedTarget, mCurrentUserId); @@ -3941,7 +3944,8 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub } @Override - public List<String> getAccessibilityShortcutTargets(@ShortcutType int shortcutType) { + public List<String> getAccessibilityShortcutTargets( + @ShortcutConstants.UserShortcutType int shortcutType) { if (mTraceManager.isA11yTracingEnabledForTypes(FLAGS_ACCESSIBILITY_MANAGER)) { mTraceManager.logTrace(LOG_TAG + ".getAccessibilityShortcutTargets", FLAGS_ACCESSIBILITY_MANAGER, "shortcutType=" + shortcutType); @@ -3955,12 +3959,13 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub return getAccessibilityShortcutTargetsInternal(shortcutType); } - private List<String> getAccessibilityShortcutTargetsInternal(@ShortcutType int shortcutType) { + private List<String> getAccessibilityShortcutTargetsInternal( + @ShortcutConstants.UserShortcutType int shortcutType) { synchronized (mLock) { final AccessibilityUserState userState = getCurrentUserStateLocked(); final ArrayList<String> shortcutTargets = new ArrayList<>( userState.getShortcutTargetsLocked(shortcutType)); - if (shortcutType != ACCESSIBILITY_BUTTON) { + if (shortcutType != ShortcutConstants.UserShortcutType.SOFTWARE) { return shortcutTargets; } // Adds legacy a11y services requesting a11y button into the list. @@ -4423,7 +4428,7 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub } } // Warning is not required if the service is already assigned to a shortcut. - for (int shortcutType : AccessibilityManager.SHORTCUT_TYPES) { + for (int shortcutType : ShortcutConstants.USER_SHORTCUT_TYPES) { if (getAccessibilityShortcutTargets(shortcutType).contains( componentName.flattenToString())) { return false; @@ -4603,8 +4608,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 { diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityUserState.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityUserState.java index 68ee78076f3d..41165b6aed5a 100644 --- a/services/accessibility/java/com/android/server/accessibility/AccessibilityUserState.java +++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityUserState.java @@ -24,9 +24,6 @@ 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; @@ -51,6 +48,7 @@ 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; @@ -757,13 +755,21 @@ class AccessibilityUserState { * @param shortcutType The shortcut type. * @return The array set of the strings */ - public ArraySet<String> getShortcutTargetsLocked(@ShortcutType int shortcutType) { - if (shortcutType == ACCESSIBILITY_SHORTCUT_KEY) { + public ArraySet<String> getShortcutTargetsLocked( + @ShortcutConstants.UserShortcutType int shortcutType) { + if (shortcutType == ShortcutConstants.UserShortcutType.HARDWARE) { return mAccessibilityShortcutKeyTargets; - } else if (shortcutType == ACCESSIBILITY_BUTTON) { + } else if (shortcutType == ShortcutConstants.UserShortcutType.SOFTWARE) { 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 null; + return new ArraySet<>(); } /** @@ -802,12 +808,22 @@ class AccessibilityUserState { /** * Removes given shortcut target in the list. * - * @param shortcutType The shortcut type. - * @param target The component name of the shortcut target. + * @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. * @return true if the shortcut target is removed. */ - public boolean removeShortcutTargetLocked(@ShortcutType int shortcutType, + public boolean removeShortcutTargetLocked(@ShortcutConstants.UserShortcutType 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/java/com/android/server/autofill/RemoteFillService.java b/services/autofill/java/com/android/server/autofill/RemoteFillService.java index 5c93991bef8c..f914ed54fbb1 100644 --- a/services/autofill/java/com/android/server/autofill/RemoteFillService.java +++ b/services/autofill/java/com/android/server/autofill/RemoteFillService.java @@ -31,9 +31,12 @@ import android.os.Handler; import android.os.ICancellationSignal; import android.os.RemoteException; import android.service.autofill.AutofillService; +import android.service.autofill.ConvertCredentialRequest; +import android.service.autofill.ConvertCredentialResponse; import android.service.autofill.FillRequest; import android.service.autofill.FillResponse; import android.service.autofill.IAutoFillService; +import android.service.autofill.IConvertCredentialCallback; import android.service.autofill.IFillCallback; import android.service.autofill.ISaveCallback; import android.service.autofill.SaveRequest; @@ -69,6 +72,7 @@ final class RemoteFillService extends ServiceConnector.Impl<IAutoFillService> { private int mPendingFillRequestId = INVALID_REQUEST_ID; private AtomicReference<IFillCallback> mFillCallback; private AtomicReference<ISaveCallback> mSaveCallback; + private AtomicReference<IConvertCredentialCallback> mConvertCredentialCallback; private final ComponentName mComponentName; private final boolean mIsCredentialAutofillService; @@ -81,13 +85,20 @@ final class RemoteFillService extends ServiceConnector.Impl<IAutoFillService> { extends AbstractRemoteService.VultureCallback<RemoteFillService> { void onFillRequestSuccess(int requestId, @Nullable FillResponse response, @NonNull String servicePackageName, int requestFlags); + void onFillRequestFailure(int requestId, @Nullable CharSequence message); + void onFillRequestTimeout(int requestId); + void onSaveRequestSuccess(@NonNull String servicePackageName, @Nullable IntentSender intentSender); + // TODO(b/80093094): add timeout here too? void onSaveRequestFailure(@Nullable CharSequence message, @NonNull String servicePackageName); + + void onConvertCredentialRequestSuccess(@NonNull ConvertCredentialResponse + convertCredentialResponse); } RemoteFillService(Context context, ComponentName componentName, int userId, @@ -211,6 +222,32 @@ final class RemoteFillService extends ServiceConnector.Impl<IAutoFillService> { } } + static class IConvertCredentialCallbackDelegate extends IConvertCredentialCallback.Stub { + + private WeakReference<IConvertCredentialCallback> mCallbackWeakRef; + + IConvertCredentialCallbackDelegate(IConvertCredentialCallback callback) { + mCallbackWeakRef = new WeakReference(callback); + } + + @Override + public void onSuccess(ConvertCredentialResponse convertCredentialResponse) + throws RemoteException { + IConvertCredentialCallback callback = mCallbackWeakRef.get(); + if (callback != null) { + callback.onSuccess(convertCredentialResponse); + } + } + + @Override + public void onFailure(CharSequence message) throws RemoteException { + IConvertCredentialCallback callback = mCallbackWeakRef.get(); + if (callback != null) { + callback.onFailure(message); + } + } + } + /** * Wraps an {@link IFillCallback} object using weak reference. * @@ -237,6 +274,18 @@ final class RemoteFillService extends ServiceConnector.Impl<IAutoFillService> { return callback; } + /** + * Wraps an {@link IConvertCredentialCallback} object using weak reference + */ + private IConvertCredentialCallback maybeWrapWithWeakReference( + IConvertCredentialCallback callback) { + if (remoteFillServiceUseWeakReference()) { + mConvertCredentialCallback = new AtomicReference<>(callback); + return new IConvertCredentialCallbackDelegate(callback); + } + return callback; + } + public void onFillCredentialRequest(@NonNull FillRequest request, IAutoFillManagerClient autofillCallback) { if (sVerbose) { @@ -378,6 +427,52 @@ final class RemoteFillService extends ServiceConnector.Impl<IAutoFillService> { })); } + public void onConvertCredentialRequest( + @NonNull ConvertCredentialRequest convertCredentialRequest) { + if (sVerbose) Slog.v(TAG, "calling onConvertCredentialRequest()"); + CompletableFuture<ConvertCredentialResponse> + connectThenConvertCredentialRequest = postAsync( + remoteService -> { + if (sVerbose) { + Slog.v(TAG, "calling onConvertCredentialRequest()"); + } + CompletableFuture<ConvertCredentialResponse> + convertCredentialCompletableFuture = new CompletableFuture<>(); + remoteService.onConvertCredentialRequest(convertCredentialRequest, + maybeWrapWithWeakReference( + new IConvertCredentialCallback.Stub() { + @Override + public void onSuccess(ConvertCredentialResponse + convertCredentialResponse) { + convertCredentialCompletableFuture + .complete(convertCredentialResponse); + } + + @Override + public void onFailure(CharSequence message) { + String errorMessage = + message == null ? "" : + String.valueOf(message); + convertCredentialCompletableFuture + .completeExceptionally( + new RuntimeException(errorMessage)); + } + }) + ); + return convertCredentialCompletableFuture; + }).orTimeout(TIMEOUT_REMOTE_REQUEST_MILLIS, TimeUnit.MILLISECONDS); + + connectThenConvertCredentialRequest.whenComplete( + (res, err) -> Handler.getMain().post(() -> { + if (err == null) { + mCallbacks.onConvertCredentialRequestSuccess(res); + } else { + // TODO: Add a callback function to log this failure + Slog.e(TAG, "Error calling on convert credential request", err); + } + })); + } + public void onSaveRequest(@NonNull SaveRequest request) { postAsync(service -> { if (sVerbose) Slog.v(TAG, "calling onSaveRequest()"); diff --git a/services/autofill/java/com/android/server/autofill/SecondaryProviderHandler.java b/services/autofill/java/com/android/server/autofill/SecondaryProviderHandler.java index 123470304783..0af703e415ff 100644 --- a/services/autofill/java/com/android/server/autofill/SecondaryProviderHandler.java +++ b/services/autofill/java/com/android/server/autofill/SecondaryProviderHandler.java @@ -25,6 +25,7 @@ import android.content.ComponentName; import android.content.Context; import android.content.IntentSender; import android.os.Bundle; +import android.service.autofill.ConvertCredentialResponse; import android.service.autofill.FillRequest; import android.service.autofill.FillResponse; import android.util.Slog; @@ -98,6 +99,12 @@ final class SecondaryProviderHandler implements RemoteFillService.FillServiceCal } + @Override + public void onConvertCredentialRequestSuccess(@NonNull ConvertCredentialResponse + convertCredentialResponse) { + + } + /** * Requests a new fill response. */ diff --git a/services/autofill/java/com/android/server/autofill/Session.java b/services/autofill/java/com/android/server/autofill/Session.java index f3b74ea00a58..049feeed2fa1 100644 --- a/services/autofill/java/com/android/server/autofill/Session.java +++ b/services/autofill/java/com/android/server/autofill/Session.java @@ -24,6 +24,7 @@ import static android.service.autofill.Dataset.PICK_REASON_PCC_DETECTION_PREFERR import static android.service.autofill.Dataset.PICK_REASON_PROVIDER_DETECTION_ONLY; import static android.service.autofill.Dataset.PICK_REASON_PROVIDER_DETECTION_PREFERRED_WITH_PCC; import static android.service.autofill.Dataset.PICK_REASON_UNKNOWN; +import static android.service.autofill.FillEventHistory.Event.UI_TYPE_CREDMAN_BOTTOM_SHEET; import static android.service.autofill.FillEventHistory.Event.UI_TYPE_DIALOG; import static android.service.autofill.FillEventHistory.Event.UI_TYPE_INLINE; import static android.service.autofill.FillEventHistory.Event.UI_TYPE_MENU; @@ -44,6 +45,7 @@ import static android.view.autofill.AutofillManager.ACTION_VIEW_ENTERED; import static android.view.autofill.AutofillManager.ACTION_VIEW_EXITED; import static android.view.autofill.AutofillManager.COMMIT_REASON_SESSION_DESTROYED; import static android.view.autofill.AutofillManager.COMMIT_REASON_UNKNOWN; +import static android.view.autofill.AutofillManager.EXTRA_AUTOFILL_REQUEST_ID; import static android.view.autofill.AutofillManager.FLAG_SMART_SUGGESTION_SYSTEM; import static android.view.autofill.AutofillManager.getSmartSuggestionModeToString; @@ -130,6 +132,7 @@ import android.service.assist.classification.FieldClassificationResponse; import android.service.autofill.AutofillFieldClassificationService.Scores; import android.service.autofill.AutofillService; import android.service.autofill.CompositeUserData; +import android.service.autofill.ConvertCredentialResponse; import android.service.autofill.Dataset; import android.service.autofill.Dataset.DatasetEligibleReason; import android.service.autofill.Field; @@ -2429,6 +2432,29 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState removeFromService(); } + // FillServiceCallbacks + @Override + public void onConvertCredentialRequestSuccess(@NonNull ConvertCredentialResponse + convertCredentialResponse) { + Dataset dataset = convertCredentialResponse.getDataset(); + Bundle clientState = convertCredentialResponse.getClientState(); + if (dataset != null) { + int requestId = -1; + if (clientState != null) { + requestId = clientState.getInt(EXTRA_AUTOFILL_REQUEST_ID); + } else { + Slog.e(TAG, "onConvertCredentialRequestSuccess(): client state is null, this " + + "would cause loss in logging."); + } + // TODO: Add autofill related logging; consider whether to log the index + fill(requestId, /* datasetIndex=*/ -1, dataset, UI_TYPE_CREDMAN_BOTTOM_SHEET); + } else { + // TODO: Add logging to log this error case + Slog.e(TAG, "onConvertCredentialRequestSuccess(): dataset inside response is " + + "null"); + } + } + /** * Gets the {@link FillContext} for a request. * diff --git a/services/backup/flags.aconfig b/services/backup/flags.aconfig index 4022e3378954..1416c888f790 100644 --- a/services/backup/flags.aconfig +++ b/services/backup/flags.aconfig @@ -11,7 +11,7 @@ flag { flag { name: "enable_metrics_system_backup_agents" - namespace: "backup" + namespace: "onboarding" description: "Enable SystemBackupAgent to collect B&R agent metrics by passing an instance of " "the logger to each BackupHelper." bug: "296844513" diff --git a/services/companion/TEST_MAPPING b/services/companion/TEST_MAPPING index 37c47baa813b..ae6d59129adb 100644 --- a/services/companion/TEST_MAPPING +++ b/services/companion/TEST_MAPPING @@ -9,5 +9,10 @@ { "name": "CtsCompanionDeviceManagerNoCompanionServicesTestCases" } + ], + "postsubmit": [ + { + "name": "CtsCompanionDeviceManagerMultiProcessTestCases" + } ] } diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java index 2e01ced2022b..5019428c5323 100644 --- a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java +++ b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java @@ -368,8 +368,10 @@ public class CompanionDeviceManagerService extends SystemService { if (blueToothDevices != null) { for (BluetoothDevice bluetoothDevice : blueToothDevices) { - final List<ParcelUuid> deviceUuids = bluetoothDevice.getUuids() == null - ? Collections.emptyList() : Arrays.asList(bluetoothDevice.getUuids()); + final ParcelUuid[] bluetoothDeviceUuids = bluetoothDevice.getUuids(); + + final List<ParcelUuid> deviceUuids = ArrayUtils.isEmpty(bluetoothDeviceUuids) + ? Collections.emptyList() : Arrays.asList(bluetoothDeviceUuids); for (AssociationInfo ai: mAssociationStore.getAssociationsByAddress(bluetoothDevice.getAddress())) { diff --git a/services/companion/java/com/android/server/companion/InactiveAssociationsRemovalService.java b/services/companion/java/com/android/server/companion/InactiveAssociationsRemovalService.java index 3482863032f9..aac628cab403 100644 --- a/services/companion/java/com/android/server/companion/InactiveAssociationsRemovalService.java +++ b/services/companion/java/com/android/server/companion/InactiveAssociationsRemovalService.java @@ -36,7 +36,8 @@ import com.android.server.LocalServices; * will be killed if association/role are revoked. */ public class InactiveAssociationsRemovalService extends JobService { - private static final int JOB_ID = InactiveAssociationsRemovalService.class.hashCode(); + private static final String JOB_NAMESPACE = "companion"; + private static final int JOB_ID = 1; private static final long ONE_DAY_INTERVAL = DAYS.toMillis(1); @Override @@ -61,7 +62,8 @@ public class InactiveAssociationsRemovalService extends JobService { static void schedule(Context context) { Slog.i(TAG, "Scheduling the Association Removal job"); - final JobScheduler jobScheduler = context.getSystemService(JobScheduler.class); + final JobScheduler jobScheduler = + context.getSystemService(JobScheduler.class).forNamespace(JOB_NAMESPACE); final JobInfo job = new JobInfo.Builder(JOB_ID, new ComponentName(context, InactiveAssociationsRemovalService.class)) .setRequiresCharging(true) @@ -71,4 +73,3 @@ public class InactiveAssociationsRemovalService extends JobService { jobScheduler.schedule(job); } } - diff --git a/services/companion/java/com/android/server/companion/presence/BluetoothCompanionDeviceConnectionListener.java b/services/companion/java/com/android/server/companion/presence/BluetoothCompanionDeviceConnectionListener.java index 7eca1193ca12..c514f3ef29d0 100644 --- a/services/companion/java/com/android/server/companion/presence/BluetoothCompanionDeviceConnectionListener.java +++ b/services/companion/java/com/android/server/companion/presence/BluetoothCompanionDeviceConnectionListener.java @@ -38,6 +38,7 @@ import android.util.Slog; import android.util.SparseArray; import com.android.internal.annotations.GuardedBy; +import com.android.internal.util.ArrayUtils; import com.android.server.companion.AssociationStore; import com.android.server.companion.ObservableUuid; import com.android.server.companion.ObservableUuidStore; @@ -172,8 +173,10 @@ public class BluetoothCompanionDeviceConnectionListener mAssociationStore.getAssociationsByAddress(device.getAddress()); final List<ObservableUuid> observableUuids = mObservableUuidStore.getObservableUuidsForUser(userId); - final List<ParcelUuid> deviceUuids = device.getUuids() == null - ? Collections.emptyList() : Arrays.asList(device.getUuids()); + final ParcelUuid[] bluetoothDeviceUuids = device.getUuids(); + + final List<ParcelUuid> deviceUuids = ArrayUtils.isEmpty(bluetoothDeviceUuids) + ? Collections.emptyList() : Arrays.asList(bluetoothDeviceUuids); if (DEBUG) { Log.d(TAG, "onDevice_ConnectivityChanged() " + btDeviceToString(device) diff --git a/services/companion/java/com/android/server/companion/virtual/InputController.java b/services/companion/java/com/android/server/companion/virtual/InputController.java index 3b9d92dc3d02..8962bf02ff2e 100644 --- a/services/companion/java/com/android/server/companion/virtual/InputController.java +++ b/services/companion/java/com/android/server/companion/virtual/InputController.java @@ -163,7 +163,7 @@ class InputController { createDeviceInternal(InputDeviceDescriptor.TYPE_MOUSE, deviceName, vendorId, productId, deviceToken, displayId, phys, () -> mNativeWrapper.openUinputMouse(deviceName, vendorId, productId, phys)); - mInputManagerInternal.setVirtualMousePointerDisplayId(displayId); + setVirtualMousePointerDisplayId(displayId); } void createTouchscreen(@NonNull String deviceName, int vendorId, int productId, @@ -235,8 +235,7 @@ class InputController { // id if there's another mouse (choose the most recent). The inputDeviceDescriptor must be // removed from the mInputDeviceDescriptors instance variable prior to this point. if (inputDeviceDescriptor.isMouse()) { - if (mInputManagerInternal.getVirtualMousePointerDisplayId() - == inputDeviceDescriptor.getDisplayId()) { + if (getVirtualMousePointerDisplayId() == inputDeviceDescriptor.getDisplayId()) { updateActivePointerDisplayIdLocked(); } } @@ -271,6 +270,7 @@ class InputController { mWindowManager.setDisplayImePolicy(displayId, policy); } + // TODO(b/293587049): Remove after pointer icon refactor is complete. @GuardedBy("mLock") private void updateActivePointerDisplayIdLocked() { InputDeviceDescriptor mostRecentlyCreatedMouse = null; @@ -285,11 +285,11 @@ class InputController { } } if (mostRecentlyCreatedMouse != null) { - mInputManagerInternal.setVirtualMousePointerDisplayId( + setVirtualMousePointerDisplayId( mostRecentlyCreatedMouse.getDisplayId()); } else { // All mice have been unregistered - mInputManagerInternal.setVirtualMousePointerDisplayId(Display.INVALID_DISPLAY); + setVirtualMousePointerDisplayId(Display.INVALID_DISPLAY); } } @@ -349,10 +349,8 @@ class InputController { if (inputDeviceDescriptor == null) { return false; } - if (inputDeviceDescriptor.getDisplayId() - != mInputManagerInternal.getVirtualMousePointerDisplayId()) { - mInputManagerInternal.setVirtualMousePointerDisplayId( - inputDeviceDescriptor.getDisplayId()); + if (inputDeviceDescriptor.getDisplayId() != getVirtualMousePointerDisplayId()) { + setVirtualMousePointerDisplayId(inputDeviceDescriptor.getDisplayId()); } return mNativeWrapper.writeButtonEvent(inputDeviceDescriptor.getNativePointer(), event.getButtonCode(), event.getAction(), event.getEventTimeNanos()); @@ -380,10 +378,8 @@ class InputController { if (inputDeviceDescriptor == null) { return false; } - if (inputDeviceDescriptor.getDisplayId() - != mInputManagerInternal.getVirtualMousePointerDisplayId()) { - mInputManagerInternal.setVirtualMousePointerDisplayId( - inputDeviceDescriptor.getDisplayId()); + if (inputDeviceDescriptor.getDisplayId() != getVirtualMousePointerDisplayId()) { + setVirtualMousePointerDisplayId(inputDeviceDescriptor.getDisplayId()); } return mNativeWrapper.writeRelativeEvent(inputDeviceDescriptor.getNativePointer(), event.getRelativeX(), event.getRelativeY(), event.getEventTimeNanos()); @@ -397,10 +393,8 @@ class InputController { if (inputDeviceDescriptor == null) { return false; } - if (inputDeviceDescriptor.getDisplayId() - != mInputManagerInternal.getVirtualMousePointerDisplayId()) { - mInputManagerInternal.setVirtualMousePointerDisplayId( - inputDeviceDescriptor.getDisplayId()); + if (inputDeviceDescriptor.getDisplayId() != getVirtualMousePointerDisplayId()) { + setVirtualMousePointerDisplayId(inputDeviceDescriptor.getDisplayId()); } return mNativeWrapper.writeScrollEvent(inputDeviceDescriptor.getNativePointer(), event.getXAxisMovement(), event.getYAxisMovement(), event.getEventTimeNanos()); @@ -415,12 +409,11 @@ class InputController { throw new IllegalArgumentException( "Could not get cursor position for input device for given token"); } - if (inputDeviceDescriptor.getDisplayId() - != mInputManagerInternal.getVirtualMousePointerDisplayId()) { - mInputManagerInternal.setVirtualMousePointerDisplayId( - inputDeviceDescriptor.getDisplayId()); + if (inputDeviceDescriptor.getDisplayId() != getVirtualMousePointerDisplayId()) { + setVirtualMousePointerDisplayId(inputDeviceDescriptor.getDisplayId()); } - return LocalServices.getService(InputManagerInternal.class).getCursorPosition(); + return LocalServices.getService(InputManagerInternal.class).getCursorPosition( + inputDeviceDescriptor.getDisplayId()); } } @@ -847,4 +840,22 @@ class InputController { /** Returns true if the calling thread is a valid thread for device creation. */ boolean isValidThread(); } + + // TODO(b/293587049): Remove after pointer icon refactor is complete. + private void setVirtualMousePointerDisplayId(int displayId) { + if (com.android.input.flags.Flags.enablePointerChoreographer()) { + // We no longer need to set the pointer display when pointer choreographer is enabled. + return; + } + mInputManagerInternal.setVirtualMousePointerDisplayId(displayId); + } + + // TODO(b/293587049): Remove after pointer icon refactor is complete. + private int getVirtualMousePointerDisplayId() { + if (com.android.input.flags.Flags.enablePointerChoreographer()) { + // We no longer need to get the pointer display when pointer choreographer is enabled. + return Display.INVALID_DISPLAY; + } + return mInputManagerInternal.getVirtualMousePointerDisplayId(); + } } diff --git a/services/core/Android.bp b/services/core/Android.bp index a54a48a7e84e..8e35b7455d02 100644 --- a/services/core/Android.bp +++ b/services/core/Android.bp @@ -139,6 +139,7 @@ java_library_static { libs: [ "services.net", + "android.frameworks.location.altitude-V2-java", "android.hardware.common-V2-java", "android.hardware.light-V2.0-java", "android.hardware.gnss-V2-java", @@ -160,7 +161,6 @@ java_library_static { ], static_libs: [ - "android.frameworks.location.altitude-V2-java", // AIDL "android.frameworks.vibrator-V1-java", // AIDL "android.hardware.authsecret-V1.0-java", "android.hardware.authsecret-V1-java", diff --git a/services/core/java/com/android/server/am/ActiveServices.java b/services/core/java/com/android/server/am/ActiveServices.java index afb8345249b1..adc0255743c2 100644 --- a/services/core/java/com/android/server/am/ActiveServices.java +++ b/services/core/java/com/android/server/am/ActiveServices.java @@ -181,7 +181,6 @@ import android.content.pm.ParceledListSlice; import android.content.pm.ResolveInfo; import android.content.pm.ServiceInfo; import android.content.pm.ServiceInfo.ForegroundServiceType; -import android.os.Binder; import android.os.Build; import android.os.Build.VERSION_CODES; import android.os.Bundle; @@ -306,6 +305,57 @@ public final class ActiveServices { @interface FgsStopReason {} /** + * The policy to be applied to the service bindings; this one means it follows the legacy + * behavior. + */ + static final int SERVICE_BIND_OOMADJ_POLICY_LEGACY = 0; + + /** + * The policy to be applied to the service bindings; this one means we'll skip + * updating the target process's oom adj score / process state for its {@link Service#onCreate}. + */ + static final int SERVICE_BIND_OOMADJ_POLICY_SKIP_OOM_UPDATE_ON_CREATE = 1; + + /** + * The policy to be applied to the service bindings; this one means we'll skip + * updating the target process's oom adj score / process state for its {@link Service#onBind}. + */ + static final int SERVICE_BIND_OOMADJ_POLICY_SKIP_OOM_UPDATE_ON_BIND = 1 << 1; + + /** + * The policy to be applied to the service bindings; this one means we'll skip + * updating the target process's oom adj score / process state on setting up the service + * connection between the client and the service host process. + */ + static final int SERVICE_BIND_OOMADJ_POLICY_SKIP_OOM_UPDATE_ON_CONNECT = 1 << 2; + /** + * The policy to be applied to the service bindings; this one means the caller + * will be frozen upon calling the bindService APIs. + */ + static final int SERVICE_BIND_OOMADJ_POLICY_FREEZE_CALLER = 1 << 3; + + @IntDef(flag = true, prefix = { "SERVICE_BIND_OOMADJ_POLICY_" }, value = { + SERVICE_BIND_OOMADJ_POLICY_LEGACY, + SERVICE_BIND_OOMADJ_POLICY_SKIP_OOM_UPDATE_ON_CREATE, + SERVICE_BIND_OOMADJ_POLICY_SKIP_OOM_UPDATE_ON_BIND, + SERVICE_BIND_OOMADJ_POLICY_SKIP_OOM_UPDATE_ON_CONNECT, + SERVICE_BIND_OOMADJ_POLICY_FREEZE_CALLER, + }) + @Retention(RetentionPolicy.SOURCE) + @interface ServiceBindingOomAdjPolicy {} + + @ServiceBindingOomAdjPolicy + static final int DEFAULT_SERVICE_NO_BUMP_BIND_POLICY_FLAG = + SERVICE_BIND_OOMADJ_POLICY_SKIP_OOM_UPDATE_ON_CREATE + | SERVICE_BIND_OOMADJ_POLICY_SKIP_OOM_UPDATE_ON_BIND + | SERVICE_BIND_OOMADJ_POLICY_SKIP_OOM_UPDATE_ON_CONNECT; + + @ServiceBindingOomAdjPolicy + static final int DEFAULT_SERVICE_CACHED_BIND_POLICY_FLAG = + SERVICE_BIND_OOMADJ_POLICY_SKIP_OOM_UPDATE_ON_CREATE + | SERVICE_BIND_OOMADJ_POLICY_SKIP_OOM_UPDATE_ON_BIND; + + /** * Disables foreground service background starts from BOOT_COMPLETED broadcasts for all types * except: * <ul> @@ -1244,7 +1294,7 @@ public final class ActiveServices { @Override public void onResult(Bundle result) { synchronized (mAm) { - final long identity = Binder.clearCallingIdentity(); + final long identity = mAm.mInjector.clearCallingIdentity(); try { if (!mPendingServices.contains(r)) { return; @@ -1263,7 +1313,8 @@ public final class ActiveServices { false /* whileRestarting */, false /* permissionsReviewRequired */, false /* packageFrozen */, - true /* enqueueOomAdj */); + true /* enqueueOomAdj */, + SERVICE_BIND_OOMADJ_POLICY_LEGACY); } catch (RemoteException e) { /* ignore - local call */ } finally { @@ -1275,7 +1326,7 @@ public final class ActiveServices { unbindServiceLocked(connection); } } finally { - Binder.restoreCallingIdentity(identity); + mAm.mInjector.restoreCallingIdentity(identity); } } } @@ -1353,7 +1404,8 @@ public final class ActiveServices { false /* whileRestarting */, false /* permissionsReviewRequired */, false /* packageFrozen */, - true /* enqueueOomAdj */); + true /* enqueueOomAdj */, + SERVICE_BIND_OOMADJ_POLICY_LEGACY); } catch (TransactionTooLargeException e) { /* ignore - local call */ } finally { @@ -1431,7 +1483,8 @@ public final class ActiveServices { false /* whileRestarting */, false /* permissionsReviewRequired */, false /* packageFrozen */, - true /* enqueueOomAdj */); + true /* enqueueOomAdj */, + SERVICE_BIND_OOMADJ_POLICY_LEGACY); /* Will be a no-op if nothing pending */ mAm.updateOomAdjPendingTargetsLocked(OOM_ADJ_REASON_START_SERVICE); if (error != null) { @@ -1550,22 +1603,22 @@ public final class ActiveServices { if (caller != null && callerApp == null) { throw new SecurityException( "Unable to find app for caller " + caller - + " (pid=" + Binder.getCallingPid() + + " (pid=" + mAm.mInjector.getCallingPid() + ") when stopping service " + service); } // If this service is active, make sure it is stopped. ServiceLookupResult r = retrieveServiceLocked(service, instanceName, isSdkSandboxService, sdkSandboxClientAppUid, sdkSandboxClientAppPackage, resolvedType, null, - Binder.getCallingPid(), Binder.getCallingUid(), userId, false, false, false, false, - null, false, false); + mAm.mInjector.getCallingPid(), mAm.mInjector.getCallingUid(), + userId, false, false, false, false, null, false, false); if (r != null) { if (r.record != null) { - final long origId = Binder.clearCallingIdentity(); + final long origId = mAm.mInjector.clearCallingIdentity(); try { stopServiceLocked(r.record, false); } finally { - Binder.restoreCallingIdentity(origId); + mAm.mInjector.restoreCallingIdentity(origId); } return 1; } @@ -1649,7 +1702,7 @@ public final class ActiveServices { IBinder peekServiceLocked(Intent service, String resolvedType, String callingPackage) { ServiceLookupResult r = retrieveServiceLocked(service, null, resolvedType, callingPackage, - Binder.getCallingPid(), Binder.getCallingUid(), + mAm.mInjector.getCallingPid(), mAm.mInjector.getCallingUid(), UserHandle.getCallingUserId(), false, false, false, false, false, false); IBinder ret = null; @@ -1658,8 +1711,8 @@ public final class ActiveServices { if (r.record == null) { throw new SecurityException( "Permission Denial: Accessing service" - + " from pid=" + Binder.getCallingPid() - + ", uid=" + Binder.getCallingUid() + + " from pid=" + mAm.mInjector.getCallingPid() + + ", uid=" + mAm.mInjector.getCallingUid() + " requires " + r.permission); } IntentBindRecord ib = r.record.bindings.get(r.record.intent); @@ -1719,9 +1772,9 @@ public final class ActiveServices { } } r.callStart = false; - final long origId = Binder.clearCallingIdentity(); + final long origId = mAm.mInjector.clearCallingIdentity(); bringDownServiceIfNeededLocked(r, false, false, false, "stopServiceToken"); - Binder.restoreCallingIdentity(origId); + mAm.mInjector.restoreCallingIdentity(origId); return true; } return false; @@ -1734,14 +1787,14 @@ public final class ActiveServices { public void setServiceForegroundLocked(ComponentName className, IBinder token, int id, Notification notification, int flags, int foregroundServiceType) { final int userId = UserHandle.getCallingUserId(); - final long origId = Binder.clearCallingIdentity(); + final long origId = mAm.mInjector.clearCallingIdentity(); try { ServiceRecord r = findServiceLocked(className, token, userId); if (r != null) { setServiceForegroundInnerLocked(r, id, notification, flags, foregroundServiceType); } } finally { - Binder.restoreCallingIdentity(origId); + mAm.mInjector.restoreCallingIdentity(origId); } } @@ -1753,7 +1806,7 @@ public final class ActiveServices { */ public int getForegroundServiceTypeLocked(ComponentName className, IBinder token) { final int userId = UserHandle.getCallingUserId(); - final long origId = Binder.clearCallingIdentity(); + final long origId = mAm.mInjector.clearCallingIdentity(); int ret = ServiceInfo.FOREGROUND_SERVICE_TYPE_NONE; try { ServiceRecord r = findServiceLocked(className, token, userId); @@ -1761,7 +1814,7 @@ public final class ActiveServices { ret = r.foregroundServiceType; } } finally { - Binder.restoreCallingIdentity(origId); + mAm.mInjector.restoreCallingIdentity(origId); } return ret; } @@ -3483,7 +3536,7 @@ public final class ActiveServices { boolean shouldServiceTimeOutLocked(ComponentName className, IBinder token) { final int userId = UserHandle.getCallingUserId(); - final long ident = Binder.clearCallingIdentity(); + final long ident = mAm.mInjector.clearCallingIdentity(); try { ServiceRecord sr = findServiceLocked(className, token, userId); if (sr == null) { @@ -3492,7 +3545,7 @@ public final class ActiveServices { final long nowUptime = SystemClock.uptimeMillis(); return sr.shouldTriggerShortFgsTimeout(nowUptime); } finally { - Binder.restoreCallingIdentity(ident); + mAm.mInjector.restoreCallingIdentity(ident); } } @@ -3636,8 +3689,8 @@ public final class ActiveServices { if (DEBUG_SERVICE) Slog.v(TAG_SERVICE, "bindService: " + service + " type=" + resolvedType + " conn=" + connection.asBinder() + " flags=0x" + Long.toHexString(flags)); - final int callingPid = Binder.getCallingPid(); - final int callingUid = Binder.getCallingUid(); + final int callingPid = mAm.mInjector.getCallingPid(); + final int callingUid = mAm.mInjector.getCallingUid(); final ProcessRecord callerApp = mAm.getRecordForAppLOSP(caller); if (callerApp == null) { throw new SecurityException( @@ -3778,7 +3831,7 @@ public final class ActiveServices { && !requestStartTargetPermissionsReviewIfNeededLocked(s, callingPackage, null, callingUid, service, callerFg, userId, true, connection); - final long origId = Binder.clearCallingIdentity(); + final long origId = mAm.mInjector.clearCallingIdentity(); try { if (unscheduleServiceRestartLocked(s, callerApp.info.uid, false)) { @@ -3859,12 +3912,34 @@ public final class ActiveServices { } clist.add(c); + final boolean isolated = (s.serviceInfo.flags & ServiceInfo.FLAG_ISOLATED_PROCESS) != 0; + final ProcessRecord hostApp = isolated + ? null + : mAm.getProcessRecordLocked(s.processName, s.appInfo.uid); + final int serviceBindingOomAdjPolicy = hostApp != null + ? getServiceBindingOomAdjPolicyForAddLocked(b.client, hostApp, c) + : SERVICE_BIND_OOMADJ_POLICY_LEGACY; + + final boolean shouldFreezeCaller = !packageFrozen && !permissionsReviewRequired + && (serviceBindingOomAdjPolicy & SERVICE_BIND_OOMADJ_POLICY_FREEZE_CALLER) != 0 + && callerApp.isFreezable(); + + if (shouldFreezeCaller) { + // Freeze the caller immediately, so the following #onBind/#onConnected will be + // queued up in the app side as they're one way calls. And we'll also hold off + // the service timeout timer until the process is unfrozen. + mAm.mOomAdjuster.updateAppFreezeStateLSP(callerApp, OOM_ADJ_REASON_BIND_SERVICE, + true); + } + boolean needOomAdj = false; if (c.hasFlag(Context.BIND_AUTO_CREATE)) { s.lastActivity = SystemClock.uptimeMillis(); - needOomAdj = true; + needOomAdj = (serviceBindingOomAdjPolicy + & SERVICE_BIND_OOMADJ_POLICY_SKIP_OOM_UPDATE_ON_CREATE) == 0; if (bringUpServiceLocked(s, service.getFlags(), callerFg, false, - permissionsReviewRequired, packageFrozen, true) != null) { + permissionsReviewRequired, packageFrozen, true, serviceBindingOomAdjPolicy) + != null) { mAm.updateOomAdjPendingTargetsLocked(OOM_ADJ_REASON_BIND_SERVICE); return 0; } @@ -3886,8 +3961,11 @@ public final class ActiveServices { || (callerApp.mState.getCurProcState() <= PROCESS_STATE_TOP && c.hasFlag(Context.BIND_TREAT_LIKE_ACTIVITY)), b.client); - needOomAdj = true; - mAm.enqueueOomAdjTargetLocked(s.app); + if ((serviceBindingOomAdjPolicy + & SERVICE_BIND_OOMADJ_POLICY_SKIP_OOM_UPDATE_ON_CONNECT) == 0) { + needOomAdj = true; + mAm.enqueueOomAdjTargetLocked(s.app); + } } if (needOomAdj) { mAm.updateOomAdjPendingTargetsLocked(OOM_ADJ_REASON_BIND_SERVICE); @@ -3937,10 +4015,12 @@ public final class ActiveServices { // and the service had previously asked to be told when // rebound, then do so. if (b.intent.apps.size() == 1 && b.intent.doRebind) { - requestServiceBindingLocked(s, b.intent, callerFg, true); + requestServiceBindingLocked(s, b.intent, callerFg, true, + serviceBindingOomAdjPolicy); } } else if (!b.intent.requested) { - requestServiceBindingLocked(s, b.intent, callerFg, false); + requestServiceBindingLocked(s, b.intent, callerFg, false, + serviceBindingOomAdjPolicy); } maybeLogBindCrossProfileService(userId, callingPackage, callerApp.info.uid); @@ -3948,7 +4028,7 @@ public final class ActiveServices { getServiceMapLocked(s.userId).ensureNotStartingBackgroundLocked(s); } finally { - Binder.restoreCallingIdentity(origId); + mAm.mInjector.restoreCallingIdentity(origId); } notifyBindingServiceEventLocked(callerApp, callingPackage); @@ -3982,7 +4062,7 @@ public final class ActiveServices { } void publishServiceLocked(ServiceRecord r, Intent intent, IBinder service) { - final long origId = Binder.clearCallingIdentity(); + final long origId = mAm.mInjector.clearCallingIdentity(); try { if (DEBUG_SERVICE) Slog.v(TAG_SERVICE, "PUBLISHING " + r + " " + intent + ": " + service); @@ -4025,10 +4105,11 @@ public final class ActiveServices { } serviceDoneExecutingLocked(r, mDestroyingServices.contains(r), false, false, - OOM_ADJ_REASON_EXECUTING_SERVICE); + !Flags.serviceBindingOomAdjPolicy() || b == null || !b.mSkippedOomAdj + ? OOM_ADJ_REASON_EXECUTING_SERVICE : OOM_ADJ_REASON_NONE); } } finally { - Binder.restoreCallingIdentity(origId); + mAm.mInjector.restoreCallingIdentity(origId); } } @@ -4078,8 +4159,8 @@ public final class ActiveServices { return false; } - final int callingPid = Binder.getCallingPid(); - final long origId = Binder.clearCallingIdentity(); + final int callingPid = mAm.mInjector.getCallingPid(); + final long origId = mAm.mInjector.clearCallingIdentity(); try { if (Trace.isTagEnabled(Trace.TRACE_TAG_ACTIVITY_MANAGER)) { String info; @@ -4092,9 +4173,10 @@ public final class ActiveServices { Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "unbindServiceLocked: " + info); } + boolean needOomAdj = false; while (clist.size() > 0) { ConnectionRecord r = clist.get(0); - removeConnectionLocked(r, null, null, true); + int serviceBindingOomAdjPolicy = removeConnectionLocked(r, null, null, true); if (clist.size() > 0 && clist.get(0) == r) { // In case it didn't get removed above, do it now. Slog.wtf(TAG, "Connection " + r + " not removed for binder " + binder); @@ -4112,22 +4194,28 @@ public final class ActiveServices { psr.setTreatLikeActivity(true); mAm.updateLruProcessLocked(app, true, null); } - mAm.enqueueOomAdjTargetLocked(app); + // If the bindee is more important than the binder, we may skip the OomAdjuster. + if (serviceBindingOomAdjPolicy == SERVICE_BIND_OOMADJ_POLICY_LEGACY) { + mAm.enqueueOomAdjTargetLocked(app); + needOomAdj = true; + } } } - mAm.updateOomAdjPendingTargetsLocked(OOM_ADJ_REASON_UNBIND_SERVICE); + if (needOomAdj) { + mAm.updateOomAdjPendingTargetsLocked(OOM_ADJ_REASON_UNBIND_SERVICE); + } } finally { Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER); - Binder.restoreCallingIdentity(origId); + mAm.mInjector.restoreCallingIdentity(origId); } return true; } void unbindFinishedLocked(ServiceRecord r, Intent intent, boolean doRebind) { - final long origId = Binder.clearCallingIdentity(); + final long origId = mAm.mInjector.clearCallingIdentity(); try { if (r != null) { Intent.FilterComparison filter @@ -4138,6 +4226,7 @@ public final class ActiveServices { + (b != null ? b.apps.size() : 0)); boolean inDestroying = mDestroyingServices.contains(r); + boolean skipOomAdj = false; if (b != null) { if (b.apps.size() > 0 && !inDestroying) { // Applications have already bound since the last @@ -4152,7 +4241,8 @@ public final class ActiveServices { } } try { - requestServiceBindingLocked(r, b, inFg, true); + requestServiceBindingLocked(r, b, inFg, true, + SERVICE_BIND_OOMADJ_POLICY_LEGACY); } catch (TransactionTooLargeException e) { // Don't pass this back to ActivityThread, it's unrelated. } @@ -4161,13 +4251,14 @@ public final class ActiveServices { // a new client. b.doRebind = true; } + skipOomAdj = Flags.serviceBindingOomAdjPolicy() && b.mSkippedOomAdj; } serviceDoneExecutingLocked(r, inDestroying, false, false, - OOM_ADJ_REASON_UNBIND_SERVICE); + skipOomAdj ? OOM_ADJ_REASON_NONE : OOM_ADJ_REASON_UNBIND_SERVICE); } } finally { - Binder.restoreCallingIdentity(origId); + mAm.mInjector.restoreCallingIdentity(origId); } } @@ -4503,7 +4594,7 @@ public final class ActiveServices { userId = 0; smap = getServiceMapLocked(0); // Bypass INTERACT_ACROSS_USERS permission check - final long token = Binder.clearCallingIdentity(); + final long token = mAm.mInjector.clearCallingIdentity(); try { ResolveInfo rInfoForUserId0 = mAm.getPackageManagerInternal().resolveService(service, @@ -4516,7 +4607,7 @@ public final class ActiveServices { } sInfo = rInfoForUserId0.serviceInfo; } finally { - Binder.restoreCallingIdentity(token); + mAm.mInjector.restoreCallingIdentity(token); } } sInfo = new ServiceInfo(sInfo); @@ -4645,7 +4736,8 @@ public final class ActiveServices { * @return {@code true} if it performed oomAdjUpdate. */ private boolean bumpServiceExecutingLocked( - ServiceRecord r, boolean fg, String why, @OomAdjReason int oomAdjReason) { + ServiceRecord r, boolean fg, String why, @OomAdjReason int oomAdjReason, + boolean skipTimeoutIfPossible) { if (DEBUG_SERVICE) Slog.v(TAG_SERVICE, ">>> EXECUTING " + why + " of " + r + " in app " + r.app); else if (DEBUG_SERVICE_EXECUTING) Slog.v(TAG_SERVICE_EXECUTING, ">>> EXECUTING " @@ -4669,6 +4761,10 @@ public final class ActiveServices { timeoutNeeded = false; } + // If the process is frozen or to be frozen, and we want to skip the timeout, skip it. + final boolean shouldSkipTimeout = skipTimeoutIfPossible && r.app != null + && (r.app.mOptRecord.isPendingFreeze() || r.app.mOptRecord.isFrozen()); + ProcessServiceRecord psr; if (r.executeNesting == 0) { r.executeFg = fg; @@ -4684,7 +4780,11 @@ public final class ActiveServices { psr.startExecutingService(r); psr.setExecServicesFg(psr.shouldExecServicesFg() || fg); if (timeoutNeeded && psr.numberOfExecutingServices() == 1) { - scheduleServiceTimeoutLocked(r.app); + if (!shouldSkipTimeout) { + scheduleServiceTimeoutLocked(r.app); + } else { + r.app.mServices.noteScheduleServiceTimeoutPending(true); + } } } } else if (r.app != null && fg) { @@ -4692,7 +4792,11 @@ public final class ActiveServices { if (!psr.shouldExecServicesFg()) { psr.setExecServicesFg(true); if (timeoutNeeded) { - scheduleServiceTimeoutLocked(r.app); + if (!shouldSkipTimeout) { + scheduleServiceTimeoutLocked(r.app); + } else { + r.app.mServices.noteScheduleServiceTimeoutPending(true); + } } } } @@ -4712,16 +4816,22 @@ public final class ActiveServices { } private final boolean requestServiceBindingLocked(ServiceRecord r, IntentBindRecord i, - boolean execInFg, boolean rebind) throws TransactionTooLargeException { + boolean execInFg, boolean rebind, + @ServiceBindingOomAdjPolicy int serviceBindingOomAdjPolicy) + throws TransactionTooLargeException { if (r.app == null || r.app.getThread() == null) { // If service is not currently running, can't yet bind. return false; } if (DEBUG_SERVICE) Slog.d(TAG_SERVICE, "requestBind " + i + ": requested=" + i.requested + " rebind=" + rebind); + final boolean skipOomAdj = (serviceBindingOomAdjPolicy + & SERVICE_BIND_OOMADJ_POLICY_SKIP_OOM_UPDATE_ON_BIND) != 0; if ((!i.requested || rebind) && i.apps.size() > 0) { try { - bumpServiceExecutingLocked(r, execInFg, "bind", OOM_ADJ_REASON_BIND_SERVICE); + i.mSkippedOomAdj = !bumpServiceExecutingLocked(r, execInFg, "bind", + skipOomAdj ? OOM_ADJ_REASON_NONE : OOM_ADJ_REASON_BIND_SERVICE, + skipOomAdj /* skipTimeoutIfPossible */); if (Trace.isTagEnabled(Trace.TRACE_TAG_ACTIVITY_MANAGER)) { Trace.instant(Trace.TRACE_TAG_ACTIVITY_MANAGER, "requestServiceBinding=" + i.intent.getIntent() + ". bindSeq=" + mBindServiceSeqCounter); @@ -4738,14 +4848,14 @@ public final class ActiveServices { if (DEBUG_SERVICE) Slog.v(TAG_SERVICE, "Crashed while binding " + r, e); final boolean inDestroying = mDestroyingServices.contains(r); serviceDoneExecutingLocked(r, inDestroying, inDestroying, false, - OOM_ADJ_REASON_UNBIND_SERVICE); + skipOomAdj ? OOM_ADJ_REASON_NONE : OOM_ADJ_REASON_UNBIND_SERVICE); throw e; } catch (RemoteException e) { if (DEBUG_SERVICE) Slog.v(TAG_SERVICE, "Crashed while binding " + r); // Keep the executeNesting count accurate. final boolean inDestroying = mDestroyingServices.contains(r); serviceDoneExecutingLocked(r, inDestroying, inDestroying, false, - OOM_ADJ_REASON_UNBIND_SERVICE); + skipOomAdj ? OOM_ADJ_REASON_NONE : OOM_ADJ_REASON_UNBIND_SERVICE); return false; } } @@ -5117,7 +5227,7 @@ public final class ActiveServices { } try { bringUpServiceLocked(r, r.intent.getIntent().getFlags(), r.createdFromFg, true, false, - false, true); + false, true, SERVICE_BIND_OOMADJ_POLICY_LEGACY); } catch (TransactionTooLargeException e) { // Ignore, it's been logged and nothing upstack cares. } finally { @@ -5217,7 +5327,7 @@ public final class ActiveServices { private String bringUpServiceLocked(ServiceRecord r, int intentFlags, boolean execInFg, boolean whileRestarting, boolean permissionsReviewRequired, boolean packageFrozen, - boolean enqueueOomAdj) + boolean enqueueOomAdj, @ServiceBindingOomAdjPolicy int serviceBindingOomAdjPolicy) throws TransactionTooLargeException { try { if (Trace.isTagEnabled(Trace.TRACE_TAG_ACTIVITY_MANAGER)) { @@ -5225,7 +5335,8 @@ public final class ActiveServices { "bringUpServiceLocked: " + r.shortInstanceName); } return bringUpServiceInnerLocked(r, intentFlags, execInFg, whileRestarting, - permissionsReviewRequired, packageFrozen, enqueueOomAdj); + permissionsReviewRequired, packageFrozen, enqueueOomAdj, + serviceBindingOomAdjPolicy); } finally { Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER); } @@ -5233,7 +5344,7 @@ public final class ActiveServices { private String bringUpServiceInnerLocked(ServiceRecord r, int intentFlags, boolean execInFg, boolean whileRestarting, boolean permissionsReviewRequired, boolean packageFrozen, - boolean enqueueOomAdj) + boolean enqueueOomAdj, @ServiceBindingOomAdjPolicy int serviceBindingOomAdjPolicy) throws TransactionTooLargeException { if (r.app != null && r.app.isThreadReady()) { sendServiceArgsLocked(r, execInFg, false); @@ -5317,7 +5428,7 @@ public final class ActiveServices { app.addPackage(r.appInfo.packageName, r.appInfo.longVersionCode, mAm.mProcessStats); realStartServiceLocked(r, app, thread, pid, uidRecord, execInFg, - enqueueOomAdj); + enqueueOomAdj, serviceBindingOomAdjPolicy); return null; } catch (TransactionTooLargeException e) { throw e; @@ -5347,7 +5458,7 @@ public final class ActiveServices { "realStartServiceLocked: " + r.shortInstanceName); } realStartServiceLocked(r, app, thread, pid, uidRecord, execInFg, - enqueueOomAdj); + enqueueOomAdj, SERVICE_BIND_OOMADJ_POLICY_LEGACY); return null; } catch (TransactionTooLargeException e) { throw e; @@ -5452,16 +5563,61 @@ public final class ActiveServices { return HostingRecord.TRIGGER_TYPE_UNKNOWN; } - private final void requestServiceBindingsLocked(ServiceRecord r, boolean execInFg) + private void requestServiceBindingsLocked(ServiceRecord r, boolean execInFg, + @ServiceBindingOomAdjPolicy int serviceBindingOomAdjPolicy) throws TransactionTooLargeException { for (int i=r.bindings.size()-1; i>=0; i--) { IntentBindRecord ibr = r.bindings.valueAt(i); - if (!requestServiceBindingLocked(r, ibr, execInFg, false)) { + if (!requestServiceBindingLocked(r, ibr, execInFg, false, serviceBindingOomAdjPolicy)) { break; } } } + @ServiceBindingOomAdjPolicy + private int getServiceBindingOomAdjPolicyForAddLocked(ProcessRecord clientApp, + ProcessRecord hostApp, ConnectionRecord cr) { + @ServiceBindingOomAdjPolicy int policy = SERVICE_BIND_OOMADJ_POLICY_LEGACY; + if (Flags.serviceBindingOomAdjPolicy() && clientApp != null && hostApp != null) { + if (clientApp == hostApp) { + policy = DEFAULT_SERVICE_NO_BUMP_BIND_POLICY_FLAG; + } else if (clientApp.isCached()) { + policy = DEFAULT_SERVICE_NO_BUMP_BIND_POLICY_FLAG; + if (clientApp.isFreezable()) { + policy |= SERVICE_BIND_OOMADJ_POLICY_FREEZE_CALLER; + } + } + if ((policy & SERVICE_BIND_OOMADJ_POLICY_SKIP_OOM_UPDATE_ON_CONNECT) == 0) { + // Binding between two different processes. + // Check if the caller has a better process state, oom adj score, + // or if the caller has more capabilities. + if (!mAm.mOomAdjuster.evaluateServiceConnectionAdd(clientApp, hostApp, cr)) { + // Running an oom adjuster won't be give the host app a better score, skip it. + policy = DEFAULT_SERVICE_NO_BUMP_BIND_POLICY_FLAG; + } + } + } + return policy; + } + + @ServiceBindingOomAdjPolicy + private int getServiceBindingOomAdjPolicyForRemovalLocked(ProcessRecord clientApp, + ProcessRecord hostApp, ConnectionRecord cr) { + @ServiceBindingOomAdjPolicy int policy = SERVICE_BIND_OOMADJ_POLICY_LEGACY; + if (Flags.serviceBindingOomAdjPolicy() && clientApp != null && hostApp != null + && cr != null) { + if (clientApp == hostApp) { + policy = DEFAULT_SERVICE_NO_BUMP_BIND_POLICY_FLAG; + } else { + if (!mAm.mOomAdjuster.evaluateServiceConnectionRemoval(clientApp, hostApp, cr)) { + // Running an oom adjuster won't be give the host app a better score, skip it. + policy = DEFAULT_SERVICE_NO_BUMP_BIND_POLICY_FLAG; + } + } + } + return policy; + } + /** * Note the name of this method should not be confused with the started services concept. * The "start" here means bring up the instance in the client, and this method is called @@ -5469,7 +5625,8 @@ public final class ActiveServices { */ private void realStartServiceLocked(ServiceRecord r, ProcessRecord app, IApplicationThread thread, int pid, UidRecord uidRecord, boolean execInFg, - boolean enqueueOomAdj) throws RemoteException { + boolean enqueueOomAdj, @ServiceBindingOomAdjPolicy int serviceBindingOomAdjPolicy) + throws RemoteException { if (thread == null) { throw new RemoteException(); } @@ -5478,17 +5635,28 @@ public final class ActiveServices { + ", ProcessRecord.uid = " + app.uid); r.setProcess(app, thread, pid, uidRecord); r.restartTime = r.lastActivity = SystemClock.uptimeMillis(); - + final boolean skipOomAdj = (serviceBindingOomAdjPolicy + & SERVICE_BIND_OOMADJ_POLICY_SKIP_OOM_UPDATE_ON_CREATE) != 0; final ProcessServiceRecord psr = app.mServices; final boolean newService = psr.startService(r); bumpServiceExecutingLocked(r, execInFg, "create", - OOM_ADJ_REASON_NONE /* use "none" to avoid extra oom adj */); + OOM_ADJ_REASON_NONE /* use "none" to avoid extra oom adj */, + skipOomAdj /* skipTimeoutIfPossible */); mAm.updateLruProcessLocked(app, false, null); updateServiceForegroundLocked(psr, /* oomAdj= */ false); - // Force an immediate oomAdjUpdate, so the client app could be in the correct process state - // before doing any service related transactions - mAm.enqueueOomAdjTargetLocked(app); - mAm.updateOomAdjPendingTargetsLocked(OOM_ADJ_REASON_START_SERVICE); + // Skip the oom adj update if it's a self-binding, the Service#onCreate() will be running + // at its current adj score. + if (!skipOomAdj) { + // Force an immediate oomAdjUpdate, so the host app could be in the correct + // process state before doing any service related transactions + mAm.enqueueOomAdjTargetLocked(app); + mAm.updateOomAdjPendingTargetsLocked(OOM_ADJ_REASON_START_SERVICE); + } else { + // Since we skipped the oom adj update, the Service#onCreate() might be running in + // the cached state, if the service process drops into the cached state after the call. + // But there is still a grace period before freezing it, so we should be fine + // in terms of not getting an ANR. + } boolean created = false; try { @@ -5523,7 +5691,7 @@ public final class ActiveServices { // Keep the executeNesting count accurate. final boolean inDestroying = mDestroyingServices.contains(r); serviceDoneExecutingLocked(r, inDestroying, inDestroying, false, - OOM_ADJ_REASON_STOP_SERVICE); + skipOomAdj ? OOM_ADJ_REASON_NONE : OOM_ADJ_REASON_STOP_SERVICE); // Cleanup. if (newService) { @@ -5542,7 +5710,7 @@ public final class ActiveServices { psr.mAllowlistManager = true; } - requestServiceBindingsLocked(r, execInFg); + requestServiceBindingsLocked(r, execInFg, serviceBindingOomAdjPolicy); updateServiceClientActivitiesLocked(psr, null, true); @@ -5610,7 +5778,8 @@ public final class ActiveServices { UserHandle.getAppId(r.appInfo.uid) ); bumpServiceExecutingLocked(r, execInFg, "start", - OOM_ADJ_REASON_NONE /* use "none" to avoid extra oom adj */); + OOM_ADJ_REASON_NONE /* use "none" to avoid extra oom adj */, + false /* skipTimeoutIfPossible */); if (r.fgRequired && !r.fgWaiting) { if (!r.isForeground) { if (DEBUG_BACKGROUND_CHECK) { @@ -5753,7 +5922,8 @@ public final class ActiveServices { if (ibr.hasBound) { try { oomAdjusted |= bumpServiceExecutingLocked(r, false, "bring down unbind", - OOM_ADJ_REASON_UNBIND_SERVICE); + OOM_ADJ_REASON_UNBIND_SERVICE, + false /* skipTimeoutIfPossible */); ibr.hasBound = false; ibr.requested = false; r.app.getThread().scheduleUnbindService(r, @@ -5909,7 +6079,8 @@ public final class ActiveServices { } else { try { oomAdjusted |= bumpServiceExecutingLocked(r, false, "destroy", - oomAdjusted ? 0 : OOM_ADJ_REASON_STOP_SERVICE); + oomAdjusted ? 0 : OOM_ADJ_REASON_STOP_SERVICE, + false /* skipTimeoutIfPossible */); mDestroyingServices.add(r); r.destroying = true; r.app.getThread().scheduleStopService(r); @@ -5992,11 +6163,17 @@ public final class ActiveServices { } } - void removeConnectionLocked(ConnectionRecord c, ProcessRecord skipApp, + /** + * @return The ServiceBindingOomAdjPolicy used in this removal. + */ + @ServiceBindingOomAdjPolicy + int removeConnectionLocked(ConnectionRecord c, ProcessRecord skipApp, ActivityServiceConnectionsHolder skipAct, boolean enqueueOomAdj) { IBinder binder = c.conn.asBinder(); AppBindRecord b = c.binding; ServiceRecord s = b.service; + @ServiceBindingOomAdjPolicy int serviceBindingOomAdjPolicy = + SERVICE_BIND_OOMADJ_POLICY_LEGACY; ArrayList<ConnectionRecord> clist = s.getConnections().get(binder); if (clist != null) { clist.remove(c); @@ -6055,8 +6232,14 @@ public final class ActiveServices { + ": shouldUnbind=" + b.intent.hasBound); if (s.app != null && s.app.isThreadReady() && b.intent.apps.size() == 0 && b.intent.hasBound) { + serviceBindingOomAdjPolicy = getServiceBindingOomAdjPolicyForRemovalLocked(b.client, + s.app, c); + final boolean skipOomAdj = (serviceBindingOomAdjPolicy + & SERVICE_BIND_OOMADJ_POLICY_SKIP_OOM_UPDATE_ON_CONNECT) != 0; try { - bumpServiceExecutingLocked(s, false, "unbind", OOM_ADJ_REASON_UNBIND_SERVICE); + b.intent.mSkippedOomAdj = !bumpServiceExecutingLocked(s, false, "unbind", + skipOomAdj ? OOM_ADJ_REASON_NONE : OOM_ADJ_REASON_UNBIND_SERVICE, + skipOomAdj /* skipTimeoutIfPossible */); if (b.client != s.app && c.notHasFlag(Context.BIND_WAIVE_PRIORITY) && s.app.mState.getSetProcState() <= PROCESS_STATE_HEAVY_WEIGHT) { // If this service's process is not already in the cached list, @@ -6096,12 +6279,14 @@ public final class ActiveServices { "removeConnection"); } } + return serviceBindingOomAdjPolicy; } void serviceDoneExecutingLocked(ServiceRecord r, int type, int startId, int res, - boolean enqueueOomAdj) { + boolean enqueueOomAdj, Intent intent) { boolean inDestroying = mDestroyingServices.contains(r); if (r != null) { + boolean skipOomAdj = false; if (type == ActivityThread.SERVICE_DONE_EXECUTING_START) { // This is a call from a service start... take care of // book-keeping. @@ -6177,14 +6362,19 @@ public final class ActiveServices { // Fake it to keep from ANR due to orphaned entry. r.executeNesting = 1; } + } else if (type == ActivityThread.SERVICE_DONE_EXECUTING_REBIND + || type == ActivityThread.SERVICE_DONE_EXECUTING_UNBIND) { + final Intent.FilterComparison filter = new Intent.FilterComparison(intent); + final IntentBindRecord b = r.bindings.get(filter); + skipOomAdj = Flags.serviceBindingOomAdjPolicy() && b != null && b.mSkippedOomAdj; } - final long origId = Binder.clearCallingIdentity(); + final long origId = mAm.mInjector.clearCallingIdentity(); serviceDoneExecutingLocked(r, inDestroying, inDestroying, enqueueOomAdj, - OOM_ADJ_REASON_EXECUTING_SERVICE); - Binder.restoreCallingIdentity(origId); + skipOomAdj ? OOM_ADJ_REASON_NONE : OOM_ADJ_REASON_EXECUTING_SERVICE); + mAm.mInjector.restoreCallingIdentity(origId); } else { Slog.w(TAG, "Done executing unknown service from pid " - + Binder.getCallingPid()); + + mAm.mInjector.getCallingPid()); } } @@ -6236,10 +6426,17 @@ public final class ActiveServices { mDestroyingServices.remove(r); r.bindings.clear(); } - if (enqueueOomAdj) { - mAm.enqueueOomAdjTargetLocked(r.app); + boolean oomAdjusted = false; + if (oomAdjReason != OOM_ADJ_REASON_NONE) { + if (enqueueOomAdj) { + mAm.enqueueOomAdjTargetLocked(r.app); + } else { + mAm.updateOomAdjLocked(r.app, oomAdjReason); + } + oomAdjusted = true; } else { - mAm.updateOomAdjLocked(r.app, oomAdjReason); + // Skip oom adj if it wasn't bumped during the bumpServiceExecutingLocked() + oomAdjusted = false; } } r.executeFg = false; @@ -6296,7 +6493,7 @@ public final class ActiveServices { "realStartServiceLocked: " + sr.shortInstanceName); } realStartServiceLocked(sr, proc, thread, pid, uidRecord, sr.createdFromFg, - true); + true, SERVICE_BIND_OOMADJ_POLICY_LEGACY); } finally { Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER); } @@ -6786,6 +6983,7 @@ public final class ActiveServices { } psr.stopAllExecutingServices(); + psr.noteScheduleServiceTimeoutPending(false); } ActivityManager.RunningServiceInfo makeRunningServiceInfoLocked(ServiceRecord r) { @@ -6836,7 +7034,7 @@ public final class ActiveServices { ArrayList<ActivityManager.RunningServiceInfo> res = new ArrayList<ActivityManager.RunningServiceInfo>(); - final long ident = Binder.clearCallingIdentity(); + final long ident = mAm.mInjector.clearCallingIdentity(); try { if (canInteractAcrossUsers) { int[] users = mAm.mUserController.getUsers(); @@ -6878,14 +7076,14 @@ public final class ActiveServices { } } } finally { - Binder.restoreCallingIdentity(ident); + mAm.mInjector.restoreCallingIdentity(ident); } return res; } public PendingIntent getRunningServiceControlPanelLocked(ComponentName name) { - int userId = UserHandle.getUserId(Binder.getCallingUid()); + int userId = UserHandle.getUserId(mAm.mInjector.getCallingUid()); ServiceRecord r = getServiceByNameLocked(name, userId); if (r != null) { ArrayMap<IBinder, ArrayList<ConnectionRecord>> connections = r.getConnections(); @@ -7077,6 +7275,7 @@ public final class ActiveServices { final long delay = proc.mServices.shouldExecServicesFg() ? mAm.mConstants.SERVICE_TIMEOUT : mAm.mConstants.SERVICE_BACKGROUND_TIMEOUT; mActiveServiceAnrTimer.start(proc, delay); + proc.mServices.noteScheduleServiceTimeoutPending(false); } void scheduleServiceForegroundTransitionTimeoutLocked(ServiceRecord r) { diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java index ca04e41769ee..74902f76b617 100644 --- a/services/core/java/com/android/server/am/ActivityManagerService.java +++ b/services/core/java/com/android/server/am/ActivityManagerService.java @@ -1751,7 +1751,7 @@ public class ActivityManagerService extends IActivityManager.Stub @GuardedBy("mProcLock") private long mLastBinderHeavyHitterAutoSamplerStart = 0L; - final AppProfiler mAppProfiler; + AppProfiler mAppProfiler; private static final int INDEX_NATIVE_PSS = 0; private static final int INDEX_NATIVE_SWAP_PSS = 1; @@ -2496,7 +2496,7 @@ public class ActivityManagerService extends IActivityManager.Stub mInjector = injector; mContext = mInjector.getContext(); mUiContext = null; - mAppErrors = null; + mAppErrors = injector.getAppErrors(); mPackageWatchdog = null; mAppOpsService = mInjector.getAppOpsService(null /* recentAccessesFile */, null /* storageFile */, null /* handler */); @@ -2514,7 +2514,7 @@ public class ActivityManagerService extends IActivityManager.Stub ? new OomAdjusterModernImpl(this, mProcessList, activeUids, handlerThread) : new OomAdjuster(this, mProcessList, activeUids, handlerThread); - mIntentFirewall = null; + mIntentFirewall = injector.getIntentFirewall(); mProcessStats = new ProcessStatsService(this, mContext.getCacheDir()); mCpHelper = new ContentProviderHelper(this, false); mServices = mInjector.getActiveServices(this); @@ -13889,13 +13889,15 @@ public class ActivityManagerService extends IActivityManager.Stub } } - public void serviceDoneExecuting(IBinder token, int type, int startId, int res) { + @Override + public void serviceDoneExecuting(IBinder token, int type, int startId, int res, Intent intent) { synchronized(this) { if (!(token instanceof ServiceRecord)) { Slog.e(TAG, "serviceDoneExecuting: Invalid service token=" + token); throw new IllegalArgumentException("Invalid service token"); } - mServices.serviceDoneExecutingLocked((ServiceRecord) token, type, startId, res, false); + mServices.serviceDoneExecutingLocked((ServiceRecord) token, type, startId, res, false, + intent); } } @@ -20236,6 +20238,36 @@ public class ActivityManagerService extends IActivityManager.Stub } return broadcastQueues; } + + /** @see Binder#getCallingUid */ + public int getCallingUid() { + return Binder.getCallingUid(); + } + + /** @see Binder#getCallingPid */ + public int getCallingPid() { + return Binder.getCallingUid(); + } + + /** @see Binder#clearCallingIdentity */ + public long clearCallingIdentity() { + return Binder.clearCallingIdentity(); + } + + /** @see Binder#clearCallingIdentity */ + public void restoreCallingIdentity(long ident) { + Binder.restoreCallingIdentity(ident); + } + + /** @return the default instance of AppErrors */ + public AppErrors getAppErrors() { + return null; + } + + /** @return the default instance of intent firewall */ + public IntentFirewall getIntentFirewall() { + return null; + } } @Override diff --git a/services/core/java/com/android/server/am/AppBatteryExemptionTracker.java b/services/core/java/com/android/server/am/AppBatteryExemptionTracker.java index b07d9a6b258c..9c2e69be7685 100644 --- a/services/core/java/com/android/server/am/AppBatteryExemptionTracker.java +++ b/services/core/java/com/android/server/am/AppBatteryExemptionTracker.java @@ -520,7 +520,7 @@ final class AppBatteryExemptionTracker /** * Default value to {@link #mTrackerEnabled}. */ - static final boolean DEFAULT_BG_BATTERY_EXEMPTION_ENABLED = true; + static final boolean DEFAULT_BG_BATTERY_EXEMPTION_ENABLED = false; AppBatteryExemptionPolicy(@NonNull Injector injector, @NonNull AppBatteryExemptionTracker tracker) { diff --git a/services/core/java/com/android/server/am/CachedAppOptimizer.java b/services/core/java/com/android/server/am/CachedAppOptimizer.java index 626b70b51093..d92a24b82764 100644 --- a/services/core/java/com/android/server/am/CachedAppOptimizer.java +++ b/services/core/java/com/android/server/am/CachedAppOptimizer.java @@ -1419,6 +1419,11 @@ public final class CachedAppOptimizer { } @GuardedBy({"mAm", "mProcLock"}) + void freezeAppAsyncAtEarliestLSP(ProcessRecord app) { + freezeAppAsyncLSP(app, updateEarliestFreezableTime(app, 0)); + } + + @GuardedBy({"mAm", "mProcLock"}) void freezeAppAsyncInternalLSP(ProcessRecord app, @UptimeMillisLong long delayMillis, boolean force) { final ProcessCachedOptimizerRecord opt = app.mOptRecord; @@ -1714,6 +1719,14 @@ public final class CachedAppOptimizer { compactApp(frozenProc, CompactProfile.FULL, CompactSource.APP, false); } } + frozenProc.onProcessFrozen(); + } + + /** + * Callback received when an attempt to freeze a process is cancelled (failed). + */ + void onProcessFrozenCancelled(ProcessRecord app) { + app.onProcessFrozenCancelled(); } /** @@ -2203,6 +2216,8 @@ public final class CachedAppOptimizer { onProcessFrozen(proc); removeMessages(DEADLOCK_WATCHDOG_MSG); sendEmptyMessageDelayed(DEADLOCK_WATCHDOG_MSG, FREEZE_DEADLOCK_TIMEOUT_MS); + } else { + onProcessFrozenCancelled(proc); } } break; case REPORT_UNFREEZE_MSG: { @@ -2460,7 +2475,7 @@ public final class CachedAppOptimizer { pr = mAm.mPidsSelfLocked.get(blocked); } if (pr != null - && pr.mState.getCurAdj() < ProcessList.CACHED_APP_MIN_ADJ) { + && pr.mState.getCurAdj() < ProcessList.FREEZER_CUTOFF_ADJ) { Slog.d(TAG_AM, app.processName + " (" + pid + ") blocks " + pr.processName + " (" + blocked + ")"); // Found at least one blocked non-cached process diff --git a/services/core/java/com/android/server/am/ContentProviderHelper.java b/services/core/java/com/android/server/am/ContentProviderHelper.java index 30f21a65b5b1..cb7898d8b862 100644 --- a/services/core/java/com/android/server/am/ContentProviderHelper.java +++ b/services/core/java/com/android/server/am/ContentProviderHelper.java @@ -35,6 +35,7 @@ import static com.android.internal.util.FrameworkStatsLog.PROVIDER_ACQUISITION_E import static com.android.internal.util.FrameworkStatsLog.PROVIDER_ACQUISITION_EVENT_REPORTED__PROC_START_TYPE__PROCESS_START_TYPE_WARM; import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_MU; import static com.android.server.am.ActivityManagerService.TAG_MU; +import static com.android.server.am.Flags.serviceBindingOomAdjPolicy; import android.annotation.Nullable; import android.annotation.UserIdInt; @@ -319,8 +320,10 @@ public class ContentProviderHelper { checkTime(startTime, "getContentProviderImpl: before updateOomAdj"); final int verifiedAdj = cpr.proc.mState.getVerifiedAdj(); - boolean success = mService.updateOomAdjLocked(cpr.proc, - OOM_ADJ_REASON_GET_PROVIDER); + boolean success = !serviceBindingOomAdjPolicy() + || mService.mOomAdjuster.evaluateProviderConnectionAdd(r, cpr.proc) + ? mService.updateOomAdjLocked(cpr.proc, OOM_ADJ_REASON_GET_PROVIDER) + : true; // XXX things have changed so updateOomAdjLocked doesn't actually tell us // if the process has been successfully adjusted. So to reduce races with // it, we will check whether the process still exists. Note that this doesn't @@ -1529,7 +1532,9 @@ public class ContentProviderHelper { } mService.stopAssociationLocked(conn.client.uid, conn.client.processName, cpr.uid, cpr.appInfo.longVersionCode, cpr.name, cpr.info.processName); - if (updateOomAdj) { + if (updateOomAdj && (!serviceBindingOomAdjPolicy() + || mService.mOomAdjuster.evaluateProviderConnectionRemoval(conn.client, + cpr.proc))) { mService.updateOomAdjLocked(conn.provider.proc, OOM_ADJ_REASON_REMOVE_PROVIDER); } } diff --git a/services/core/java/com/android/server/am/IntentBindRecord.java b/services/core/java/com/android/server/am/IntentBindRecord.java index abc7ab110f89..db47e3f26a6d 100644 --- a/services/core/java/com/android/server/am/IntentBindRecord.java +++ b/services/core/java/com/android/server/am/IntentBindRecord.java @@ -46,9 +46,17 @@ final class IntentBindRecord { boolean hasBound; /** Set when the service's onUnbind() has asked to be told about new clients. */ boolean doRebind; - + String stringName; // caching of toString - + + /** + * Mark if we've skipped oom adj update before calling into the {@link Service#onBind()} + * or {@link Service#onUnbind()}. + * + * <p>If it's true, we'll skip the oom adj update too during the serviceDoneExecuting. + */ + boolean mSkippedOomAdj; + void dump(PrintWriter pw, String prefix) { pw.print(prefix); pw.print("service="); pw.println(service); dumpInService(pw, prefix); diff --git a/services/core/java/com/android/server/am/OomAdjuster.java b/services/core/java/com/android/server/am/OomAdjuster.java index ef7a0e058db0..862542e3d3d0 100644 --- a/services/core/java/com/android/server/am/OomAdjuster.java +++ b/services/core/java/com/android/server/am/OomAdjuster.java @@ -103,6 +103,7 @@ import static com.android.server.am.ProcessList.CACHED_APP_IMPORTANCE_LEVELS; import static com.android.server.am.ProcessList.CACHED_APP_MAX_ADJ; import static com.android.server.am.ProcessList.CACHED_APP_MIN_ADJ; import static com.android.server.am.ProcessList.FOREGROUND_APP_ADJ; +import static com.android.server.am.ProcessList.FREEZER_CUTOFF_ADJ; import static com.android.server.am.ProcessList.HEAVY_WEIGHT_APP_ADJ; import static com.android.server.am.ProcessList.HOME_APP_ADJ; import static com.android.server.am.ProcessList.INVALID_ADJ; @@ -2309,7 +2310,7 @@ public class OomAdjuster { } computeServiceHostOomAdjLSP(cr, app, cr.binding.client, now, topApp, doingAll, - cycleReEval, computeClients, oomAdjReason, cachedAdj, true); + cycleReEval, computeClients, oomAdjReason, cachedAdj, true, false); adj = state.getCurRawAdj(); procState = state.getCurRawProcState(); @@ -2341,7 +2342,7 @@ public class OomAdjuster { ContentProviderConnection conn = cpr.connections.get(i); ProcessRecord client = conn.client; computeProviderHostOomAdjLSP(conn, app, client, now, topApp, doingAll, - cycleReEval, computeClients, oomAdjReason, cachedAdj, true); + cycleReEval, computeClients, oomAdjReason, cachedAdj, true, false); adj = state.getCurRawAdj(); procState = state.getCurRawProcState(); @@ -2558,17 +2559,18 @@ public class OomAdjuster { } @GuardedBy({"mService", "mProcLock"}) - protected void computeServiceHostOomAdjLSP(ConnectionRecord cr, ProcessRecord app, + protected boolean computeServiceHostOomAdjLSP(ConnectionRecord cr, ProcessRecord app, ProcessRecord client, long now, ProcessRecord topApp, boolean doingAll, boolean cycleReEval, boolean computeClients, int oomAdjReason, int cachedAdj, - boolean couldRecurse) { + boolean couldRecurse, boolean dryRun) { if (app.isPendingFinishAttach()) { // We've set the attaching process state in the computeInitialOomAdjLSP. Skip it here. - return; + return false; } final ProcessStateRecord state = app.mState; ProcessStateRecord cstate = client.mState; + boolean updated = false; if (couldRecurse) { if (app.isSdkSandbox && cr.binding.attributedClient != null) { @@ -2599,19 +2601,25 @@ public class OomAdjuster { final int prevRawAdj = adj; final int prevProcState = procState; final int prevSchedGroup = schedGroup; + final int prevCapability = capability; final int appUid = app.info.uid; final int logUid = mService.mCurOomAdjUid; - state.setCurBoundByNonBgRestrictedApp(state.isCurBoundByNonBgRestrictedApp() - || cstate.isCurBoundByNonBgRestrictedApp() - || clientProcState <= PROCESS_STATE_BOUND_TOP - || (clientProcState == PROCESS_STATE_FOREGROUND_SERVICE - && !cstate.isBackgroundRestricted())); + if (!dryRun) { + state.setCurBoundByNonBgRestrictedApp(state.isCurBoundByNonBgRestrictedApp() + || cstate.isCurBoundByNonBgRestrictedApp() + || clientProcState <= PROCESS_STATE_BOUND_TOP + || (clientProcState == PROCESS_STATE_FOREGROUND_SERVICE + && !cstate.isBackgroundRestricted())); + } if (client.mOptRecord.shouldNotFreeze()) { // Propagate the shouldNotFreeze flag down the bindings. - app.mOptRecord.setShouldNotFreeze(true); + if (app.mOptRecord.setShouldNotFreeze(true, dryRun)) { + // Bail out early, as we only care about the return value for a dryrun. + return true; + } } boolean trackedProcState = false; @@ -2653,7 +2661,7 @@ public class OomAdjuster { } if (couldRecurse && shouldSkipDueToCycle(app, cstate, procState, adj, cycleReEval)) { - return; + return false; } if (clientProcState >= PROCESS_STATE_CACHED_ACTIVITY) { @@ -2666,7 +2674,10 @@ public class OomAdjuster { if (cr.hasFlag(Context.BIND_ALLOW_OOM_MANAGEMENT)) { // Similar to BIND_WAIVE_PRIORITY, keep it unfrozen. if (clientAdj < CACHED_APP_MIN_ADJ) { - app.mOptRecord.setShouldNotFreeze(true); + if (app.mOptRecord.setShouldNotFreeze(true, dryRun)) { + // Bail out early, as we only care about the return value for a dryrun. + return true; + } } // Not doing bind OOM management, so treat // this guy more like a started service. @@ -2678,7 +2689,10 @@ public class OomAdjuster { if (adj > clientAdj) { adjType = "cch-bound-ui-services"; } - state.setCached(false); + if (state.setCached(false, dryRun)) { + // Bail out early, as we only care about the return value for a dryrun. + return true; + } clientAdj = adj; clientProcState = procState; } else { @@ -2721,7 +2735,9 @@ public class OomAdjuster { newAdj = PERSISTENT_SERVICE_ADJ; schedGroup = SCHED_GROUP_DEFAULT; procState = ActivityManager.PROCESS_STATE_PERSISTENT; - cr.trackProcState(procState, mAdjSeq); + if (!dryRun) { + cr.trackProcState(procState, mAdjSeq); + } trackedProcState = true; } } else if (cr.hasFlag(Context.BIND_NOT_PERCEPTIBLE) @@ -2762,11 +2778,16 @@ public class OomAdjuster { } } if (!cstate.isCached()) { - state.setCached(false); + if (state.setCached(false, dryRun)) { + // Bail out early, as we only care about the return value for a dryrun. + return true; + } } if (adj > newAdj) { adj = newAdj; - state.setCurRawAdj(adj); + if (state.setCurRawAdj(adj, dryRun)) { + // Bail out early, as we only care about the return value for a dryrun. + } adjType = "service"; } } @@ -2833,25 +2854,35 @@ public class OomAdjuster { if (cr.hasFlag(Context.BIND_SCHEDULE_LIKE_TOP_APP) && clientIsSystem) { schedGroup = SCHED_GROUP_TOP_APP; - state.setScheduleLikeTopApp(true); + if (dryRun) { + if (prevSchedGroup < schedGroup) { + // Bail out early, as we only care about the return value for a dryrun. + return true; + } + } else { + state.setScheduleLikeTopApp(true); + } } - if (!trackedProcState) { + if (!trackedProcState && !dryRun) { cr.trackProcState(clientProcState, mAdjSeq); } if (procState > clientProcState) { procState = clientProcState; - state.setCurRawProcState(procState); + if (state.setCurRawProcState(procState, dryRun)) { + // Bail out early, as we only care about the return value for a dryrun. + return true; + } if (adjType == null) { adjType = "service"; } } if (procState < PROCESS_STATE_IMPORTANT_BACKGROUND - && cr.hasFlag(Context.BIND_SHOWING_UI)) { + && cr.hasFlag(Context.BIND_SHOWING_UI) && !dryRun) { app.setPendingUiClean(true); } - if (adjType != null) { + if (adjType != null && !dryRun) { state.setAdjType(adjType); state.setAdjTypeCode(ActivityManager.RunningAppProcessInfo .REASON_SERVICE_IN_USE); @@ -2876,11 +2907,16 @@ public class OomAdjuster { // bound by an unfrozen app via a WPRI binding has to remain // unfrozen. if (clientAdj < CACHED_APP_MIN_ADJ) { - app.mOptRecord.setShouldNotFreeze(true); + if (app.mOptRecord.setShouldNotFreeze(true, dryRun)) { + // Bail out early, as we only care about the return value for a dryrun. + return true; + } } } if (cr.hasFlag(Context.BIND_TREAT_LIKE_ACTIVITY)) { - app.mServices.setTreatLikeActivity(true); + if (!dryRun) { + app.mServices.setTreatLikeActivity(true); + } if (clientProcState <= PROCESS_STATE_CACHED_ACTIVITY && procState > PROCESS_STATE_CACHED_ACTIVITY) { // This is a cached process, but somebody wants us to treat it like it has @@ -2894,7 +2930,9 @@ public class OomAdjuster { if (a != null && adj > FOREGROUND_APP_ADJ && a.isActivityVisible()) { adj = FOREGROUND_APP_ADJ; - state.setCurRawAdj(adj); + if (state.setCurRawAdj(adj, dryRun)) { + return true; + } if (cr.notHasFlag(Context.BIND_NOT_FOREGROUND)) { if (cr.hasFlag(Context.BIND_IMPORTANT)) { schedGroup = SCHED_GROUP_TOP_APP_BOUND; @@ -2902,16 +2940,18 @@ public class OomAdjuster { schedGroup = SCHED_GROUP_DEFAULT; } } - state.setCached(false); - state.setAdjType("service"); - state.setAdjTypeCode(ActivityManager.RunningAppProcessInfo - .REASON_SERVICE_IN_USE); - state.setAdjSource(a); - state.setAdjSourceProcState(procState); - state.setAdjTarget(cr.binding.service.instanceName); - if (DEBUG_OOM_ADJ_REASON || logUid == appUid) { - reportOomAdjMessageLocked(TAG_OOM_ADJ, - "Raise to service w/activity: " + app); + if (!dryRun) { + state.setCached(false); + state.setAdjType("service"); + state.setAdjTypeCode(ActivityManager.RunningAppProcessInfo + .REASON_SERVICE_IN_USE); + state.setAdjSource(a); + state.setAdjSourceProcState(procState); + state.setAdjTarget(cr.binding.service.instanceName); + if (DEBUG_OOM_ADJ_REASON || logUid == appUid) { + reportOomAdjMessageLocked(TAG_OOM_ADJ, + "Raise to service w/activity: " + app); + } } } } @@ -2922,7 +2962,15 @@ public class OomAdjuster { if (procState > PROCESS_STATE_BOUND_FOREGROUND_SERVICE) { capability &= ~PROCESS_CAPABILITY_BFSL; } + if (!updated) { + updated = adj < prevRawAdj || procState < prevProcState || schedGroup > prevSchedGroup + || (capability != prevCapability + && (capability & prevCapability) == prevCapability); + } + if (dryRun) { + return updated; + } if (adj < prevRawAdj) { schedGroup = setIntermediateAdjLSP(app, adj, prevRawAdj, schedGroup); } @@ -2935,15 +2983,16 @@ public class OomAdjuster { state.setCurCapability(capability); state.setEmpty(false); + return updated; } - protected void computeProviderHostOomAdjLSP(ContentProviderConnection conn, ProcessRecord app, - ProcessRecord client, long now, ProcessRecord topApp, boolean doingAll, - boolean cycleReEval, boolean computeClients, int oomAdjReason, int cachedAdj, - boolean couldRecurse) { + protected boolean computeProviderHostOomAdjLSP(ContentProviderConnection conn, + ProcessRecord app, ProcessRecord client, long now, ProcessRecord topApp, + boolean doingAll, boolean cycleReEval, boolean computeClients, int oomAdjReason, + int cachedAdj, boolean couldRecurse, boolean dryRun) { if (app.isPendingFinishAttach()) { // We've set the attaching process state in the computeInitialOomAdjLSP. Skip it here. - return; + return false; } final ProcessStateRecord state = app.mState; @@ -2951,7 +3000,7 @@ public class OomAdjuster { if (client == app) { // Being our own client is not interesting. - return; + return false; } if (couldRecurse) { if (computeClients) { @@ -2964,7 +3013,7 @@ public class OomAdjuster { if (shouldSkipDueToCycle(app, cstate, state.getCurRawProcState(), state.getCurRawAdj(), cycleReEval)) { - return; + return false; } } @@ -2979,6 +3028,7 @@ public class OomAdjuster { final int prevRawAdj = adj; final int prevProcState = procState; final int prevSchedGroup = schedGroup; + final int prevCapability = capability; final int appUid = app.info.uid; final int logUid = mService.mCurOomAdjUid; @@ -2995,14 +3045,19 @@ public class OomAdjuster { } if (client.mOptRecord.shouldNotFreeze()) { // Propagate the shouldNotFreeze flag down the bindings. - app.mOptRecord.setShouldNotFreeze(true); + if (app.mOptRecord.setShouldNotFreeze(true, dryRun)) { + // Bail out early, as we only care about the return value for a dryrun. + return true; + } } - state.setCurBoundByNonBgRestrictedApp(state.isCurBoundByNonBgRestrictedApp() - || cstate.isCurBoundByNonBgRestrictedApp() - || clientProcState <= PROCESS_STATE_BOUND_TOP - || (clientProcState == PROCESS_STATE_FOREGROUND_SERVICE - && !cstate.isBackgroundRestricted())); + if (!dryRun) { + state.setCurBoundByNonBgRestrictedApp(state.isCurBoundByNonBgRestrictedApp() + || cstate.isCurBoundByNonBgRestrictedApp() + || clientProcState <= PROCESS_STATE_BOUND_TOP + || (clientProcState == PROCESS_STATE_FOREGROUND_SERVICE + && !cstate.isBackgroundRestricted())); + } String adjType = null; if (adj > clientAdj) { @@ -3011,10 +3066,16 @@ public class OomAdjuster { adjType = "cch-ui-provider"; } else { adj = Math.max(clientAdj, FOREGROUND_APP_ADJ); - state.setCurRawAdj(adj); + if (state.setCurRawAdj(adj, dryRun)) { + // Bail out early, as we only care about the return value for a dryrun. + return true; + } adjType = "provider"; } - state.setCached(state.isCached() & cstate.isCached()); + if (state.setCached(state.isCached() & cstate.isCached(), dryRun)) { + // Bail out early, as we only care about the return value for a dryrun. + return true; + } } if (clientProcState <= PROCESS_STATE_FOREGROUND_SERVICE) { @@ -3028,15 +3089,20 @@ public class OomAdjuster { } } - conn.trackProcState(clientProcState, mAdjSeq); + if (!dryRun) { + conn.trackProcState(clientProcState, mAdjSeq); + } if (procState > clientProcState) { procState = clientProcState; - state.setCurRawProcState(procState); + if (state.setCurRawProcState(procState, dryRun)) { + // Bail out early, as we only care about the return value for a dryrun. + return true; + } } if (cstate.getCurrentSchedulingGroup() > schedGroup) { schedGroup = SCHED_GROUP_DEFAULT; } - if (adjType != null) { + if (adjType != null && !dryRun) { state.setAdjType(adjType); state.setAdjTypeCode(ActivityManager.RunningAppProcessInfo .REASON_PROVIDER_IN_USE); @@ -3056,6 +3122,12 @@ public class OomAdjuster { capability &= ~PROCESS_CAPABILITY_BFSL; } + if (dryRun && (adj < prevRawAdj || procState < prevProcState || schedGroup > prevSchedGroup + || (capability != prevCapability + && (capability & prevCapability) == prevCapability))) { + return true; + } + if (adj < prevRawAdj) { schedGroup = setIntermediateAdjLSP(app, adj, prevRawAdj, schedGroup); } @@ -3068,6 +3140,7 @@ public class OomAdjuster { state.setCurCapability(capability); state.setEmpty(false); + return false; } protected int getDefaultCapability(ProcessRecord app, int procState) { @@ -3342,7 +3415,7 @@ public class OomAdjuster { changes |= ActivityManagerService.ProcessChangeItem.CHANGE_ACTIVITIES; } - updateAppFreezeStateLSP(app, oomAdjReson); + updateAppFreezeStateLSP(app, oomAdjReson, false); if (state.getReportedProcState() != state.getCurProcState()) { state.setReportedProcState(state.getCurProcState()); @@ -3727,7 +3800,8 @@ public class OomAdjuster { } @GuardedBy({"mService", "mProcLock"}) - private void updateAppFreezeStateLSP(ProcessRecord app, @OomAdjReason int oomAdjReason) { + void updateAppFreezeStateLSP(ProcessRecord app, @OomAdjReason int oomAdjReason, + boolean immediate) { if (!mCachedAppOptimizer.useFreezer()) { return; } @@ -3746,10 +3820,14 @@ public class OomAdjuster { final ProcessStateRecord state = app.mState; // Use current adjustment when freezing, set adjustment when unfreezing. - if (state.getCurAdj() >= CACHED_APP_MIN_ADJ && !opt.isFrozen() + if (state.getCurAdj() >= FREEZER_CUTOFF_ADJ && !opt.isFrozen() && !opt.shouldNotFreeze()) { - mCachedAppOptimizer.freezeAppAsyncLSP(app); - } else if (state.getSetAdj() < CACHED_APP_MIN_ADJ) { + if (!immediate) { + mCachedAppOptimizer.freezeAppAsyncLSP(app); + } else { + mCachedAppOptimizer.freezeAppAsyncAtEarliestLSP(app); + } + } else if (state.getSetAdj() < FREEZER_CUTOFF_ADJ) { mCachedAppOptimizer.unfreezeAppLSP(app, CachedAppOptimizer.getUnfreezeReasonCodeFromOomAdjReason(oomAdjReason)); } @@ -3826,4 +3904,89 @@ public class OomAdjuster { // The caller will set the initial value in this implementation. return app.mState.isCurBoundByNonBgRestrictedApp(); } + + /** + * Evaluate the service connection, return {@code true} if the client will change the state + * of the service host process by the given connection. + */ + @GuardedBy("mService") + boolean evaluateServiceConnectionAdd(ProcessRecord client, ProcessRecord app, + ConnectionRecord cr) { + if (evaluateConnectionPrelude(client, app)) { + return true; + } + if (app.getSetAdj() <= client.getSetAdj() + && app.getSetProcState() <= client.getSetProcState() + && ((app.getSetCapability() & client.getSetCapability()) + == client.getSetCapability() + || cr.notHasFlag(Context.BIND_INCLUDE_CAPABILITIES + | Context.BIND_BYPASS_USER_NETWORK_RESTRICTIONS))) { + // The service host process has better states than the client, so no change. + return false; + } + // Take a dry run of the computeServiceHostOomAdjLSP, this would't be expensive + // since it's only evaluating one service connection. + return computeServiceHostOomAdjLSP(cr, app, client, SystemClock.uptimeMillis(), + mService.getTopApp(), false, false, false, OOM_ADJ_REASON_NONE, + CACHED_APP_MIN_ADJ, false, true /* dryRun */); + } + + @GuardedBy("mService") + boolean evaluateServiceConnectionRemoval(ProcessRecord client, ProcessRecord app, + ConnectionRecord cr) { + if (evaluateConnectionPrelude(client, app)) { + return true; + } + + if (app.getSetAdj() < client.getSetAdj() + && app.getSetProcState() < client.getSetProcState()) { + // The service host process has better states than the client. + if (((app.getSetCapability() & client.getSetCapability()) == PROCESS_CAPABILITY_NONE) + || cr.notHasFlag(Context.BIND_INCLUDE_CAPABILITIES + | Context.BIND_BYPASS_USER_NETWORK_RESTRICTIONS)) { + // The service host app doesn't get any capabilities from the client. + return false; + } + } + return true; + } + + @GuardedBy("mService") + boolean evaluateProviderConnectionAdd(ProcessRecord client, ProcessRecord app) { + if (evaluateConnectionPrelude(client, app)) { + return true; + } + if (app.getSetAdj() <= client.getSetAdj() + && app.getSetProcState() <= client.getSetProcState()) { + // The provider host process has better states than the client, so no change. + return false; + } + return computeProviderHostOomAdjLSP(null, app, client, SystemClock.uptimeMillis(), + mService.getTopApp(), false, false, false, OOM_ADJ_REASON_NONE, CACHED_APP_MIN_ADJ, + false, true /* dryRun */); + } + + @GuardedBy("mService") + boolean evaluateProviderConnectionRemoval(ProcessRecord client, ProcessRecord app) { + if (evaluateConnectionPrelude(client, app)) { + return true; + } + if (app.getSetAdj() < client.getSetAdj() + && app.getSetProcState() < client.getSetProcState()) { + // The provider host process has better states than the client, so no change. + return false; + } + return true; + } + + private boolean evaluateConnectionPrelude(ProcessRecord client, ProcessRecord app) { + if (client == null || app == null) { + return true; + } + if (app.isSdkSandbox || app.isolated || app.isKilledByAm() || app.isKilled()) { + // Let's always re-evaluate them for now. + return true; + } + return false; + } } diff --git a/services/core/java/com/android/server/am/OomAdjusterModernImpl.java b/services/core/java/com/android/server/am/OomAdjusterModernImpl.java index 5a3fbe9a66ac..f85b03e8b4eb 100644 --- a/services/core/java/com/android/server/am/OomAdjusterModernImpl.java +++ b/services/core/java/com/android/server/am/OomAdjusterModernImpl.java @@ -1002,7 +1002,7 @@ public class OomAdjusterModernImpl extends OomAdjuster { computeServiceHostOomAdjLSP(cr, service, app, now, topApp, fullUpdate, false, false, - oomAdjReason, cachedAdj, false); + oomAdjReason, cachedAdj, false, false); } for (int i = psr.numberOfSdkSandboxConnections() - 1; i >= 0; i--) { @@ -1018,7 +1018,7 @@ public class OomAdjusterModernImpl extends OomAdjuster { } computeServiceHostOomAdjLSP(cr, service, app, now, topApp, fullUpdate, false, false, - oomAdjReason, cachedAdj, false); + oomAdjReason, cachedAdj, false, false); } final ProcessProviderRecord ppr = app.mProviders; @@ -1035,7 +1035,7 @@ public class OomAdjusterModernImpl extends OomAdjuster { } computeProviderHostOomAdjLSP(cpc, provider, app, now, topApp, fullUpdate, false, false, - oomAdjReason, cachedAdj, false); + oomAdjReason, cachedAdj, false, false); } } } diff --git a/services/core/java/com/android/server/am/ProcessCachedOptimizerRecord.java b/services/core/java/com/android/server/am/ProcessCachedOptimizerRecord.java index f5c5ea8ae55a..a8fe734f59b7 100644 --- a/services/core/java/com/android/server/am/ProcessCachedOptimizerRecord.java +++ b/services/core/java/com/android/server/am/ProcessCachedOptimizerRecord.java @@ -274,7 +274,20 @@ final class ProcessCachedOptimizerRecord { @GuardedBy("mProcLock") void setShouldNotFreeze(boolean shouldNotFreeze) { + setShouldNotFreeze(shouldNotFreeze, false); + } + + /** + * @return {@code true} if it's a dry run and it's going to unfreeze the process + * if it was a real run. + */ + @GuardedBy("mProcLock") + boolean setShouldNotFreeze(boolean shouldNotFreeze, boolean dryRun) { + if (dryRun) { + return mFrozen && !shouldNotFreeze; + } mShouldNotFreeze = shouldNotFreeze; + return false; } @GuardedBy("mProcLock") diff --git a/services/core/java/com/android/server/am/ProcessList.java b/services/core/java/com/android/server/am/ProcessList.java index f5c34a5da1c1..10cd6e5bb136 100644 --- a/services/core/java/com/android/server/am/ProcessList.java +++ b/services/core/java/com/android/server/am/ProcessList.java @@ -371,6 +371,12 @@ public final class ProcessList { private static final long LMKD_RECONNECT_DELAY_MS = 1000; /** + * The cuttoff adj for the freezer, app processes with adj greater than this value will be + * eligible for the freezer. + */ + static final int FREEZER_CUTOFF_ADJ = CACHED_APP_MIN_ADJ; + + /** * Apps have no access to the private data directories of any other app, even if the other * app has made them world-readable. */ diff --git a/services/core/java/com/android/server/am/ProcessRecord.java b/services/core/java/com/android/server/am/ProcessRecord.java index e5c4a66562c3..de6f034b62ee 100644 --- a/services/core/java/com/android/server/am/ProcessRecord.java +++ b/services/core/java/com/android/server/am/ProcessRecord.java @@ -720,6 +720,11 @@ class ProcessRecord implements WindowProcessListener { return mState.getSetProcState(); } + @GuardedBy(anyOf = {"mService", "mProcLock"}) + int getSetCapability() { + return mState.getSetCapability(); + } + @GuardedBy({"mService", "mProcLock"}) public void makeActive(IApplicationThread thread, ProcessStatsService tracker) { mProfile.onProcessActive(thread, tracker); @@ -1401,8 +1406,12 @@ class ProcessRecord implements WindowProcessListener { void onProcessUnfrozen() { mProfile.onProcessUnfrozen(); + mServices.onProcessUnfrozen(); } + void onProcessFrozenCancelled() { + mServices.onProcessFrozenCancelled(); + } /* * Delete all packages from list except the package indicated in info @@ -1644,6 +1653,13 @@ class ProcessRecord implements WindowProcessListener { return mWasForceStopped; } + boolean isFreezable() { + return mService.mOomAdjuster.mCachedAppOptimizer.useFreezer() + && !mOptRecord.isFreezeExempt() + && !mOptRecord.shouldNotFreeze() + && mState.getCurAdj() >= ProcessList.FREEZER_CUTOFF_ADJ; + } + /** * Traverses all client processes and feed them to consumer. */ diff --git a/services/core/java/com/android/server/am/ProcessServiceRecord.java b/services/core/java/com/android/server/am/ProcessServiceRecord.java index f5f2b102596b..57d233e7c503 100644 --- a/services/core/java/com/android/server/am/ProcessServiceRecord.java +++ b/services/core/java/com/android/server/am/ProcessServiceRecord.java @@ -19,6 +19,8 @@ package com.android.server.am; import static android.app.ProcessMemoryState.HOSTING_COMPONENT_TYPE_BOUND_SERVICE; import static android.app.ProcessMemoryState.HOSTING_COMPONENT_TYPE_FOREGROUND_SERVICE; +import static com.android.server.am.Flags.serviceBindingOomAdjPolicy; + import android.annotation.Nullable; import android.app.ActivityManager; import android.content.Context; @@ -144,6 +146,11 @@ final class ProcessServiceRecord { */ private ArraySet<Integer> mBoundClientUids = new ArraySet<>(); + /** + * The process should schedule a service timeout timer but haven't done so. + */ + private boolean mScheduleServiceTimeoutPending; + final ProcessRecord mApp; private final ActivityManagerService mService; @@ -657,6 +664,41 @@ final class ProcessServiceRecord { setHasClientActivities(false); } + @GuardedBy("mService") + void noteScheduleServiceTimeoutPending(boolean pending) { + mScheduleServiceTimeoutPending = pending; + } + + @GuardedBy("mService") + boolean isScheduleServiceTimeoutPending() { + return mScheduleServiceTimeoutPending; + } + + @GuardedBy("mService") + void onProcessUnfrozen() { + scheduleServiceTimeoutIfNeededLocked(); + } + + @GuardedBy("mService") + void onProcessFrozenCancelled() { + scheduleServiceTimeoutIfNeededLocked(); + } + + @GuardedBy("mService") + private void scheduleServiceTimeoutIfNeededLocked() { + if (!serviceBindingOomAdjPolicy()) { + return; + } + if (mScheduleServiceTimeoutPending && mExecutingServices.size() > 0) { + mService.mServices.scheduleServiceTimeoutLocked(mApp); + // We'll need to reset the executingStart since the app was frozen. + final long now = SystemClock.uptimeMillis(); + for (int i = 0, size = mExecutingServices.size(); i < size; i++) { + mExecutingServices.valueAt(i).executingStart = now; + } + } + } + void dump(PrintWriter pw, String prefix, long nowUptime) { if (mHasForegroundServices || mApp.mState.getForcingToImportant() != null) { pw.print(prefix); pw.print("mHasForegroundServices="); pw.print(mHasForegroundServices); @@ -701,5 +743,10 @@ final class ProcessServiceRecord { pw.print(prefix); pw.print(" - "); pw.println(mConnections.valueAt(i)); } } + if (serviceBindingOomAdjPolicy()) { + pw.print(prefix); + pw.print("scheduleServiceTimeoutPending="); + pw.println(mScheduleServiceTimeoutPending); + } } } diff --git a/services/core/java/com/android/server/am/ProcessStateRecord.java b/services/core/java/com/android/server/am/ProcessStateRecord.java index 3391ec7c4e68..8362eaf76d55 100644 --- a/services/core/java/com/android/server/am/ProcessStateRecord.java +++ b/services/core/java/com/android/server/am/ProcessStateRecord.java @@ -479,9 +479,22 @@ final class ProcessStateRecord { @GuardedBy({"mService", "mProcLock"}) void setCurRawAdj(int curRawAdj) { + setCurRawAdj(curRawAdj, false); + } + + /** + * @return {@code true} if it's a dry run and it's going to bump the adj score of the process + * if it was a real run. + */ + @GuardedBy({"mService", "mProcLock"}) + boolean setCurRawAdj(int curRawAdj, boolean dryRun) { + if (dryRun) { + return mCurRawAdj > curRawAdj; + } mCurRawAdj = curRawAdj; mApp.getWindowProcessController().setPerceptible( curRawAdj <= ProcessList.PERCEPTIBLE_APP_ADJ); + return false; } @GuardedBy(anyOf = {"mService", "mProcLock"}) @@ -594,7 +607,20 @@ final class ProcessStateRecord { @GuardedBy({"mService", "mProcLock"}) void setCurRawProcState(int curRawProcState) { + setCurRawProcState(curRawProcState, false); + } + + /** + * @return {@code true} if it's a dry run and it's going to bump the procstate of the process + * if it was a real run. + */ + @GuardedBy({"mService", "mProcLock"}) + boolean setCurRawProcState(int curRawProcState, boolean dryRun) { + if (dryRun) { + return mCurRawProcState > curRawProcState; + } mCurRawProcState = curRawProcState; + return false; } @GuardedBy(anyOf = {"mService", "mProcLock"}) @@ -900,7 +926,20 @@ final class ProcessStateRecord { @GuardedBy("mService") void setCached(boolean cached) { + setCached(cached, false); + } + + /** + * @return {@code true} if it's a dry run and it's going to uncache the process + * if it was a real run. + */ + @GuardedBy("mService") + boolean setCached(boolean cached, boolean dryRun) { + if (dryRun) { + return mCached && !cached; + } mCached = cached; + return false; } @GuardedBy("mService") diff --git a/services/core/java/com/android/server/am/flags.aconfig b/services/core/java/com/android/server/am/flags.aconfig index 654aebd89de2..31d9cc927223 100644 --- a/services/core/java/com/android/server/am/flags.aconfig +++ b/services/core/java/com/android/server/am/flags.aconfig @@ -35,3 +35,10 @@ flag { description: "Enable the new FGS restriction logic" bug: "276963716" } + +flag { + name: "service_binding_oom_adj_policy" + namespace: "backstage_power" + description: "Optimize the service bindings by different policies like skipping oom adjuster" + bug: "318717054" +} diff --git a/services/core/java/com/android/server/audio/AudioDeviceInventory.java b/services/core/java/com/android/server/audio/AudioDeviceInventory.java index 57b19cda7c12..690c37a9349a 100644 --- a/services/core/java/com/android/server/audio/AudioDeviceInventory.java +++ b/services/core/java/com/android/server/audio/AudioDeviceInventory.java @@ -914,28 +914,27 @@ public class AudioDeviceInventory { di.mDeviceCodecFormat = codec; mConnectedDevices.replace(key, di); codecChange = true; - } - final int res = mAudioSystem.handleDeviceConfigChange( - btInfo.mAudioSystemDevice, address, BtHelper.getName(btDevice), codec); - - if (res != AudioSystem.AUDIO_STATUS_OK) { - AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent( - "APM handleDeviceConfigChange failed for A2DP device addr=" - + address + " codec=" - + AudioSystem.audioFormatToString(codec)) - .printLog(TAG)); - - // force A2DP device disconnection in case of error so that AudioService - // state is consistent with audio policy manager state - setBluetoothActiveDevice(new AudioDeviceBroker.BtDeviceInfo(btInfo, - BluetoothProfile.STATE_DISCONNECTED)); - } else { - AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent( - "APM handleDeviceConfigChange success for A2DP device addr=" - + address - + " codec=" + AudioSystem.audioFormatToString(codec)) - .printLog(TAG)); - + final int res = mAudioSystem.handleDeviceConfigChange( + btInfo.mAudioSystemDevice, address, + BtHelper.getName(btDevice), codec); + if (res != AudioSystem.AUDIO_STATUS_OK) { + AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent( + "APM handleDeviceConfigChange failed for A2DP device addr=" + + address + " codec=" + + AudioSystem.audioFormatToString(codec)) + .printLog(TAG)); + + // force A2DP device disconnection in case of error so that AudioService + // state is consistent with audio policy manager state + setBluetoothActiveDevice(new AudioDeviceBroker.BtDeviceInfo(btInfo, + BluetoothProfile.STATE_DISCONNECTED)); + } else { + AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent( + "APM handleDeviceConfigChange success for A2DP device addr=" + + address + + " codec=" + AudioSystem.audioFormatToString(codec)) + .printLog(TAG)); + } } } if (!codecChange) { diff --git a/services/core/java/com/android/server/audio/SoundDoseHelper.java b/services/core/java/com/android/server/audio/SoundDoseHelper.java index a30cdc47a461..9610034caf01 100644 --- a/services/core/java/com/android/server/audio/SoundDoseHelper.java +++ b/services/core/java/com/android/server/audio/SoundDoseHelper.java @@ -1203,7 +1203,7 @@ public class SoundDoseHelper { @GuardedBy("mCsdStateLock") private void sanitizeDoseRecords_l() { if (mDoseRecords.size() > MAX_NUMBER_OF_CACHED_RECORDS) { - int nrToRemove = MAX_NUMBER_OF_CACHED_RECORDS - mDoseRecords.size(); + int nrToRemove = mDoseRecords.size() - MAX_NUMBER_OF_CACHED_RECORDS; Log.w(TAG, "Removing " + nrToRemove + " records from the total of " + mDoseRecords.size()); // Remove older elements to fit into persisted settings max length diff --git a/services/core/java/com/android/server/biometrics/AuthService.java b/services/core/java/com/android/server/biometrics/AuthService.java index 21e6bac53cde..3f3540e2868c 100644 --- a/services/core/java/com/android/server/biometrics/AuthService.java +++ b/services/core/java/com/android/server/biometrics/AuthService.java @@ -20,7 +20,7 @@ package com.android.server.biometrics; // TODO(b/141025588): Create separate internal and external permissions for AuthService. // TODO(b/141025588): Get rid of the USE_FINGERPRINT permission. -import static android.Manifest.permission.MANAGE_BIOMETRIC_DIALOG; +import static android.Manifest.permission.SET_BIOMETRIC_DIALOG_LOGO; import static android.Manifest.permission.TEST_BIOMETRIC; import static android.Manifest.permission.USE_BIOMETRIC; import static android.Manifest.permission.USE_BIOMETRIC_INTERNAL; @@ -305,7 +305,7 @@ public class AuthService extends SystemService { if (promptInfo.containsPrivateApiConfigurations()) { checkInternalPermission(); } - if (promptInfo.containsManageBioApiConfigurations()) { + if (promptInfo.containsSetLogoApiConfigurations()) { checkManageBiometricPermission(); } @@ -997,8 +997,8 @@ public class AuthService extends SystemService { } private void checkManageBiometricPermission() { - getContext().enforceCallingOrSelfPermission(MANAGE_BIOMETRIC_DIALOG, - "Must have MANAGE_BIOMETRIC_DIALOG permission"); + getContext().enforceCallingOrSelfPermission(SET_BIOMETRIC_DIALOG_LOGO, + "Must have SET_BIOMETRIC_DIALOG_LOGO permission"); } private void checkPermission() { diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java index 9cf9119aff82..245fcea06eac 100644 --- a/services/core/java/com/android/server/display/DisplayManagerService.java +++ b/services/core/java/com/android/server/display/DisplayManagerService.java @@ -2776,17 +2776,17 @@ public final class DisplayManagerService extends SystemService { } private ScreenCapture.ScreenshotHardwareBuffer userScreenshotInternal(int displayId) { + final ScreenCapture.DisplayCaptureArgs captureArgs; synchronized (mSyncRoot) { final IBinder token = getDisplayToken(displayId); if (token == null) { return null; } - final ScreenCapture.DisplayCaptureArgs captureArgs = - new ScreenCapture.DisplayCaptureArgs.Builder(token) + captureArgs = new ScreenCapture.DisplayCaptureArgs.Builder(token) .build(); - return ScreenCapture.captureDisplay(captureArgs); } + return ScreenCapture.captureDisplay(captureArgs); } @VisibleForTesting diff --git a/services/core/java/com/android/server/input/InputManagerInternal.java b/services/core/java/com/android/server/input/InputManagerInternal.java index 380106ba486d..b963a4b614e8 100644 --- a/services/core/java/com/android/server/input/InputManagerInternal.java +++ b/services/core/java/com/android/server/input/InputManagerInternal.java @@ -87,12 +87,16 @@ public abstract class InputManagerInternal { * connected, the caller may be blocked for an arbitrary period of time. * * @return true if the pointer displayId was set successfully, or false if it fails. + * + * @deprecated TODO(b/293587049): Not needed - remove after Pointer Icon Refactor is complete. */ public abstract boolean setVirtualMousePointerDisplayId(int pointerDisplayId); /** * Gets the display id that the MouseCursorController is being forced to target. Returns * {@link android.view.Display#INVALID_DISPLAY} if there is no override + * + * @deprecated TODO(b/293587049): Not needed - remove after Pointer Icon Refactor is complete. */ public abstract int getVirtualMousePointerDisplayId(); @@ -101,7 +105,7 @@ public abstract class InputManagerInternal { * * Returns NaN-s as the coordinates if the cursor is not available. */ - public abstract PointF getCursorPosition(); + public abstract PointF getCursorPosition(int displayId); /** * Enables or disables pointer acceleration for mouse movements. diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java index 67c23fc4db12..687def05b1d7 100644 --- a/services/core/java/com/android/server/input/InputManagerService.java +++ b/services/core/java/com/android/server/input/InputManagerService.java @@ -1448,6 +1448,10 @@ public class InputManagerService extends IInputManager.Stub } private boolean setVirtualMousePointerDisplayIdBlocking(int overrideDisplayId) { + if (com.android.input.flags.Flags.enablePointerChoreographer()) { + throw new IllegalStateException( + "This must not be used when PointerChoreographer is enabled"); + } final boolean isRemovingOverride = overrideDisplayId == Display.INVALID_DISPLAY; // Take care to not make calls to window manager while holding internal locks. @@ -1486,6 +1490,10 @@ public class InputManagerService extends IInputManager.Stub } private int getVirtualMousePointerDisplayId() { + if (com.android.input.flags.Flags.enablePointerChoreographer()) { + throw new IllegalStateException( + "This must not be used when PointerChoreographer is enabled"); + } synchronized (mAdditionalDisplayInputPropertiesLock) { return mOverriddenPointerDisplayId; } @@ -3332,8 +3340,8 @@ public class InputManagerService extends IInputManager.Stub } @Override - public PointF getCursorPosition() { - final float[] p = mNative.getMouseCursorPosition(); + public PointF getCursorPosition(int displayId) { + final float[] p = mNative.getMouseCursorPosition(displayId); if (p == null || p.length != 2) { throw new IllegalStateException("Failed to get mouse cursor position"); } @@ -3614,6 +3622,13 @@ public class InputManagerService extends IInputManager.Stub } /** + * Sets Accessibility slow keys threshold in milliseconds. + */ + public void setAccessibilitySlowKeysThreshold(int thresholdTimeMs) { + mNative.setAccessibilitySlowKeysThreshold(thresholdTimeMs); + } + + /** * Sets whether Accessibility sticky keys is enabled. */ public void setAccessibilityStickyKeysEnabled(boolean enabled) { diff --git a/services/core/java/com/android/server/input/InputSettingsObserver.java b/services/core/java/com/android/server/input/InputSettingsObserver.java index 572d844d752d..165dfe445751 100644 --- a/services/core/java/com/android/server/input/InputSettingsObserver.java +++ b/services/core/java/com/android/server/input/InputSettingsObserver.java @@ -89,6 +89,8 @@ class InputSettingsObserver extends ContentObserver { (reason) -> updateShowRotaryInput()), Map.entry(Settings.Secure.getUriFor(Settings.Secure.ACCESSIBILITY_BOUNCE_KEYS), (reason) -> updateAccessibilityBounceKeys()), + Map.entry(Settings.Secure.getUriFor(Settings.Secure.ACCESSIBILITY_SLOW_KEYS), + (reason) -> updateAccessibilitySlowKeys()), Map.entry(Settings.Secure.getUriFor(Settings.Secure.ACCESSIBILITY_STICKY_KEYS), (reason) -> updateAccessibilityStickyKeys())); } @@ -228,6 +230,11 @@ class InputSettingsObserver extends ContentObserver { InputSettings.getAccessibilityBounceKeysThreshold(mContext)); } + private void updateAccessibilitySlowKeys() { + mService.setAccessibilitySlowKeysThreshold( + InputSettings.getAccessibilitySlowKeysThreshold(mContext)); + } + private void updateAccessibilityStickyKeys() { mService.setAccessibilityStickyKeysEnabled( InputSettings.isAccessibilityStickyKeysEnabled(mContext)); diff --git a/services/core/java/com/android/server/input/NativeInputManagerService.java b/services/core/java/com/android/server/input/NativeInputManagerService.java index 8aec8ca7a23f..bc8207835a6e 100644 --- a/services/core/java/com/android/server/input/NativeInputManagerService.java +++ b/services/core/java/com/android/server/input/NativeInputManagerService.java @@ -233,14 +233,15 @@ interface NativeInputManagerService { void setStylusButtonMotionEventsEnabled(boolean enabled); /** - * Get the current position of the mouse cursor. + * Get the current position of the mouse cursor on the given display. * - * If the mouse cursor is not currently shown, the coordinate values will be NaN-s. + * If the mouse cursor is not currently shown, the coordinate values will be NaN-s. Use + * {@link android.view.Display#INVALID_DISPLAY} to get the position of the default mouse cursor. * * NOTE: This will grab the PointerController's lock, so we must be careful about calling this * from the InputReader or Display threads, which may result in a deadlock. */ - float[] getMouseCursorPosition(); + float[] getMouseCursorPosition(int displayId); /** Set whether showing a pointer icon for styluses is enabled. */ void setStylusPointerIconEnabled(boolean enabled); @@ -257,6 +258,11 @@ interface NativeInputManagerService { void setAccessibilityBounceKeysThreshold(int thresholdTimeMs); /** + * Notify if Accessibility slow keys threshold is changed from InputSettings. + */ + void setAccessibilitySlowKeysThreshold(int thresholdTimeMs); + + /** * Notify if Accessibility sticky keys is enabled/disabled from InputSettings. */ void setAccessibilityStickyKeysEnabled(boolean enabled); @@ -514,7 +520,7 @@ interface NativeInputManagerService { public native void setStylusButtonMotionEventsEnabled(boolean enabled); @Override - public native float[] getMouseCursorPosition(); + public native float[] getMouseCursorPosition(int displayId); @Override public native void setStylusPointerIconEnabled(boolean enabled); @@ -526,6 +532,9 @@ interface NativeInputManagerService { public native void setAccessibilityBounceKeysThreshold(int thresholdTimeMs); @Override + public native void setAccessibilitySlowKeysThreshold(int thresholdTimeMs); + + @Override public native void setAccessibilityStickyKeysEnabled(boolean enabled); } } diff --git a/services/core/java/com/android/server/inputmethod/ClientController.java b/services/core/java/com/android/server/inputmethod/ClientController.java index 21b952bb7760..ece236a5f18c 100644 --- a/services/core/java/com/android/server/inputmethod/ClientController.java +++ b/services/core/java/com/android/server/inputmethod/ClientController.java @@ -21,8 +21,6 @@ import android.content.pm.PackageManagerInternal; import android.os.IBinder; import android.os.RemoteException; import android.util.ArrayMap; -import android.util.SparseArray; -import android.view.inputmethod.InputBinding; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; @@ -156,48 +154,4 @@ final class ClientController { return InputMethodUtils.checkIfPackageBelongsToUid( mPackageManagerInternal, cs.mUid, packageName); } - - static final class ClientState { - final IInputMethodClientInvoker mClient; - final IRemoteInputConnection mFallbackInputConnection; - final int mUid; - final int mPid; - final int mSelfReportedDisplayId; - final InputBinding mBinding; - final IBinder.DeathRecipient mClientDeathRecipient; - - @GuardedBy("ImfLock.class") - boolean mSessionRequested; - - @GuardedBy("ImfLock.class") - boolean mSessionRequestedForAccessibility; - - @GuardedBy("ImfLock.class") - InputMethodManagerService.SessionState mCurSession; - - @GuardedBy("ImfLock.class") - SparseArray<InputMethodManagerService.AccessibilitySessionState> mAccessibilitySessions = - new SparseArray<>(); - - @Override - public String toString() { - return "ClientState{" + Integer.toHexString( - System.identityHashCode(this)) + " mUid=" + mUid - + " mPid=" + mPid + " mSelfReportedDisplayId=" + mSelfReportedDisplayId + "}"; - } - - ClientState(IInputMethodClientInvoker client, - IRemoteInputConnection fallbackInputConnection, - int uid, int pid, int selfReportedDisplayId, - IBinder.DeathRecipient clientDeathRecipient) { - mClient = client; - mFallbackInputConnection = fallbackInputConnection; - mUid = uid; - mPid = pid; - mSelfReportedDisplayId = selfReportedDisplayId; - mBinding = new InputBinding(null /*conn*/, mFallbackInputConnection.asBinder(), mUid, - mPid); - mClientDeathRecipient = clientDeathRecipient; - } - } } diff --git a/services/core/java/com/android/server/inputmethod/ClientState.java b/services/core/java/com/android/server/inputmethod/ClientState.java new file mode 100644 index 000000000000..e98a5a73ab90 --- /dev/null +++ b/services/core/java/com/android/server/inputmethod/ClientState.java @@ -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.server.inputmethod; + +import android.os.IBinder; +import android.util.SparseArray; +import android.view.inputmethod.InputBinding; + +import com.android.internal.annotations.GuardedBy; +import com.android.internal.inputmethod.IRemoteInputConnection; + +final class ClientState { + final IInputMethodClientInvoker mClient; + final IRemoteInputConnection mFallbackInputConnection; + final int mUid; + final int mPid; + final int mSelfReportedDisplayId; + final InputBinding mBinding; + final IBinder.DeathRecipient mClientDeathRecipient; + + @GuardedBy("ImfLock.class") + boolean mSessionRequested; + + @GuardedBy("ImfLock.class") + boolean mSessionRequestedForAccessibility; + + @GuardedBy("ImfLock.class") + InputMethodManagerService.SessionState mCurSession; + + @GuardedBy("ImfLock.class") + SparseArray<InputMethodManagerService.AccessibilitySessionState> mAccessibilitySessions = + new SparseArray<>(); + + @Override + public String toString() { + return "ClientState{" + Integer.toHexString( + System.identityHashCode(this)) + " mUid=" + mUid + + " mPid=" + mPid + " mSelfReportedDisplayId=" + mSelfReportedDisplayId + "}"; + } + + ClientState(IInputMethodClientInvoker client, + IRemoteInputConnection fallbackInputConnection, + int uid, int pid, int selfReportedDisplayId, + IBinder.DeathRecipient clientDeathRecipient) { + mClient = client; + mFallbackInputConnection = fallbackInputConnection; + mUid = uid; + mPid = pid; + mSelfReportedDisplayId = selfReportedDisplayId; + mBinding = new InputBinding(null /*conn*/, mFallbackInputConnection.asBinder(), mUid, + mPid); + mClientDeathRecipient = clientDeathRecipient; + } +} diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java index 50340d241347..4767ebd0aab0 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java @@ -48,7 +48,6 @@ import static android.view.Display.INVALID_DISPLAY; import static android.view.WindowManager.DISPLAY_IME_POLICY_HIDE; import static android.view.WindowManager.DISPLAY_IME_POLICY_LOCAL; -import static com.android.server.inputmethod.ClientController.ClientState; import static com.android.server.inputmethod.ImeVisibilityStateComputer.ImeTargetWindowState; import static com.android.server.inputmethod.ImeVisibilityStateComputer.ImeVisibilityResult; import static com.android.server.inputmethod.ImeVisibilityStateComputer.STATE_HIDE_IME; @@ -116,7 +115,6 @@ import android.util.PrintWriterPrinter; import android.util.Printer; import android.util.Slog; import android.util.SparseArray; -import android.util.SparseBooleanArray; import android.util.proto.ProtoOutputStream; import android.view.InputChannel; import android.view.InputDevice; @@ -282,8 +280,6 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub @NonNull private InputMethodSettings mSettings; final SettingsObserver mSettingsObserver; - private final SparseBooleanArray mLoggedDeniedGetInputMethodWindowVisibleHeightForUid = - new SparseBooleanArray(0); final WindowManagerInternal mWindowManagerInternal; private final ActivityManagerInternal mActivityManagerInternal; final PackageManagerInternal mPackageManagerInternal; @@ -1355,13 +1351,6 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub clearPackageChangeState(); } - @Override - public void onUidRemoved(int uid) { - synchronized (ImfLock.class) { - mLoggedDeniedGetInputMethodWindowVisibleHeightForUid.delete(uid); - } - } - private void clearPackageChangeState() { // No need to lock them because we access these fields only on getRegisteredHandler(). mChangedPackages.clear(); @@ -2176,7 +2165,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub /** * Hide the IME if the removed user is the current user. */ - private void onClientRemoved(ClientController.ClientState client) { + private void onClientRemoved(ClientState client) { synchronized (ImfLock.class) { clearClientSessionLocked(client); clearClientSessionForAccessibilityLocked(client); @@ -2400,16 +2389,6 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub @StartInputReason int startInputReason, int unverifiedTargetSdkVersion, @NonNull ImeOnBackInvokedDispatcher imeDispatcher) { - String selectedMethodId = getSelectedMethodIdLocked(); - - if (!mSystemReady) { - // If the system is not yet ready, we shouldn't be running third - // party code. - return new InputBindResult( - InputBindResult.ResultCode.ERROR_SYSTEM_NOT_READY, - null, null, null, selectedMethodId, getSequenceNumberLocked(), false); - } - if (!InputMethodUtils.checkIfPackageBelongsToUid(mPackageManagerInternal, cs.mUid, editorInfo.packageName)) { Slog.e(TAG, "Rejecting this client as it reported an invalid package name." @@ -2430,6 +2409,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub // Potentially override the selected input method if the new display belongs to a virtual // device with a custom IME. + String selectedMethodId = getSelectedMethodIdLocked(); if (oldDisplayIdToShowIme != mDisplayIdToShowIme) { final String deviceMethodId = computeCurrentDeviceMethodIdLocked(selectedMethodId); if (deviceMethodId == null) { @@ -3674,7 +3654,6 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub + "specified for cross-user startInputOrWindowGainedFocus()"); } } - if (windowToken == null) { Slog.e(TAG, "windowToken cannot be null."); return InputBindResult.NULL; @@ -3686,6 +3665,14 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub "InputMethodManagerService#startInputOrWindowGainedFocus"); final InputBindResult result; synchronized (ImfLock.class) { + 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 long ident = Binder.clearCallingIdentity(); try { result = startInputOrWindowGainedFocusInternalLocked(startInputReason, @@ -4277,10 +4264,6 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub synchronized (ImfLock.class) { if (!canInteractWithImeLocked(callingUid, client, "getInputMethodWindowVisibleHeight", null /* statsToken */)) { - if (!mLoggedDeniedGetInputMethodWindowVisibleHeightForUid.get(callingUid)) { - EventLog.writeEvent(0x534e4554, "204906124", callingUid, ""); - mLoggedDeniedGetInputMethodWindowVisibleHeightForUid.put(callingUid, true); - } return 0; } // This should probably use the caller's display id, but because this is unsupported diff --git a/services/core/java/com/android/server/location/provider/StationaryThrottlingLocationProvider.java b/services/core/java/com/android/server/location/provider/StationaryThrottlingLocationProvider.java index 5e38bca78a7c..2522f7b82353 100644 --- a/services/core/java/com/android/server/location/provider/StationaryThrottlingLocationProvider.java +++ b/services/core/java/com/android/server/location/provider/StationaryThrottlingLocationProvider.java @@ -27,6 +27,7 @@ import static java.lang.Math.max; import android.annotation.Nullable; import android.location.Location; +import android.location.LocationRequest; import android.location.LocationResult; import android.location.provider.ProviderRequest; import android.os.SystemClock; @@ -179,6 +180,7 @@ public final class StationaryThrottlingLocationProvider extends DelegateLocation private void onThrottlingChangedLocked(boolean deliverImmediate) { long throttlingIntervalMs = INTERVAL_DISABLED; if (mDeviceStationary && mDeviceIdle && !mIncomingRequest.isLocationSettingsIgnored() + && mIncomingRequest.getQuality() != LocationRequest.QUALITY_HIGH_ACCURACY && mLastLocation != null && mLastLocation.getElapsedRealtimeAgeMillis(mDeviceStationaryRealtimeMs) <= MAX_STATIONARY_LOCATION_AGE_MS) { diff --git a/services/core/java/com/android/server/media/MediaSessionService.java b/services/core/java/com/android/server/media/MediaSessionService.java index 2cd3ab1ddbbb..1d516e2931d7 100644 --- a/services/core/java/com/android/server/media/MediaSessionService.java +++ b/services/core/java/com/android/server/media/MediaSessionService.java @@ -287,10 +287,14 @@ public class MediaSessionService extends SystemService implements Monitor { } user.mPriorityStack.onSessionActiveStateChanged(record); } - setForegroundServiceAllowance( - record, - /* allowRunningInForeground= */ record.isActive() - && (playbackState == null || playbackState.isActive())); + boolean allowRunningInForeground = record.isActive() + && (playbackState == null || playbackState.isActive()); + + Log.d(TAG, "onSessionActiveStateChanged: " + + "record=" + record + + "playbackState=" + playbackState + + "allowRunningInForeground=" + allowRunningInForeground); + setForegroundServiceAllowance(record, allowRunningInForeground); mHandler.postSessionsChanged(record); } } @@ -388,10 +392,12 @@ public class MediaSessionService extends SystemService implements Monitor { } user.mPriorityStack.onPlaybackStateChanged(record, shouldUpdatePriority); if (playbackState != null) { - setForegroundServiceAllowance( - record, - /* allowRunningInForeground= */ playbackState.isActive() - && record.isActive()); + boolean allowRunningInForeground = playbackState.isActive() && record.isActive(); + Log.d(TAG, "onSessionPlaybackStateChanged: " + + "record=" + record + + "playbackState=" + playbackState + + "allowRunningInForeground=" + allowRunningInForeground); + setForegroundServiceAllowance(record, allowRunningInForeground); } } } @@ -556,6 +562,8 @@ public class MediaSessionService extends SystemService implements Monitor { } session.close(); + + Log.d(TAG, "destroySessionLocked: record=" + session); setForegroundServiceAllowance(session, /* allowRunningInForeground= */ false); mHandler.postSessionsChanged(session); } diff --git a/services/core/java/com/android/server/media/metrics/MediaMetricsManagerService.java b/services/core/java/com/android/server/media/metrics/MediaMetricsManagerService.java index df612e63927f..bbe6d3a0c8fa 100644 --- a/services/core/java/com/android/server/media/metrics/MediaMetricsManagerService.java +++ b/services/core/java/com/android/server/media/metrics/MediaMetricsManagerService.java @@ -20,6 +20,7 @@ import android.content.Context; import android.content.pm.PackageManager; import android.media.MediaMetrics; import android.media.metrics.BundleSession; +import android.media.metrics.EditingEndedEvent; import android.media.metrics.IMediaMetricsManager; import android.media.metrics.NetworkEvent; import android.media.metrics.PlaybackErrorEvent; @@ -346,6 +347,24 @@ public final class MediaMetricsManagerService extends SystemService { StatsLog.write(statsEvent); } + @Override + public void reportEditingEndedEvent(String sessionId, EditingEndedEvent event, int userId) { + int level = loggingLevel(); + if (level == LOGGING_LEVEL_BLOCKED) { + return; + } + StatsEvent statsEvent = + StatsEvent.newBuilder() + .setAtomId(798) + .writeString(sessionId) + .writeInt(event.getFinalState()) + .writeInt(event.getErrorCode()) + .writeLong(event.getTimeSinceCreatedMillis()) + .usePooledBuffer() + .build(); + StatsLog.write(statsEvent); + } + private int loggingLevel() { synchronized (mLock) { int uid = Binder.getCallingUid(); diff --git a/services/core/java/com/android/server/notification/ZenModeHelper.java b/services/core/java/com/android/server/notification/ZenModeHelper.java index c067fa068b12..923be56dd545 100644 --- a/services/core/java/com/android/server/notification/ZenModeHelper.java +++ b/services/core/java/com/android/server/notification/ZenModeHelper.java @@ -1183,7 +1183,7 @@ public class ZenModeHelper { != newPolicy.getPriorityConversationSenders()) { userModifiedFields |= ZenPolicy.FIELD_CONVERSATIONS; } - if (oldPolicy.getPriorityChannels() != newPolicy.getPriorityChannels()) { + if (oldPolicy.getPriorityChannelsAllowed() != newPolicy.getPriorityChannelsAllowed()) { userModifiedFields |= ZenPolicy.FIELD_ALLOW_CHANNELS; } if (oldPolicy.getPriorityCategoryReminders() diff --git a/services/core/java/com/android/server/os/BugreportManagerServiceImpl.java b/services/core/java/com/android/server/os/BugreportManagerServiceImpl.java index 1660c3ef952a..8452c0e61a81 100644 --- a/services/core/java/com/android/server/os/BugreportManagerServiceImpl.java +++ b/services/core/java/com/android/server/os/BugreportManagerServiceImpl.java @@ -152,14 +152,13 @@ class BugreportManagerServiceImpl extends IDumpstate.Stub { @RequiresPermission(value = android.Manifest.permission.INTERACT_ACROSS_USERS, conditional = true) void ensureCallerPreviouslyGeneratedFile( - Context context, Pair<Integer, String> callingInfo, int userId, - String bugreportFile, boolean forceUpdateMapping) { + Context context, PackageManager packageManager, Pair<Integer, String> callingInfo, + int userId, String bugreportFile, boolean forceUpdateMapping) { synchronized (mLock) { if (onboardingBugreportV2Enabled()) { final int uidForUser = Binder.withCleanCallingIdentity(() -> { try { - return context.getPackageManager() - .getPackageUidAsUser(callingInfo.second, userId); + return packageManager.getPackageUidAsUser(callingInfo.second, userId); } catch (PackageManager.NameNotFoundException exception) { throwInvalidBugreportFileForCallerException( bugreportFile, callingInfo.second); @@ -441,8 +440,8 @@ class BugreportManagerServiceImpl extends IDumpstate.Stub { Slogf.i(TAG, "Retrieving bugreport for %s / %d", callingPackage, callingUid); try { mBugreportFileManager.ensureCallerPreviouslyGeneratedFile( - mContext, new Pair<>(callingUid, callingPackage), userId, bugreportFile, - /* forceUpdateMapping= */ false); + mContext, mContext.getPackageManager(), new Pair<>(callingUid, callingPackage), + userId, bugreportFile, /* forceUpdateMapping= */ false); } catch (IllegalArgumentException e) { Slog.e(TAG, e.getMessage()); reportError(listener, IDumpstateListener.BUGREPORT_ERROR_NO_BUGREPORT_TO_RETRIEVE); diff --git a/services/core/java/com/android/server/pm/BACKGROUND_INSTALL_OWNERS b/services/core/java/com/android/server/pm/BACKGROUND_INSTALL_OWNERS new file mode 100644 index 000000000000..baa41a55c519 --- /dev/null +++ b/services/core/java/com/android/server/pm/BACKGROUND_INSTALL_OWNERS @@ -0,0 +1,2 @@ +georgechan@google.com +wenhaowang@google.com
\ No newline at end of file diff --git a/services/core/java/com/android/server/pm/OWNERS b/services/core/java/com/android/server/pm/OWNERS index 84324f2524fc..c8bc56ce7dcd 100644 --- a/services/core/java/com/android/server/pm/OWNERS +++ b/services/core/java/com/android/server/pm/OWNERS @@ -51,3 +51,5 @@ per-file ShortcutRequestPinProcessor.java = omakoto@google.com, yamasani@google. per-file ShortcutService.java = omakoto@google.com, yamasani@google.com, sunnygoyal@google.com, mett@google.com, pinyaoting@google.com per-file ShortcutUser.java = omakoto@google.com, yamasani@google.com, sunnygoyal@google.com, mett@google.com, pinyaoting@google.com +# background install control service +per-file BackgroundInstall* = file:BACKGROUND_INSTALL_OWNERS
\ No newline at end of file diff --git a/services/core/java/com/android/server/pm/PreferredComponent.java b/services/core/java/com/android/server/pm/PreferredComponent.java index 18caafdaa56a..f3b146464864 100644 --- a/services/core/java/com/android/server/pm/PreferredComponent.java +++ b/services/core/java/com/android/server/pm/PreferredComponent.java @@ -28,7 +28,8 @@ import com.android.internal.util.XmlUtils; import com.android.modules.utils.TypedXmlPullParser; import com.android.modules.utils.TypedXmlSerializer; import com.android.server.LocalServices; -import com.android.server.pm.pkg.PackageUserState; +import com.android.server.pm.pkg.PackageStateInternal; +import com.android.server.pm.pkg.PackageUserStateInternal; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; @@ -218,11 +219,15 @@ public class PreferredComponent { continue; } - // Avoid showing the disambiguation dialog if the package which is installed with - // reason INSTALL_REASON_DEVICE_SETUP. - final PackageUserState pkgUserState = - pmi.getPackageStateInternal(ai.packageName).getUserStates().get(userId); - if (pkgUserState != null && pkgUserState.getInstallReason() + // Avoid showing the disambiguation dialog if the package is not installed or + // installed with reason INSTALL_REASON_DEVICE_SETUP. + final PackageStateInternal ps = pmi.getPackageStateInternal(ai.packageName); + if (ps == null) { + continue; + } + final PackageUserStateInternal pkgUserState = ps.getUserStates().get(userId); + if (pkgUserState == null + || pkgUserState.getInstallReason() == PackageManager.INSTALL_REASON_DEVICE_SETUP) { continue; } diff --git a/services/core/java/com/android/server/pm/Settings.java b/services/core/java/com/android/server/pm/Settings.java index a97652c8e167..7e3254de2385 100644 --- a/services/core/java/com/android/server/pm/Settings.java +++ b/services/core/java/com/android/server/pm/Settings.java @@ -3437,7 +3437,7 @@ public final class Settings implements Watchable, Snappable, ResilientAtomicFile } str.close(); - } catch (IOException | XmlPullParserException e) { + } catch (IOException | XmlPullParserException | ArrayIndexOutOfBoundsException e) { // Remove corrupted file and retry. atomicFile.failRead(str, e); diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java index a6598d602d01..c0596bb10823 100644 --- a/services/core/java/com/android/server/pm/UserManagerService.java +++ b/services/core/java/com/android/server/pm/UserManagerService.java @@ -295,8 +295,6 @@ public class UserManagerService extends IUserManager.Stub { private static final int USER_VERSION = 11; - private static final int MAX_USER_STRING_LENGTH = 500; - private static final long EPOCH_PLUS_30_YEARS = 30L * 365 * 24 * 60 * 60 * 1000L; // ms static final int WRITE_USER_MSG = 1; @@ -4692,16 +4690,18 @@ public class UserManagerService extends IUserManager.Stub { if (userData.persistSeedData) { if (userData.seedAccountName != null) { serializer.attribute(null, ATTR_SEED_ACCOUNT_NAME, - truncateString(userData.seedAccountName)); + truncateString(userData.seedAccountName, + UserManager.MAX_ACCOUNT_STRING_LENGTH)); } if (userData.seedAccountType != null) { serializer.attribute(null, ATTR_SEED_ACCOUNT_TYPE, - truncateString(userData.seedAccountType)); + truncateString(userData.seedAccountType, + UserManager.MAX_ACCOUNT_STRING_LENGTH)); } } if (userInfo.name != null) { serializer.startTag(null, TAG_NAME); - serializer.text(truncateString(userInfo.name)); + serializer.text(truncateString(userInfo.name, UserManager.MAX_USER_NAME_LENGTH)); serializer.endTag(null, TAG_NAME); } synchronized (mRestrictionsLock) { @@ -4765,11 +4765,11 @@ public class UserManagerService extends IUserManager.Stub { serializer.endDocument(); } - private String truncateString(String original) { - if (original == null || original.length() <= MAX_USER_STRING_LENGTH) { + private String truncateString(String original, int limit) { + if (original == null || original.length() <= limit) { return original; } - return original.substring(0, MAX_USER_STRING_LENGTH); + return original.substring(0, limit); } /* @@ -5236,7 +5236,7 @@ public class UserManagerService extends IUserManager.Stub { @UserIdInt int parentId, boolean preCreate, @Nullable String[] disallowedPackages, @NonNull TimingsTraceAndSlog t, @Nullable Object token) throws UserManager.CheckedUserOperationException { - String truncatedName = truncateString(name); + String truncatedName = truncateString(name, UserManager.MAX_USER_NAME_LENGTH); final UserTypeDetails userTypeDetails = mUserTypes.get(userType); if (userTypeDetails == null) { throwCheckedUserOperationException( @@ -6821,9 +6821,14 @@ public class UserManagerService extends IUserManager.Stub { Slog.e(LOG_TAG, "No such user for settings seed data u=" + userId); return; } - userData.seedAccountName = truncateString(accountName); - userData.seedAccountType = truncateString(accountType); - userData.seedAccountOptions = accountOptions; + userData.seedAccountName = truncateString(accountName, + UserManager.MAX_ACCOUNT_STRING_LENGTH); + userData.seedAccountType = truncateString(accountType, + UserManager.MAX_ACCOUNT_STRING_LENGTH); + if (accountOptions != null && accountOptions.isBundleContentsWithinLengthLimit( + UserManager.MAX_ACCOUNT_OPTIONS_LENGTH)) { + userData.seedAccountOptions = accountOptions; + } userData.persistSeedData = persist; } if (persist) { diff --git a/services/core/java/com/android/server/pm/UserTypeFactory.java b/services/core/java/com/android/server/pm/UserTypeFactory.java index 14db70e5f72e..23d0230a6080 100644 --- a/services/core/java/com/android/server/pm/UserTypeFactory.java +++ b/services/core/java/com/android/server/pm/UserTypeFactory.java @@ -288,6 +288,28 @@ public final class UserTypeFactory { * configuration. */ private static UserTypeDetails.Builder getDefaultTypeProfilePrivate() { + UserProperties.Builder userPropertiesBuilder = new UserProperties.Builder() + .setStartWithParent(true) + .setCredentialShareableWithParent(true) + .setAuthAlwaysRequiredToDisableQuietMode(true) + .setAllowStoppingUserWithDelayedLocking(true) + .setMediaSharedWithParent(false) + .setShowInLauncher(UserProperties.SHOW_IN_LAUNCHER_SEPARATE) + .setShowInSettings(UserProperties.SHOW_IN_SETTINGS_SEPARATE) + .setShowInQuietMode( + UserProperties.SHOW_IN_QUIET_MODE_HIDDEN) + .setShowInSharingSurfaces( + UserProperties.SHOW_IN_SHARING_SURFACES_SEPARATE) + .setCrossProfileIntentFilterAccessControl( + UserProperties.CROSS_PROFILE_INTENT_FILTER_ACCESS_LEVEL_SYSTEM) + .setInheritDevicePolicy(UserProperties.INHERIT_DEVICE_POLICY_FROM_PARENT) + .setCrossProfileContentSharingStrategy( + UserProperties.CROSS_PROFILE_CONTENT_SHARING_DELEGATE_FROM_PARENT); + if (android.multiuser.Flags.supportHidingProfiles()) { + userPropertiesBuilder.setProfileApiVisibility( + UserProperties.PROFILE_API_VISIBILITY_HIDDEN); + } + return new UserTypeDetails.Builder() .setName(USER_TYPE_PROFILE_PRIVATE) .setBaseType(FLAG_PROFILE) @@ -306,23 +328,7 @@ public final class UserTypeFactory { .setDarkThemeBadgeColors( R.color.white) .setDefaultRestrictions(getDefaultProfileRestrictions()) - .setDefaultUserProperties(new UserProperties.Builder() - .setStartWithParent(true) - .setCredentialShareableWithParent(true) - .setAuthAlwaysRequiredToDisableQuietMode(true) - .setAllowStoppingUserWithDelayedLocking(true) - .setMediaSharedWithParent(false) - .setShowInLauncher(UserProperties.SHOW_IN_LAUNCHER_SEPARATE) - .setShowInSettings(UserProperties.SHOW_IN_SETTINGS_SEPARATE) - .setShowInQuietMode( - UserProperties.SHOW_IN_QUIET_MODE_HIDDEN) - .setShowInSharingSurfaces( - UserProperties.SHOW_IN_SHARING_SURFACES_SEPARATE) - .setCrossProfileIntentFilterAccessControl( - UserProperties.CROSS_PROFILE_INTENT_FILTER_ACCESS_LEVEL_SYSTEM) - .setInheritDevicePolicy(UserProperties.INHERIT_DEVICE_POLICY_FROM_PARENT) - .setCrossProfileContentSharingStrategy( - UserProperties.CROSS_PROFILE_CONTENT_SHARING_DELEGATE_FROM_PARENT)); + .setDefaultUserProperties(userPropertiesBuilder); } /** diff --git a/services/core/java/com/android/server/pm/VerifyingSession.java b/services/core/java/com/android/server/pm/VerifyingSession.java index f0ff85df13d1..dd2b409c7100 100644 --- a/services/core/java/com/android/server/pm/VerifyingSession.java +++ b/services/core/java/com/android/server/pm/VerifyingSession.java @@ -357,7 +357,8 @@ final class VerifyingSession { verifierUser = UserHandle.of(mPm.mUserManager.getCurrentUserId()); } // TODO(b/300965895): Remove when inconsistencies loading classpaths from apex for - // user > 1 are fixed. + // user > 1 are fixed. Tests should cover verifiers from apex classpaths run on + // primary user, secondary user and work profile. if (pkgLite.isSdkLibrary) { verifierUser = UserHandle.SYSTEM; } 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 d0fe9647618a..6ed2d3126455 100644 --- a/services/core/java/com/android/server/pm/parsing/PackageInfoUtils.java +++ b/services/core/java/com/android/server/pm/parsing/PackageInfoUtils.java @@ -421,6 +421,11 @@ public class PackageInfoUtils { if (ai.isArchived) { ai.nonLocalizedLabel = state.getArchiveState().getActivityInfos().get(0).getTitle(); } + if (!state.isInstalled() && !state.dataExists() + && android.content.pm.Flags.nullableDataDir()) { + // The data dir has been deleted + ai.dataDir = null; + } } @Nullable diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java index 0abf304c34ee..1fdcc64a90c8 100644 --- a/services/core/java/com/android/server/policy/PhoneWindowManager.java +++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java @@ -2185,6 +2185,10 @@ public class PhoneWindowManager implements WindowManagerPolicy { TalkbackShortcutController getTalkbackShortcutController() { return new TalkbackShortcutController(mContext); } + + WindowWakeUpPolicy getWindowWakeUpPolicy() { + return new WindowWakeUpPolicy(mContext); + } } /** {@inheritDoc} */ @@ -2433,7 +2437,7 @@ public class PhoneWindowManager implements WindowManagerPolicy { com.android.internal.R.integer.config_keyguardDrawnTimeout); mKeyguardDelegate = injector.getKeyguardServiceDelegate(); mTalkbackShortcutController = injector.getTalkbackShortcutController(); - mWindowWakeUpPolicy = new WindowWakeUpPolicy(mContext); + mWindowWakeUpPolicy = injector.getWindowWakeUpPolicy(); initKeyCombinationRules(); initSingleKeyGestureRules(injector.getLooper()); mButtonOverridePermissionChecker = injector.getButtonOverridePermissionChecker(); diff --git a/services/core/java/com/android/server/selinux/QuotaLimiter.java b/services/core/java/com/android/server/selinux/QuotaLimiter.java new file mode 100644 index 000000000000..e89ddfd2627c --- /dev/null +++ b/services/core/java/com/android/server/selinux/QuotaLimiter.java @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.server.selinux; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.os.Clock; + +import java.time.Duration; +import java.time.Instant; + +/** + * A QuotaLimiter allows to define a maximum number of Atom pushes within a specific time window. + * + * <p>The limiter divides the time line in windows of a fixed size. Every time a new permit is + * requested, the limiter checks whether the previous request was in the same time window as the + * current one. If the two windows are the same, it grants a permit only if the number of permits + * granted within the window does not exceed the quota. If the two windows are different, it resets + * the quota. + */ +public class QuotaLimiter { + + private final Clock mClock; + private final Duration mWindowSize; + private final int mMaxPermits; + + private long mCurrentWindow = 0; + private int mPermitsGranted = 0; + + @VisibleForTesting + QuotaLimiter(Clock clock, Duration windowSize, int maxPermits) { + mClock = clock; + mWindowSize = windowSize; + mMaxPermits = maxPermits; + } + + public QuotaLimiter(Duration windowSize, int maxPermits) { + this(Clock.SYSTEM_CLOCK, windowSize, maxPermits); + } + + public QuotaLimiter(int maxPermitsPerDay) { + this(Clock.SYSTEM_CLOCK, Duration.ofDays(1), maxPermitsPerDay); + } + + /** + * Acquires a permit if there is one available in the current time window. + * + * @return true if a permit was acquired. + */ + boolean acquire() { + long nowWindow = + Duration.between(Instant.EPOCH, Instant.ofEpochMilli(mClock.currentTimeMillis())) + .dividedBy(mWindowSize); + if (nowWindow > mCurrentWindow) { + mCurrentWindow = nowWindow; + mPermitsGranted = 0; + } + + if (mPermitsGranted < mMaxPermits) { + mPermitsGranted++; + return true; + } + + return false; + } +} diff --git a/services/core/java/com/android/server/selinux/RateLimiter.java b/services/core/java/com/android/server/selinux/RateLimiter.java new file mode 100644 index 000000000000..599b8409cbc3 --- /dev/null +++ b/services/core/java/com/android/server/selinux/RateLimiter.java @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.server.selinux; + +import android.os.SystemClock; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.os.Clock; + +import java.time.Duration; +import java.time.Instant; +import java.time.temporal.ChronoUnit; + +/** + * Rate limiter to ensure Atoms are pushed only within the allowed QPS window. This class is not + * thread-safe. + * + * <p>The rate limiter is smoothed, meaning that a rate limiter allowing X permits per second (or X + * QPS) will grant permits at a ratio of one every 1/X seconds. + */ +public final class RateLimiter { + + private Instant mNextPermit = Instant.EPOCH; + + private final Clock mClock; + private final Duration mWindow; + + @VisibleForTesting + RateLimiter(Clock clock, Duration window) { + mClock = clock; + // Truncating because the system clock does not support units smaller than milliseconds. + mWindow = window; + } + + /** + * Create a rate limiter generating one permit every {@code window} of time, using the {@link + * Clock.SYSTEM_CLOCK}. + */ + public RateLimiter(Duration window) { + this(Clock.SYSTEM_CLOCK, window); + } + + /** + * Acquire a permit if allowed by the rate limiter. If not, wait until a permit becomes + * available. + */ + public void acquire() { + Instant now = Instant.ofEpochMilli(mClock.currentTimeMillis()); + + if (mNextPermit.isAfter(now)) { // Sleep until we can acquire. + SystemClock.sleep(ChronoUnit.MILLIS.between(now, mNextPermit)); + mNextPermit = mNextPermit.plus(mWindow); + } else { + mNextPermit = now.plus(mWindow); + } + } + + /** + * Try to acquire a permit if allowed by the rate limiter. Non-blocking. + * + * @return true if a permit was acquired. Otherwise, return false. + */ + public boolean tryAcquire() { + final Instant now = Instant.ofEpochMilli(mClock.currentTimeMillis()); + + if (mNextPermit.isAfter(now)) { + return false; + } + mNextPermit = now.plus(mWindow); + return true; + } +} diff --git a/services/core/java/com/android/server/selinux/SelinuxAuditLogBuilder.java b/services/core/java/com/android/server/selinux/SelinuxAuditLogBuilder.java new file mode 100644 index 000000000000..8d8d5960038e --- /dev/null +++ b/services/core/java/com/android/server/selinux/SelinuxAuditLogBuilder.java @@ -0,0 +1,155 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.server.selinux; + +import java.util.Arrays; +import java.util.Iterator; +import java.util.Optional; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Stream; + +/** Builder for SelinuxAuditLogs. */ +class SelinuxAuditLogBuilder { + + // Currently logs collection is hardcoded for the sdk_sandbox_audit. + private static final String SDK_SANDBOX_AUDIT = "sdk_sandbox_audit"; + static final Matcher SCONTEXT_MATCHER = + Pattern.compile( + "u:r:(?<stype>" + + SDK_SANDBOX_AUDIT + + "):s0(:c)?(?<scategories>((,c)?\\d+)+)*") + .matcher(""); + + static final Matcher TCONTEXT_MATCHER = + Pattern.compile("u:object_r:(?<ttype>\\w+):s0(:c)?(?<tcategories>((,c)?\\d+)+)*") + .matcher(""); + + static final Matcher PATH_MATCHER = + Pattern.compile("\"(?<path>/\\w+(/\\w+)?)(/\\w+)*\"").matcher(""); + + private Iterator<String> mTokens; + private final SelinuxAuditLog mAuditLog = new SelinuxAuditLog(); + + void reset(String denialString) { + mTokens = + Arrays.asList( + Optional.ofNullable(denialString) + .map(s -> s.split("\\s+|=")) + .orElse(new String[0])) + .iterator(); + mAuditLog.reset(); + } + + SelinuxAuditLog build() { + while (mTokens.hasNext()) { + final String token = mTokens.next(); + + switch (token) { + case "granted": + mAuditLog.mGranted = true; + break; + case "denied": + mAuditLog.mGranted = false; + break; + case "{": + Stream.Builder<String> permissionsStream = Stream.builder(); + boolean closed = false; + while (!closed && mTokens.hasNext()) { + String permission = mTokens.next(); + if ("}".equals(permission)) { + closed = true; + } else { + permissionsStream.add(permission); + } + } + if (!closed) { + return null; + } + mAuditLog.mPermissions = permissionsStream.build().toArray(String[]::new); + break; + case "scontext": + if (!nextTokenMatches(SCONTEXT_MATCHER)) { + return null; + } + mAuditLog.mSType = SCONTEXT_MATCHER.group("stype"); + mAuditLog.mSCategories = toCategories(SCONTEXT_MATCHER.group("scategories")); + break; + case "tcontext": + if (!nextTokenMatches(TCONTEXT_MATCHER)) { + return null; + } + mAuditLog.mTType = TCONTEXT_MATCHER.group("ttype"); + mAuditLog.mTCategories = toCategories(TCONTEXT_MATCHER.group("tcategories")); + break; + case "tclass": + if (!mTokens.hasNext()) { + return null; + } + mAuditLog.mTClass = mTokens.next(); + break; + case "path": + if (nextTokenMatches(PATH_MATCHER)) { + mAuditLog.mPath = PATH_MATCHER.group("path"); + } + break; + case "permissive": + if (!mTokens.hasNext()) { + return null; + } + mAuditLog.mPermissive = "1".equals(mTokens.next()); + break; + default: + break; + } + } + return mAuditLog; + } + + boolean nextTokenMatches(Matcher matcher) { + return mTokens.hasNext() && matcher.reset(mTokens.next()).matches(); + } + + static int[] toCategories(String categories) { + return categories == null + ? null + : Arrays.stream(categories.split(",c")).mapToInt(Integer::parseInt).toArray(); + } + + static class SelinuxAuditLog { + boolean mGranted = false; + String[] mPermissions = null; + String mSType = null; + int[] mSCategories = null; + String mTType = null; + int[] mTCategories = null; + String mTClass = null; + String mPath = null; + boolean mPermissive = false; + + private void reset() { + mGranted = false; + mPermissions = null; + mSType = null; + mSCategories = null; + mTType = null; + mTCategories = null; + mTClass = null; + mPath = null; + mPermissive = false; + } + } +} diff --git a/services/core/java/com/android/server/selinux/SelinuxAuditLogsCollector.java b/services/core/java/com/android/server/selinux/SelinuxAuditLogsCollector.java new file mode 100644 index 000000000000..0219645bee38 --- /dev/null +++ b/services/core/java/com/android/server/selinux/SelinuxAuditLogsCollector.java @@ -0,0 +1,144 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.server.selinux; + +import android.util.EventLog; +import android.util.EventLog.Event; +import android.util.Log; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.util.FrameworkStatsLog; +import com.android.server.selinux.SelinuxAuditLogBuilder.SelinuxAuditLog; + +import java.io.IOException; +import java.time.Instant; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.List; +import java.util.Queue; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** Class in charge of collecting SELinux audit logs and push the SELinux atoms. */ +class SelinuxAuditLogsCollector { + + private static final String TAG = "SelinuxAuditLogs"; + + private static final String SELINUX_PATTERN = "^.*\\bavc:\\s+(?<denial>.*)$"; + + @VisibleForTesting + static final Matcher SELINUX_MATCHER = Pattern.compile(SELINUX_PATTERN).matcher(""); + + private final RateLimiter mRateLimiter; + private final QuotaLimiter mQuotaLimiter; + + @VisibleForTesting Instant mLastWrite = Instant.MIN; + + final AtomicBoolean mStopRequested = new AtomicBoolean(false); + + SelinuxAuditLogsCollector(RateLimiter rateLimiter, QuotaLimiter quotaLimiter) { + mRateLimiter = rateLimiter; + mQuotaLimiter = quotaLimiter; + } + + /** + * Collect and push SELinux audit logs for the provided {@code tagCode}. + * + * @return true if the job was completed. If the job was interrupted, return false. + */ + boolean collect(int tagCode) { + Queue<Event> logLines = new ArrayDeque<>(); + Instant latestTimestamp = collectLogLines(tagCode, logLines); + + boolean quotaExceeded = writeAuditLogs(logLines); + if (quotaExceeded) { + Log.w(TAG, "Too many SELinux logs in the queue, I am giving up."); + mLastWrite = latestTimestamp; // next run we will ignore all these logs. + logLines.clear(); + } + + return logLines.isEmpty(); + } + + private Instant collectLogLines(int tagCode, Queue<Event> logLines) { + List<Event> events = new ArrayList<>(); + try { + EventLog.readEvents(new int[] {tagCode}, events); + } catch (IOException e) { + Log.e(TAG, "Error reading event logs", e); + } + + Instant latestTimestamp = mLastWrite; + for (Event event : events) { + Instant eventTime = Instant.ofEpochSecond(0, event.getTimeNanos()); + if (eventTime.isAfter(latestTimestamp)) { + latestTimestamp = eventTime; + } + if (eventTime.isBefore(mLastWrite)) { + continue; + } + Object eventData = event.getData(); + if (!(eventData instanceof String)) { + continue; + } + logLines.add(event); + } + return latestTimestamp; + } + + private boolean writeAuditLogs(Queue<Event> logLines) { + final SelinuxAuditLogBuilder auditLogBuilder = new SelinuxAuditLogBuilder(); + + while (!mStopRequested.get() && !logLines.isEmpty()) { + Event event = logLines.poll(); + String logLine = (String) event.getData(); + Instant logTime = Instant.ofEpochSecond(0, event.getTimeNanos()); + if (!SELINUX_MATCHER.reset(logLine).matches()) { + continue; + } + + auditLogBuilder.reset(SELINUX_MATCHER.group("denial")); + final SelinuxAuditLog auditLog = auditLogBuilder.build(); + if (auditLog == null) { + continue; + } + + if (!mQuotaLimiter.acquire()) { + return true; + } + mRateLimiter.acquire(); + + FrameworkStatsLog.write( + FrameworkStatsLog.SELINUX_AUDIT_LOG, + auditLog.mGranted, + auditLog.mPermissions, + auditLog.mSType, + auditLog.mSCategories, + auditLog.mTType, + auditLog.mTCategories, + auditLog.mTClass, + auditLog.mPath, + auditLog.mPermissive); + + if (logTime.isAfter(mLastWrite)) { + mLastWrite = logTime; + } + } + + return false; + } +} diff --git a/services/core/java/com/android/server/selinux/SelinuxAuditLogsService.java b/services/core/java/com/android/server/selinux/SelinuxAuditLogsService.java new file mode 100644 index 000000000000..8a661bcc13af --- /dev/null +++ b/services/core/java/com/android/server/selinux/SelinuxAuditLogsService.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.server.selinux; + +import static com.android.sdksandbox.flags.Flags.selinuxSdkSandboxAudit; + +import android.app.job.JobInfo; +import android.app.job.JobParameters; +import android.app.job.JobScheduler; +import android.app.job.JobService; +import android.content.ComponentName; +import android.content.Context; +import android.util.EventLog; +import android.util.Log; + +import java.time.Duration; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +/** + * Scheduled jobs related to logging of SELinux denials and audits. The job runs daily on idle + * devices. + */ +public class SelinuxAuditLogsService extends JobService { + + private static final String TAG = "SelinuxAuditLogs"; + private static final String SELINUX_AUDIT_NAMESPACE = "SelinuxAuditLogsNamespace"; + + static final int AUDITD_TAG_CODE = EventLog.getTagCode("auditd"); + + private static final int SELINUX_AUDIT_JOB_ID = 25327386; + private static final JobInfo SELINUX_AUDIT_JOB = + new JobInfo.Builder( + SELINUX_AUDIT_JOB_ID, + new ComponentName("android", SelinuxAuditLogsService.class.getName())) + .setPeriodic(TimeUnit.DAYS.toMillis(1)) + .setRequiresDeviceIdle(true) + .setRequiresCharging(true) + .setRequiresBatteryNotLow(true) + .build(); + + private static final ExecutorService EXECUTOR_SERVICE = Executors.newSingleThreadExecutor(); + private static final AtomicReference<Boolean> IS_RUNNING = new AtomicReference<>(false); + + // Audit logging is subject to both rate and quota limiting. We can only push one atom every 10 + // milliseconds, and no more than 50K atoms can be pushed each day. + private static final SelinuxAuditLogsCollector AUDIT_LOGS_COLLECTOR = + new SelinuxAuditLogsCollector( + new RateLimiter(/* window= */ Duration.ofMillis(10)), + new QuotaLimiter(/* maxPermitsPerDay= */ 50000)); + + /** Schedule jobs with the {@link JobScheduler}. */ + public static void schedule(Context context) { + if (!selinuxSdkSandboxAudit()) { + Log.d(TAG, "SelinuxAuditLogsService not enabled"); + return; + } + + if (AUDITD_TAG_CODE == -1) { + Log.e(TAG, "auditd is not a registered tag on this system"); + return; + } + + if (context.getSystemService(JobScheduler.class) + .forNamespace(SELINUX_AUDIT_NAMESPACE) + .schedule(SELINUX_AUDIT_JOB) + == JobScheduler.RESULT_FAILURE) { + Log.e(TAG, "SelinuxAuditLogsService could not be started."); + } + } + + @Override + public boolean onStartJob(JobParameters params) { + if (params.getJobId() != SELINUX_AUDIT_JOB_ID) { + Log.e(TAG, "The job id does not match the expected selinux job id."); + return false; + } + + AUDIT_LOGS_COLLECTOR.mStopRequested.set(false); + IS_RUNNING.set(true); + EXECUTOR_SERVICE.execute(new LogsCollectorJob(this, params)); + + return true; // the job is running + } + + @Override + public boolean onStopJob(JobParameters params) { + if (params.getJobId() != SELINUX_AUDIT_JOB_ID) { + return false; + } + + AUDIT_LOGS_COLLECTOR.mStopRequested.set(true); + return IS_RUNNING.get(); + } + + private static class LogsCollectorJob implements Runnable { + private final JobService mAuditLogService; + private final JobParameters mParams; + + LogsCollectorJob(JobService auditLogService, JobParameters params) { + mAuditLogService = auditLogService; + mParams = params; + } + + @Override + public void run() { + IS_RUNNING.updateAndGet( + isRunning -> { + boolean done = AUDIT_LOGS_COLLECTOR.collect(AUDITD_TAG_CODE); + if (done) { + mAuditLogService.jobFinished(mParams, /* wantsReschedule= */ false); + } + return !done; + }); + } + } +} diff --git a/services/core/java/com/android/server/tv/interactive/TvInteractiveAppManagerService.java b/services/core/java/com/android/server/tv/interactive/TvInteractiveAppManagerService.java index d7b8495929a2..b6d0ca19d484 100644 --- a/services/core/java/com/android/server/tv/interactive/TvInteractiveAppManagerService.java +++ b/services/core/java/com/android/server/tv/interactive/TvInteractiveAppManagerService.java @@ -2390,6 +2390,33 @@ public class TvInteractiveAppManagerService extends SystemService { } @Override + public void sendCertificate(IBinder sessionToken, String host, int port, + Bundle certBundle, int userId) { + if (DEBUG) { + Slogf.d(TAG, "sendCertificate(host=%s port=%d cert=%s)", host, port, + certBundle); + } + final int callingUid = Binder.getCallingUid(); + final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(), callingUid, + userId, "sendCertificate"); + SessionState sessionState = null; + final long identity = Binder.clearCallingIdentity(); + try { + synchronized (mLock) { + try { + sessionState = getSessionStateLocked(sessionToken, callingUid, + resolvedUserId); + getSessionLocked(sessionState).sendCertificate(host, port, certBundle); + } catch (RemoteException | SessionNotFoundException e) { + Slogf.e(TAG, "error in sendCertificate", e); + } + } + } finally { + Binder.restoreCallingIdentity(identity); + } + } + + @Override public void notifyError(IBinder sessionToken, String errMsg, Bundle params, int userId) { if (DEBUG) { Slogf.d(TAG, "notifyError(errMsg=%s)", errMsg); @@ -4125,6 +4152,24 @@ public class TvInteractiveAppManagerService extends SystemService { } @Override + public void onRequestCertificate(String host, int port) { + synchronized (mLock) { + if (DEBUG) { + Slogf.d(TAG, "onRequestCertificate"); + } + if (mSessionState.mSession == null || mSessionState.mClient == null) { + return; + } + try { + mSessionState.mClient.onRequestCertificate(host, port, mSessionState.mSeq); + } catch (RemoteException e) { + Slogf.e(TAG, "error in onRequestCertificate", e); + } + } + } + + + @Override public void onAdRequest(AdRequest request) { synchronized (mLock) { if (DEBUG) { diff --git a/services/core/java/com/android/server/wallpaper/WallpaperDisplayHelper.java b/services/core/java/com/android/server/wallpaper/WallpaperDisplayHelper.java index 19fd9a90518d..9e1b5d238d48 100644 --- a/services/core/java/com/android/server/wallpaper/WallpaperDisplayHelper.java +++ b/services/core/java/com/android/server/wallpaper/WallpaperDisplayHelper.java @@ -96,7 +96,8 @@ class WallpaperDisplayHelper { } if (populateOrientationPairs) { int orientation = WallpaperManager.getOrientation(displaySize); - float newSurface = displaySize.x * displaySize.y * metric.getDensity(); + float newSurface = displaySize.x * displaySize.y + / (metric.getDensity() * metric.getDensity()); if (surface <= 0) { surface = newSurface; firstOrientation = orientation; diff --git a/services/core/java/com/android/server/wm/AccessibilityController.java b/services/core/java/com/android/server/wm/AccessibilityController.java index 2d584c428d4c..f2d9bf810b53 100644 --- a/services/core/java/com/android/server/wm/AccessibilityController.java +++ b/services/core/java/com/android/server/wm/AccessibilityController.java @@ -462,17 +462,16 @@ final class AccessibilityController { } } - // TODO(b/318327737): Remove parameter 't' when removing flag DRAW_IN_WM_LOCK. - void drawMagnifiedRegionBorderIfNeeded(int displayId, SurfaceControl.Transaction t) { + void drawMagnifiedRegionBorderIfNeeded(int displayId) { if (mAccessibilityTracing.isTracingEnabled(FLAGS_MAGNIFICATION_CALLBACK)) { mAccessibilityTracing.logTrace( TAG + ".drawMagnifiedRegionBorderIfNeeded", FLAGS_MAGNIFICATION_CALLBACK, - "displayId=" + displayId + "; transaction={" + t + "}"); + "displayId=" + displayId); } final DisplayMagnifier displayMagnifier = mDisplayMagnifiers.get(displayId); if (displayMagnifier != null) { - displayMagnifier.drawMagnifiedRegionBorderIfNeeded(t); + displayMagnifier.drawMagnifiedRegionBorderIfNeeded(); } // Not relevant for the window observer. } @@ -870,12 +869,12 @@ final class AccessibilityController { .sendToTarget(); } - void drawMagnifiedRegionBorderIfNeeded(SurfaceControl.Transaction t) { + void drawMagnifiedRegionBorderIfNeeded() { if (mAccessibilityTracing.isTracingEnabled(FLAGS_MAGNIFICATION_CALLBACK)) { mAccessibilityTracing.logTrace(LOG_TAG + ".drawMagnifiedRegionBorderIfNeeded", - FLAGS_MAGNIFICATION_CALLBACK, "transition={" + t + "}"); + FLAGS_MAGNIFICATION_CALLBACK); } - mMagnifedViewport.drawWindowIfNeeded(t); + mMagnifedViewport.drawWindowIfNeeded(); } void dump(PrintWriter pw, String prefix) { @@ -1121,14 +1120,6 @@ final class AccessibilityController { } void setMagnifiedRegionBorderShown(boolean shown, boolean animate) { - if (ViewportWindow.DRAW_IN_WM_LOCK) { - if (shown) { - mFullRedrawNeeded = true; - mOldMagnificationRegion.set(0, 0, 0, 0); - } - mWindow.setShown(shown, animate); - return; - } if (mWindow.setShown(shown, animate)) { mFullRedrawNeeded = true; // Clear the old region, so recomputeBounds will refresh the current region. @@ -1151,12 +1142,8 @@ final class AccessibilityController { return mMagnificationSpec; } - void drawWindowIfNeeded(SurfaceControl.Transaction t) { + void drawWindowIfNeeded() { recomputeBounds(); - if (ViewportWindow.DRAW_IN_WM_LOCK) { - mWindow.drawOrRemoveIfNeeded(t); - return; - } mWindow.postDrawIfNeeded(); } @@ -1187,8 +1174,6 @@ final class AccessibilityController { private final class ViewportWindow implements Runnable { private static final String SURFACE_TITLE = "Magnification Overlay"; - // TODO(b/318327737): Remove if it is stable. - static final boolean DRAW_IN_WM_LOCK = !Flags.drawMagnifierBorderOutsideWmlock(); private final Region mBounds = new Region(); private final Rect mDirtyRect = new Rect(); @@ -1328,14 +1313,14 @@ final class AccessibilityController { @Override public void run() { - drawOrRemoveIfNeeded(mTransaction); + drawOrRemoveIfNeeded(); } /** * This method must only be called by animation handler directly to make sure * thread safe and there is no lock held outside. */ - private void drawOrRemoveIfNeeded(SurfaceControl.Transaction t) { + private void drawOrRemoveIfNeeded() { // Drawing variables (alpha, dirty rect, and bounds) access is synchronized // using WindowManagerGlobalLock. Grab copies of these values before // drawing on the canvas so that drawing can be performed outside of the lock. @@ -1343,7 +1328,7 @@ final class AccessibilityController { Rect drawingRect = null; Region drawingBounds = null; synchronized (mService.mGlobalLock) { - if (!DRAW_IN_WM_LOCK && mBlastBufferQueue.mNativeObject == 0) { + if (mBlastBufferQueue.mNativeObject == 0) { // Complete removal since releaseSurface has been called. if (mSurface.isValid()) { mTransaction.remove(mSurfaceControl).apply(); @@ -1388,16 +1373,8 @@ final class AccessibilityController { mPaint.setAlpha(alpha); canvas.drawPath(drawingBounds.getBoundaryPath(), mPaint); mSurface.unlockCanvasAndPost(canvas); - if (DRAW_IN_WM_LOCK) { - t.show(mSurfaceControl); - return; - } showSurface = true; } else { - if (DRAW_IN_WM_LOCK) { - t.hide(mSurfaceControl); - return; - } showSurface = false; } @@ -1413,11 +1390,6 @@ final class AccessibilityController { @GuardedBy("mService.mGlobalLock") void releaseSurface() { mBlastBufferQueue.destroy(); - if (DRAW_IN_WM_LOCK) { - mService.mTransactionFactory.get().remove(mSurfaceControl).apply(); - mSurface.release(); - return; - } // Post to perform cleanup on the thread which handles mSurface. mService.mAnimationHandler.post(this); } diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java index 3d492bbd4d37..03d55d9edfda 100644 --- a/services/core/java/com/android/server/wm/ActivityRecord.java +++ b/services/core/java/com/android/server/wm/ActivityRecord.java @@ -53,6 +53,7 @@ import static android.app.WindowConfiguration.activityTypeToString; import static android.app.admin.DevicePolicyResources.Drawables.Source.PROFILE_SWITCH_ANIMATION; import static android.app.admin.DevicePolicyResources.Drawables.Style.OUTLINE; import static android.app.admin.DevicePolicyResources.Drawables.WORK_PROFILE_ICON; +import static android.content.Context.CONTEXT_RESTRICTED; import static android.content.Intent.ACTION_MAIN; import static android.content.Intent.CATEGORY_HOME; import static android.content.Intent.CATEGORY_LAUNCHER; @@ -2231,7 +2232,17 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A mOptInOnBackInvoked = WindowOnBackInvokedDispatcher .isOnBackInvokedCallbackEnabled(info, info.applicationInfo, - () -> ent != null ? ent.array : null, false); + () -> { + Context appContext = null; + try { + appContext = mAtmService.mContext.createPackageContextAsUser( + info.packageName, CONTEXT_RESTRICTED, + UserHandle.of(mUserId)); + appContext.setTheme(theme); + } catch (PackageManager.NameNotFoundException ignore) { + } + return appContext; + }); } /** @@ -2635,10 +2646,20 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A if (snapshot == null) { return false; } - if (!snapshot.getTopActivityComponent().equals(mActivityComponent)) { - // Obsoleted snapshot. - return false; - } + return isSnapshotComponentCompatible(snapshot) && isSnapshotOrientationCompatible(snapshot); + } + + /** + * Returns {@code true} if the top activity component of task snapshot equals to this activity. + */ + boolean isSnapshotComponentCompatible(@NonNull TaskSnapshot snapshot) { + return snapshot.getTopActivityComponent().equals(mActivityComponent); + } + + /** + * Returns {@code true} if the orientation of task snapshot is compatible with this activity. + */ + boolean isSnapshotOrientationCompatible(@NonNull TaskSnapshot snapshot) { final int rotation = mDisplayContent.rotationForActivityInDifferentOrientation(this); final int currentRotation = task.getWindowConfiguration().getRotation(); final int targetRotation = rotation != ROTATION_UNDEFINED diff --git a/services/core/java/com/android/server/wm/BackNavigationController.java b/services/core/java/com/android/server/wm/BackNavigationController.java index a4d15e07a3ed..83ccbdc1a4d1 100644 --- a/services/core/java/com/android/server/wm/BackNavigationController.java +++ b/services/core/java/com/android/server/wm/BackNavigationController.java @@ -103,10 +103,6 @@ class BackNavigationController { static final boolean sPredictBackEnable = SystemProperties.getBoolean("persist.wm.debug.predictive_back", true); - static boolean isScreenshotEnabled() { - return SystemProperties.getInt("persist.wm.debug.predictive_back_screenshot", 0) != 0; - } - // Notify focus window changed void onFocusChanged(WindowState newFocus) { mNavigationMonitor.onFocusWindowChanged(newFocus); @@ -310,9 +306,11 @@ class BackNavigationController { // keyguard locked and activities are unable to show when locked. backType = BackNavigationInfo.TYPE_CALLBACK; } + } else if (currentTask.mAtmService.getLockTaskController().isTaskLocked(currentTask)) { + // Do not predict if current task is in task locked. + backType = BackNavigationInfo.TYPE_CALLBACK; } else { - // TODO(208789724): Create single source of truth for this, maybe in - // RootWindowContainer + // Check back-to-home or cross-task prevTask = currentTask.mRootWindowContainer.getTask(t -> { if (t.showToCurrentUser() && !t.mChildren.isEmpty()) { final ActivityRecord ar = t.getTopNonFinishingActivity(); @@ -958,6 +956,18 @@ class BackNavigationController { return; } + // Start fixed rotation for previous activity before create animation. + if (openingActivities.length == 1) { + final ActivityRecord next = openingActivities[0]; + final DisplayContent dc = next.mDisplayContent; + dc.rotateInDifferentOrientationIfNeeded(next); + if (next.hasFixedRotationTransform()) { + // Set the record so we can recognize it to continue to update display + // orientation if the previous activity becomes the top later. + dc.setFixedRotationLaunchingApp(next, + next.getWindowConfiguration().getRotation()); + } + } mOpenAnimAdaptor = new BackWindowAnimationAdaptorWrapper(true, mSwitchType, open); if (!mOpenAnimAdaptor.isValid()) { Slog.w(TAG, "compose animations fail, skip"); @@ -1623,16 +1633,6 @@ class BackNavigationController { } activity.mLaunchTaskBehind = true; - // Handle fixed rotation launching app. - final DisplayContent dc = activity.mDisplayContent; - dc.rotateInDifferentOrientationIfNeeded(activity); - if (activity.hasFixedRotationTransform()) { - // Set the record so we can recognize it to continue to update display - // orientation if the previous activity becomes the top later. - dc.setFixedRotationLaunchingApp(activity, - activity.getWindowConfiguration().getRotation()); - } - ProtoLog.d(WM_DEBUG_BACK_PREVIEW, "Setting Activity.mLauncherTaskBehind to true. Activity=%s", activity); activity.mTaskSupervisor.mStoppingActivities.remove(activity); @@ -1700,21 +1700,38 @@ class BackNavigationController { static TaskSnapshot getSnapshot(@NonNull WindowContainer w, ActivityRecord[] visibleOpenActivities) { + TaskSnapshot snapshot = null; if (w.asTask() != null) { final Task task = w.asTask(); - return task.mRootWindowContainer.mWindowManager.mTaskSnapshotController.getSnapshot( + snapshot = task.mRootWindowContainer.mWindowManager.mTaskSnapshotController.getSnapshot( task.mTaskId, task.mUserId, false /* restoreFromDisk */, false /* isLowResolution */); - } - - if (w.asActivityRecord() != null) { + } else if (w.asActivityRecord() != null) { final ActivityRecord ar = w.asActivityRecord(); - return ar.mWmService.mSnapshotController.mActivitySnapshotController + snapshot = ar.mWmService.mSnapshotController.mActivitySnapshotController .getSnapshot(visibleOpenActivities); } - return null; + + return isSnapshotCompatible(snapshot, visibleOpenActivities) ? snapshot : null; } + static boolean isSnapshotCompatible(@NonNull TaskSnapshot snapshot, + @NonNull ActivityRecord[] visibleOpenActivities) { + if (snapshot == null) { + return false; + } + boolean oneComponentMatch = false; + for (int i = visibleOpenActivities.length - 1; i >= 0; --i) { + final ActivityRecord ar = visibleOpenActivities[i]; + if (!ar.isSnapshotOrientationCompatible(snapshot)) { + return false; + } + oneComponentMatch |= ar.isSnapshotComponentCompatible(snapshot); + } + return oneComponentMatch; + } + + void setWindowManager(WindowManagerService wm) { mWindowManagerService = wm; mAnimationHandler = new AnimationHandler(wm); diff --git a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java index 9ac4a5c4ad5c..5423c6636786 100644 --- a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java +++ b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java @@ -717,31 +717,6 @@ public class BackgroundActivityStartController { boolean callerCanAllow = resultForCaller.allows() && !state.callerExplicitOptOut(); boolean realCallerCanAllow = resultForRealCaller.allows() && !state.realCallerExplicitOptOut(); - if (callerCanAllow && realCallerCanAllow) { - // Both caller and real caller allow with system defined behavior - if (state.mBalAllowedByPiCreatorWithHardening.allowsBackgroundActivityStarts()) { - // Will be allowed even with BAL hardening. - if (DEBUG_ACTIVITY_STARTS) { - Slog.d(TAG, "Activity start allowed by caller. " - + state.dump()); - } - return allowBasedOnCaller(state); - } - if (state.mBalAllowedByPiCreator.allowsBackgroundActivityStarts()) { - Slog.wtf(TAG, - "With Android 15 BAL hardening this activity start may be blocked" - + " if the PI creator upgrades target_sdk to 35+" - + " AND the PI sender upgrades target_sdk to 34+! " - + state.dump()); - showBalRiskToast(); - return allowBasedOnCaller(state); - } - Slog.wtf(TAG, - "Without Android 15 BAL hardening this activity start would be allowed" - + " (missing opt in by PI creator or sender)! " - + state.dump()); - return abortLaunch(state); - } if (callerCanAllow) { // Allowed before V by creator if (state.mBalAllowedByPiCreatorWithHardening.allowsBackgroundActivityStarts()) { @@ -753,35 +728,29 @@ public class BackgroundActivityStartController { return allowBasedOnCaller(state); } if (state.mBalAllowedByPiCreator.allowsBackgroundActivityStarts()) { - Slog.wtf(TAG, - "With Android 15 BAL hardening this activity start may be blocked" + Slog.wtf(TAG, "With Android 15 BAL hardening this activity start may be blocked" + " if the PI creator upgrades target_sdk to 35+! " + " (missing opt in by PI creator)! " + state.dump()); showBalRiskToast(); return allowBasedOnCaller(state); } - Slog.wtf(TAG, - "Without Android 15 BAL hardening this activity start would be allowed" - + " (missing opt in by PI creator)! " - + state.dump()); - return abortLaunch(state); } if (realCallerCanAllow) { // Allowed before U by sender if (state.mBalAllowedByPiSender.allowsBackgroundActivityStarts()) { - Slog.wtf(TAG, - "With Android 14 BAL hardening this activity start will be blocked" + Slog.wtf(TAG, "With Android 14 BAL hardening this activity start will be blocked" + " if the PI sender upgrades target_sdk to 34+! " + " (missing opt in by PI sender)! " + state.dump()); showBalRiskToast(); return allowBasedOnRealCaller(state); } - Slog.wtf(TAG, "Without Android 14 BAL hardening this activity start would be allowed" - + " (missing opt in by PI sender)! " - + state.dump()); - return abortLaunch(state); + } + // caller or real caller could start the activity, but would need to explicitly opt in + if (callerCanAllow || realCallerCanAllow) { + Slog.wtf(TAG, "Without BAL hardening this activity start would be allowed " + + state.dump()); } // neither the caller not the realCaller can allow or have explicitly opted out return abortLaunch(state); @@ -1028,7 +997,7 @@ public class BackgroundActivityStartController { BalVerdict balAllowedForUid = proc.areBackgroundActivityStartsAllowed( state.mAppSwitchState); if (balAllowedForUid.allows()) { - return balAllowedForCaller.withProcessInfo("process", proc); + return balAllowedForUid.withProcessInfo("process", proc); } } } diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java index e7ecf520425d..5dcd335baf2d 100644 --- a/services/core/java/com/android/server/wm/DisplayContent.java +++ b/services/core/java/com/android/server/wm/DisplayContent.java @@ -160,6 +160,7 @@ import static com.android.server.wm.utils.DisplayInfoOverrides.WM_OVERRIDE_FIELD import static com.android.server.wm.utils.DisplayInfoOverrides.copyDisplayInfoFields; import static com.android.server.wm.utils.RegionUtils.forEachRectReverse; import static com.android.server.wm.utils.RegionUtils.rectListToRegion; +import static com.android.window.flags.Flags.deferDisplayUpdates; import static com.android.window.flags.Flags.explicitRefreshRateHints; import android.annotation.IntDef; @@ -174,7 +175,6 @@ import android.content.pm.ActivityInfo.ScreenOrientation; import android.content.res.CompatibilityInfo; import android.content.res.Configuration; import android.content.res.Resources; -import android.graphics.Bitmap; import android.graphics.ColorSpace; import android.graphics.Insets; import android.graphics.Matrix; @@ -246,7 +246,6 @@ import android.view.inputmethod.ImeTracker; import android.window.DisplayWindowPolicyController; import android.window.IDisplayAreaOrganizer; import android.window.ScreenCapture; -import android.window.ScreenCapture.SynchronousScreenCaptureListener; import android.window.SystemPerformanceHinter; import android.window.TransitionRequestInfo; @@ -276,7 +275,6 @@ import java.util.Objects; import java.util.Set; import java.util.function.Consumer; import java.util.function.Predicate; -import static com.android.window.flags.Flags.deferDisplayUpdates; /** * Utility class for keeping track of the WindowStates and other pertinent contents of a @@ -5207,10 +5205,9 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp } /** - * Takes a snapshot of the display. In landscape mode this grabs the whole screen. - * In portrait mode, it grabs the full screenshot. + * Creates a LayerCaptureArgs object to represent the entire DisplayContent */ - Bitmap screenshotDisplayLocked() { + ScreenCapture.LayerCaptureArgs getLayerCaptureArgs() { if (!mWmService.mPolicy.isScreenOn()) { if (DEBUG_SCREENSHOT) { Slog.i(TAG_WM, "Attempted to take screenshot while display was off."); @@ -5218,24 +5215,10 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp return null; } - SynchronousScreenCaptureListener syncScreenCapture = - ScreenCapture.createSyncCaptureListener(); - getBounds(mTmpRect); mTmpRect.offsetTo(0, 0); - ScreenCapture.LayerCaptureArgs args = - new ScreenCapture.LayerCaptureArgs.Builder(getSurfaceControl()) - .setSourceCrop(mTmpRect).build(); - - ScreenCapture.captureLayers(args, syncScreenCapture); - - final ScreenCapture.ScreenshotHardwareBuffer screenshotBuffer = - syncScreenCapture.getBuffer(); - final Bitmap bitmap = screenshotBuffer == null ? null : screenshotBuffer.asBitmap(); - if (bitmap == null) { - Slog.w(TAG_WM, "Failed to take screenshot"); - } - return bitmap; + return new ScreenCapture.LayerCaptureArgs.Builder(getSurfaceControl()) + .setSourceCrop(mTmpRect).build(); } @Override diff --git a/services/core/java/com/android/server/wm/LetterboxUiController.java b/services/core/java/com/android/server/wm/LetterboxUiController.java index f2796895d639..0e2d3d151db0 100644 --- a/services/core/java/com/android/server/wm/LetterboxUiController.java +++ b/services/core/java/com/android/server/wm/LetterboxUiController.java @@ -1424,7 +1424,7 @@ final class LetterboxUiController { @VisibleForTesting boolean shouldShowLetterboxUi(WindowState mainWindow) { - if (mIsRelaunchingAfterRequestedOrientationChanged || !isSurfaceReadyToShow(mainWindow)) { + if (mIsRelaunchingAfterRequestedOrientationChanged) { return mLastShouldShowLetterboxUi; } @@ -1442,13 +1442,6 @@ final class LetterboxUiController { } @VisibleForTesting - boolean isSurfaceReadyToShow(WindowState mainWindow) { - return mainWindow.isDrawn() // Regular case - // Waiting for relayoutWindow to call preserveSurface - || mainWindow.isDragResizeChanged(); - } - - @VisibleForTesting boolean isSurfaceVisible(WindowState mainWindow) { return mainWindow.isOnScreen() && (mActivityRecord.isVisible() || mActivityRecord.isVisibleRequested()); diff --git a/services/core/java/com/android/server/wm/RecentTasks.java b/services/core/java/com/android/server/wm/RecentTasks.java index 10405ec7cd88..e027eb63f1d5 100644 --- a/services/core/java/com/android/server/wm/RecentTasks.java +++ b/services/core/java/com/android/server/wm/RecentTasks.java @@ -1135,16 +1135,17 @@ class RecentTasks { if (!mFreezeTaskListReordering) { // Simple case: this is not an affiliated task, so we just move it to the // front unless overridden by the provided activity options + int indexToAdd = findIndexToAdd(task); mTasks.remove(taskIndex); - mTasks.add(0, task); + mTasks.add(indexToAdd, task); if (taskIndex != 0) { // Only notify when position changes mTaskNotificationController.notifyTaskListUpdated(); } if (DEBUG_RECENTS) { - Slog.d(TAG_RECENTS, "addRecent: moving to top " + task - + " from " + taskIndex); + Slog.d(TAG_RECENTS, "addRecent: moving " + task + " to index " + + indexToAdd + " from " + taskIndex); } } notifyTaskPersisterLocked(task, false); @@ -1231,6 +1232,37 @@ class RecentTasks { notifyTaskPersisterLocked(task, false /* flush */); } + // Looks for a new index to move the recent Task. Note that the recent Task should not be + // placed higher than another recent Task that has higher hierarchical z-ordering. + private int findIndexToAdd(Task task) { + int indexToAdd = 0; + for (int i = 0; i < mTasks.size(); i++) { + final Task otherTask = mTasks.get(i); + if (task == otherTask) { + break; + } + + if (!otherTask.isAttached()) { + // Stop searching if not attached. + break; + } + + if (otherTask.inPinnedWindowingMode()) { + // Skip pip task without increasing index since pip is always on screen. + continue; + } + + // Stop searching if the task has higher z-ordering, or increase the index and + // continue the search. + if (task.compareTo(otherTask) > 0) { + break; + } + + indexToAdd = i + 1; + } + return indexToAdd; + } + /** * Add the task to the bottom if possible. */ diff --git a/services/core/java/com/android/server/wm/Session.java b/services/core/java/com/android/server/wm/Session.java index f10a733040ed..083872a03edd 100644 --- a/services/core/java/com/android/server/wm/Session.java +++ b/services/core/java/com/android/server/wm/Session.java @@ -669,7 +669,7 @@ class Session extends IWindowSession.Stub implements IBinder.DeathRecipient { } @Override - public Bundle sendWallpaperCommand(IBinder window, String action, int x, int y, + public void sendWallpaperCommand(IBinder window, String action, int x, int y, int z, Bundle extras, boolean sync) { synchronized (mService.mGlobalLock) { final long ident = Binder.clearCallingIdentity(); @@ -680,10 +680,9 @@ class Session extends IWindowSession.Stub implements IBinder.DeathRecipient { if (mCanAlwaysUpdateWallpaper || windowState == wallpaperController.getWallpaperTarget() || windowState == wallpaperController.getPrevWallpaperTarget()) { - return wallpaperController.sendWindowWallpaperCommandUnchecked( + wallpaperController.sendWindowWallpaperCommandUnchecked( windowState, action, x, y, z, extras, sync); } - return null; } finally { Binder.restoreCallingIdentity(ident); } diff --git a/services/core/java/com/android/server/wm/TaskFragment.java b/services/core/java/com/android/server/wm/TaskFragment.java index 0c6b174b2408..b2b547e7d9e5 100644 --- a/services/core/java/com/android/server/wm/TaskFragment.java +++ b/services/core/java/com/android/server/wm/TaskFragment.java @@ -45,7 +45,6 @@ import static android.view.WindowManager.TRANSIT_FLAG_OPEN_BEHIND; import static android.view.WindowManager.TRANSIT_NONE; import static android.view.WindowManager.TRANSIT_OPEN; -import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_BACK_PREVIEW; import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_STATES; import static com.android.server.wm.ActivityRecord.State.PAUSED; import static com.android.server.wm.ActivityRecord.State.PAUSING; @@ -89,7 +88,6 @@ import android.content.pm.PackageManager; import android.content.res.Configuration; import android.graphics.Point; import android.graphics.Rect; -import android.hardware.HardwareBuffer; import android.os.IBinder; import android.os.UserHandle; import android.util.DisplayMetrics; @@ -99,7 +97,6 @@ import android.view.DisplayInfo; import android.view.RemoteAnimationTarget; import android.view.SurfaceControl; import android.window.ITaskFragmentOrganizer; -import android.window.ScreenCapture; import android.window.TaskFragmentAnimationParams; import android.window.TaskFragmentInfo; import android.window.TaskFragmentOrganizerToken; @@ -113,7 +110,6 @@ import com.android.window.flags.Flags; import java.io.FileDescriptor; import java.io.PrintWriter; import java.util.ArrayList; -import java.util.HashMap; import java.util.List; import java.util.Set; import java.util.function.Consumer; @@ -403,10 +399,6 @@ class TaskFragment extends WindowContainer<WindowContainer> { /** For calculating app bounds, i.e. the area without the nav bar and display cutout. */ private final Rect mTmpNonDecorBounds = new Rect(); - //TODO(b/207481538) Remove once the infrastructure to support per-activity screenshot is - // implemented - HashMap<String, ScreenCapture.ScreenshotHardwareBuffer> mBackScreenshots = new HashMap<>(); - private final EnsureActivitiesVisibleHelper mEnsureActivitiesVisibleHelper = new EnsureActivitiesVisibleHelper(this); @@ -2092,17 +2084,6 @@ class TaskFragment extends WindowContainer<WindowContainer> { super.addChild(child, index); if (isAddingActivity && task != null) { - // TODO(b/207481538): temporary per-activity screenshoting - if (r != null && BackNavigationController.isScreenshotEnabled()) { - ProtoLog.v(WM_DEBUG_BACK_PREVIEW, "Screenshotting Activity %s", - r.mActivityComponent.flattenToString()); - Rect outBounds = r.getBounds(); - ScreenCapture.ScreenshotHardwareBuffer backBuffer = ScreenCapture.captureLayers( - r.mSurfaceControl, - new Rect(0, 0, outBounds.width(), outBounds.height()), - 1f); - mBackScreenshots.put(r.mActivityComponent.flattenToString(), backBuffer); - } addingActivity.inHistory = true; task.onDescendantActivityAdded(taskHadActivity, activityType, addingActivity); } @@ -2905,19 +2886,6 @@ class TaskFragment extends WindowContainer<WindowContainer> { return !mCreatedByOrganizer || mIsRemovalRequested; } - @Nullable - HardwareBuffer getSnapshotForActivityRecord(@Nullable ActivityRecord r) { - if (!BackNavigationController.isScreenshotEnabled()) { - return null; - } - if (r != null && r.mActivityComponent != null) { - ScreenCapture.ScreenshotHardwareBuffer backBuffer = - mBackScreenshots.get(r.mActivityComponent.flattenToString()); - return backBuffer != null ? backBuffer.getHardwareBuffer() : null; - } - return null; - } - @Override void removeChild(WindowContainer child) { removeChild(child, true /* removeSelfIfPossible */); @@ -2926,13 +2894,6 @@ class TaskFragment extends WindowContainer<WindowContainer> { void removeChild(WindowContainer child, boolean removeSelfIfPossible) { super.removeChild(child); final ActivityRecord r = child.asActivityRecord(); - if (BackNavigationController.isScreenshotEnabled()) { - //TODO(b/207481538) Remove once the infrastructure to support per-activity screenshot is - // implemented - if (r != null) { - mBackScreenshots.remove(r.mActivityComponent.flattenToString()); - } - } final WindowProcessController hostProcess = getOrganizerProcessIfDifferent(r); if (hostProcess != null) { hostProcess.removeEmbeddedActivity(r); diff --git a/services/core/java/com/android/server/wm/TransitionController.java b/services/core/java/com/android/server/wm/TransitionController.java index 59e3350d5c13..d7b4a399514d 100644 --- a/services/core/java/com/android/server/wm/TransitionController.java +++ b/services/core/java/com/android/server/wm/TransitionController.java @@ -496,6 +496,9 @@ class TransitionController { if (mCollectingTransition != null && mCollectingTransition.isInTransientHide(task)) { return true; } + for (int i = mWaitingTransitions.size() - 1; i >= 0; --i) { + if (mWaitingTransitions.get(i).isInTransientHide(task)) return true; + } for (int i = mPlayingTransitions.size() - 1; i >= 0; --i) { if (mPlayingTransitions.get(i).isInTransientHide(task)) return true; } @@ -506,6 +509,9 @@ class TransitionController { if (mCollectingTransition != null && mCollectingTransition.isTransientVisible(task)) { return true; } + for (int i = mWaitingTransitions.size() - 1; i >= 0; --i) { + if (mWaitingTransitions.get(i).isTransientVisible(task)) return true; + } for (int i = mPlayingTransitions.size() - 1; i >= 0; --i) { if (mPlayingTransitions.get(i).isTransientVisible(task)) return true; } diff --git a/services/core/java/com/android/server/wm/WallpaperController.java b/services/core/java/com/android/server/wm/WallpaperController.java index 0fc62a758c5e..6949a874b533 100644 --- a/services/core/java/com/android/server/wm/WallpaperController.java +++ b/services/core/java/com/android/server/wm/WallpaperController.java @@ -279,13 +279,14 @@ class WallpaperController { return null; } Point largestDisplaySize = new Point(); + float largestWidth = 0; List<DisplayInfo> possibleDisplayInfo = mService.getPossibleDisplayInfoLocked(DEFAULT_DISPLAY); for (int i = 0; i < possibleDisplayInfo.size(); i++) { DisplayInfo displayInfo = possibleDisplayInfo.get(i); - if (displayInfo.type == Display.TYPE_INTERNAL - && Math.max(displayInfo.logicalWidth, displayInfo.logicalHeight) - > Math.max(largestDisplaySize.x, largestDisplaySize.y)) { + float width = (float) displayInfo.logicalWidth / displayInfo.physicalXDpi; + if (displayInfo.type == Display.TYPE_INTERNAL && width > largestWidth) { + largestWidth = width; largestDisplaySize.set(displayInfo.logicalWidth, displayInfo.logicalHeight); } @@ -641,11 +642,10 @@ class WallpaperController { } } - Bundle sendWindowWallpaperCommandUnchecked( + void sendWindowWallpaperCommandUnchecked( WindowState window, String action, int x, int y, int z, Bundle extras, boolean sync) { sendWindowWallpaperCommand(action, x, y, z, extras, sync); - return null; } private void sendWindowWallpaperCommand( diff --git a/services/core/java/com/android/server/wm/WindowAnimator.java b/services/core/java/com/android/server/wm/WindowAnimator.java index 750fd509e50f..b43a4540bbde 100644 --- a/services/core/java/com/android/server/wm/WindowAnimator.java +++ b/services/core/java/com/android/server/wm/WindowAnimator.java @@ -146,10 +146,11 @@ public class WindowAnimator { for (int i = 0; i < numDisplays; i++) { final DisplayContent dc = root.getChildAt(i); - dc.checkAppWindowsReadyToShow(); + if (!useShellTransition) { + dc.checkAppWindowsReadyToShow(); + } if (accessibilityController.hasCallbacks()) { - accessibilityController.drawMagnifiedRegionBorderIfNeeded(dc.mDisplayId, - mTransaction); + accessibilityController.drawMagnifiedRegionBorderIfNeeded(dc.mDisplayId); } if (dc.isAnimating(animationFlags, ANIMATION_TYPE_ALL)) { diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java index 9650b8bc2281..426694d178af 100644 --- a/services/core/java/com/android/server/wm/WindowManagerService.java +++ b/services/core/java/com/android/server/wm/WindowManagerService.java @@ -4087,7 +4087,7 @@ public class WindowManagerService extends IWindowManager.Stub throw new SecurityException("Requires READ_FRAME_BUFFER permission"); } - final Bitmap bm; + ScreenCapture.LayerCaptureArgs captureArgs; synchronized (mGlobalLock) { final DisplayContent displayContent = mRoot.getDisplayContent(DEFAULT_DISPLAY); if (displayContent == null) { @@ -4095,12 +4095,30 @@ public class WindowManagerService extends IWindowManager.Stub Slog.i(TAG_WM, "Screenshot returning null. No Display for displayId=" + DEFAULT_DISPLAY); } - bm = null; + captureArgs = null; } else { - bm = displayContent.screenshotDisplayLocked(); + captureArgs = displayContent.getLayerCaptureArgs(); } } + final Bitmap bm; + if (captureArgs != null) { + ScreenCapture.SynchronousScreenCaptureListener syncScreenCapture = + ScreenCapture.createSyncCaptureListener(); + + ScreenCapture.captureLayers(captureArgs, syncScreenCapture); + + final ScreenCapture.ScreenshotHardwareBuffer screenshotBuffer = + syncScreenCapture.getBuffer(); + bm = screenshotBuffer == null ? null : screenshotBuffer.asBitmap(); + } else { + bm = null; + } + + if (bm == null) { + Slog.w(TAG_WM, "Failed to take screenshot"); + } + FgThread.getHandler().post(() -> { try { receiver.onHandleAssistScreenshot(bm); diff --git a/services/core/java/com/android/server/wm/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java index 4ba52e4c0fd7..3f889c01bafb 100644 --- a/services/core/java/com/android/server/wm/WindowOrganizerController.java +++ b/services/core/java/com/android/server/wm/WindowOrganizerController.java @@ -1471,7 +1471,12 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub final int index = task.mChildren.indexOf(topTaskFragment); task.mChildren.remove(taskFragment); task.mChildren.add(index, taskFragment); - effects |= TRANSACT_EFFECTS_LIFECYCLE; + if (taskFragment.hasChild()) { + effects |= TRANSACT_EFFECTS_LIFECYCLE; + } else { + // Ensure that the child layers are updated if the TaskFragment is empty + task.assignChildLayers(); + } } } break; @@ -1486,7 +1491,12 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub if (task != null) { task.mChildren.remove(taskFragment); task.mChildren.add(0, taskFragment); - effects |= TRANSACT_EFFECTS_LIFECYCLE; + if (taskFragment.hasChild()) { + effects |= TRANSACT_EFFECTS_LIFECYCLE; + } else { + // Ensure that the child layers are updated if the TaskFragment is empty. + task.assignChildLayers(); + } } break; } @@ -1495,7 +1505,12 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub if (task != null) { task.mChildren.remove(taskFragment); task.mChildren.add(taskFragment); - effects |= TRANSACT_EFFECTS_LIFECYCLE; + if (taskFragment.hasChild()) { + effects |= TRANSACT_EFFECTS_LIFECYCLE; + } else { + // Ensure that the child layers are updated if the TaskFragment is empty. + task.assignChildLayers(); + } } break; } diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java index 7ad87ed8f094..24e50c54aa61 100644 --- a/services/core/java/com/android/server/wm/WindowState.java +++ b/services/core/java/com/android/server/wm/WindowState.java @@ -169,6 +169,7 @@ import static com.android.server.wm.WindowStateProto.PENDING_SEAMLESS_ROTATION; import static com.android.server.wm.WindowStateProto.REMOVED; import static com.android.server.wm.WindowStateProto.REMOVE_ON_EXIT; import static com.android.server.wm.WindowStateProto.REQUESTED_HEIGHT; +import static com.android.server.wm.WindowStateProto.REQUESTED_VISIBLE_TYPES; import static com.android.server.wm.WindowStateProto.REQUESTED_WIDTH; import static com.android.server.wm.WindowStateProto.STACK_ID; import static com.android.server.wm.WindowStateProto.SURFACE_INSETS; @@ -3988,6 +3989,7 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP proto.write(FORCE_SEAMLESS_ROTATION, mForceSeamlesslyRotate); proto.write(HAS_COMPAT_SCALE, hasCompatScale()); proto.write(GLOBAL_SCALE, mGlobalScale); + proto.write(REQUESTED_VISIBLE_TYPES, mRequestedVisibleTypes); for (Rect r : mKeepClearAreas) { r.dumpDebug(proto, KEEP_CLEAR_AREAS); } diff --git a/services/core/jni/com_android_server_input_InputManagerService.cpp b/services/core/jni/com_android_server_input_InputManagerService.cpp index 8bc41af8af62..cbc301b87295 100644 --- a/services/core/jni/com_android_server_input_InputManagerService.cpp +++ b/services/core/jni/com_android_server_input_InputManagerService.cpp @@ -306,7 +306,7 @@ public: void setMotionClassifierEnabled(bool enabled); std::optional<std::string> getBluetoothAddress(int32_t deviceId); void setStylusButtonMotionEventsEnabled(bool enabled); - FloatPoint getMouseCursorPosition(); + FloatPoint getMouseCursorPosition(int32_t displayId); void setStylusPointerIconEnabled(bool enabled); /* --- InputReaderPolicyInterface implementation --- */ @@ -1784,10 +1784,12 @@ void NativeInputManager::setStylusButtonMotionEventsEnabled(bool enabled) { InputReaderConfiguration::Change::STYLUS_BUTTON_REPORTING); } -FloatPoint NativeInputManager::getMouseCursorPosition() { +FloatPoint NativeInputManager::getMouseCursorPosition(int32_t displayId) { if (ENABLE_POINTER_CHOREOGRAPHER) { - return mInputManager->getChoreographer().getMouseCursorPosition(ADISPLAY_ID_NONE); + return mInputManager->getChoreographer().getMouseCursorPosition(displayId); } + // To maintain the status-quo, the displayId parameter (used when PointerChoreographer is + // enabled) is ignored in the old pipeline. std::scoped_lock _l(mLock); const auto pc = mLocked.legacyPointerController.lock(); if (!pc) return {AMOTION_EVENT_INVALID_CURSOR_POSITION, AMOTION_EVENT_INVALID_CURSOR_POSITION}; @@ -2751,9 +2753,10 @@ static void nativeSetStylusButtonMotionEventsEnabled(JNIEnv* env, jobject native im->setStylusButtonMotionEventsEnabled(enabled); } -static jfloatArray nativeGetMouseCursorPosition(JNIEnv* env, jobject nativeImplObj) { +static jfloatArray nativeGetMouseCursorPosition(JNIEnv* env, jobject nativeImplObj, + jint displayId) { NativeInputManager* im = getNativeInputManager(env, nativeImplObj); - const auto p = im->getMouseCursorPosition(); + const auto p = im->getMouseCursorPosition(displayId); const std::array<float, 2> arr = {{p.x, p.y}}; jfloatArray outArr = env->NewFloatArray(2); env->SetFloatArrayRegion(outArr, 0, arr.size(), arr.data()); @@ -2775,6 +2778,15 @@ static void nativeSetAccessibilityBounceKeysThreshold(JNIEnv* env, jobject nativ } } +static void nativeSetAccessibilitySlowKeysThreshold(JNIEnv* env, jobject nativeImplObj, + jint thresholdTimeMs) { + NativeInputManager* im = getNativeInputManager(env, nativeImplObj); + if (ENABLE_INPUT_FILTER_RUST) { + im->getInputManager()->getInputFilter().setAccessibilitySlowKeysThreshold( + static_cast<nsecs_t>(thresholdTimeMs) * 1000000); + } +} + static void nativeSetAccessibilityStickyKeysEnabled(JNIEnv* env, jobject nativeImplObj, jboolean enabled) { NativeInputManager* im = getNativeInputManager(env, nativeImplObj); @@ -2883,10 +2895,12 @@ static const JNINativeMethod gInputManagerMethods[] = { {"getBluetoothAddress", "(I)Ljava/lang/String;", (void*)nativeGetBluetoothAddress}, {"setStylusButtonMotionEventsEnabled", "(Z)V", (void*)nativeSetStylusButtonMotionEventsEnabled}, - {"getMouseCursorPosition", "()[F", (void*)nativeGetMouseCursorPosition}, + {"getMouseCursorPosition", "(I)[F", (void*)nativeGetMouseCursorPosition}, {"setStylusPointerIconEnabled", "(Z)V", (void*)nativeSetStylusPointerIconEnabled}, {"setAccessibilityBounceKeysThreshold", "(I)V", (void*)nativeSetAccessibilityBounceKeysThreshold}, + {"setAccessibilitySlowKeysThreshold", "(I)V", + (void*)nativeSetAccessibilitySlowKeysThreshold}, {"setAccessibilityStickyKeysEnabled", "(Z)V", (void*)nativeSetAccessibilityStickyKeysEnabled}, }; diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java index f288103bd954..519c9bb16eed 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java @@ -71,6 +71,7 @@ import static android.Manifest.permission.MANAGE_DEVICE_POLICY_STATUS_BAR; import static android.Manifest.permission.MANAGE_DEVICE_POLICY_SUPPORT_MESSAGE; import static android.Manifest.permission.MANAGE_DEVICE_POLICY_SYSTEM_DIALOGS; import static android.Manifest.permission.MANAGE_DEVICE_POLICY_SYSTEM_UPDATES; +import static android.Manifest.permission.MANAGE_DEVICE_POLICY_THREAD_NETWORK; import static android.Manifest.permission.MANAGE_DEVICE_POLICY_TIME; import static android.Manifest.permission.MANAGE_DEVICE_POLICY_USB_DATA_SIGNALLING; import static android.Manifest.permission.MANAGE_DEVICE_POLICY_USB_FILE_TRANSFER; @@ -484,6 +485,7 @@ import com.android.internal.widget.PasswordValidationError; import com.android.modules.utils.TypedXmlPullParser; import com.android.modules.utils.TypedXmlSerializer; import com.android.net.module.util.ProxyUtils; +import com.android.net.thread.flags.Flags; import com.android.server.AlarmManagerInternal; import com.android.server.LocalManagerRegistry; import com.android.server.LocalServices; @@ -13339,6 +13341,11 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { UserManager.DISALLOW_SMS, new String[]{MANAGE_DEVICE_POLICY_SMS}); USER_RESTRICTION_PERMISSIONS.put( UserManager.DISALLOW_SYSTEM_ERROR_DIALOGS, new String[]{MANAGE_DEVICE_POLICY_SYSTEM_DIALOGS}); + if (Flags.threadUserRestrictionEnabled()) { + USER_RESTRICTION_PERMISSIONS.put( + UserManager.DISALLOW_THREAD_NETWORK, + new String[]{MANAGE_DEVICE_POLICY_THREAD_NETWORK}); + } USER_RESTRICTION_PERMISSIONS.put( UserManager.DISALLOW_ULTRA_WIDEBAND_RADIO, new String[]{MANAGE_DEVICE_POLICY_NEARBY_COMMUNICATION}); USER_RESTRICTION_PERMISSIONS.put( diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java index 86ad49458c48..2b8bcc77281a 100644 --- a/services/java/com/android/server/SystemServer.java +++ b/services/java/com/android/server/SystemServer.java @@ -203,6 +203,7 @@ import com.android.server.security.FileIntegrityService; import com.android.server.security.KeyAttestationApplicationIdProviderService; import com.android.server.security.KeyChainSystemService; import com.android.server.security.rkp.RemoteProvisioningService; +import com.android.server.selinux.SelinuxAuditLogsService; import com.android.server.sensorprivacy.SensorPrivacyService; import com.android.server.sensors.SensorService; import com.android.server.signedconfig.SignedConfigService; @@ -433,6 +434,9 @@ public final class SystemServer implements Dumpable { private static final String ROLE_SERVICE_CLASS = "com.android.role.RoleService"; private static final String GAME_MANAGER_SERVICE_CLASS = "com.android.server.app.GameManagerService$Lifecycle"; + private static final String ENHANCED_CONFIRMATION_SERVICE_CLASS = + "com.android.ecm.EnhancedConfirmationService"; + private static final String UWB_APEX_SERVICE_JAR_PATH = "/apex/com.android.uwb/javalib/service-uwb.jar"; private static final String UWB_SERVICE_CLASS = "com.android.server.uwb.UwbService"; @@ -1592,6 +1596,12 @@ public final class SystemServer implements Dumpable { mSystemServiceManager.startService(DropBoxManagerService.class); t.traceEnd(); + if (android.permission.flags.Flags.enhancedConfirmationModeApisEnabled()) { + t.traceBegin("StartEnhancedConfirmationService"); + mSystemServiceManager.startService(ENHANCED_CONFIRMATION_SERVICE_CLASS); + t.traceEnd(); + } + // Grants default permissions and defines roles t.traceBegin("StartRoleManagerService"); LocalManagerRegistry.addManager(RoleServicePlatformHelper.class, @@ -2609,6 +2619,14 @@ public final class SystemServer implements Dumpable { t.traceEnd(); } + t.traceBegin("StartSelinuxAuditLogsService"); + try { + SelinuxAuditLogsService.schedule(context); + } catch (Throwable e) { + reportWtf("starting SelinuxAuditLogsService", e); + } + t.traceEnd(); + // LauncherAppsService uses ShortcutService. t.traceBegin("StartShortcutServiceLifecycle"); mSystemServiceManager.startService(ShortcutService.Lifecycle.class); diff --git a/services/permission/java/com/android/server/permission/access/permission/DevicePermissionPolicy.kt b/services/permission/java/com/android/server/permission/access/permission/DevicePermissionPolicy.kt index 24d49523b9d1..3284cf19db43 100644 --- a/services/permission/java/com/android/server/permission/access/permission/DevicePermissionPolicy.kt +++ b/services/permission/java/com/android/server/permission/access/permission/DevicePermissionPolicy.kt @@ -129,7 +129,10 @@ class DevicePermissionPolicy : SchemePolicy() { val packageState = newState.externalState.packageStates[packageName] ?: return val androidPackage = packageState.androidPackage ?: return val appId = packageState.appId - val appIdPermissionFlags = newState.userStates[userId]!!.appIdDevicePermissionFlags + // The user may happen removed due to DeletePackageHelper.removeUnusedPackagesLPw() calling + // deletePackageX() asynchronously. + val userState = newState.userStates[userId] ?: return + val devicePermissionFlags = userState.appIdDevicePermissionFlags[appId] ?: return androidPackage.requestedPermissions.forEach { permissionName -> val isRequestedByOtherPackages = anyPackageInAppId(appId) { @@ -139,7 +142,7 @@ class DevicePermissionPolicy : SchemePolicy() { if (isRequestedByOtherPackages) { return@forEach } - appIdPermissionFlags[appId]?.forEachIndexed { _, deviceId, _ -> + devicePermissionFlags.forEachIndexed { _, deviceId, _ -> setPermissionFlags(appId, deviceId, userId, permissionName, 0) } } diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/ClientControllerTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/ClientControllerTest.java index 30afa72e0f03..b9f1ea06aebe 100644 --- a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/ClientControllerTest.java +++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/ClientControllerTest.java @@ -16,13 +16,14 @@ package com.android.server.inputmethod; import static com.android.server.inputmethod.ClientController.ClientControllerCallback; -import static com.android.server.inputmethod.ClientController.ClientState; import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertWithMessage; import static org.junit.Assert.assertThrows; import static org.mockito.Mockito.any; +import static org.mockito.Mockito.anyInt; +import static org.mockito.Mockito.anyLong; import static org.mockito.Mockito.eq; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -53,6 +54,7 @@ public final class ClientControllerTest { private static final int ANY_DISPLAY_ID = Display.DEFAULT_DISPLAY; private static final int ANY_CALLER_UID = 1; private static final int ANY_CALLER_PID = 1; + private static final String SOME_PACKAGE_NAME = "some.package"; @Rule public final RavenwoodRule mRavenwood = new RavenwoodRule.Builder() @@ -81,7 +83,8 @@ public final class ClientControllerTest { } @Test - // TODO(b/314150112): Enable host side mode for this test once b/315544364 is fixed. + // TODO(b/314150112): Enable host side mode for this test once Ravenwood is enabled for + // inputmethod server classes. @IgnoreUnderRavenwood(blockedBy = {InputBinding.class, IInputMethodClientInvoker.class}) public void testAddClient_cannotAddTheSameClientTwice() { var invoker = IInputMethodClientInvoker.create(mClient, mHandler); @@ -103,7 +106,8 @@ public final class ClientControllerTest { } @Test - // TODO(b/314150112): Enable host side mode for this test once b/315544364 is fixed. + // TODO(b/314150112): Enable host side mode for this test once Ravenwood is enabled for + // inputmethod server classes. @IgnoreUnderRavenwood(blockedBy = {InputBinding.class, IInputMethodClientInvoker.class}) public void testAddClient() throws Exception { synchronized (ImfLock.class) { @@ -117,7 +121,8 @@ public final class ClientControllerTest { } @Test - // TODO(b/314150112): Enable host side mode for this test once b/315544364 is fixed. + // TODO(b/314150112): Enable host side mode for this test once Ravenwood is enabled for + // inputmethod server classes. @IgnoreUnderRavenwood(blockedBy = {InputBinding.class, IInputMethodClientInvoker.class}) public void testRemoveClient() { var callback = new TestClientControllerCallback(); @@ -137,6 +142,36 @@ public final class ClientControllerTest { assertThat(removed).isSameInstanceAs(added); } + @Test + // TODO(b/314150112): Enable host side mode for this test once Ravenwood is enabled for + // inputmethod server classes and updated to newer Mockito with static mock support (mock + // InputMethodUtils#checkIfPackageBelongsToUid instead of PackageManagerInternal#isSameApp) + @IgnoreUnderRavenwood(blockedBy = {InputMethodUtils.class}) + public void testVerifyClientAndPackageMatch() { + when(mMockPackageManagerInternal.isSameApp(eq(SOME_PACKAGE_NAME), /* flags= */ + anyLong(), eq(ANY_CALLER_UID), /* userId= */ anyInt())).thenReturn(true); + + synchronized (ImfLock.class) { + var invoker = IInputMethodClientInvoker.create(mClient, mHandler); + mController.addClient(invoker, mConnection, ANY_DISPLAY_ID, ANY_CALLER_UID, + ANY_CALLER_PID); + assertThat( + mController.verifyClientAndPackageMatch(mClient, SOME_PACKAGE_NAME)).isTrue(); + } + } + + @Test + public void testVerifyClientAndPackageMatch_unknownClient() { + synchronized (ImfLock.class) { + assertThrows(IllegalArgumentException.class, + () -> { + synchronized (ImfLock.class) { + mController.verifyClientAndPackageMatch(mClient, SOME_PACKAGE_NAME); + } + }); + } + } + private static class TestClientControllerCallback implements ClientControllerCallback { private final CountDownLatch mLatch = new CountDownLatch(1); diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/DefaultImeVisibilityApplierTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/DefaultImeVisibilityApplierTest.java index 438bea458c47..1c71a6287c79 100644 --- a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/DefaultImeVisibilityApplierTest.java +++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/DefaultImeVisibilityApplierTest.java @@ -22,7 +22,6 @@ import static android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VI import static com.android.internal.inputmethod.SoftInputShowHideReason.HIDE_SOFT_INPUT; import static com.android.internal.inputmethod.SoftInputShowHideReason.HIDE_SWITCH_USER; import static com.android.internal.inputmethod.SoftInputShowHideReason.SHOW_SOFT_INPUT; -import static com.android.server.inputmethod.ClientController.ClientState; import static com.android.server.inputmethod.ImeVisibilityStateComputer.STATE_HIDE_IME; import static com.android.server.inputmethod.ImeVisibilityStateComputer.STATE_HIDE_IME_EXPLICIT; import static com.android.server.inputmethod.ImeVisibilityStateComputer.STATE_HIDE_IME_NOT_ALWAYS; diff --git a/services/tests/PackageManagerServiceTests/server/Android.bp b/services/tests/PackageManagerServiceTests/server/Android.bp index 3aca1cafbf75..f8accc309931 100644 --- a/services/tests/PackageManagerServiceTests/server/Android.bp +++ b/services/tests/PackageManagerServiceTests/server/Android.bp @@ -103,6 +103,7 @@ android_test { ":PackageParserTestApp4", ":PackageParserTestApp5", ":PackageParserTestApp6", + ":PackageParserTestApp7", ], resource_zips: [":PackageManagerServiceServerTests_apks_as_resources"], diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageParserTest.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageParserTest.java index 71f5c754f22f..a0e0e1ef36ee 100644 --- a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageParserTest.java +++ b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageParserTest.java @@ -15,6 +15,17 @@ */ package com.android.server.pm; +import static android.content.UriRelativeFilter.PATH; +import static android.content.UriRelativeFilter.QUERY; +import static android.content.UriRelativeFilter.FRAGMENT; +import static android.content.UriRelativeFilterGroup.ACTION_ALLOW; +import static android.content.UriRelativeFilterGroup.ACTION_BLOCK; +import static android.os.PatternMatcher.PATTERN_ADVANCED_GLOB; +import static android.os.PatternMatcher.PATTERN_LITERAL; +import static android.os.PatternMatcher.PATTERN_PREFIX; +import static android.os.PatternMatcher.PATTERN_SIMPLE_GLOB; +import static android.os.PatternMatcher.PATTERN_SUFFIX; + import static com.android.internal.pm.permission.CompatibilityPermissionInfo.COMPAT_PERMS; import static com.google.common.truth.Truth.assertThat; @@ -36,11 +47,15 @@ import static java.util.stream.Collectors.toList; import android.annotation.NonNull; import android.content.Context; +import android.content.IntentFilter; +import android.content.UriRelativeFilter; +import android.content.UriRelativeFilterGroup; import android.content.pm.ActivityInfo; import android.content.pm.ApplicationInfo; import android.content.pm.ConfigurationInfo; import android.content.pm.FeatureGroupInfo; import android.content.pm.FeatureInfo; +import android.content.pm.Flags; import android.content.pm.PackageInfo; import android.content.pm.PackageManager.Property; import android.content.pm.ServiceInfo; @@ -50,6 +65,9 @@ import android.os.Bundle; import android.os.Parcel; import android.os.Parcelable; 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.ArraySet; import androidx.annotation.Nullable; @@ -106,6 +124,7 @@ import java.lang.reflect.Field; import java.nio.file.Files; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.Iterator; @@ -123,6 +142,9 @@ public class PackageParserTest { @Rule public TemporaryFolder mTemporaryFolder = new TemporaryFolder(); + @Rule + public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); + private File mTmpDir; private static final File FRAMEWORK = new File("/system/framework/framework-res.apk"); private static final String TEST_APP1_APK = "PackageParserTestApp1.apk"; @@ -131,6 +153,7 @@ public class PackageParserTest { private static final String TEST_APP4_APK = "PackageParserTestApp4.apk"; private static final String TEST_APP5_APK = "PackageParserTestApp5.apk"; private static final String TEST_APP6_APK = "PackageParserTestApp6.apk"; + private static final String TEST_APP7_APK = "PackageParserTestApp7.apk"; private static final String PACKAGE_NAME = "com.android.servicestests.apps.packageparserapp"; @Before @@ -375,6 +398,87 @@ public class PackageParserTest { assertNotEquals("$automotive", actualDisplayCategory); } + @Test + @RequiresFlagsEnabled(Flags.FLAG_RELATIVE_REFERENCE_INTENT_FILTERS) + public void testParseUriRelativeFilterGroups() throws Exception { + final File testFile = extractFile(TEST_APP7_APK); + try { + final ParsedPackage pkg = new TestPackageParser2().parsePackage(testFile, 0, false); + final List<ParsedActivity> activities = pkg.getActivities(); + final List<ParsedIntentInfo> intents = activities.get(0).getIntents(); + final IntentFilter intentFilter = intents.get(0).getIntentFilter(); + assertEquals(7, intentFilter.countUriRelativeFilterGroups()); + + UriRelativeFilterGroup group = intentFilter.getUriRelativeFilterGroup(0); + Collection<UriRelativeFilter> filters = group.getUriRelativeFilters(); + assertEquals(ACTION_BLOCK, group.getAction()); + assertEquals(3, filters.size()); + assertTrue(filters.contains(new UriRelativeFilter(PATH, PATTERN_PREFIX, "/gizmos"))); + assertTrue(filters.contains(new UriRelativeFilter(QUERY, PATTERN_SIMPLE_GLOB, + ".*query=string.*"))); + assertTrue(filters.contains(new UriRelativeFilter(FRAGMENT, PATTERN_LITERAL, + "fragment"))); + + group = intentFilter.getUriRelativeFilterGroup(1); + filters = group.getUriRelativeFilters(); + assertEquals(ACTION_ALLOW, group.getAction()); + assertEquals(2, filters.size()); + assertTrue(filters.contains(new UriRelativeFilter(QUERY, PATTERN_LITERAL, + "query=string"))); + assertTrue(filters.contains(new UriRelativeFilter(FRAGMENT, PATTERN_SUFFIX, + "fragment"))); + + group = intentFilter.getUriRelativeFilterGroup(2); + filters = group.getUriRelativeFilters(); + assertEquals(ACTION_ALLOW, group.getAction()); + assertTrue(filters.contains(new UriRelativeFilter(PATH, PATTERN_LITERAL, "/gizmos"))); + assertTrue(filters.contains(new UriRelativeFilter(QUERY, PATTERN_LITERAL, + ".*query=string.*"))); + assertTrue(filters.contains(new UriRelativeFilter(FRAGMENT, PATTERN_LITERAL, + "fragment"))); + + group = intentFilter.getUriRelativeFilterGroup(3); + filters = group.getUriRelativeFilters(); + assertEquals(ACTION_ALLOW, group.getAction()); + assertTrue(filters.contains(new UriRelativeFilter(PATH, PATTERN_PREFIX, "/gizmos"))); + assertTrue(filters.contains(new UriRelativeFilter(QUERY, PATTERN_PREFIX, + ".*query=string.*"))); + assertTrue(filters.contains(new UriRelativeFilter(FRAGMENT, PATTERN_PREFIX, + "fragment"))); + + group = intentFilter.getUriRelativeFilterGroup(4); + filters = group.getUriRelativeFilters(); + assertEquals(ACTION_ALLOW, group.getAction()); + assertTrue(filters.contains(new UriRelativeFilter(PATH, PATTERN_SIMPLE_GLOB, + "/gizmos"))); + assertTrue(filters.contains(new UriRelativeFilter(QUERY, PATTERN_SIMPLE_GLOB, + ".*query=string.*"))); + assertTrue(filters.contains(new UriRelativeFilter(FRAGMENT, PATTERN_SIMPLE_GLOB, + "fragment"))); + + group = intentFilter.getUriRelativeFilterGroup(5); + filters = group.getUriRelativeFilters(); + assertEquals(ACTION_ALLOW, group.getAction()); + assertTrue(filters.contains(new UriRelativeFilter(PATH, PATTERN_ADVANCED_GLOB, + "/gizmos"))); + assertTrue(filters.contains(new UriRelativeFilter(QUERY, PATTERN_ADVANCED_GLOB, + ".*query=string.*"))); + assertTrue(filters.contains(new UriRelativeFilter(FRAGMENT, PATTERN_ADVANCED_GLOB, + "fragment"))); + + group = intentFilter.getUriRelativeFilterGroup(6); + filters = group.getUriRelativeFilters(); + assertEquals(ACTION_ALLOW, group.getAction()); + assertTrue(filters.contains(new UriRelativeFilter(PATH, PATTERN_SUFFIX, "/gizmos"))); + assertTrue(filters.contains(new UriRelativeFilter(QUERY, PATTERN_SUFFIX, + ".*query=string.*"))); + assertTrue(filters.contains(new UriRelativeFilter(FRAGMENT, PATTERN_SUFFIX, + "fragment"))); + } finally { + testFile.delete(); + } + } + private static final int PROPERTY_TYPE_BOOLEAN = 1; private static final int PROPERTY_TYPE_FLOAT = 2; private static final int PROPERTY_TYPE_INTEGER = 3; diff --git a/services/tests/mockingservicestests/src/com/android/server/am/ServiceBindingOomAdjPolicyTest.java b/services/tests/mockingservicestests/src/com/android/server/am/ServiceBindingOomAdjPolicyTest.java new file mode 100644 index 000000000000..2f12a3b858d2 --- /dev/null +++ b/services/tests/mockingservicestests/src/com/android/server/am/ServiceBindingOomAdjPolicyTest.java @@ -0,0 +1,677 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.am; + +import static android.app.ActivityManager.PROCESS_CAPABILITY_FOREGROUND_MICROPHONE; +import static android.app.ActivityManager.PROCESS_CAPABILITY_NONE; +import static android.app.ActivityManager.PROCESS_STATE_CACHED_EMPTY; +import static android.app.ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE; +import static android.app.ActivityManager.PROCESS_STATE_HOME; +import static android.app.ActivityManager.PROCESS_STATE_LAST_ACTIVITY; +import static android.app.ActivityManager.PROCESS_STATE_SERVICE; +import static android.content.Context.BIND_AUTO_CREATE; +import static android.content.Context.BIND_INCLUDE_CAPABILITIES; +import static android.content.Context.BIND_WAIVE_PRIORITY; +import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SYSTEM_EXEMPTED; +import static android.os.UserHandle.USER_SYSTEM; +import static android.platform.test.flag.junit.SetFlagsRule.DefaultInitValueType.DEVICE_DEFAULT; + +import static com.android.server.am.ProcessList.HOME_APP_ADJ; +import static com.android.server.am.ProcessList.PERCEPTIBLE_APP_ADJ; +import static com.android.server.am.ProcessList.SERVICE_ADJ; +import static com.android.server.am.ProcessList.CACHED_APP_MIN_ADJ; + +import static org.junit.Assert.assertNotEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.clearInvocations; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import android.app.IApplicationThread; +import android.app.IServiceConnection; +import android.app.usage.UsageStatsManagerInternal; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManagerInternal; +import android.content.pm.ResolveInfo; +import android.content.pm.ServiceInfo; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.IBinder; +import android.platform.test.annotations.Presubmit; +import android.platform.test.flag.junit.SetFlagsRule; + +import androidx.test.platform.app.InstrumentationRegistry; + +import com.android.server.DropBoxManagerInternal; +import com.android.server.LocalServices; +import com.android.server.am.ActivityManagerService.Injector; +import com.android.server.am.ApplicationExitInfoTest.ServiceThreadRule; +import com.android.server.appop.AppOpsService; +import com.android.server.firewall.IntentFirewall; +import com.android.server.wm.ActivityTaskManagerService; +import com.android.server.wm.WindowProcessController; + +import org.junit.After; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Rule; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.mockito.verification.VerificationMode; + +import java.io.File; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.function.Consumer; + +/** + * Test class for the service timeout. + * + * Build/Install/Run: + * atest ServiceBindingOomAdjPolicyTest + */ +@Presubmit +public final class ServiceBindingOomAdjPolicyTest { + private static final String TAG = ServiceBindingOomAdjPolicyTest.class.getSimpleName(); + + private static final String TEST_APP1_NAME = "com.example.foo"; + private static final String TEST_SERVICE1_NAME = "com.example.foo.Foobar"; + private static final int TEST_APP1_UID = 10123; + private static final int TEST_APP1_PID = 12345; + + private static final String TEST_APP2_NAME = "com.example.bar"; + private static final String TEST_SERVICE2_NAME = "com.example.bar.Buz"; + private static final int TEST_APP2_UID = 10124; + private static final int TEST_APP2_PID = 12346; + + @Rule + public final ServiceThreadRule mServiceThreadRule = new ServiceThreadRule(); + @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(DEVICE_DEFAULT); + + private Context mContext; + private HandlerThread mHandlerThread; + + @Mock + private AppOpsService mAppOpsService; + @Mock + private DropBoxManagerInternal mDropBoxManagerInt; + @Mock + private PackageManagerInternal mPackageManagerInt; + @Mock + private UsageStatsManagerInternal mUsageStatsManagerInt; + @Mock + private AppErrors mAppErrors; + @Mock + private IntentFirewall mIntentFirewall; + + private ActivityManagerService mAms; + private ProcessList mProcessList; + private ActiveServices mActiveServices; + + private int mCurrentCallingUid; + private int mCurrentCallingPid; + + /** Run at the test class initialization */ + @BeforeClass + public static void setUpOnce() { + System.setProperty("dexmaker.share_classloader", "true"); + } + + @SuppressWarnings("GuardedBy") + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + + mContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); + + mHandlerThread = new HandlerThread(TAG); + mHandlerThread.start(); + final ProcessList realProcessList = new ProcessList(); + mProcessList = spy(realProcessList); + + LocalServices.removeServiceForTest(DropBoxManagerInternal.class); + LocalServices.addService(DropBoxManagerInternal.class, mDropBoxManagerInt); + LocalServices.removeServiceForTest(PackageManagerInternal.class); + LocalServices.addService(PackageManagerInternal.class, mPackageManagerInt); + doReturn(new ComponentName("", "")).when(mPackageManagerInt).getSystemUiServiceComponent(); + + final ActivityManagerService realAms = new ActivityManagerService( + new TestInjector(mContext), mServiceThreadRule.getThread()); + final ActivityTaskManagerService realAtm = new ActivityTaskManagerService(mContext); + realAtm.initialize(null, null, mContext.getMainLooper()); + realAms.mActivityTaskManager = spy(realAtm); + realAms.mAtmInternal = spy(realAms.mActivityTaskManager.getAtmInternal()); + realAms.mOomAdjuster = spy(realAms.mOomAdjuster); + realAms.mOomAdjuster.mCachedAppOptimizer = spy(realAms.mOomAdjuster.mCachedAppOptimizer); + realAms.mPackageManagerInt = mPackageManagerInt; + realAms.mUsageStatsService = mUsageStatsManagerInt; + realAms.mAppProfiler = spy(realAms.mAppProfiler); + realAms.mProcessesReady = true; + mAms = spy(realAms); + realProcessList.mService = mAms; + + doReturn(false).when(mPackageManagerInt).filterAppAccess(anyString(), anyInt(), anyInt()); + doReturn(true).when(mIntentFirewall).checkService(any(), any(), anyInt(), anyInt(), any(), + any()); + doReturn(false).when(mAms.mAtmInternal).hasSystemAlertWindowPermission(anyInt(), anyInt(), + any()); + doReturn(true).when(mAms.mOomAdjuster.mCachedAppOptimizer).useFreezer(); + doNothing().when(mAms.mOomAdjuster.mCachedAppOptimizer).freezeAppAsyncInternalLSP( + any(), anyLong(), anyBoolean()); + doReturn(false).when(mAms.mAppProfiler).updateLowMemStateLSP(anyInt(), anyInt(), + anyInt(), anyLong()); + + mCurrentCallingUid = TEST_APP1_UID; + mCurrentCallingPid = TEST_APP1_PID; + } + + @After + public void tearDown() throws Exception { + LocalServices.removeServiceForTest(DropBoxManagerInternal.class); + LocalServices.removeServiceForTest(PackageManagerInternal.class); + mHandlerThread.quit(); + } + + @Test + public void testServiceSelfBindingOomAdj() throws Exception { + // Enable the flags. + mSetFlagsRule.enableFlags(Flags.FLAG_SERVICE_BINDING_OOM_ADJ_POLICY); + + // Verify that there should be 0 oom adj updates. + performTestServiceSelfBindingOomAdj(never(), never()); + + // Disable the flags. + mSetFlagsRule.disableFlags(Flags.FLAG_SERVICE_BINDING_OOM_ADJ_POLICY); + + // Verify that there should be at least 1 oom adj update. + performTestServiceSelfBindingOomAdj(atLeastOnce(), atLeastOnce()); + } + + @SuppressWarnings("GuardedBy") + private void performTestServiceSelfBindingOomAdj(VerificationMode bindMode, + VerificationMode unbindMode) throws Exception { + final ProcessRecord app = addProcessRecord( + TEST_APP1_PID, // pid + TEST_APP1_UID, // uid + PROCESS_STATE_SERVICE, // procstate + SERVICE_ADJ, // adj + PROCESS_CAPABILITY_NONE, // capabilities + TEST_APP1_NAME // packageName + ); + final Intent serviceIntent = createServiceIntent(TEST_APP1_NAME, TEST_SERVICE1_NAME, + TEST_APP1_UID); + final IServiceConnection serviceConnection = mock(IServiceConnection.class); + + // Make a self binding. + assertNotEquals(0, mAms.bindService( + app.getThread(), // caller + null, // token + serviceIntent, // service + null, // resolveType + serviceConnection, // connection + BIND_AUTO_CREATE, // flags + TEST_APP1_NAME, // callingPackage + USER_SYSTEM // userId + )); + + verify(mAms.mOomAdjuster, bindMode).updateOomAdjPendingTargetsLocked(anyInt()); + clearInvocations(mAms.mOomAdjuster); + + // Unbind the service. + mAms.unbindService(serviceConnection); + + verify(mAms.mOomAdjuster, unbindMode).updateOomAdjPendingTargetsLocked(anyInt()); + clearInvocations(mAms.mOomAdjuster); + + removeProcessRecord(app); + } + + @Test + public void testServiceDistinctBindingOomAdjMoreImportant() throws Exception { + // Enable the flags. + mSetFlagsRule.enableFlags(Flags.FLAG_SERVICE_BINDING_OOM_ADJ_POLICY); + + // Verify that there should be at least 1 oom adj update + // because the client is more important. + performTestServiceDistinctBindingOomAdj(TEST_APP1_PID, TEST_APP1_UID, + PROCESS_STATE_FOREGROUND_SERVICE, PERCEPTIBLE_APP_ADJ, + PROCESS_CAPABILITY_NONE, TEST_APP1_NAME, + this::setHasForegroundServices, + TEST_APP2_PID, TEST_APP2_UID, PROCESS_STATE_HOME, + HOME_APP_ADJ, PROCESS_CAPABILITY_NONE, TEST_APP2_NAME, TEST_SERVICE2_NAME, + this::setHomeProcess, + BIND_AUTO_CREATE, + atLeastOnce(), atLeastOnce()); + + // Disable the flags. + mSetFlagsRule.disableFlags(Flags.FLAG_SERVICE_BINDING_OOM_ADJ_POLICY); + + // Verify that there should be at least 1 oom adj update + // because the client is more important. + performTestServiceDistinctBindingOomAdj(TEST_APP1_PID, TEST_APP1_UID, + PROCESS_STATE_FOREGROUND_SERVICE, PERCEPTIBLE_APP_ADJ, + PROCESS_CAPABILITY_NONE, TEST_APP1_NAME, + this::setHasForegroundServices, + TEST_APP2_PID, TEST_APP2_UID, PROCESS_STATE_HOME, + HOME_APP_ADJ, PROCESS_CAPABILITY_NONE, TEST_APP2_NAME, TEST_SERVICE2_NAME, + this::setHomeProcess, + BIND_AUTO_CREATE, + atLeastOnce(), atLeastOnce()); + } + + @Test + public void testServiceDistinctBindingOomAdjLessImportant() throws Exception { + // Enable the flags. + mSetFlagsRule.enableFlags(Flags.FLAG_SERVICE_BINDING_OOM_ADJ_POLICY); + + // Verify that there should be 0 oom adj update + performTestServiceDistinctBindingOomAdj(TEST_APP1_PID, TEST_APP1_UID, + PROCESS_STATE_HOME, HOME_APP_ADJ, PROCESS_CAPABILITY_NONE, TEST_APP1_NAME, + this::setHomeProcess, + TEST_APP2_PID, TEST_APP2_UID, PROCESS_STATE_FOREGROUND_SERVICE, + PERCEPTIBLE_APP_ADJ, PROCESS_CAPABILITY_NONE, TEST_APP2_NAME, TEST_SERVICE2_NAME, + this::setHasForegroundServices, + BIND_AUTO_CREATE, + never(), never()); + + // Disable the flags. + mSetFlagsRule.disableFlags(Flags.FLAG_SERVICE_BINDING_OOM_ADJ_POLICY); + + // Verify that there should be at least 1 oom adj update + performTestServiceDistinctBindingOomAdj(TEST_APP1_PID, TEST_APP1_UID, + PROCESS_STATE_HOME, HOME_APP_ADJ, PROCESS_CAPABILITY_NONE, TEST_APP1_NAME, + this::setHomeProcess, + TEST_APP2_PID, TEST_APP2_UID, PROCESS_STATE_FOREGROUND_SERVICE, + PERCEPTIBLE_APP_ADJ, PROCESS_CAPABILITY_NONE, TEST_APP2_NAME, TEST_SERVICE2_NAME, + this::setHasForegroundServices, + BIND_AUTO_CREATE, + atLeastOnce(), atLeastOnce()); + } + + @Test + public void testServiceDistinctBindingOomAdjWaivePriority() throws Exception { + // Enable the flags. + mSetFlagsRule.enableFlags(Flags.FLAG_SERVICE_BINDING_OOM_ADJ_POLICY); + + // Verify that there should be 0 oom adj update for binding + // because we're using the BIND_WAIVE_PRIORITY; + // but for the unbinding, because client is better than service, we can't skip it safely. + performTestServiceDistinctBindingOomAdj(TEST_APP1_PID, TEST_APP1_UID, + PROCESS_STATE_FOREGROUND_SERVICE, PERCEPTIBLE_APP_ADJ, + PROCESS_CAPABILITY_NONE, TEST_APP1_NAME, + this::setHasForegroundServices, + TEST_APP2_PID, TEST_APP2_UID, PROCESS_STATE_HOME, + HOME_APP_ADJ, PROCESS_CAPABILITY_NONE, TEST_APP2_NAME, TEST_SERVICE2_NAME, + this::setHomeProcess, + BIND_AUTO_CREATE | BIND_WAIVE_PRIORITY, + never(), atLeastOnce()); + + // Verify that there should be 0 oom adj update + // because we're using the BIND_WAIVE_PRIORITY; + performTestServiceDistinctBindingOomAdj(TEST_APP1_PID, TEST_APP1_UID, + PROCESS_STATE_HOME, HOME_APP_ADJ, PROCESS_CAPABILITY_NONE, TEST_APP1_NAME, + this::setHomeProcess, + TEST_APP2_PID, TEST_APP2_UID, PROCESS_STATE_FOREGROUND_SERVICE, + PERCEPTIBLE_APP_ADJ, PROCESS_CAPABILITY_NONE, TEST_APP2_NAME, TEST_SERVICE2_NAME, + this::setHasForegroundServices, + BIND_AUTO_CREATE | BIND_WAIVE_PRIORITY, + never(), never()); + + // Disable the flags. + mSetFlagsRule.disableFlags(Flags.FLAG_SERVICE_BINDING_OOM_ADJ_POLICY); + + // Verify that there should be at least 1 oom adj update + // because the client is more important. + performTestServiceDistinctBindingOomAdj(TEST_APP1_PID, TEST_APP1_UID, + PROCESS_STATE_FOREGROUND_SERVICE, PERCEPTIBLE_APP_ADJ, + PROCESS_CAPABILITY_NONE, TEST_APP1_NAME, + this::setHasForegroundServices, + TEST_APP2_PID, TEST_APP2_UID, PROCESS_STATE_HOME, + HOME_APP_ADJ, PROCESS_CAPABILITY_NONE, TEST_APP2_NAME, TEST_SERVICE2_NAME, + this::setHomeProcess, + BIND_AUTO_CREATE, + atLeastOnce(), atLeastOnce()); + } + + @Test + public void testServiceDistinctBindingOomAdjNoIncludeCapabilities() throws Exception { + // Enable the flags. + mSetFlagsRule.enableFlags(Flags.FLAG_SERVICE_BINDING_OOM_ADJ_POLICY); + + // Verify that there should be 0 oom adj update + // because we didn't specify the "BIND_INCLUDE_CAPABILITIES" + performTestServiceDistinctBindingOomAdj(TEST_APP1_PID, TEST_APP1_UID, + PROCESS_STATE_HOME, HOME_APP_ADJ, + PROCESS_CAPABILITY_FOREGROUND_MICROPHONE, TEST_APP1_NAME, + this::setHomeProcess, + TEST_APP2_PID, TEST_APP2_UID, PROCESS_STATE_FOREGROUND_SERVICE, + PERCEPTIBLE_APP_ADJ, PROCESS_CAPABILITY_NONE, TEST_APP2_NAME, TEST_SERVICE2_NAME, + this::setHasForegroundServices, + BIND_AUTO_CREATE, + never(), never()); + + // Disable the flags. + mSetFlagsRule.disableFlags(Flags.FLAG_SERVICE_BINDING_OOM_ADJ_POLICY); + + // Verify that there should be at least 1 oom adj update + performTestServiceDistinctBindingOomAdj(TEST_APP1_PID, TEST_APP1_UID, + PROCESS_STATE_HOME, HOME_APP_ADJ, + PROCESS_CAPABILITY_FOREGROUND_MICROPHONE, TEST_APP1_NAME, + this::setHomeProcess, + TEST_APP2_PID, TEST_APP2_UID, PROCESS_STATE_FOREGROUND_SERVICE, + PERCEPTIBLE_APP_ADJ, PROCESS_CAPABILITY_NONE, TEST_APP2_NAME, TEST_SERVICE2_NAME, + this::setHasForegroundServices, + BIND_AUTO_CREATE, + atLeastOnce(), atLeastOnce()); + } + + @Test + public void testServiceDistinctBindingOomAdjWithIncludeCapabilities() throws Exception { + // Enable the flags. + mSetFlagsRule.enableFlags(Flags.FLAG_SERVICE_BINDING_OOM_ADJ_POLICY); + + // Verify that there should be at least 1 oom adj update + // because we use the "BIND_INCLUDE_CAPABILITIES" + performTestServiceDistinctBindingOomAdj(TEST_APP1_PID, TEST_APP1_UID, + PROCESS_STATE_HOME, HOME_APP_ADJ, + PROCESS_CAPABILITY_FOREGROUND_MICROPHONE, TEST_APP1_NAME, + this::setHomeProcess, + TEST_APP2_PID, TEST_APP2_UID, PROCESS_STATE_FOREGROUND_SERVICE, + PERCEPTIBLE_APP_ADJ, PROCESS_CAPABILITY_NONE, TEST_APP2_NAME, TEST_SERVICE2_NAME, + this::setHasForegroundServices, + BIND_AUTO_CREATE | BIND_INCLUDE_CAPABILITIES, + atLeastOnce(), atLeastOnce()); + + // Disable the flags. + mSetFlagsRule.disableFlags(Flags.FLAG_SERVICE_BINDING_OOM_ADJ_POLICY); + + // Verify that there should be at least 1 oom adj update + performTestServiceDistinctBindingOomAdj(TEST_APP1_PID, TEST_APP1_UID, + PROCESS_STATE_HOME, HOME_APP_ADJ, + PROCESS_CAPABILITY_FOREGROUND_MICROPHONE, TEST_APP1_NAME, + this::setHomeProcess, + TEST_APP2_PID, TEST_APP2_UID, PROCESS_STATE_FOREGROUND_SERVICE, + PERCEPTIBLE_APP_ADJ, PROCESS_CAPABILITY_NONE, TEST_APP2_NAME, TEST_SERVICE2_NAME, + this::setHasForegroundServices, + BIND_AUTO_CREATE | BIND_INCLUDE_CAPABILITIES, + atLeastOnce(), atLeastOnce()); + } + + @Test + public void testServiceDistinctBindingOomAdjFreezeCaller() throws Exception { + // Enable the flags. + mSetFlagsRule.enableFlags(Flags.FLAG_SERVICE_BINDING_OOM_ADJ_POLICY); + + // Verify that there should be 0 oom adj update + performTestServiceDistinctBindingOomAdj(TEST_APP1_PID, TEST_APP1_UID, + PROCESS_STATE_CACHED_EMPTY, CACHED_APP_MIN_ADJ, PROCESS_CAPABILITY_NONE, + TEST_APP1_NAME, null, + TEST_APP2_PID, TEST_APP2_UID, PROCESS_STATE_FOREGROUND_SERVICE, + PERCEPTIBLE_APP_ADJ, PROCESS_CAPABILITY_NONE, TEST_APP2_NAME, TEST_SERVICE2_NAME, + this::setHasForegroundServices, + BIND_AUTO_CREATE, + never(), never()); + + // Disable the flags. + mSetFlagsRule.disableFlags(Flags.FLAG_SERVICE_BINDING_OOM_ADJ_POLICY); + + // Verify that there should be at least 1 oom adj update + performTestServiceDistinctBindingOomAdj(TEST_APP1_PID, TEST_APP1_UID, + PROCESS_STATE_CACHED_EMPTY, CACHED_APP_MIN_ADJ, PROCESS_CAPABILITY_NONE, + TEST_APP1_NAME, null, + TEST_APP2_PID, TEST_APP2_UID, PROCESS_STATE_FOREGROUND_SERVICE, + PERCEPTIBLE_APP_ADJ, PROCESS_CAPABILITY_NONE, TEST_APP2_NAME, TEST_SERVICE2_NAME, + this::setHasForegroundServices, + BIND_AUTO_CREATE, + atLeastOnce(), atLeastOnce()); + } + + @SuppressWarnings("GuardedBy") + private void performTestServiceDistinctBindingOomAdj(int clientPid, int clientUid, + int clientProcState, int clientAdj, int clientCap, String clientPackageName, + Consumer<ProcessRecord> clientAppFixer, + int servicePid, int serviceUid, int serviceProcState, int serviceAdj, + int serviceCap, String servicePackageName, String serviceName, + Consumer<ProcessRecord> serviceAppFixer, int bindingFlags, + VerificationMode bindMode, VerificationMode unbindMode) throws Exception { + final ProcessRecord clientApp = addProcessRecord( + clientPid, + clientUid, + clientProcState, + clientAdj, + clientCap, + clientPackageName + ); + final ProcessRecord serviceApp = addProcessRecord( + servicePid, + serviceUid, + serviceProcState, + serviceAdj, + serviceCap, + servicePackageName + ); + final Intent serviceIntent = createServiceIntent(servicePackageName, serviceName, + serviceUid); + final IServiceConnection serviceConnection = mock(IServiceConnection.class); + if (clientAppFixer != null) clientAppFixer.accept(clientApp); + if (serviceAppFixer != null) serviceAppFixer.accept(serviceApp); + + // Make a self binding. + assertNotEquals(0, mAms.bindService( + clientApp.getThread(), // caller + null, // token + serviceIntent, // service + null, // resolveType + serviceConnection, // connection + bindingFlags, // flags + clientPackageName, // callingPackage + USER_SYSTEM // userId + )); + + verify(mAms.mOomAdjuster, bindMode).updateOomAdjPendingTargetsLocked(anyInt()); + clearInvocations(mAms.mOomAdjuster); + + if (clientApp.isFreezable()) { + verify(mAms.mOomAdjuster.mCachedAppOptimizer, + times(Flags.serviceBindingOomAdjPolicy() ? 1 : 0)) + .freezeAppAsyncInternalLSP(eq(clientApp), eq(0L), anyBoolean()); + clearInvocations(mAms.mOomAdjuster.mCachedAppOptimizer); + } + + // Unbind the service. + mAms.unbindService(serviceConnection); + + verify(mAms.mOomAdjuster, unbindMode).updateOomAdjPendingTargetsLocked(anyInt()); + clearInvocations(mAms.mOomAdjuster); + + removeProcessRecord(clientApp); + removeProcessRecord(serviceApp); + } + + private void setHasForegroundServices(ProcessRecord app) { + app.mServices.setHasForegroundServices(true, + FOREGROUND_SERVICE_TYPE_SYSTEM_EXEMPTED, false); + } + + private void setHomeProcess(ProcessRecord app) { + final WindowProcessController wpc = app.getWindowProcessController(); + doReturn(true).when(wpc).isHomeProcess(); + } + + @SuppressWarnings("GuardedBy") + private ProcessRecord addProcessRecord(int pid, int uid, int procState, int adj, int cap, + String packageName) { + final IApplicationThread appThread = mock(IApplicationThread.class); + final IBinder threadBinder = mock(IBinder.class); + final ProcessRecord app = makeProcessRecord(pid, uid, uid, null, 0, + procState, adj, cap, 0L, 0L, packageName, packageName, mAms); + + app.makeActive(appThread, mAms.mProcessStats); + doReturn(threadBinder).when(appThread).asBinder(); + mProcessList.addProcessNameLocked(app); + mProcessList.updateLruProcessLocked(app, false, null); + + setFieldValue(ProcessRecord.class, app, "mWindowProcessController", + mock(WindowProcessController.class)); + + doReturn(app.getSetCapability()).when(mAms.mOomAdjuster).getDefaultCapability( + eq(app), anyInt()); + + return app; + } + + @SuppressWarnings("GuardedBy") + private Intent createServiceIntent(String packageName, String serviceName, int serviceUid) { + final ComponentName compName = new ComponentName(packageName, serviceName); + final Intent serviceIntent = new Intent().setComponent(compName); + final ResolveInfo rInfo = new ResolveInfo(); + rInfo.serviceInfo = makeServiceInfo(compName.getClassName(), compName.getPackageName(), + serviceUid); + doReturn(rInfo).when(mPackageManagerInt).resolveService(any(Intent.class), any(), + anyLong(), anyInt(), anyInt()); + + return serviceIntent; + } + + @SuppressWarnings("GuardedBy") + private void removeProcessRecord(ProcessRecord app) { + app.setKilled(true); + mProcessList.removeProcessNameLocked(app.processName, app.uid); + mProcessList.removeLruProcessLocked(app); + } + + @SuppressWarnings("GuardedBy") + private ProcessRecord makeProcessRecord(int pid, int uid, int packageUid, Integer definingUid, + int connectionGroup, int procState, int adj, int cap, long pss, long rss, + String processName, String packageName, ActivityManagerService ams) { + final ProcessRecord app = ApplicationExitInfoTest.makeProcessRecord(pid, uid, packageUid, + definingUid, connectionGroup, procState, pss, rss, processName, packageName, ams); + app.mState.setCurProcState(procState); + app.mState.setSetProcState(procState); + app.mState.setCurAdj(adj); + app.mState.setSetAdj(adj); + app.mState.setCurCapability(cap); + app.mState.setSetCapability(cap); + app.mState.setCached(procState >= PROCESS_STATE_LAST_ACTIVITY || adj >= CACHED_APP_MIN_ADJ); + return app; + } + + @SuppressWarnings("GuardedBy") + private ServiceInfo makeServiceInfo(String serviceName, String packageName, int packageUid) { + final ServiceInfo sInfo = new ServiceInfo(); + sInfo.name = serviceName; + sInfo.processName = packageName; + sInfo.packageName = packageName; + sInfo.applicationInfo = new ApplicationInfo(); + sInfo.applicationInfo.uid = packageUid; + sInfo.applicationInfo.packageName = packageName; + sInfo.exported = true; + return sInfo; + } + + private static <T> void setFieldValue(Class clazz, Object obj, String fieldName, T val) { + try { + Field field = clazz.getDeclaredField(fieldName); + field.setAccessible(true); + Field mfield = Field.class.getDeclaredField("accessFlags"); + mfield.setAccessible(true); + mfield.setInt(field, mfield.getInt(field) & ~(Modifier.FINAL | Modifier.PRIVATE)); + field.set(obj, val); + } catch (NoSuchFieldException | IllegalAccessException e) { + } + } + + private class TestInjector extends Injector { + TestInjector(Context context) { + super(context); + } + + @Override + public AppOpsService getAppOpsService(File recentAccessesFile, File storageFile, + Handler handler) { + return mAppOpsService; + } + + @Override + public Handler getUiHandler(ActivityManagerService service) { + return mHandlerThread.getThreadHandler(); + } + + @Override + public ProcessList getProcessList(ActivityManagerService service) { + return mProcessList; + } + + @Override + public ActiveServices getActiveServices(ActivityManagerService service) { + if (mActiveServices == null) { + mActiveServices = spy(new ActiveServices(service)); + } + return mActiveServices; + } + + @Override + public int getCallingUid() { + return mCurrentCallingUid; + } + + @Override + public int getCallingPid() { + return mCurrentCallingPid; + } + + @Override + public long clearCallingIdentity() { + return (((long) mCurrentCallingUid) << 32) | mCurrentCallingPid; + } + + @Override + public void restoreCallingIdentity(long ident) { + } + + @Override + public AppErrors getAppErrors() { + return mAppErrors; + } + + @Override + public IntentFirewall getIntentFirewall() { + return mIntentFirewall; + } + } + + // TODO: [b/302724778] Remove manual JNI load + static { + System.loadLibrary("mockingservicestestjni"); + } +} diff --git a/services/tests/mockingservicestests/src/com/android/server/job/controllers/FlexibilityControllerTest.java b/services/tests/mockingservicestests/src/com/android/server/job/controllers/FlexibilityControllerTest.java index 93a2eefba5b1..28471b37d2a0 100644 --- a/services/tests/mockingservicestests/src/com/android/server/job/controllers/FlexibilityControllerTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/job/controllers/FlexibilityControllerTest.java @@ -180,6 +180,7 @@ public class FlexibilityControllerTest { JobSchedulerService.sElapsedRealtimeClock = Clock.fixed(Instant.ofEpochMilli(FROZEN_TIME), ZoneOffset.UTC); // Initialize real objects. + doReturn(Long.MAX_VALUE).when(mPrefetchController).getNextEstimatedLaunchTimeLocked(any()); ArgumentCaptor<BroadcastReceiver> receiverCaptor = ArgumentCaptor.forClass(BroadcastReceiver.class); mFlexibilityController = new FlexibilityController(mJobSchedulerService, diff --git a/services/tests/mockingservicestests/src/com/android/server/job/controllers/JobStatusTest.java b/services/tests/mockingservicestests/src/com/android/server/job/controllers/JobStatusTest.java index 293391f43828..c6608e61fc62 100644 --- a/services/tests/mockingservicestests/src/com/android/server/job/controllers/JobStatusTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/job/controllers/JobStatusTest.java @@ -45,6 +45,7 @@ import static com.android.server.job.controllers.JobStatus.CONSTRAINT_WITHIN_QUO import static com.android.server.job.controllers.JobStatus.NO_EARLIEST_RUNTIME; import static com.android.server.job.controllers.JobStatus.NO_LATEST_RUNTIME; +import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; @@ -78,6 +79,7 @@ import org.mockito.quality.Strictness; import java.time.Clock; import java.time.ZoneOffset; +import java.util.Arrays; @RunWith(AndroidJUnit4.class) public class JobStatusTest { @@ -138,6 +140,35 @@ public class JobStatusTest { } @Test + public void testApplyBasicPiiFilters_email() { + assertEquals("[EMAIL]", JobStatus.applyBasicPiiFilters("test@email.com")); + assertEquals("[EMAIL]", JobStatus.applyBasicPiiFilters("test+plus@email.com")); + assertEquals("[EMAIL]", JobStatus.applyBasicPiiFilters("t.e_st+plus-minus@email.com")); + + assertEquals("prefix:[EMAIL]", JobStatus.applyBasicPiiFilters("prefix:test@email.com")); + + assertEquals("not-an-email", JobStatus.applyBasicPiiFilters("not-an-email")); + } + + @Test + public void testApplyBasicPiiFilters_mixture() { + assertEquals("[PHONE]:[EMAIL]", + JobStatus.applyBasicPiiFilters("123-456-7890:test+plus@email.com")); + assertEquals("prefix:[PHONE]:[EMAIL]", + JobStatus.applyBasicPiiFilters("prefix:123-456-7890:test+plus@email.com")); + } + + @Test + public void testApplyBasicPiiFilters_phone() { + assertEquals("[PHONE]", JobStatus.applyBasicPiiFilters("123-456-7890")); + assertEquals("[PHONE]", JobStatus.applyBasicPiiFilters("+1-234-567-8900")); + + assertEquals("prefix:[PHONE]", JobStatus.applyBasicPiiFilters("prefix:123-456-7890")); + + assertEquals("not-a-phone-number", JobStatus.applyBasicPiiFilters("not-a-phone-number")); + } + + @Test public void testCanRunInBatterySaver_regular() { final JobInfo jobInfo = new JobInfo.Builder(101, new ComponentName("foo", "bar")).build(); @@ -245,6 +276,42 @@ public class JobStatusTest { } @Test + public void testGetFilteredDebugTags() { + final JobInfo jobInfo = new JobInfo.Builder(101, new ComponentName("foo", "bar")) + .addDebugTag("test@email.com") + .addDebugTag("123-456-7890") + .addDebugTag("random") + .build(); + JobStatus job = createJobStatus(jobInfo); + String[] expected = new String[]{"[EMAIL]", "[PHONE]", "random"}; + String[] result = job.getFilteredDebugTags(); + Arrays.sort(expected); + Arrays.sort(result); + assertArrayEquals(expected, result); + } + + @Test + public void testGetFilteredTraceTag() { + JobInfo jobInfo = new JobInfo.Builder(101, new ComponentName("foo", "bar")) + .setTraceTag("test@email.com") + .build(); + JobStatus job = createJobStatus(jobInfo); + assertEquals("[EMAIL]", job.getFilteredTraceTag()); + + jobInfo = new JobInfo.Builder(101, new ComponentName("foo", "bar")) + .setTraceTag("123-456-7890") + .build(); + job = createJobStatus(jobInfo); + assertEquals("[PHONE]", job.getFilteredTraceTag()); + + jobInfo = new JobInfo.Builder(101, new ComponentName("foo", "bar")) + .setTraceTag("random") + .build(); + job = createJobStatus(jobInfo); + assertEquals("random", job.getFilteredTraceTag()); + } + + @Test public void testIsUserVisibleJob() { JobInfo jobInfo = new JobInfo.Builder(101, new ComponentName("foo", "bar")) .setUserInitiated(false) 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 8d9a6c510576..9a143d5b3743 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 @@ -21,6 +21,8 @@ import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertThrows; import android.content.Context; +import android.frameworks.location.altitude.GetGeoidHeightRequest; +import android.frameworks.location.altitude.GetGeoidHeightResponse; import android.location.Location; import android.location.altitude.AltitudeConverter; @@ -176,4 +178,20 @@ public class AltitudeConverterTest { assertThrows(IllegalArgumentException.class, () -> mAltitudeConverter.addMslAltitudeToLocation(mContext, location)); } + + @Test + public void testGetGeoidHeight_expectedBehavior() throws IOException { + GetGeoidHeightRequest request = new GetGeoidHeightRequest(); + request.latitudeDegrees = -35.334815; + request.longitudeDegrees = -45; + // Requires data to be loaded from raw assets. + GetGeoidHeightResponse response = mAltitudeConverter.getGeoidHeight(mContext, request); + 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.additionalGeoidHeightErrorMeters).isGreaterThan(0f); + assertThat(response.additionalGeoidHeightErrorMeters).isLessThan(1f); + assertThat(response.success).isTrue(); + } } diff --git a/services/tests/mockingservicestests/src/com/android/server/location/provider/StationaryThrottlingLocationProviderTest.java b/services/tests/mockingservicestests/src/com/android/server/location/provider/StationaryThrottlingLocationProviderTest.java index 4eba21934a4e..efab19cc9663 100644 --- a/services/tests/mockingservicestests/src/com/android/server/location/provider/StationaryThrottlingLocationProviderTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/location/provider/StationaryThrottlingLocationProviderTest.java @@ -29,6 +29,7 @@ import static org.mockito.MockitoAnnotations.initMocks; import android.content.Context; import android.location.Location; +import android.location.LocationRequest; import android.location.LocationResult; import android.location.provider.ProviderRequest; import android.platform.test.annotations.Presubmit; @@ -218,4 +219,22 @@ public class StationaryThrottlingLocationProviderTest { verify(mDelegate, never()).onSetRequest(ProviderRequest.EMPTY_REQUEST); verify(mListener, after(75).times(1)).onReportLocation(any(LocationResult.class)); } + + @Test + public void testNoThrottle_highAccuracy() { + ProviderRequest request = new ProviderRequest.Builder().setIntervalMillis( + 50).setQuality(LocationRequest.QUALITY_HIGH_ACCURACY).build(); + + mProvider.getController().setRequest(request); + verify(mDelegate).onSetRequest(request); + + LocationResult loc = createLocationResult("test_provider", mRandom); + mDelegateProvider.reportLocation(loc); + verify(mListener, times(1)).onReportLocation(loc); + + mInjector.getDeviceStationaryHelper().setStationary(true); + mInjector.getDeviceIdleHelper().setIdle(true); + verify(mDelegate, never()).onSetRequest(ProviderRequest.EMPTY_REQUEST); + verify(mListener, after(75).times(1)).onReportLocation(any(LocationResult.class)); + } } diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/UserManagerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/pm/UserManagerServiceTest.java index 5bec903e6414..656bc71eebca 100644 --- a/services/tests/mockingservicestests/src/com/android/server/pm/UserManagerServiceTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/pm/UserManagerServiceTest.java @@ -556,7 +556,7 @@ public final class UserManagerServiceTest { @Test public void testCreateUserWithLongName_TruncatesName() { UserInfo user = mUms.createUserWithThrow(generateLongString(), USER_TYPE_FULL_SECONDARY, 0); - assertThat(user.name.length()).isEqualTo(500); + assertThat(user.name.length()).isEqualTo(UserManager.MAX_USER_NAME_LENGTH); UserInfo user1 = mUms.createUserWithThrow("Test", USER_TYPE_FULL_SECONDARY, 0); assertThat(user1.name.length()).isEqualTo(4); } diff --git a/services/tests/mockingservicestests/src/com/android/server/selinux/RateLimiterTest.java b/services/tests/mockingservicestests/src/com/android/server/selinux/RateLimiterTest.java new file mode 100644 index 000000000000..01c7fbe5bfe9 --- /dev/null +++ b/services/tests/mockingservicestests/src/com/android/server/selinux/RateLimiterTest.java @@ -0,0 +1,113 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.server.selinux; + +import static com.google.common.truth.Truth.assertThat; + +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import com.android.internal.os.Clock; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.time.Duration; +import java.time.temporal.ChronoUnit; + +@RunWith(AndroidJUnit4.class) +public class RateLimiterTest { + + private final MockClock mMockClock = new MockClock(); + + @Test + public void testRateLimiter_1QPS() { + RateLimiter rateLimiter = new RateLimiter(mMockClock, Duration.ofSeconds(1)); + + // First acquire is granted. + assertThat(rateLimiter.tryAcquire()).isTrue(); + // Next acquire is negated because it's too soon. + assertThat(rateLimiter.tryAcquire()).isFalse(); + // Wait >=1 seconds. + mMockClock.currentTimeMillis += Duration.ofSeconds(1).toMillis(); + assertThat(rateLimiter.tryAcquire()).isTrue(); + } + + @Test + public void testRateLimiter_3QPS() { + RateLimiter rateLimiter = + new RateLimiter( + mMockClock, + Duration.ofSeconds(1).dividedBy(3).truncatedTo(ChronoUnit.MILLIS)); + + assertThat(rateLimiter.tryAcquire()).isTrue(); + mMockClock.currentTimeMillis += Duration.ofSeconds(1).dividedBy(2).toMillis(); + assertThat(rateLimiter.tryAcquire()).isTrue(); + mMockClock.currentTimeMillis += Duration.ofSeconds(1).dividedBy(3).toMillis(); + assertThat(rateLimiter.tryAcquire()).isTrue(); + mMockClock.currentTimeMillis += Duration.ofSeconds(1).dividedBy(4).toMillis(); + assertThat(rateLimiter.tryAcquire()).isFalse(); + } + + @Test + public void testRateLimiter_infiniteQPS() { + RateLimiter rateLimiter = new RateLimiter(mMockClock, Duration.ofMillis(0)); + + // so many permits. + assertThat(rateLimiter.tryAcquire()).isTrue(); + assertThat(rateLimiter.tryAcquire()).isTrue(); + assertThat(rateLimiter.tryAcquire()).isTrue(); + assertThat(rateLimiter.tryAcquire()).isTrue(); + assertThat(rateLimiter.tryAcquire()).isTrue(); + + mMockClock.currentTimeMillis += Duration.ofSeconds(10).toMillis(); + // still so many permits. + assertThat(rateLimiter.tryAcquire()).isTrue(); + assertThat(rateLimiter.tryAcquire()).isTrue(); + assertThat(rateLimiter.tryAcquire()).isTrue(); + assertThat(rateLimiter.tryAcquire()).isTrue(); + assertThat(rateLimiter.tryAcquire()).isTrue(); + + mMockClock.currentTimeMillis += Duration.ofDays(-10).toMillis(); + // only going backwards in time you will stop the permits. + assertThat(rateLimiter.tryAcquire()).isFalse(); + assertThat(rateLimiter.tryAcquire()).isFalse(); + assertThat(rateLimiter.tryAcquire()).isFalse(); + } + + @Test + public void testRateLimiter_negativeQPS() { + RateLimiter rateLimiter = new RateLimiter(mMockClock, Duration.ofMillis(-10)); + + // Negative QPS is effectively turning of the rate limiter. + assertThat(rateLimiter.tryAcquire()).isTrue(); + assertThat(rateLimiter.tryAcquire()).isTrue(); + assertThat(rateLimiter.tryAcquire()).isTrue(); + mMockClock.currentTimeMillis += Duration.ofSeconds(1000).toMillis(); + assertThat(rateLimiter.tryAcquire()).isTrue(); + assertThat(rateLimiter.tryAcquire()).isTrue(); + assertThat(rateLimiter.tryAcquire()).isTrue(); + } + + private static final class MockClock extends Clock { + + public long currentTimeMillis = 0; + + @Override + public long currentTimeMillis() { + return currentTimeMillis; + } + } +} diff --git a/services/tests/mockingservicestests/src/com/android/server/selinux/SelinuxAuditLogsBuilderTest.java b/services/tests/mockingservicestests/src/com/android/server/selinux/SelinuxAuditLogsBuilderTest.java new file mode 100644 index 000000000000..b36c9bdaf456 --- /dev/null +++ b/services/tests/mockingservicestests/src/com/android/server/selinux/SelinuxAuditLogsBuilderTest.java @@ -0,0 +1,202 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.server.selinux; + +import static com.android.server.selinux.SelinuxAuditLogBuilder.PATH_MATCHER; +import static com.android.server.selinux.SelinuxAuditLogBuilder.SCONTEXT_MATCHER; +import static com.android.server.selinux.SelinuxAuditLogBuilder.TCONTEXT_MATCHER; +import static com.android.server.selinux.SelinuxAuditLogBuilder.toCategories; + +import static com.google.common.truth.Truth.assertThat; + +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import com.android.server.selinux.SelinuxAuditLogBuilder.SelinuxAuditLog; + +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +public class SelinuxAuditLogsBuilderTest { + + private final SelinuxAuditLogBuilder mAuditLogBuilder = new SelinuxAuditLogBuilder(); + + @Test + public void testMatcher_scontext() { + assertThat(SCONTEXT_MATCHER.reset("u:r:sdk_sandbox_audit:s0").matches()).isTrue(); + assertThat(SCONTEXT_MATCHER.group("stype")).isEqualTo("sdk_sandbox_audit"); + assertThat(SCONTEXT_MATCHER.group("scategories")).isNull(); + + assertThat(SCONTEXT_MATCHER.reset("u:r:sdk_sandbox_audit:s0:c123,c456").matches()).isTrue(); + assertThat(SCONTEXT_MATCHER.group("stype")).isEqualTo("sdk_sandbox_audit"); + assertThat(toCategories(SCONTEXT_MATCHER.group("scategories"))) + .isEqualTo(new int[] {123, 456}); + + assertThat(SCONTEXT_MATCHER.reset("u:r:not_sdk_sandbox:s0").matches()).isFalse(); + assertThat(SCONTEXT_MATCHER.reset("u:object_r:sdk_sandbox_audit:s0").matches()).isFalse(); + assertThat(SCONTEXT_MATCHER.reset("u:r:sdk_sandbox_audit:s0:p123").matches()).isFalse(); + } + + @Test + public void testMatcher_tcontext() { + assertThat(TCONTEXT_MATCHER.reset("u:object_r:target_type:s0").matches()).isTrue(); + assertThat(TCONTEXT_MATCHER.group("ttype")).isEqualTo("target_type"); + assertThat(TCONTEXT_MATCHER.group("tcategories")).isNull(); + + assertThat(TCONTEXT_MATCHER.reset("u:object_r:target_type2:s0:c666").matches()).isTrue(); + assertThat(TCONTEXT_MATCHER.group("ttype")).isEqualTo("target_type2"); + assertThat(toCategories(TCONTEXT_MATCHER.group("tcategories"))).isEqualTo(new int[] {666}); + + assertThat(TCONTEXT_MATCHER.reset("u:r:target_type:s0").matches()).isFalse(); + assertThat(TCONTEXT_MATCHER.reset("u:r:sdk_sandbox_audit:s0:x456").matches()).isFalse(); + } + + @Test + public void testMatcher_path() { + assertThat(PATH_MATCHER.reset("\"/data\"").matches()).isTrue(); + assertThat(PATH_MATCHER.group("path")).isEqualTo("/data"); + assertThat(PATH_MATCHER.reset("\"/data/local\"").matches()).isTrue(); + assertThat(PATH_MATCHER.group("path")).isEqualTo("/data/local"); + assertThat(PATH_MATCHER.reset("\"/data/local/tmp\"").matches()).isTrue(); + assertThat(PATH_MATCHER.group("path")).isEqualTo("/data/local"); + + assertThat(PATH_MATCHER.reset("\"/data/local").matches()).isFalse(); + assertThat(PATH_MATCHER.reset("\"_data_local\"").matches()).isFalse(); + } + + @Test + public void testSelinuxAuditLogsBuilder_noOptionals() { + mAuditLogBuilder.reset( + "granted { p } scontext=u:r:sdk_sandbox_audit:s0 tcontext=u:object_r:t:s0" + + " tclass=c"); + assertAuditLog( + mAuditLogBuilder.build(), true, new String[] {"p"}, "sdk_sandbox_audit", "t", "c"); + + mAuditLogBuilder.reset( + "tclass=c2 granted { p2 } tcontext=u:object_r:t2:s0" + + " scontext=u:r:sdk_sandbox_audit:s0"); + assertAuditLog( + mAuditLogBuilder.build(), + true, + new String[] {"p2"}, + "sdk_sandbox_audit", + "t2", + "c2"); + } + + @Test + public void testSelinuxAuditLogsBuilder_withCategories() { + mAuditLogBuilder.reset( + "granted { p } scontext=u:r:sdk_sandbox_audit:s0:c123" + + " tcontext=u:object_r:t:s0:c456,c666 tclass=c"); + assertAuditLog( + mAuditLogBuilder.build(), + true, + new String[] {"p"}, + "sdk_sandbox_audit", + new int[] {123}, + "t", + new int[] {456, 666}, + "c", + null, + false); + } + + @Test + public void testSelinuxAuditLogsBuilder_withPath() { + mAuditLogBuilder.reset( + "granted { p } scontext=u:r:sdk_sandbox_audit:s0 path=\"/very/long/path\"" + + " tcontext=u:object_r:t:s0 tclass=c"); + assertAuditLog( + mAuditLogBuilder.build(), + true, + new String[] {"p"}, + "sdk_sandbox_audit", + null, + "t", + null, + "c", + "/very/long", + false); + } + + @Test + public void testSelinuxAuditLogsBuilder_withPermissive() { + mAuditLogBuilder.reset( + "granted { p } scontext=u:r:sdk_sandbox_audit:s0 permissive=0" + + " tcontext=u:object_r:t:s0 tclass=c"); + assertAuditLog( + mAuditLogBuilder.build(), + true, + new String[] {"p"}, + "sdk_sandbox_audit", + null, + "t", + null, + "c", + null, + false); + + mAuditLogBuilder.reset( + "granted { p } scontext=u:r:sdk_sandbox_audit:s0 tcontext=u:object_r:t:s0 tclass=c" + + " permissive=1"); + assertAuditLog( + mAuditLogBuilder.build(), + true, + new String[] {"p"}, + "sdk_sandbox_audit", + null, + "t", + null, + "c", + null, + true); + } + + private void assertAuditLog( + SelinuxAuditLog auditLog, + boolean granted, + String[] permissions, + String sType, + String tType, + String tClass) { + assertAuditLog( + auditLog, granted, permissions, sType, null, tType, null, tClass, null, false); + } + + private void assertAuditLog( + SelinuxAuditLog auditLog, + boolean granted, + String[] permissions, + String sType, + int[] sCategories, + String tType, + int[] tCategories, + String tClass, + String path, + boolean permissive) { + assertThat(auditLog).isNotNull(); + assertThat(auditLog.mGranted).isEqualTo(granted); + assertThat(auditLog.mPermissions).isEqualTo(permissions); + assertThat(auditLog.mSType).isEqualTo(sType); + assertThat(auditLog.mSCategories).isEqualTo(sCategories); + assertThat(auditLog.mTType).isEqualTo(tType); + assertThat(auditLog.mTCategories).isEqualTo(tCategories); + assertThat(auditLog.mTClass).isEqualTo(tClass); + assertThat(auditLog.mPath).isEqualTo(path); + assertThat(auditLog.mPermissive).isEqualTo(permissive); + } +} diff --git a/services/tests/mockingservicestests/src/com/android/server/selinux/SelinuxAuditLogsCollectorTest.java b/services/tests/mockingservicestests/src/com/android/server/selinux/SelinuxAuditLogsCollectorTest.java new file mode 100644 index 000000000000..9758ea596335 --- /dev/null +++ b/services/tests/mockingservicestests/src/com/android/server/selinux/SelinuxAuditLogsCollectorTest.java @@ -0,0 +1,644 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.server.selinux; + +import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; + +import android.util.EventLog; + +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import com.android.internal.os.Clock; +import com.android.internal.util.FrameworkStatsLog; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.MockitoSession; + +import java.time.Duration; +import java.time.Instant; +import java.util.Arrays; +import java.util.stream.Collectors; + +@RunWith(AndroidJUnit4.class) +public class SelinuxAuditLogsCollectorTest { + + // Fake tag to use for testing + private static final int ANSWER_TAG = 42; + + private final MockClock mClock = new MockClock(); + + private final SelinuxAuditLogsCollector mSelinuxAutidLogsCollector = + // Ignore rate limiting for tests + new SelinuxAuditLogsCollector( + new RateLimiter(mClock, /* window= */ Duration.ofMillis(0)), + new QuotaLimiter( + mClock, /* windowSize= */ Duration.ofHours(1), /* maxPermits= */ 5)); + + private MockitoSession mMockitoSession; + + @Before + public void setUp() { + // move the clock forward for the limiters. + mClock.currentTimeMillis += Duration.ofHours(1).toMillis(); + // Ignore what was written in the event logs by previous tests. + mSelinuxAutidLogsCollector.mLastWrite = Instant.now(); + + mMockitoSession = + mockitoSession().initMocks(this).mockStatic(FrameworkStatsLog.class).startMocking(); + } + + @After + public void tearDown() { + mMockitoSession.finishMocking(); + } + + @Test + public void testWriteSdkSandboxAuditLogs() { + writeTestLog("granted", "perm", "sdk_sandbox_audit", "ttype", "tclass"); + writeTestLog("denied", "perm1", "sdk_sandbox_audit", "ttype1", "tclass1"); + + boolean done = mSelinuxAutidLogsCollector.collect(ANSWER_TAG); + + assertThat(done).isTrue(); + verify( + () -> + FrameworkStatsLog.write( + FrameworkStatsLog.SELINUX_AUDIT_LOG, + true, + new String[] {"perm"}, + "sdk_sandbox_audit", + null, + "ttype", + null, + "tclass", + null, + false)); + verify( + () -> + FrameworkStatsLog.write( + FrameworkStatsLog.SELINUX_AUDIT_LOG, + false, + new String[] {"perm1"}, + "sdk_sandbox_audit", + null, + "ttype1", + null, + "tclass1", + null, + false)); + } + + @Test + public void testWriteSdkSandboxAuditLogs_multiplePerms() { + writeTestLog("denied", "perm1 perm2", "sdk_sandbox_audit", "ttype", "tclass"); + writeTestLog("denied", "perm3 perm4", "sdk_sandbox_audit", "ttype", "tclass"); + + boolean done = mSelinuxAutidLogsCollector.collect(ANSWER_TAG); + + assertThat(done).isTrue(); + verify( + () -> + FrameworkStatsLog.write( + FrameworkStatsLog.SELINUX_AUDIT_LOG, + false, + new String[] {"perm1", "perm2"}, + "sdk_sandbox_audit", + null, + "ttype", + null, + "tclass", + null, + false)); + verify( + () -> + FrameworkStatsLog.write( + FrameworkStatsLog.SELINUX_AUDIT_LOG, + false, + new String[] {"perm3", "perm4"}, + "sdk_sandbox_audit", + null, + "ttype", + null, + "tclass", + null, + false)); + } + + @Test + public void testWriteSdkSandboxAuditLogs_withPaths() { + writeTestLog("denied", "perm", "sdk_sandbox_audit", "ttype", "tclass", "/good/path"); + writeTestLog("denied", "perm", "sdk_sandbox_audit", "ttype", "tclass", "/very/long/path"); + writeTestLog("denied", "perm", "sdk_sandbox_audit", "ttype", "tclass", "/short_path"); + writeTestLog("denied", "perm", "sdk_sandbox_audit", "ttype", "tclass", "not_a_path"); + + boolean done = mSelinuxAutidLogsCollector.collect(ANSWER_TAG); + + assertThat(done).isTrue(); + verify( + () -> + FrameworkStatsLog.write( + FrameworkStatsLog.SELINUX_AUDIT_LOG, + false, + new String[] {"perm"}, + "sdk_sandbox_audit", + null, + "ttype", + null, + "tclass", + "/good/path", + false)); + verify( + () -> + FrameworkStatsLog.write( + FrameworkStatsLog.SELINUX_AUDIT_LOG, + false, + new String[] {"perm"}, + "sdk_sandbox_audit", + null, + "ttype", + null, + "tclass", + "/very/long", + false)); + verify( + () -> + FrameworkStatsLog.write( + FrameworkStatsLog.SELINUX_AUDIT_LOG, + false, + new String[] {"perm"}, + "sdk_sandbox_audit", + null, + "ttype", + null, + "tclass", + "/short_path", + false)); + verify( + () -> + FrameworkStatsLog.write( + FrameworkStatsLog.SELINUX_AUDIT_LOG, + false, + new String[] {"perm"}, + "sdk_sandbox_audit", + null, + "ttype", + null, + "tclass", + null, + false)); + } + + @Test + public void testWriteSdkSandboxAuditLogs_withCategories() { + writeTestLog( + "denied", "perm", "sdk_sandbox_audit", new int[] {123}, "ttype", null, "tclass"); + writeTestLog( + "denied", + "perm", + "sdk_sandbox_audit", + new int[] {123, 456}, + "ttype", + null, + "tclass"); + writeTestLog( + "denied", "perm", "sdk_sandbox_audit", null, "ttype", new int[] {666}, "tclass"); + writeTestLog( + "denied", + "perm", + "sdk_sandbox_audit", + new int[] {123, 456}, + "ttype", + new int[] {666, 777}, + "tclass"); + + boolean done = mSelinuxAutidLogsCollector.collect(ANSWER_TAG); + + assertThat(done).isTrue(); + verify( + () -> + FrameworkStatsLog.write( + FrameworkStatsLog.SELINUX_AUDIT_LOG, + false, + new String[] {"perm"}, + "sdk_sandbox_audit", + new int[] {123}, + "ttype", + null, + "tclass", + null, + false)); + verify( + () -> + FrameworkStatsLog.write( + FrameworkStatsLog.SELINUX_AUDIT_LOG, + false, + new String[] {"perm"}, + "sdk_sandbox_audit", + new int[] {123, 456}, + "ttype", + null, + "tclass", + null, + false)); + verify( + () -> + FrameworkStatsLog.write( + FrameworkStatsLog.SELINUX_AUDIT_LOG, + false, + new String[] {"perm"}, + "sdk_sandbox_audit", + null, + "ttype", + new int[] {666}, + "tclass", + null, + false)); + verify( + () -> + FrameworkStatsLog.write( + FrameworkStatsLog.SELINUX_AUDIT_LOG, + false, + new String[] {"perm"}, + "sdk_sandbox_audit", + new int[] {123, 456}, + "ttype", + new int[] {666, 777}, + "tclass", + null, + false)); + } + + @Test + public void testWriteSdkSandboxAuditLogs_withPathAndCategories() { + writeTestLog( + "denied", + "perm", + "sdk_sandbox_audit", + new int[] {123}, + "ttype", + new int[] {666}, + "tclass", + "/a/path"); + + boolean done = mSelinuxAutidLogsCollector.collect(ANSWER_TAG); + + assertThat(done).isTrue(); + verify( + () -> + FrameworkStatsLog.write( + FrameworkStatsLog.SELINUX_AUDIT_LOG, + false, + new String[] {"perm"}, + "sdk_sandbox_audit", + new int[] {123}, + "ttype", + new int[] {666}, + "tclass", + "/a/path", + false)); + } + + @Test + public void testWriteSdkSandboxAuditLogs_permissive() { + writeTestLog("denied", "perm", "sdk_sandbox_audit", "ttype", "tclass"); + writeTestLog("denied", "perm", "sdk_sandbox_audit", "ttype", "tclass", true); + writeTestLog("denied", "perm", "sdk_sandbox_audit", "ttype", "tclass", false); + + boolean done = mSelinuxAutidLogsCollector.collect(ANSWER_TAG); + + assertThat(done).isTrue(); + verify( + () -> + FrameworkStatsLog.write( + FrameworkStatsLog.SELINUX_AUDIT_LOG, + false, + new String[] {"perm"}, + "sdk_sandbox_audit", + null, + "ttype", + null, + "tclass", + null, + false), + times(2)); + verify( + () -> + FrameworkStatsLog.write( + FrameworkStatsLog.SELINUX_AUDIT_LOG, + false, + new String[] {"perm"}, + "sdk_sandbox_audit", + null, + "ttype", + null, + "tclass", + null, + true)); + } + + @Test + public void testNotWriteAuditLogs_notSdkSandbox() { + writeTestLog("denied", "perm", "stype", "ttype", "tclass"); + + boolean done = mSelinuxAutidLogsCollector.collect(ANSWER_TAG); + + assertThat(done).isTrue(); + verify( + () -> + FrameworkStatsLog.write( + anyInt(), + anyBoolean(), + any(), + anyString(), + any(), + anyString(), + any(), + anyString(), + any(), + anyBoolean()), + never()); + } + + @Test + public void testWriteSdkSandboxAuditLogs_upToQuota() { + writeTestLog("denied", "perm", "sdk_sandbox_audit", "ttype", "tclass"); + writeTestLog("denied", "perm", "sdk_sandbox_audit", "ttype", "tclass"); + writeTestLog("denied", "perm", "sdk_sandbox_audit", "ttype", "tclass"); + writeTestLog("denied", "perm", "sdk_sandbox_audit", "ttype", "tclass"); + writeTestLog("denied", "perm", "sdk_sandbox_audit", "ttype", "tclass"); + // These are not pushed. + writeTestLog("denied", "perm", "sdk_sandbox_audit", "ttype", "tclass"); + writeTestLog("denied", "perm", "sdk_sandbox_audit", "ttype", "tclass"); + + boolean done = mSelinuxAutidLogsCollector.collect(ANSWER_TAG); + + assertThat(done).isTrue(); + verify( + () -> + FrameworkStatsLog.write( + anyInt(), + anyBoolean(), + any(), + anyString(), + any(), + anyString(), + any(), + anyString(), + any(), + anyBoolean()), + times(5)); + } + + @Test + public void testWriteSdkSandboxAuditLogs_resetQuota() { + writeTestLog("denied", "perm", "sdk_sandbox_audit", "ttype", "tclass"); + writeTestLog("denied", "perm", "sdk_sandbox_audit", "ttype", "tclass"); + writeTestLog("denied", "perm", "sdk_sandbox_audit", "ttype", "tclass"); + writeTestLog("denied", "perm", "sdk_sandbox_audit", "ttype", "tclass"); + writeTestLog("denied", "perm", "sdk_sandbox_audit", "ttype", "tclass"); + writeTestLog("denied", "perm", "sdk_sandbox_audit", "ttype", "tclass"); + writeTestLog("denied", "perm", "sdk_sandbox_audit", "ttype", "tclass"); + + boolean done = mSelinuxAutidLogsCollector.collect(ANSWER_TAG); + assertThat(done).isTrue(); + verify( + () -> + FrameworkStatsLog.write( + anyInt(), + anyBoolean(), + any(), + anyString(), + any(), + anyString(), + any(), + anyString(), + any(), + anyBoolean()), + times(5)); + + writeTestLog("denied", "perm", "sdk_sandbox_audit", "ttype", "tclass"); + writeTestLog("denied", "perm", "sdk_sandbox_audit", "ttype", "tclass"); + writeTestLog("denied", "perm", "sdk_sandbox_audit", "ttype", "tclass"); + writeTestLog("denied", "perm", "sdk_sandbox_audit", "ttype", "tclass"); + writeTestLog("denied", "perm", "sdk_sandbox_audit", "ttype", "tclass"); + // move the clock forward to reset the quota limiter. + mClock.currentTimeMillis += Duration.ofHours(1).toMillis(); + done = mSelinuxAutidLogsCollector.collect(ANSWER_TAG); + assertThat(done).isTrue(); + verify( + () -> + FrameworkStatsLog.write( + anyInt(), + anyBoolean(), + any(), + anyString(), + any(), + anyString(), + any(), + anyString(), + any(), + anyBoolean()), + times(10)); + } + + @Test + public void testNotWriteAuditLogs_stopRequested() { + writeTestLog("denied", "perm", "sdk_sandbox_audit", "ttype", "tclass"); + writeTestLog("denied", "perm", "sdk_sandbox_audit", "ttype", "tclass"); + writeTestLog("denied", "perm", "sdk_sandbox_audit", "ttype", "tclass"); + writeTestLog("denied", "perm", "sdk_sandbox_audit", "ttype", "tclass"); + writeTestLog("denied", "perm", "sdk_sandbox_audit", "ttype", "tclass"); + // These are not pushed. + writeTestLog("denied", "perm", "sdk_sandbox_audit", "ttype", "tclass"); + writeTestLog("denied", "perm", "sdk_sandbox_audit", "ttype", "tclass"); + + mSelinuxAutidLogsCollector.mStopRequested.set(true); + boolean done = mSelinuxAutidLogsCollector.collect(ANSWER_TAG); + assertThat(done).isFalse(); + verify( + () -> + FrameworkStatsLog.write( + anyInt(), + anyBoolean(), + any(), + anyString(), + any(), + anyString(), + any(), + anyString(), + any(), + anyBoolean()), + never()); + + mSelinuxAutidLogsCollector.mStopRequested.set(false); + done = mSelinuxAutidLogsCollector.collect(ANSWER_TAG); + assertThat(done).isTrue(); + verify( + () -> + FrameworkStatsLog.write( + anyInt(), + anyBoolean(), + any(), + anyString(), + any(), + anyString(), + any(), + anyString(), + any(), + anyBoolean()), + times(5)); + } + + @Test + public void testAuditLogs_resumeJobDoesNotExceedLimit() { + writeTestLog("denied", "perm", "sdk_sandbox_audit", "ttype", "tclass"); + mSelinuxAutidLogsCollector.mStopRequested.set(true); + + boolean done = mSelinuxAutidLogsCollector.collect(ANSWER_TAG); + + assertThat(done).isFalse(); + verify( + () -> + FrameworkStatsLog.write( + anyInt(), + anyBoolean(), + any(), + anyString(), + any(), + anyString(), + any(), + anyString(), + any(), + anyBoolean()), + never()); + } + + private static void writeTestLog( + String granted, String permissions, String sType, String tType, String tClass) { + EventLog.writeEvent( + ANSWER_TAG, + String.format( + "avc: %s { %s } scontext=u:r:%s:s0 tcontext=u:object_r:%s:s0 tclass=%s", + granted, permissions, sType, tType, tClass)); + } + + private static void writeTestLog( + String granted, + String permissions, + String sType, + String tType, + String tClass, + String path) { + EventLog.writeEvent( + ANSWER_TAG, + String.format( + "avc: %s { %s } path=\"%s\" scontext=u:r:%s:s0 tcontext=u:object_r:%s:s0" + + " tclass=%s", + granted, permissions, path, sType, tType, tClass)); + } + + private static void writeTestLog( + String granted, + String permissions, + String sType, + int[] sCategories, + String tType, + int[] tCategories, + String tClass) { + EventLog.writeEvent( + ANSWER_TAG, + String.format( + "avc: %s { %s } scontext=u:r:%s:s0%s tcontext=u:object_r:%s:s0%s tclass=%s", + granted, + permissions, + sType, + toCategoriesString(sCategories), + tType, + toCategoriesString(tCategories), + tClass)); + } + + private static void writeTestLog( + String granted, + String permissions, + String sType, + int[] sCategories, + String tType, + int[] tCategories, + String tClass, + String path) { + EventLog.writeEvent( + ANSWER_TAG, + String.format( + "avc: %s { %s } path=\"%s\" scontext=u:r:%s:s0%s" + + " tcontext=u:object_r:%s:s0%s tclass=%s", + granted, + permissions, + path, + sType, + toCategoriesString(sCategories), + tType, + toCategoriesString(tCategories), + tClass)); + } + + private static void writeTestLog( + String granted, + String permissions, + String sType, + String tType, + String tClass, + boolean permissive) { + EventLog.writeEvent( + ANSWER_TAG, + String.format( + "avc: %s { %s } scontext=u:r:%s:s0 tcontext=u:object_r:%s:s0 tclass=%s" + + " permissive=%s", + granted, permissions, sType, tType, tClass, permissive ? "1" : "0")); + } + + private static String toCategoriesString(int[] categories) { + return (categories == null || categories.length == 0) + ? "" + : ":c" + + Arrays.stream(categories) + .mapToObj(String::valueOf) + .collect(Collectors.joining(",c")); + } + + private static final class MockClock extends Clock { + + public long currentTimeMillis = 0; + + @Override + public long currentTimeMillis() { + return currentTimeMillis; + } + } +} diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/InputControllerTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/InputControllerTest.java index ccbbaa52ac21..5943832586b3 100644 --- a/services/tests/servicestests/src/com/android/server/companion/virtual/InputControllerTest.java +++ b/services/tests/servicestests/src/com/android/server/companion/virtual/InputControllerTest.java @@ -33,19 +33,21 @@ import android.os.Binder; import android.os.Handler; import android.os.IBinder; import android.platform.test.annotations.Presubmit; +import android.platform.test.flag.junit.SetFlagsRule; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; -import android.view.Display; import android.view.DisplayInfo; import android.view.WindowManager; import androidx.test.InstrumentationRegistry; +import com.android.input.flags.Flags; import com.android.server.LocalServices; import com.android.server.input.InputManagerInternal; import org.junit.After; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; @@ -58,6 +60,9 @@ public class InputControllerTest { private static final String LANGUAGE_TAG = "en-US"; private static final String LAYOUT_TYPE = "qwerty"; + @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + @Mock private InputManagerInternal mInputManagerInternalMock; @Mock @@ -72,11 +77,12 @@ public class InputControllerTest { @Before public void setUp() throws Exception { + mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_POINTER_CHOREOGRAPHER); + MockitoAnnotations.initMocks(this); mInputManagerMockHelper = new InputManagerMockHelper( TestableLooper.get(this), mNativeWrapperMock, mIInputManagerMock); - doReturn(true).when(mInputManagerInternalMock).setVirtualMousePointerDisplayId(anyInt()); LocalServices.removeServiceForTest(InputManagerInternal.class); LocalServices.addService(InputManagerInternal.class, mInputManagerInternalMock); @@ -129,11 +135,7 @@ public class InputControllerTest { mInputController.createMouse("name", /*vendorId= */ 1, /*productId= */ 1, deviceToken, /* displayId= */ 1); verify(mNativeWrapperMock).openUinputMouse(eq("name"), eq(1), eq(1), anyString()); - verify(mInputManagerInternalMock).setVirtualMousePointerDisplayId(eq(1)); - doReturn(1).when(mInputManagerInternalMock).getVirtualMousePointerDisplayId(); mInputController.unregisterInputDevice(deviceToken); - verify(mInputManagerInternalMock).setVirtualMousePointerDisplayId( - eq(Display.INVALID_DISPLAY)); } @Test @@ -143,14 +145,11 @@ public class InputControllerTest { mInputController.createMouse("mouse1", /*vendorId= */ 1, /*productId= */ 1, deviceToken, /* displayId= */ 1); verify(mNativeWrapperMock).openUinputMouse(eq("mouse1"), eq(1), eq(1), anyString()); - verify(mInputManagerInternalMock).setVirtualMousePointerDisplayId(eq(1)); final IBinder deviceToken2 = new Binder(); mInputController.createMouse("mouse2", /*vendorId= */ 1, /*productId= */ 1, deviceToken2, /* displayId= */ 2); verify(mNativeWrapperMock).openUinputMouse(eq("mouse2"), eq(1), eq(1), anyString()); - verify(mInputManagerInternalMock).setVirtualMousePointerDisplayId(eq(2)); mInputController.unregisterInputDevice(deviceToken); - verify(mInputManagerInternalMock).setVirtualMousePointerDisplayId(eq(1)); } @Test diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java index 9ff29d208dc0..5442af875e86 100644 --- a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java @@ -339,8 +339,8 @@ public class VirtualDeviceManagerServiceTest { LocalServices.addService(DisplayManagerInternal.class, mDisplayManagerInternalMock); mSetFlagsRule.initAllFlagsToReleaseConfigDefault(); + mSetFlagsRule.enableFlags(com.android.input.flags.Flags.FLAG_ENABLE_POINTER_CHOREOGRAPHER); - doReturn(true).when(mInputManagerInternalMock).setVirtualMousePointerDisplayId(anyInt()); doNothing().when(mInputManagerInternalMock) .setMousePointerAccelerationEnabled(anyBoolean(), anyInt()); doNothing().when(mInputManagerInternalMock).setPointerIconVisible(anyBoolean(), anyInt()); @@ -1333,7 +1333,6 @@ public class VirtualDeviceManagerServiceTest { mInputController.addDeviceForTesting(BINDER, fd, InputController.InputDeviceDescriptor.TYPE_MOUSE, DISPLAY_ID_1, PHYS, DEVICE_NAME_1, INPUT_DEVICE_ID); - doReturn(DISPLAY_ID_1).when(mInputManagerInternalMock).getVirtualMousePointerDisplayId(); assertThat(mDeviceImpl.sendButtonEvent(BINDER, new VirtualMouseButtonEvent.Builder() .setButtonCode(buttonCode) @@ -1363,7 +1362,6 @@ public class VirtualDeviceManagerServiceTest { mInputController.addDeviceForTesting(BINDER, fd, InputController.InputDeviceDescriptor.TYPE_MOUSE, DISPLAY_ID_1, PHYS, DEVICE_NAME_1, INPUT_DEVICE_ID); - doReturn(DISPLAY_ID_1).when(mInputManagerInternalMock).getVirtualMousePointerDisplayId(); assertThat(mDeviceImpl.sendRelativeEvent(BINDER, new VirtualMouseRelativeEvent.Builder() .setRelativeX(x) @@ -1394,7 +1392,6 @@ public class VirtualDeviceManagerServiceTest { mInputController.addDeviceForTesting(BINDER, fd, InputController.InputDeviceDescriptor.TYPE_MOUSE, DISPLAY_ID_1, PHYS, DEVICE_NAME_1, INPUT_DEVICE_ID); - doReturn(DISPLAY_ID_1).when(mInputManagerInternalMock).getVirtualMousePointerDisplayId(); assertThat(mDeviceImpl.sendScrollEvent(BINDER, new VirtualMouseScrollEvent.Builder() .setXAxisMovement(x) diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/camera/VirtualCameraControllerTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/camera/VirtualCameraControllerTest.java index 3e4f1df0e1d4..81981e6b16ca 100644 --- a/services/tests/servicestests/src/com/android/server/companion/virtual/camera/VirtualCameraControllerTest.java +++ b/services/tests/servicestests/src/com/android/server/companion/virtual/camera/VirtualCameraControllerTest.java @@ -183,9 +183,8 @@ public class VirtualCameraControllerTest { private VirtualCameraConfig createVirtualCameraConfig( int width, int height, int format, int maximumFramesPerSecond, String name, int sensorOrientation, int lensFacing) { - return new VirtualCameraConfig.Builder() + return new VirtualCameraConfig.Builder(name) .addStreamConfig(width, height, format, maximumFramesPerSecond) - .setName(name) .setVirtualCameraCallback(mCallbackHandler, mVirtualCameraCallbackMock) .setSensorOrientation(sensorOrientation) .setLensFacing(lensFacing) 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 dc1d2c5e54b6..1c6d36b0a0d2 100644 --- a/services/tests/servicestests/src/com/android/server/os/BugreportManagerServiceImplTest.java +++ b/services/tests/servicestests/src/com/android/server/os/BugreportManagerServiceImplTest.java @@ -17,16 +17,19 @@ package com.android.server.os; import android.app.admin.flags.Flags; -import static android.app.admin.flags.Flags.onboardingBugreportV2Enabled; import static com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertThrows; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.when; import android.app.role.RoleManager; import android.content.Context; +import android.content.pm.PackageManager; import android.os.Binder; import android.os.BugreportManager.BugreportCallback; import android.os.IBinder; @@ -48,6 +51,8 @@ import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; import java.io.FileDescriptor; import java.util.concurrent.CompletableFuture; @@ -66,6 +71,9 @@ public class BugreportManagerServiceImplTest { private BugreportManagerServiceImpl mService; private BugreportManagerServiceImpl.BugreportFileManager mBugreportFileManager; + @Mock + private PackageManager mPackageManager; + private int mCallingUid = 1234; private String mCallingPackage = "test.package"; private AtomicFile mMappingFile; @@ -74,7 +82,8 @@ public class BugreportManagerServiceImplTest { private String mBugreportFile2 = "bugreport-file2.zip"; @Before - public void setUp() { + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); mContext = InstrumentationRegistry.getInstrumentation().getContext(); mMappingFile = new AtomicFile(mContext.getFilesDir(), "bugreport-mapping.xml"); ArraySet<String> mAllowlistedPackages = new ArraySet<>(); @@ -83,6 +92,7 @@ public class BugreportManagerServiceImplTest { new BugreportManagerServiceImpl.Injector(mContext, mAllowlistedPackages, mMappingFile)); mBugreportFileManager = new BugreportManagerServiceImpl.BugreportFileManager(mMappingFile); + when(mPackageManager.getPackageUidAsUser(anyString(), anyInt())).thenReturn(mCallingUid); } @After @@ -115,12 +125,13 @@ public class BugreportManagerServiceImplTest { assertThrows(IllegalArgumentException.class, () -> mBugreportFileManager.ensureCallerPreviouslyGeneratedFile( - mContext, callingInfo, Process.myUserHandle().getIdentifier(), - "unknown-file.zip", /* forceUpdateMapping= */ true)); + mContext, mPackageManager, callingInfo, + Process.myUserHandle().getIdentifier(), "unknown-file.zip", + /* forceUpdateMapping= */ true)); // No exception should be thrown. mBugreportFileManager.ensureCallerPreviouslyGeneratedFile( - mContext, callingInfo, mContext.getUserId(), mBugreportFile, + mContext, mPackageManager, callingInfo, mContext.getUserId(), mBugreportFile, /* forceUpdateMapping= */ true); } @@ -132,7 +143,7 @@ public class BugreportManagerServiceImplTest { callingInfo, mBugreportFile, /* keepOnRetrieval= */ true); mBugreportFileManager.ensureCallerPreviouslyGeneratedFile( - mContext, callingInfo, mContext.getUserId(), mBugreportFile, + mContext, mPackageManager, callingInfo, mContext.getUserId(), mBugreportFile, /* forceUpdateMapping= */ true); assertThat(mBugreportFileManager.mBugreportFilesToPersist).containsExactly(mBugreportFile); @@ -148,10 +159,10 @@ public class BugreportManagerServiceImplTest { // No exception should be thrown. mBugreportFileManager.ensureCallerPreviouslyGeneratedFile( - mContext, callingInfo, mContext.getUserId(), mBugreportFile, + mContext, mPackageManager, callingInfo, mContext.getUserId(), mBugreportFile, /* forceUpdateMapping= */ true); mBugreportFileManager.ensureCallerPreviouslyGeneratedFile( - mContext, callingInfo, mContext.getUserId(), mBugreportFile2, + mContext, mPackageManager, callingInfo, mContext.getUserId(), mBugreportFile2, /* forceUpdateMapping= */ true); } @@ -160,8 +171,9 @@ public class BugreportManagerServiceImplTest { Pair<Integer, String> callingInfo = new Pair<>(mCallingUid, mCallingPackage); assertThrows(IllegalArgumentException.class, () -> mBugreportFileManager.ensureCallerPreviouslyGeneratedFile( - mContext, callingInfo, Process.myUserHandle().getIdentifier(), - "test-file.zip", /* forceUpdateMapping= */ true)); + mContext, mPackageManager, callingInfo, + Process.myUserHandle().getIdentifier(), "test-file.zip", + /* forceUpdateMapping= */ true)); } @Test 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 d7ed7c2d6469..8d8dc9cc45b1 100644 --- a/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceUserPropertiesTest.java +++ b/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceUserPropertiesTest.java @@ -23,6 +23,7 @@ import static org.testng.Assert.assertThrows; import android.content.pm.UserProperties; import android.os.Parcel; import android.platform.test.annotations.Presubmit; +import android.platform.test.flag.junit.SetFlagsRule; import android.util.Xml; import androidx.test.filters.MediumTest; @@ -31,6 +32,7 @@ import androidx.test.runner.AndroidJUnit4; import com.android.modules.utils.TypedXmlPullParser; import com.android.modules.utils.TypedXmlSerializer; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; @@ -52,10 +54,13 @@ import java.util.function.Supplier; @RunWith(AndroidJUnit4.class) @MediumTest public class UserManagerServiceUserPropertiesTest { + @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); /** Test that UserProperties can properly read the xml information that it writes. */ @Test public void testWriteReadXml() throws Exception { + mSetFlagsRule.enableFlags(android.multiuser.Flags.FLAG_SUPPORT_HIDING_PROFILES); final UserProperties defaultProps = new UserProperties.Builder() .setShowInLauncher(21) .setStartWithParent(false) @@ -73,6 +78,7 @@ public class UserManagerServiceUserPropertiesTest { .setDeleteAppWithParent(false) .setAlwaysVisible(false) .setCrossProfileContentSharingStrategy(0) + .setProfileApiVisibility(34) .build(); final UserProperties actualProps = new UserProperties(defaultProps); actualProps.setShowInLauncher(14); @@ -90,6 +96,7 @@ public class UserManagerServiceUserPropertiesTest { actualProps.setDeleteAppWithParent(true); actualProps.setAlwaysVisible(true); actualProps.setCrossProfileContentSharingStrategy(1); + actualProps.setProfileApiVisibility(36); // Write the properties to xml. final ByteArrayOutputStream baos = new ByteArrayOutputStream(); @@ -114,6 +121,7 @@ public class UserManagerServiceUserPropertiesTest { /** Tests parcelling an object in which all properties are present. */ @Test public void testParcelUnparcel() throws Exception { + mSetFlagsRule.enableFlags(android.multiuser.Flags.FLAG_SUPPORT_HIDING_PROFILES); final UserProperties originalProps = new UserProperties.Builder() .setShowInLauncher(2145) .build(); @@ -124,6 +132,7 @@ public class UserManagerServiceUserPropertiesTest { /** Tests copying a UserProperties object varying permissions. */ @Test public void testCopyLacksPermissions() throws Exception { + mSetFlagsRule.enableFlags(android.multiuser.Flags.FLAG_SUPPORT_HIDING_PROFILES); final UserProperties defaultProps = new UserProperties.Builder() .setShowInLauncher(2145) .setStartWithParent(true) @@ -134,6 +143,7 @@ public class UserManagerServiceUserPropertiesTest { .setAuthAlwaysRequiredToDisableQuietMode(false) .setAllowStoppingUserWithDelayedLocking(false) .setAlwaysVisible(true) + .setProfileApiVisibility(110) .build(); final UserProperties orig = new UserProperties(defaultProps); orig.setShowInLauncher(2841); @@ -209,6 +219,8 @@ public class UserManagerServiceUserPropertiesTest { copy::isCredentialShareableWithParent, true); assertEqualGetterOrThrows(orig::getCrossProfileContentSharingStrategy, copy::getCrossProfileContentSharingStrategy, true); + assertEqualGetterOrThrows(orig::getProfileApiVisibility, copy::getProfileApiVisibility, + true); } /** @@ -270,5 +282,6 @@ public class UserManagerServiceUserPropertiesTest { assertThat(expected.getAlwaysVisible()).isEqualTo(actual.getAlwaysVisible()); assertThat(expected.getCrossProfileContentSharingStrategy()) .isEqualTo(actual.getCrossProfileContentSharingStrategy()); + assertThat(expected.getProfileApiVisibility()).isEqualTo(actual.getProfileApiVisibility()); } } 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 70837061b0bb..1ee604e78d5f 100644 --- a/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceUserTypeTest.java +++ b/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceUserTypeTest.java @@ -41,6 +41,7 @@ import android.content.res.XmlResourceParser; import android.os.Bundle; import android.os.UserManager; import android.platform.test.annotations.Presubmit; +import android.platform.test.flag.junit.SetFlagsRule; import android.util.ArrayMap; import androidx.test.InstrumentationRegistry; @@ -50,6 +51,7 @@ import androidx.test.runner.AndroidJUnit4; import com.android.frameworks.servicestests.R; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; @@ -71,9 +73,11 @@ public class UserManagerServiceUserTypeTest { public void setup() { mResources = InstrumentationRegistry.getTargetContext().getResources(); } - + @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); @Test public void testUserTypeBuilder_createUserType() { + mSetFlagsRule.enableFlags(android.multiuser.Flags.FLAG_SUPPORT_HIDING_PROFILES); final Bundle restrictions = makeRestrictionsBundle("r1", "r2"); final Bundle systemSettings = makeSettingsBundle("s1", "s2"); final Bundle secureSettings = makeSettingsBundle("secure_s1", "secure_s2"); @@ -97,7 +101,8 @@ public class UserManagerServiceUserTypeTest { .setInheritDevicePolicy(340) .setDeleteAppWithParent(true) .setAlwaysVisible(true) - .setCrossProfileContentSharingStrategy(1); + .setCrossProfileContentSharingStrategy(1) + .setProfileApiVisibility(34); final UserTypeDetails type = new UserTypeDetails.Builder() .setName("a.name") @@ -180,6 +185,7 @@ public class UserManagerServiceUserTypeTest { assertTrue(type.getDefaultUserPropertiesReference().getAlwaysVisible()); assertEquals(1, type.getDefaultUserPropertiesReference() .getCrossProfileContentSharingStrategy()); + assertEquals(34, type.getDefaultUserPropertiesReference().getProfileApiVisibility()); assertEquals(23, type.getBadgeLabel(0)); assertEquals(24, type.getBadgeLabel(1)); @@ -199,6 +205,7 @@ public class UserManagerServiceUserTypeTest { @Test public void testUserTypeBuilder_defaults() { + mSetFlagsRule.enableFlags(android.multiuser.Flags.FLAG_SUPPORT_HIDING_PROFILES); UserTypeDetails type = new UserTypeDetails.Builder() .setName("name") // Required (no default allowed) .setBaseType(FLAG_FULL) // Required (no default allowed) @@ -238,6 +245,8 @@ public class UserManagerServiceUserTypeTest { props.getShowInQuietMode()); assertEquals(UserProperties.CROSS_PROFILE_CONTENT_SHARING_NO_DELEGATION, props.getCrossProfileContentSharingStrategy()); + assertEquals(UserProperties.PROFILE_API_VISIBILITY_VISIBLE, + props.getProfileApiVisibility()); assertFalse(type.hasBadge()); } @@ -310,6 +319,7 @@ public class UserManagerServiceUserTypeTest { /** Tests {@link UserTypeFactory#customizeBuilders} for a reasonable xml file. */ @Test public void testUserTypeFactoryCustomize_profile() throws Exception { + mSetFlagsRule.enableFlags(android.multiuser.Flags.FLAG_SUPPORT_HIDING_PROFILES); final String userTypeAosp1 = "android.test.1"; // Profile user that is not customized final String userTypeAosp2 = "android.test.2"; // Profile user that is customized final String userTypeOem1 = "custom.test.1"; // Custom-defined profile @@ -332,7 +342,8 @@ public class UserManagerServiceUserTypeTest { .setShowInQuietMode(24) .setDeleteAppWithParent(true) .setAlwaysVisible(false) - .setCrossProfileContentSharingStrategy(1); + .setCrossProfileContentSharingStrategy(1) + .setProfileApiVisibility(36); final ArrayMap<String, UserTypeDetails.Builder> builders = new ArrayMap<>(); builders.put(userTypeAosp1, new UserTypeDetails.Builder() @@ -383,6 +394,7 @@ public class UserManagerServiceUserTypeTest { assertFalse(aospType.getDefaultUserPropertiesReference().getAlwaysVisible()); assertEquals(1, aospType.getDefaultUserPropertiesReference() .getCrossProfileContentSharingStrategy()); + assertEquals(36, aospType.getDefaultUserPropertiesReference().getProfileApiVisibility()); // userTypeAosp2 should be modified. aospType = builders.get(userTypeAosp2).createUserTypeDetails(); @@ -439,6 +451,7 @@ public class UserManagerServiceUserTypeTest { assertTrue(aospType.getDefaultUserPropertiesReference().getAlwaysVisible()); assertEquals(0, aospType.getDefaultUserPropertiesReference() .getCrossProfileContentSharingStrategy()); + assertEquals(36, aospType.getDefaultUserPropertiesReference().getProfileApiVisibility()); // 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 a743fff5d2ea..db561c436269 100644 --- a/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java +++ b/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java @@ -19,6 +19,7 @@ package com.android.server.pm; import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertWithMessage; +import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static org.junit.Assume.assumeTrue; import static org.testng.Assert.assertEquals; @@ -33,10 +34,12 @@ import android.content.pm.UserProperties; import android.content.res.Resources; import android.graphics.drawable.Drawable; import android.os.Bundle; +import android.os.PersistableBundle; import android.os.UserHandle; import android.os.UserManager; import android.platform.test.annotations.Postsubmit; import android.platform.test.annotations.RequiresFlagsEnabled; +import android.platform.test.flag.junit.SetFlagsRule; import android.provider.Settings; import android.test.suitebuilder.annotation.LargeTest; import android.test.suitebuilder.annotation.MediumTest; @@ -53,6 +56,7 @@ import com.google.common.collect.Range; import org.junit.After; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; @@ -95,6 +99,8 @@ public final class UserManagerTest { private UserSwitchWaiter mUserSwitchWaiter; private UserRemovalWaiter mUserRemovalWaiter; private int mOriginalCurrentUserId; + @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); @Before public void setUp() throws Exception { @@ -166,6 +172,7 @@ public final class UserManagerTest { @Test public void testCloneUser() throws Exception { + mSetFlagsRule.enableFlags(android.multiuser.Flags.FLAG_SUPPORT_HIDING_PROFILES); assumeCloneEnabled(); UserHandle mainUser = mUserManager.getMainUser(); assumeTrue("Main user is null", mainUser != null); @@ -222,6 +229,7 @@ public final class UserManagerTest { .isEqualTo(cloneUserProperties.getCrossProfileContentSharingStrategy()); assertThrows(SecurityException.class, cloneUserProperties::getDeleteAppWithParent); assertThrows(SecurityException.class, cloneUserProperties::getAlwaysVisible); + assertThrows(SecurityException.class, cloneUserProperties::getProfileApiVisibility); compareDrawables(mUserManager.getUserBadge(), Resources.getSystem().getDrawable(userTypeDetails.getBadgePlain())); @@ -303,6 +311,7 @@ public final class UserManagerTest { @Test public void testPrivateProfile() throws Exception { + mSetFlagsRule.enableFlags(android.multiuser.Flags.FLAG_SUPPORT_HIDING_PROFILES); UserHandle mainUser = mUserManager.getMainUser(); assumeTrue("Main user is null", mainUser != null); // Get the default properties for private profile user type. @@ -344,7 +353,8 @@ public final class UserManagerTest { assertThrows(SecurityException.class, privateProfileUserProperties::getDeleteAppWithParent); assertThrows(SecurityException.class, privateProfileUserProperties::getAllowStoppingUserWithDelayedLocking); - + assertThrows(SecurityException.class, + privateProfileUserProperties::getProfileApiVisibility); compareDrawables(mUserManager.getUserBadge(), Resources.getSystem().getDrawable(userTypeDetails.getBadgePlain())); @@ -1632,6 +1642,106 @@ public final class UserManagerTest { assertThat(mainUserCount).isEqualTo(1); } + @Test + public void testAddUserAccountData_validStringValuesAreSaved_validBundleIsSaved() { + assumeManagedUsersSupported(); + + String userName = "User"; + String accountName = "accountName"; + String accountType = "accountType"; + String arrayKey = "StringArrayKey"; + String stringKey = "StringKey"; + String intKey = "IntKey"; + String nestedBundleKey = "PersistableBundleKey"; + String value1 = "Value 1"; + String value2 = "Value 2"; + String value3 = "Value 3"; + + UserInfo userInfo = mUserManager.createUser(userName, + UserManager.USER_TYPE_FULL_SECONDARY, 0); + + PersistableBundle accountOptions = new PersistableBundle(); + String[] stringArray = {value1, value2}; + accountOptions.putInt(intKey, 1234); + PersistableBundle nested = new PersistableBundle(); + nested.putString(stringKey, value3); + accountOptions.putPersistableBundle(nestedBundleKey, nested); + accountOptions.putStringArray(arrayKey, stringArray); + + mUserManager.clearSeedAccountData(); + mUserManager.setSeedAccountData(mContext.getUserId(), accountName, + accountType, accountOptions); + + //assert userName accountName and accountType were saved correctly + assertTrue(mUserManager.getUserInfo(userInfo.id).name.equals(userName)); + assertTrue(mUserManager.getSeedAccountName().equals(accountName)); + assertTrue(mUserManager.getSeedAccountType().equals(accountType)); + + //assert bundle with correct values was added + assertThat(mUserManager.getSeedAccountOptions().containsKey(arrayKey)).isTrue(); + assertThat(mUserManager.getSeedAccountOptions().getPersistableBundle(nestedBundleKey) + .getString(stringKey)).isEqualTo(value3); + assertThat(mUserManager.getSeedAccountOptions().getStringArray(arrayKey)[0]) + .isEqualTo(value1); + + mUserManager.removeUser(userInfo.id); + } + + @Test + public void testAddUserAccountData_invalidStringValuesAreTruncated_invalidBundleIsDropped() { + assumeManagedUsersSupported(); + + String tooLongString = generateLongString(); + String userName = "User " + tooLongString; + String accountType = "Account Type " + tooLongString; + String accountName = "accountName " + tooLongString; + String arrayKey = "StringArrayKey"; + String stringKey = "StringKey"; + String intKey = "IntKey"; + String nestedBundleKey = "PersistableBundleKey"; + String value1 = "Value 1"; + String value2 = "Value 2"; + + UserInfo userInfo = mUserManager.createUser(userName, + UserManager.USER_TYPE_FULL_SECONDARY, 0); + + PersistableBundle accountOptions = new PersistableBundle(); + String[] stringArray = {value1, value2}; + accountOptions.putInt(intKey, 1234); + PersistableBundle nested = new PersistableBundle(); + nested.putString(stringKey, tooLongString); + accountOptions.putPersistableBundle(nestedBundleKey, nested); + accountOptions.putStringArray(arrayKey, stringArray); + mUserManager.clearSeedAccountData(); + mUserManager.setSeedAccountData(mContext.getUserId(), accountName, + accountType, accountOptions); + + //assert userName was truncated + assertTrue(mUserManager.getUserInfo(userInfo.id).name.length() + == UserManager.MAX_USER_NAME_LENGTH); + + //assert accountName and accountType got truncated + assertTrue(mUserManager.getSeedAccountName().length() + == UserManager.MAX_ACCOUNT_STRING_LENGTH); + assertTrue(mUserManager.getSeedAccountType().length() + == UserManager.MAX_ACCOUNT_STRING_LENGTH); + + //assert bundle with invalid values was dropped + assertThat(mUserManager.getSeedAccountOptions() == null).isTrue(); + + mUserManager.removeUser(userInfo.id); + } + + private String generateLongString() { + String partialString = "Test Name Test Name Test Name Test Name Test Name Test Name Test " + + "Name Test Name Test Name Test Name "; //String of length 100 + StringBuilder resultString = new StringBuilder(); + for (int i = 0; i < 600; i++) { + resultString.append(partialString); + } + return resultString.toString(); + } + private boolean isPackageInstalledForUser(String packageName, int userId) { try { return mPackageManager.getPackageInfoAsUser(packageName, 0, userId) != null; diff --git a/services/tests/servicestests/test-apps/PackageParserApp/Android.bp b/services/tests/servicestests/test-apps/PackageParserApp/Android.bp index 3e78f9a9674a..131b380d9215 100644 --- a/services/tests/servicestests/test-apps/PackageParserApp/Android.bp +++ b/services/tests/servicestests/test-apps/PackageParserApp/Android.bp @@ -102,3 +102,17 @@ android_test_helper_app { resource_dirs: ["res"], manifest: "AndroidManifestApp6.xml", } + +android_test_helper_app { + name: "PackageParserTestApp7", + sdk_version: "current", + srcs: ["**/*.java"], + dex_preopt: { + enabled: false, + }, + optimize: { + enabled: false, + }, + resource_dirs: ["res"], + manifest: "AndroidManifestApp7.xml", +} diff --git a/services/tests/servicestests/test-apps/PackageParserApp/AndroidManifestApp7.xml b/services/tests/servicestests/test-apps/PackageParserApp/AndroidManifestApp7.xml new file mode 100644 index 000000000000..cb87a48eb524 --- /dev/null +++ b/services/tests/servicestests/test-apps/PackageParserApp/AndroidManifestApp7.xml @@ -0,0 +1,67 @@ +<?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. +--> + +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.servicestests.apps.packageparserapp" > + + <application> + <activity android:name=".TestActivity" + android:exported="true" > + <intent-filter> + <action android:name="android.intent.action.VIEW" /> + <category android:name="android.intent.category.DEFAULT" /> + <category android:name="android.intent.category.BROWSABLE" /> + <data android:scheme="http" + android:host="www.example.com" /> + <uri-relative-filter-group android:allow="false"> + <data android:pathPrefix="/gizmos" /> + <data android:queryPattern=".*query=string.*" /> + <data android:fragment="fragment" /> + </uri-relative-filter-group> + <uri-relative-filter-group> + <data android:query="query=string" /> + <data android:fragmentSuffix="fragment" /> + </uri-relative-filter-group> + <uri-relative-filter-group> + <data android:path="/gizmos" /> + <data android:query=".*query=string.*" /> + <data android:fragment="fragment" /> + </uri-relative-filter-group> + <uri-relative-filter-group> + <data android:pathPrefix="/gizmos" /> + <data android:queryPrefix=".*query=string.*" /> + <data android:fragmentPrefix="fragment" /> + </uri-relative-filter-group> + <uri-relative-filter-group> + <data android:pathPattern="/gizmos" /> + <data android:queryPattern=".*query=string.*" /> + <data android:fragmentPattern="fragment" /> + </uri-relative-filter-group> + <uri-relative-filter-group> + <data android:pathAdvancedPattern="/gizmos" /> + <data android:queryAdvancedPattern=".*query=string.*" /> + <data android:fragmentAdvancedPattern="fragment" /> + </uri-relative-filter-group> + <uri-relative-filter-group> + <data android:pathSuffix="/gizmos" /> + <data android:querySuffix=".*query=string.*" /> + <data android:fragmentSuffix="fragment" /> + </uri-relative-filter-group> + </intent-filter> + </activity> + </application> + +</manifest>
\ No newline at end of file diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenAdaptersTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenAdaptersTest.java index 0e20daf2c0f1..99d5a6d9118a 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/ZenAdaptersTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenAdaptersTest.java @@ -143,12 +143,13 @@ public class ZenAdaptersTest extends UiServiceTestCase { Policy.policyState(false, true), 0); ZenPolicy zenPolicy = notificationPolicyToZenPolicy(policy); - assertThat(zenPolicy.getPriorityChannels()).isEqualTo(ZenPolicy.STATE_ALLOW); + assertThat(zenPolicy.getPriorityChannelsAllowed()).isEqualTo(ZenPolicy.STATE_ALLOW); Policy notAllowed = new Policy(0, 0, 0, 0, Policy.policyState(false, false), 0); ZenPolicy zenPolicyNotAllowed = notificationPolicyToZenPolicy(notAllowed); - assertThat(zenPolicyNotAllowed.getPriorityChannels()).isEqualTo(ZenPolicy.STATE_DISALLOW); + assertThat(zenPolicyNotAllowed.getPriorityChannelsAllowed()).isEqualTo( + ZenPolicy.STATE_DISALLOW); } @Test @@ -158,11 +159,12 @@ public class ZenAdaptersTest extends UiServiceTestCase { Policy.policyState(false, true), 0); ZenPolicy zenPolicy = notificationPolicyToZenPolicy(policy); - assertThat(zenPolicy.getPriorityChannels()).isEqualTo(ZenPolicy.STATE_UNSET); + assertThat(zenPolicy.getPriorityChannelsAllowed()).isEqualTo(ZenPolicy.STATE_UNSET); Policy notAllowed = new Policy(0, 0, 0, 0, Policy.policyState(false, false), 0); ZenPolicy zenPolicyNotAllowed = notificationPolicyToZenPolicy(notAllowed); - assertThat(zenPolicyNotAllowed.getPriorityChannels()).isEqualTo(ZenPolicy.STATE_UNSET); + assertThat(zenPolicyNotAllowed.getPriorityChannelsAllowed()).isEqualTo( + ZenPolicy.STATE_UNSET); } } diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeConfigTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeConfigTest.java index e523e79f6370..539bb37419f1 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeConfigTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeConfigTest.java @@ -284,7 +284,7 @@ public class ZenModeConfigTest extends UiServiceTestCase { actual.getPriorityConversationSenders()); assertEquals(expected.getPriorityCallSenders(), actual.getPriorityCallSenders()); assertEquals(expected.getPriorityMessageSenders(), actual.getPriorityMessageSenders()); - assertEquals(expected.getPriorityChannels(), actual.getPriorityChannels()); + assertEquals(expected.getPriorityChannelsAllowed(), actual.getPriorityChannelsAllowed()); } @Test @@ -716,7 +716,7 @@ public class ZenModeConfigTest extends UiServiceTestCase { assertEquals(policy.getPriorityCategorySystem(), fromXml.getPriorityCategorySystem()); assertEquals(policy.getPriorityCategoryReminders(), fromXml.getPriorityCategoryReminders()); assertEquals(policy.getPriorityCategoryEvents(), fromXml.getPriorityCategoryEvents()); - assertEquals(policy.getPriorityChannels(), fromXml.getPriorityChannels()); + assertEquals(policy.getPriorityChannelsAllowed(), fromXml.getPriorityChannelsAllowed()); assertEquals(policy.getVisualEffectFullScreenIntent(), fromXml.getVisualEffectFullScreenIntent()); 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 25ad7dbac30c..227265aa0a5b 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java @@ -4075,7 +4075,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { // UPDATE_ORIGIN_USER should change the bitmask and change the values. assertThat(rule.getInterruptionFilter()).isEqualTo(INTERRUPTION_FILTER_PRIORITY); - assertThat(rule.getZenPolicy().getPriorityChannels()).isEqualTo(ZenPolicy.STATE_DISALLOW); + assertThat(rule.getZenPolicy().getPriorityChannelsAllowed()).isEqualTo( + ZenPolicy.STATE_DISALLOW); assertThat(rule.getDeviceEffects().shouldDisplayGrayscale()).isTrue(); @@ -4339,7 +4340,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { // New ZenPolicy differs from the default config assertThat(rule.getZenPolicy()).isNotNull(); - assertThat(rule.getZenPolicy().getPriorityChannels()).isEqualTo(ZenPolicy.STATE_DISALLOW); + assertThat(rule.getZenPolicy().getPriorityChannelsAllowed()).isEqualTo( + ZenPolicy.STATE_DISALLOW); ZenRule storedRule = mZenModeHelper.mConfig.automaticRules.get(ruleId); assertThat(storedRule.canBeUpdatedByApp()).isFalse(); diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenPolicyTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenPolicyTest.java index 57e11328d5e1..3a88294a0fa1 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/ZenPolicyTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenPolicyTest.java @@ -218,7 +218,7 @@ public class ZenPolicyTest extends UiServiceTestCase { // unset applied, channels setting keeps its state channelsPriority.apply(unset); - assertThat(channelsPriority.getPriorityChannels()).isEqualTo(ZenPolicy.STATE_ALLOW); + assertThat(channelsPriority.getPriorityChannelsAllowed()).isEqualTo(ZenPolicy.STATE_ALLOW); } @Test @@ -234,7 +234,7 @@ public class ZenPolicyTest extends UiServiceTestCase { // priority channels (less strict state) cannot override a setting that sets it to none none.apply(priority); - assertThat(none.getPriorityChannels()).isEqualTo(ZenPolicy.STATE_DISALLOW); + assertThat(none.getPriorityChannelsAllowed()).isEqualTo(ZenPolicy.STATE_DISALLOW); } @Test @@ -250,7 +250,7 @@ public class ZenPolicyTest extends UiServiceTestCase { // applying a policy with channelType=none overrides priority setting priority.apply(none); - assertThat(priority.getPriorityChannels()).isEqualTo(ZenPolicy.STATE_DISALLOW); + assertThat(priority.getPriorityChannelsAllowed()).isEqualTo(ZenPolicy.STATE_DISALLOW); } @Test @@ -265,7 +265,7 @@ public class ZenPolicyTest extends UiServiceTestCase { // applying a policy with a set channel type actually goes through unset.apply(priority); - assertThat(unset.getPriorityChannels()).isEqualTo(ZenPolicy.STATE_ALLOW); + assertThat(unset.getPriorityChannelsAllowed()).isEqualTo(ZenPolicy.STATE_ALLOW); } @Test @@ -379,7 +379,7 @@ public class ZenPolicyTest extends UiServiceTestCase { ZenPolicy.Builder builder = new ZenPolicy.Builder(); ZenPolicy policy = builder.build(); - assertThat(policy.getPriorityChannels()).isEqualTo(ZenPolicy.STATE_UNSET); + assertThat(policy.getPriorityChannelsAllowed()).isEqualTo(ZenPolicy.STATE_UNSET); } @Test @@ -696,7 +696,7 @@ public class ZenPolicyTest extends UiServiceTestCase { builder.allowPriorityChannels(true); ZenPolicy policy = builder.build(); - assertThat(policy.getPriorityChannels()).isEqualTo(ZenPolicy.STATE_UNSET); + assertThat(policy.getPriorityChannelsAllowed()).isEqualTo(ZenPolicy.STATE_UNSET); assertThat(policy.toString().contains("allowChannels")).isFalse(); } @@ -708,12 +708,12 @@ public class ZenPolicyTest extends UiServiceTestCase { ZenPolicy.Builder builder = new ZenPolicy.Builder(); builder.allowPriorityChannels(true); ZenPolicy policy = builder.build(); - assertThat(policy.getPriorityChannels()).isEqualTo(ZenPolicy.STATE_ALLOW); + assertThat(policy.getPriorityChannelsAllowed()).isEqualTo(ZenPolicy.STATE_ALLOW); // disallow priority channels builder.allowPriorityChannels(false); policy = builder.build(); - assertThat(policy.getPriorityChannels()).isEqualTo(ZenPolicy.STATE_DISALLOW); + assertThat(policy.getPriorityChannelsAllowed()).isEqualTo(ZenPolicy.STATE_DISALLOW); } @Test @@ -734,7 +734,7 @@ public class ZenPolicyTest extends UiServiceTestCase { assertThat(newPolicy.getVisualEffectBadge()).isEqualTo(ZenPolicy.STATE_DISALLOW); assertThat(newPolicy.getVisualEffectPeek()).isEqualTo(ZenPolicy.STATE_UNSET); - assertThat(newPolicy.getPriorityChannels()).isEqualTo(ZenPolicy.STATE_ALLOW); + assertThat(newPolicy.getPriorityChannelsAllowed()).isEqualTo(ZenPolicy.STATE_ALLOW); } @Test diff --git a/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java b/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java index 7c2f7eedff9d..c8abd8d7297e 100644 --- a/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java +++ b/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java @@ -166,6 +166,7 @@ class TestPhoneWindowManager { @Mock private PhoneWindowManager.ButtonOverridePermissionChecker mButtonOverridePermissionChecker; + @Mock private WindowWakeUpPolicy mWindowWakeUpPolicy; @Mock private IBinder mInputToken; @Mock private IBinder mImeTargetWindowToken; @@ -230,6 +231,10 @@ class TestPhoneWindowManager { TalkbackShortcutController getTalkbackShortcutController() { return new TestTalkbackShortcutController(mContext); } + + WindowWakeUpPolicy getWindowWakeUpPolicy() { + return mWindowWakeUpPolicy; + } } TestPhoneWindowManager(Context context, boolean supportSettingsUpdate) { @@ -620,7 +625,8 @@ class TestPhoneWindowManager { void assertPowerWakeUp() { mTestLooper.dispatchAll(); - verify(mPowerManager).wakeUp(anyLong(), anyInt(), anyString()); + verify(mWindowWakeUpPolicy) + .wakeUpFromKey(anyLong(), eq(KeyEvent.KEYCODE_POWER), anyBoolean()); } void assertNoPowerSleep() { diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityOptionsTest.java b/services/tests/wmtests/src/com/android/server/wm/ActivityOptionsTest.java index 6e5baee3dc67..37e0818eb083 100644 --- a/services/tests/wmtests/src/com/android/server/wm/ActivityOptionsTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/ActivityOptionsTest.java @@ -46,6 +46,7 @@ import android.content.Intent; import android.os.Binder; import android.os.Bundle; import android.os.IBinder; +import android.os.IRemoteCallback; import android.platform.test.annotations.Presubmit; import android.util.Log; import android.util.Rational; @@ -63,6 +64,7 @@ import java.util.ArrayList; import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; /** * Build/Install/Run: @@ -119,6 +121,29 @@ public class ActivityOptionsTest { } @Test + public void testAbortListenerCalled() { + AtomicBoolean callbackCalled = new AtomicBoolean(false); + + ActivityOptions options = ActivityOptions.makeBasic(); + options.setOnAnimationAbortListener(new IRemoteCallback.Stub() { + @Override + public void sendResult(Bundle data) { + callbackCalled.set(true); + } + }); + + // Verify that the callback is called on abort + options.abort(); + assertTrue(callbackCalled.get()); + + // Verify that the callback survives saving to bundle + ActivityOptions optionsCopy = ActivityOptions.fromBundle(options.toBundle()); + callbackCalled.set(false); + optionsCopy.abort(); + assertTrue(callbackCalled.get()); + } + + @Test public void testTransferLaunchCookie() { final Binder cookie = new Binder(); final ActivityOptions options = ActivityOptions.makeBasic(); @@ -279,7 +304,9 @@ public class ActivityOptionsTest { case "android.activity.pendingIntentCreatorBackgroundActivityStartMode": // KEY_PENDING_INTENT_CREATOR_BACKGROUND_ACTIVITY_START_MODE case "android.activity.launchCookie": // KEY_LAUNCH_COOKIE + case "android:activity.animAbortListener": // KEY_ANIM_ABORT_LISTENER // Existing keys + break; default: unknownKeys.add(key); diff --git a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java index 752dc5e8e7f6..0c1a9c3ab5b9 100644 --- a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java @@ -886,8 +886,6 @@ public class SizeCompatTests extends WindowTestsBase { spyOn(mActivity.mLetterboxUiController); doReturn(true).when(mActivity.mLetterboxUiController) - .isSurfaceReadyToShow(any()); - doReturn(true).when(mActivity.mLetterboxUiController) .isSurfaceVisible(any()); assertTrue(mActivity.mLetterboxUiController.shouldShowLetterboxUi( diff --git a/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java index 51df1d4cb14d..7d8eb90ea79c 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java @@ -1602,6 +1602,33 @@ public class TransitionTests extends WindowTestsBase { } @Test + public void testTransientWithParallelLaunch() { + final Task recentTask = mDisplayContent.getDefaultTaskDisplayArea().getRootHomeTask(); + final ActivityRecord recent = new ActivityBuilder(mAtm).setTask(recentTask) + .setVisible(false).build(); + final ActivityRecord app = new ActivityBuilder(mAtm).setCreateTask(true).build(); + final Task appTask = app.getTask(); + registerTestTransitionPlayer(); + final TransitionController controller = mRootWindowContainer.mTransitionController; + final Transition transition = createTestTransition(TRANSIT_OPEN, controller); + transition.mParallelCollectType = Transition.PARALLEL_TYPE_RECENTS; + controller.moveToCollecting(transition); + transition.collect(recentTask); + transition.collect(appTask); + transition.setTransientLaunch(recent, appTask); + recentTask.moveToFront("move-recent-to-front"); + transition.setAllReady(); + transition.start(); + // Assume that the app starts another activity in its task. + final Transition newTransition = controller.createAndStartCollecting(TRANSIT_OPEN); + + assertEquals(newTransition, controller.getCollectingTransition()); + assertTrue(controller.mWaitingTransitions.contains(transition)); + assertTrue(controller.isTransientHide(appTask)); + assertTrue(controller.isTransientVisible(appTask)); + } + + @Test public void testNotReadyPushPop() { final TransitionController controller = new TestTransitionController(mAtm); controller.setSyncEngine(mWm.mSyncEngine); diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowContainerTransactionTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowContainerTransactionTests.java index d2552718f218..e9ece5dbdcc4 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowContainerTransactionTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowContainerTransactionTests.java @@ -16,6 +16,9 @@ package com.android.server.wm; +import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; +import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; + import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify; import static org.junit.Assert.assertEquals; @@ -23,6 +26,7 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.atLeast; +import static org.mockito.Mockito.times; import android.content.Intent; import android.platform.test.annotations.Presubmit; @@ -35,6 +39,9 @@ import androidx.test.filters.SmallTest; import org.junit.Test; import org.junit.runner.RunWith; +import java.util.ArrayList; +import java.util.List; + /** * Test class for {@link WindowContainerTransaction}. * @@ -45,7 +52,6 @@ import org.junit.runner.RunWith; @Presubmit @RunWith(WindowTestRunner.class) public class WindowContainerTransactionTests extends WindowTestsBase { - @Test public void testRemoveTask() { final Task rootTask = createTask(mDisplayContent); @@ -72,6 +78,123 @@ public class WindowContainerTransactionTests extends WindowTestsBase { verify(mAtm.getLockTaskController(), atLeast(1)).clearLockedTask(rootTask); } + @Test + public void testDesktopMode_tasksAreBroughtToFront() { + final TestDesktopOrganizer desktopOrganizer = new TestDesktopOrganizer(mAtm); + TaskDisplayArea tda = desktopOrganizer.mDefaultTDA; + List<ActivityRecord> activityRecords = new ArrayList<>(); + int numberOfTasks = 4; + desktopOrganizer.createFreeformTasksWithActivities(desktopOrganizer, + activityRecords, numberOfTasks); + + final Task task = createTask(mDisplayContent); + final ActivityRecord activity = createActivityRecord(mDisplayContent, task); + task.setWindowingMode(WINDOWING_MODE_FULLSCREEN); + + // Bring home to front of the tasks + desktopOrganizer.bringHomeToFront(); + + // Bring tasks in front of the home + WindowContainerTransaction wct = new WindowContainerTransaction(); + desktopOrganizer.bringDesktopTasksToFront(wct); + applyTransaction(wct); + + // Verify tasks are resumed and in correct z-order + verify(mRootWindowContainer, times(2)).ensureActivitiesVisible(); + for (int i = 0; i < numberOfTasks - 1; i++) { + assertTrue(tda.mChildren + .indexOf(desktopOrganizer.mTasks.get(i).getRootTask()) + < tda.mChildren.indexOf(desktopOrganizer.mTasks.get(i + 1).getRootTask())); + } + } + + @Test + public void testDesktopMode_moveTaskToDesktop() { + final TestDesktopOrganizer desktopOrganizer = new TestDesktopOrganizer(mAtm); + TaskDisplayArea tda = desktopOrganizer.mDefaultTDA; + List<ActivityRecord> activityRecords = new ArrayList<>(); + int numberOfTasks = 4; + desktopOrganizer.createFreeformTasksWithActivities(desktopOrganizer, + activityRecords, numberOfTasks); + + final Task task = createTask(mDisplayContent); + final ActivityRecord activity = createActivityRecord(mDisplayContent, task); + task.setWindowingMode(WINDOWING_MODE_FULLSCREEN); + + // Bring home to front of the tasks + desktopOrganizer.bringHomeToFront(); + + // Bring tasks in front of the home and newly moved task to on top of them + WindowContainerTransaction wct = new WindowContainerTransaction(); + desktopOrganizer.bringDesktopTasksToFront(wct); + desktopOrganizer.addMoveToDesktopChanges(wct, task, true); + wct.setBounds(task.getTaskInfo().token, desktopOrganizer.getDefaultDesktopTaskBounds()); + applyTransaction(wct); + + // Verify tasks are resumed + verify(mRootWindowContainer, times(2)).ensureActivitiesVisible(); + + // Tasks are in correct z-order + for (int i = 0; i < numberOfTasks - 1; i++) { + assertTrue(tda.mChildren + .indexOf(desktopOrganizer.mTasks.get(i).getRootTask()) + < tda.mChildren.indexOf(desktopOrganizer.mTasks.get(i + 1).getRootTask())); + } + // New task is on top of other tasks + assertTrue(tda.mChildren + .indexOf(desktopOrganizer.mTasks.get(3).getRootTask()) + < tda.mChildren.indexOf(task)); + + // New task is in freeform and has specified bounds + assertEquals(WINDOWING_MODE_FREEFORM, task.getWindowingMode()); + assertEquals(desktopOrganizer.getDefaultDesktopTaskBounds(), task.getBounds()); + } + + + @Test + public void testDesktopMode_moveTaskToFullscreen() { + final TestDesktopOrganizer desktopOrganizer = new TestDesktopOrganizer(mAtm); + List<ActivityRecord> activityRecords = new ArrayList<>(); + int numberOfTasks = 4; + desktopOrganizer.createFreeformTasksWithActivities(desktopOrganizer, + activityRecords, numberOfTasks); + + Task taskToMove = desktopOrganizer.mTasks.get(numberOfTasks - 1); + + // Bring tasks in front of the home and newly moved task to on top of them + WindowContainerTransaction wct = new WindowContainerTransaction(); + desktopOrganizer.addMoveToFullscreen(wct, taskToMove, false); + applyTransaction(wct); + + // New task is in freeform + assertEquals(WINDOWING_MODE_FULLSCREEN, taskToMove.getWindowingMode()); + } + + @Test + public void testDesktopMode_moveTaskToFront() { + final TestDesktopOrganizer desktopOrganizer = new TestDesktopOrganizer(mAtm); + TaskDisplayArea tda = desktopOrganizer.mDefaultTDA; + List<ActivityRecord> activityRecords = new ArrayList<>(); + int numberOfTasks = 5; + desktopOrganizer.createFreeformTasksWithActivities(desktopOrganizer, + activityRecords, numberOfTasks); + + // Bring task 2 on top of other tasks + WindowContainerTransaction wct = new WindowContainerTransaction(); + wct.reorder(desktopOrganizer.mTasks.get(2).getTaskInfo().token, true /* onTop */); + applyTransaction(wct); + + // Tasks are in correct z-order + assertTrue(tda.mChildren.indexOf(desktopOrganizer.mTasks.get(0).getRootTask()) + < tda.mChildren.indexOf(desktopOrganizer.mTasks.get(1).getRootTask())); + assertTrue(tda.mChildren.indexOf(desktopOrganizer.mTasks.get(1).getRootTask()) + < tda.mChildren.indexOf(desktopOrganizer.mTasks.get(3).getRootTask())); + assertTrue(tda.mChildren.indexOf(desktopOrganizer.mTasks.get(3).getRootTask()) + < tda.mChildren.indexOf(desktopOrganizer.mTasks.get(4).getRootTask())); + assertTrue(tda.mChildren.indexOf(desktopOrganizer.mTasks.get(4).getRootTask()) + < tda.mChildren.indexOf(desktopOrganizer.mTasks.get(2).getRootTask())); + } + private Task createTask(int taskId) { return new Task.Builder(mAtm) .setTaskId(taskId) @@ -87,3 +210,4 @@ public class WindowContainerTransactionTests extends WindowTestsBase { } } } + diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java index fe9d83776ad9..06afa381c9c0 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java @@ -1068,7 +1068,7 @@ public class WindowManagerServiceTests extends WindowTestsBase { invocationOnMock.callRealMethod(); return null; }).when(surface).lockCanvas(any()); - mWm.mAccessibilityController.drawMagnifiedRegionBorderIfNeeded(displayId, mTransaction); + mWm.mAccessibilityController.drawMagnifiedRegionBorderIfNeeded(displayId); waitUntilHandlersIdle(); try { verify(surface).lockCanvas(any()); @@ -1076,14 +1076,9 @@ public class WindowManagerServiceTests extends WindowTestsBase { clearInvocations(surface); // Invalidate and redraw. mWm.mAccessibilityController.onDisplaySizeChanged(mDisplayContent); - mWm.mAccessibilityController.drawMagnifiedRegionBorderIfNeeded(displayId, mTransaction); + mWm.mAccessibilityController.drawMagnifiedRegionBorderIfNeeded(displayId); // Turn off magnification to release surface. mWm.mAccessibilityController.setMagnificationCallbacks(displayId, null); - if (!com.android.window.flags.Flags.drawMagnifierBorderOutsideWmlock()) { - verify(surface).release(); - assertTrue(lockCanvasInWmLock[0]); - return; - } waitUntilHandlersIdle(); // lockCanvas must not be called after releasing. verify(surface, never()).lockCanvas(any()); 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 a0bafb64090f..be837441ef94 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java @@ -116,6 +116,7 @@ import android.window.StartingWindowRemovalInfo; import android.window.TaskFragmentOrganizer; import android.window.TransitionInfo; import android.window.TransitionRequestInfo; +import android.window.WindowContainerTransaction; import com.android.internal.policy.AttributeCache; import com.android.internal.util.ArrayUtils; @@ -1899,12 +1900,14 @@ class WindowTestsBase extends SystemServiceTestsBase { final int mDesktopModeDefaultWidthDp = 840; final int mDesktopModeDefaultHeightDp = 630; final int mDesktopDensity = 284; + final int mOverrideDensity = 285; final ActivityTaskManagerService mService; final TaskDisplayArea mDefaultTDA; List<Task> mTasks; final DisplayContent mDisplay; Rect mStableBounds; + Task mHomeTask; TestDesktopOrganizer(ActivityTaskManagerService service, DisplayContent display) { mService = service; @@ -1913,8 +1916,8 @@ class WindowTestsBase extends SystemServiceTestsBase { mService.mTaskOrganizerController.registerTaskOrganizer(this); mTasks = new ArrayList<>(); mStableBounds = display.getBounds(); + mHomeTask = mDefaultTDA.getRootHomeTask(); } - TestDesktopOrganizer(ActivityTaskManagerService service) { this(service, service.mTaskSupervisor.mRootWindowContainer.getDefaultDisplay()); } @@ -1929,8 +1932,10 @@ class WindowTestsBase extends SystemServiceTestsBase { } public Rect getDefaultDesktopTaskBounds() { - int width = (int) (mDesktopModeDefaultWidthDp * mDesktopDensity + 0.5f); - int height = (int) (mDesktopModeDefaultHeightDp * mDesktopDensity + 0.5f); + int width = (int) (mDesktopModeDefaultWidthDp + * (mOverrideDensity / mDesktopDensity) + 0.5f); + int height = (int) (mDesktopModeDefaultHeightDp + * (mOverrideDensity / mDesktopDensity) + 0.5f); Rect outBounds = new Rect(); outBounds.set(0, 0, width, height); @@ -1942,8 +1947,69 @@ class WindowTestsBase extends SystemServiceTestsBase { return outBounds; } + public void createFreeformTasksWithActivities(TestDesktopOrganizer desktopOrganizer, + List<ActivityRecord> activityRecords, int numberOfTasks) { + for (int i = 0; i < numberOfTasks; i++) { + Rect bounds = new Rect(desktopOrganizer.getDefaultDesktopTaskBounds()); + bounds.offset(20 * i, 20 * i); + desktopOrganizer.createTask(bounds); + } + + for (int i = 0; i < numberOfTasks; i++) { + activityRecords.add(new TaskBuilder(mService.mTaskSupervisor) + .setParentTask(desktopOrganizer.mTasks.get(i)) + .setCreateActivity(true) + .build() + .getTopMostActivity()); + } + + for (int i = 0; i < numberOfTasks; i++) { + activityRecords.get(i).setVisibleRequested(true); + } + + for (int i = 0; i < numberOfTasks; i++) { + assertEquals(desktopOrganizer.mTasks.get(i), activityRecords.get(i).getRootTask()); + } + } + + public void bringHomeToFront() { + WindowContainerTransaction wct = new WindowContainerTransaction(); + wct.reorder(mHomeTask.getTaskInfo().token, true /* onTop */); + applyTransaction(wct); + } + + public void bringDesktopTasksToFront(WindowContainerTransaction wct) { + for (Task task: mTasks) { + wct.reorder(task.getTaskInfo().token, true /* onTop */); + } + } + + public void addMoveToDesktopChanges(WindowContainerTransaction wct, Task task, + boolean overrideDensity) { + wct.setWindowingMode(task.getTaskInfo().token, WINDOWING_MODE_FREEFORM); + wct.reorder(task.getTaskInfo().token, true /* onTop */); + if (overrideDensity) { + wct.setDensityDpi(task.getTaskInfo().token, mOverrideDensity); + } + } + + public void addMoveToFullscreen(WindowContainerTransaction wct, Task task, + boolean overrideDensity) { + wct.setWindowingMode(task.getTaskInfo().token, WINDOWING_MODE_FULLSCREEN); + wct.setBounds(task.getTaskInfo().token, new Rect()); + if (overrideDensity) { + wct.setDensityDpi(task.getTaskInfo().token, mOverrideDensity); + } + } + + private void applyTransaction(@androidx.annotation.NonNull WindowContainerTransaction wct) { + if (!wct.isEmpty()) { + mService.mWindowOrganizerController.applyTransaction(wct); + } + } } + static TestWindowToken createTestWindowToken(int type, DisplayContent dc) { return createTestWindowToken(type, dc, false /* persistOnEmpty */); } diff --git a/telecomm/java/android/telecom/PhoneAccount.java b/telecomm/java/android/telecom/PhoneAccount.java index 63db29713825..b7706a926a3d 100644 --- a/telecomm/java/android/telecom/PhoneAccount.java +++ b/telecomm/java/android/telecom/PhoneAccount.java @@ -1283,6 +1283,17 @@ public final class PhoneAccount implements Parcelable { sb.append(mExtras); sb.append(" GroupId: "); sb.append(Log.pii(mGroupId)); + sb.append(" SC Restrictions: "); + if (hasSimultaneousCallingRestriction()) { + sb.append("[ "); + for (PhoneAccountHandle handle : mSimultaneousCallingRestriction) { + sb.append(handle); + sb.append(" "); + } + sb.append("]"); + } else { + sb.append("[NONE]"); + } sb.append("]"); return sb.toString(); } diff --git a/telecomm/java/android/telecom/TelecomManager.java b/telecomm/java/android/telecom/TelecomManager.java index e81f48280e46..2c6e1e48b57f 100644 --- a/telecomm/java/android/telecom/TelecomManager.java +++ b/telecomm/java/android/telecom/TelecomManager.java @@ -1360,7 +1360,7 @@ 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 other users visible to the user. + * the user or enabled by other users but visible to the user. * * @see #EXTRA_PHONE_ACCOUNT_HANDLE * @return A list of {@code PhoneAccountHandle} objects. @@ -1486,7 +1486,7 @@ public class TelecomManager { return service.getCallCapablePhoneAccounts(includeDisabledAccounts, mContext.getOpPackageName(), mContext.getAttributionTag(), true).getList(); } catch (RemoteException e) { - throw e.rethrowAsRuntimeException(); + throw e.rethrowFromSystemServer(); } } diff --git a/telephony/java/android/telephony/CarrierConfigManager.java b/telephony/java/android/telephony/CarrierConfigManager.java index a73c46b12c53..e5a94c302c89 100644 --- a/telephony/java/android/telephony/CarrierConfigManager.java +++ b/telephony/java/android/telephony/CarrierConfigManager.java @@ -2179,6 +2179,14 @@ public class CarrierConfigManager { */ public static final String KEY_MMS_NETWORK_RELEASE_TIMEOUT_MILLIS_INT = "mms_network_release_timeout_millis_int"; + /** + * Maximum size in bytes of the PDU to send or download when connected to a non-terrestrial + * network. MmsService will return a result code of MMS_ERROR_TOO_LARGE_FOR_TRANSPORT if + * the PDU exceeds this limit when connected to a non-terrestrial network. + * @hide + */ + public static final String KEY_MMS_MAX_NTN_PAYLOAD_SIZE_BYTES_INT = + "mms_max_ntn_payload_size_bytes_int"; /** * The flatten {@link android.content.ComponentName componentName} of the activity that can @@ -10376,6 +10384,7 @@ public class CarrierConfigManager { sDefaults.putInt(KEY_MMS_SMS_TO_MMS_TEXT_THRESHOLD_INT, -1); sDefaults.putInt(KEY_MMS_SUBJECT_MAX_LENGTH_INT, 40); sDefaults.putInt(KEY_MMS_NETWORK_RELEASE_TIMEOUT_MILLIS_INT, 5 * 1000); + sDefaults.putInt(KEY_MMS_MAX_NTN_PAYLOAD_SIZE_BYTES_INT, 3 * 1000); sDefaults.putString(KEY_MMS_EMAIL_GATEWAY_NUMBER_STRING, ""); sDefaults.putString(KEY_MMS_HTTP_PARAMS_STRING, ""); sDefaults.putString(KEY_MMS_NAI_SUFFIX_STRING, ""); diff --git a/telephony/java/android/telephony/SmsManager.java b/telephony/java/android/telephony/SmsManager.java index c958aba1d758..b7baabf7c319 100644 --- a/telephony/java/android/telephony/SmsManager.java +++ b/telephony/java/android/telephony/SmsManager.java @@ -3123,6 +3123,12 @@ public final class SmsManager { @FlaggedApi(Flags.FLAG_MMS_DISABLED_ERROR) public static final int MMS_ERROR_MMS_DISABLED_BY_CARRIER = 12; + /** + * The MMS pdu was too large to send or too large to download over the current connection. + * @hide + */ + public static final int MMS_ERROR_TOO_LARGE_FOR_TRANSPORT = 13; + /** Intent extra name for MMS sending result data in byte array type */ public static final String EXTRA_MMS_DATA = "android.telephony.extra.MMS_DATA"; /** Intent extra name for HTTP status code for MMS HTTP failure in integer type */ diff --git a/telephony/java/android/telephony/TelephonyManager.java b/telephony/java/android/telephony/TelephonyManager.java index cbd552454642..c1ceaef64d5d 100644 --- a/telephony/java/android/telephony/TelephonyManager.java +++ b/telephony/java/android/telephony/TelephonyManager.java @@ -25,6 +25,7 @@ import static com.android.internal.util.Preconditions.checkNotNull; import android.Manifest; import android.annotation.BytesLong; import android.annotation.CallbackExecutor; +import android.annotation.CurrentTimeMillisLong; import android.annotation.FlaggedApi; import android.annotation.IntDef; import android.annotation.LongDef; @@ -18714,51 +18715,93 @@ public class TelephonyManager { * call diagnostic data * @hide */ - public static class EmergencyCallDiagnosticParams { + @SystemApi + @FlaggedApi(com.android.server.telecom.flags.Flags.FLAG_TELECOM_RESOLVE_HIDDEN_DEPENDENCIES) + public static final class EmergencyCallDiagnosticParams { + public static final class Builder { + private boolean mCollectTelecomDumpSys; + private boolean mCollectTelephonyDumpsys; + + // If this is set to a value other than -1L, then the logcat collection is enabled. + // Logcat lines with this time or greater are collected how much is collected is + // dependent on internal implementation. Time represented as milliseconds since boot. + private long mLogcatStartTimeMillis = sUnsetLogcatStartTime; + + /** + * Allows enabling of telecom dumpsys collection. + * @param collectTelecomDumpsys Determines whether telecom dumpsys should be collected. + * @return Builder instance corresponding to the configured call diagnostic params. + */ + public @NonNull Builder setTelecomDumpSysCollectionEnabled( + boolean collectTelecomDumpsys) { + mCollectTelecomDumpSys = collectTelecomDumpsys; + return this; + } + + /** + * Allows enabling of telephony dumpsys collection. + * @param collectTelephonyDumpsys Determines if telephony dumpsys should be collected. + * @return Builder instance corresponding to the configured call diagnostic params. + */ + public @NonNull Builder setTelephonyDumpSysCollectionEnabled( + boolean collectTelephonyDumpsys) { + mCollectTelephonyDumpsys = collectTelephonyDumpsys; + return this; + } + + /** + * Allows enabling of logcat (system,radio) collection. + * @param startTimeMillis Enables logcat collection as of the indicated timestamp. + * @return Builder instance corresponding to the configured call diagnostic params. + */ + public @NonNull Builder setLogcatCollectionStartTimeMillis( + @CurrentTimeMillisLong long startTimeMillis) { + mLogcatStartTimeMillis = startTimeMillis; + return this; + } - private boolean mCollectTelecomDumpSys; - private boolean mCollectTelephonyDumpsys; - private boolean mCollectLogcat; + /** + * Build the EmergencyCallDiagnosticParams from the provided Builder config. + * @return {@link EmergencyCallDiagnosticParams} instance from provided builder. + */ + public @NonNull EmergencyCallDiagnosticParams build() { + return new EmergencyCallDiagnosticParams(mCollectTelecomDumpSys, + mCollectTelephonyDumpsys, mLogcatStartTimeMillis); + } + } - //logcat lines with this time or greater are collected - //how much is collected is dependent on internal implementation. - //Time represented as milliseconds since January 1, 1970 UTC + private boolean mCollectTelecomDumpSys; + private boolean mCollectTelephonyDumpsys; + private boolean mCollectLogcat; private long mLogcatStartTimeMillis; + private static long sUnsetLogcatStartTime = -1L; - public boolean isTelecomDumpSysCollectionEnabled() { - return mCollectTelecomDumpSys; + private EmergencyCallDiagnosticParams(boolean collectTelecomDumpSys, + boolean collectTelephonyDumpsys, long logcatStartTimeMillis) { + mCollectTelecomDumpSys = collectTelecomDumpSys; + mCollectTelephonyDumpsys = collectTelephonyDumpsys; + mLogcatStartTimeMillis = logcatStartTimeMillis; + mCollectLogcat = logcatStartTimeMillis != sUnsetLogcatStartTime; } - public void setTelecomDumpSysCollection(boolean collectTelecomDumpSys) { - mCollectTelecomDumpSys = collectTelecomDumpSys; + public boolean isTelecomDumpSysCollectionEnabled() { + return mCollectTelecomDumpSys; } public boolean isTelephonyDumpSysCollectionEnabled() { return mCollectTelephonyDumpsys; } - public void setTelephonyDumpSysCollection(boolean collectTelephonyDumpsys) { - mCollectTelephonyDumpsys = collectTelephonyDumpsys; - } - public boolean isLogcatCollectionEnabled() { return mCollectLogcat; } - public long getLogcatStartTime() + public long getLogcatCollectionStartTimeMillis() { return mLogcatStartTimeMillis; } - public void setLogcatCollection(boolean collectLogcat, long startTimeMillis) { - mCollectLogcat = collectLogcat; - if(mCollectLogcat) - { - mLogcatStartTimeMillis = startTimeMillis; - } - } - @Override public String toString() { return "EmergencyCallDiagnosticParams{" + @@ -18774,11 +18817,12 @@ public class TelephonyManager { * Request telephony to persist state for debugging emergency call failures. * * @param dropboxTag Tag to use when persisting data to dropbox service. - * - * @see params Parameters controlling what is collected + * @param params Parameters controlling what is collected. * * @hide */ + @SystemApi + @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) { @@ -18791,7 +18835,7 @@ public class TelephonyManager { if (telephony != null) { telephony.persistEmergencyCallDiagnosticData(dropboxTag, params.isLogcatCollectionEnabled(), - params.getLogcatStartTime(), + params.getLogcatCollectionStartTimeMillis(), params.isTelecomDumpSysCollectionEnabled(), params.isTelephonyDumpSysCollectionEnabled()); } diff --git a/telephony/java/android/telephony/satellite/SatelliteManager.java b/telephony/java/android/telephony/satellite/SatelliteManager.java index 70047a6feb9c..a1ac477d3519 100644 --- a/telephony/java/android/telephony/satellite/SatelliteManager.java +++ b/telephony/java/android/telephony/satellite/SatelliteManager.java @@ -34,7 +34,6 @@ import android.os.ICancellationSignal; import android.os.OutcomeReceiver; import android.os.RemoteException; import android.os.ResultReceiver; -import android.os.ServiceSpecificException; import android.telephony.SubscriptionManager; import android.telephony.TelephonyCallback; import android.telephony.TelephonyFrameworkInitializer; @@ -336,6 +335,12 @@ public final class SatelliteManager { @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) public static final int SATELLITE_RESULT_MODEM_BUSY = 22; + /** + * Telephony process is not currently available or satellite is not supported. + */ + @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) + public static final int SATELLITE_RESULT_ILLEGAL_STATE = 23; + /** @hide */ @IntDef(prefix = {"SATELLITE_RESULT_"}, value = { SATELLITE_RESULT_SUCCESS, @@ -360,7 +365,8 @@ public final class SatelliteManager { SATELLITE_RESULT_NOT_AUTHORIZED, SATELLITE_RESULT_NOT_SUPPORTED, SATELLITE_RESULT_REQUEST_IN_PROGRESS, - SATELLITE_RESULT_MODEM_BUSY + SATELLITE_RESULT_MODEM_BUSY, + SATELLITE_RESULT_ILLEGAL_STATE }) @Retention(RetentionPolicy.SOURCE) public @interface SatelliteResult {} @@ -510,7 +516,7 @@ public final class SatelliteManager { } } catch (RemoteException ex) { Rlog.e(TAG, "requestSatelliteEnabled() RemoteException: ", ex); - ex.rethrowFromSystemServer(); + ex.rethrowAsRuntimeException(); } } @@ -526,7 +532,6 @@ public final class SatelliteManager { * will return a {@link SatelliteException} with the {@link SatelliteResult}. * * @throws SecurityException if the caller doesn't have required permission. - * @throws IllegalStateException if the Telephony process is not currently available. */ @RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION) @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) @@ -561,11 +566,12 @@ public final class SatelliteManager { }; telephony.requestIsSatelliteEnabled(mSubId, receiver); } else { - throw new IllegalStateException("telephony service is null."); + executor.execute(() -> Binder.withCleanCallingIdentity(() -> callback.onError( + new SatelliteException(SATELLITE_RESULT_ILLEGAL_STATE)))); } } catch (RemoteException ex) { loge("requestIsSatelliteEnabled() RemoteException: " + ex); - ex.rethrowFromSystemServer(); + ex.rethrowAsRuntimeException(); } } @@ -581,7 +587,6 @@ public final class SatelliteManager { * will return a {@link SatelliteException} with the {@link SatelliteResult}. * * @throws SecurityException if the caller doesn't have required permission. - * @throws IllegalStateException if the Telephony process is not currently available. */ @RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION) @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) @@ -616,11 +621,12 @@ public final class SatelliteManager { }; telephony.requestIsDemoModeEnabled(mSubId, receiver); } else { - throw new IllegalStateException("telephony service is null."); + executor.execute(() -> Binder.withCleanCallingIdentity(() -> callback.onError( + new SatelliteException(SATELLITE_RESULT_ILLEGAL_STATE)))); } } catch (RemoteException ex) { loge("requestIsDemoModeEnabled() RemoteException: " + ex); - ex.rethrowFromSystemServer(); + ex.rethrowAsRuntimeException(); } } @@ -639,8 +645,6 @@ public final class SatelliteManager { * service is supported on the device and {@code false} otherwise. * If the request is not successful, {@link OutcomeReceiver#onError(Throwable)} * will return a {@link SatelliteException} with the {@link SatelliteResult}. - * - * @throws IllegalStateException if the Telephony process is not currently available. */ @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) public void requestIsSatelliteSupported(@NonNull @CallbackExecutor Executor executor, @@ -674,11 +678,12 @@ public final class SatelliteManager { }; telephony.requestIsSatelliteSupported(mSubId, receiver); } else { - throw new IllegalStateException("telephony service is null."); + executor.execute(() -> Binder.withCleanCallingIdentity(() -> callback.onError( + new SatelliteException(SATELLITE_RESULT_ILLEGAL_STATE)))); } } catch (RemoteException ex) { loge("requestIsSatelliteSupported() RemoteException: " + ex); - ex.rethrowFromSystemServer(); + ex.rethrowAsRuntimeException(); } } @@ -693,7 +698,6 @@ public final class SatelliteManager { * will return a {@link SatelliteException} with the {@link SatelliteResult}. * * @throws SecurityException if the caller doesn't have required permission. - * @throws IllegalStateException if the Telephony process is not currently available. */ @RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION) @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) @@ -729,11 +733,12 @@ public final class SatelliteManager { }; telephony.requestSatelliteCapabilities(mSubId, receiver); } else { - throw new IllegalStateException("telephony service is null."); + executor.execute(() -> Binder.withCleanCallingIdentity(() -> callback.onError( + new SatelliteException(SATELLITE_RESULT_ILLEGAL_STATE)))); } } catch (RemoteException ex) { loge("requestSatelliteCapabilities() RemoteException: " + ex); - ex.rethrowFromSystemServer(); + ex.rethrowAsRuntimeException(); } } @@ -959,7 +964,6 @@ public final class SatelliteManager { * @param callback The callback to notify of satellite transmission updates. * * @throws SecurityException if the caller doesn't have required permission. - * @throws IllegalStateException if the Telephony process is not currently available. */ @RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION) @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) @@ -1009,11 +1013,12 @@ public final class SatelliteManager { telephony.startSatelliteTransmissionUpdates(mSubId, errorCallback, internalCallback); } else { - throw new IllegalStateException("telephony service is null."); + executor.execute(() -> Binder.withCleanCallingIdentity( + () -> resultListener.accept(SATELLITE_RESULT_ILLEGAL_STATE))); } } catch (RemoteException ex) { loge("startSatelliteTransmissionUpdates() RemoteException: " + ex); - ex.rethrowFromSystemServer(); + ex.rethrowAsRuntimeException(); } } @@ -1029,7 +1034,6 @@ public final class SatelliteManager { * @param resultListener Listener for the {@link SatelliteResult} result of the operation. * * @throws SecurityException if the caller doesn't have required permission. - * @throws IllegalStateException if the Telephony process is not currently available. */ @RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION) @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) @@ -1063,11 +1067,12 @@ public final class SatelliteManager { () -> resultListener.accept(SATELLITE_RESULT_INVALID_ARGUMENTS))); } } else { - throw new IllegalStateException("telephony service is null."); + executor.execute(() -> Binder.withCleanCallingIdentity( + () -> resultListener.accept(SATELLITE_RESULT_ILLEGAL_STATE))); } } catch (RemoteException ex) { loge("stopSatelliteTransmissionUpdates() RemoteException: " + ex); - ex.rethrowFromSystemServer(); + ex.rethrowAsRuntimeException(); } } @@ -1112,11 +1117,12 @@ public final class SatelliteManager { cancelRemote = telephony.provisionSatelliteService(mSubId, token, provisionData, errorCallback); } else { - throw new IllegalStateException("telephony service is null."); + executor.execute(() -> Binder.withCleanCallingIdentity( + () -> resultListener.accept(SATELLITE_RESULT_ILLEGAL_STATE))); } } catch (RemoteException ex) { loge("provisionSatelliteService() RemoteException=" + ex); - ex.rethrowFromSystemServer(); + ex.rethrowAsRuntimeException(); } if (cancellationSignal != null) { cancellationSignal.setRemote(cancelRemote); @@ -1138,7 +1144,6 @@ public final class SatelliteManager { * @param resultListener Listener for the {@link SatelliteResult} result of the operation. * * @throws SecurityException if the caller doesn't have required permission. - * @throws IllegalStateException if the Telephony process is not currently available. */ @RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION) @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) @@ -1161,11 +1166,12 @@ public final class SatelliteManager { }; telephony.deprovisionSatelliteService(mSubId, token, errorCallback); } else { - throw new IllegalStateException("telephony service is null."); + executor.execute(() -> Binder.withCleanCallingIdentity( + () -> resultListener.accept(SATELLITE_RESULT_ILLEGAL_STATE))); } } catch (RemoteException ex) { loge("deprovisionSatelliteService() RemoteException=" + ex); - ex.rethrowFromSystemServer(); + ex.rethrowAsRuntimeException(); } } @@ -1208,7 +1214,7 @@ public final class SatelliteManager { } } catch (RemoteException ex) { loge("registerForSatelliteProvisionStateChanged() RemoteException: " + ex); - ex.rethrowFromSystemServer(); + ex.rethrowAsRuntimeException(); } return SATELLITE_RESULT_REQUEST_FAILED; } @@ -1244,7 +1250,7 @@ public final class SatelliteManager { } } catch (RemoteException ex) { loge("unregisterForSatelliteProvisionStateChanged() RemoteException: " + ex); - ex.rethrowFromSystemServer(); + ex.rethrowAsRuntimeException(); } } @@ -1260,7 +1266,6 @@ public final class SatelliteManager { * will return a {@link SatelliteException} with the {@link SatelliteResult}. * * @throws SecurityException if the caller doesn't have required permission. - * @throws IllegalStateException if the Telephony process is not currently available. */ @RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION) @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) @@ -1295,11 +1300,12 @@ public final class SatelliteManager { }; telephony.requestIsSatelliteProvisioned(mSubId, receiver); } else { - throw new IllegalStateException("telephony service is null."); + executor.execute(() -> Binder.withCleanCallingIdentity(() -> callback.onError( + new SatelliteException(SATELLITE_RESULT_ILLEGAL_STATE)))); } } catch (RemoteException ex) { loge("requestIsSatelliteProvisioned() RemoteException: " + ex); - ex.rethrowFromSystemServer(); + ex.rethrowAsRuntimeException(); } } @@ -1340,7 +1346,7 @@ public final class SatelliteManager { } } catch (RemoteException ex) { loge("registerForSatelliteModemStateChanged() RemoteException:" + ex); - ex.rethrowFromSystemServer(); + ex.rethrowAsRuntimeException(); } return SATELLITE_RESULT_REQUEST_FAILED; } @@ -1376,7 +1382,7 @@ public final class SatelliteManager { } } catch (RemoteException ex) { loge("unregisterForSatelliteModemStateChanged() RemoteException:" + ex); - ex.rethrowFromSystemServer(); + ex.rethrowAsRuntimeException(); } } @@ -1436,7 +1442,7 @@ public final class SatelliteManager { } } catch (RemoteException ex) { loge("registerForSatelliteDatagram() RemoteException:" + ex); - ex.rethrowFromSystemServer(); + ex.rethrowAsRuntimeException(); } return SATELLITE_RESULT_REQUEST_FAILED; } @@ -1471,7 +1477,7 @@ public final class SatelliteManager { } } catch (RemoteException ex) { loge("unregisterForSatelliteDatagram() RemoteException:" + ex); - ex.rethrowFromSystemServer(); + ex.rethrowAsRuntimeException(); } } @@ -1488,7 +1494,6 @@ public final class SatelliteManager { * @param resultListener Listener for the {@link SatelliteResult} result of the operation. * * @throws SecurityException if the caller doesn't have required permission. - * @throws IllegalStateException if the Telephony process is not currently available. */ @RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION) @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) @@ -1509,11 +1514,12 @@ public final class SatelliteManager { }; telephony.pollPendingSatelliteDatagrams(mSubId, internalCallback); } else { - throw new IllegalStateException("telephony service is null."); + executor.execute(() -> Binder.withCleanCallingIdentity( + () -> resultListener.accept(SATELLITE_RESULT_ILLEGAL_STATE))); } } catch (RemoteException ex) { loge("pollPendingSatelliteDatagrams() RemoteException:" + ex); - ex.rethrowFromSystemServer(); + ex.rethrowAsRuntimeException(); } } @@ -1541,7 +1547,6 @@ public final class SatelliteManager { * @param resultListener Listener for the {@link SatelliteResult} result of the operation. * * @throws SecurityException if the caller doesn't have required permission. - * @throws IllegalStateException if the Telephony process is not currently available. */ @RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION) @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) @@ -1566,11 +1571,12 @@ public final class SatelliteManager { telephony.sendSatelliteDatagram(mSubId, datagramType, datagram, needFullScreenPointingUI, internalCallback); } else { - throw new IllegalStateException("telephony service is null."); + executor.execute(() -> Binder.withCleanCallingIdentity( + () -> resultListener.accept(SATELLITE_RESULT_ILLEGAL_STATE))); } } catch (RemoteException ex) { loge("sendSatelliteDatagram() RemoteException:" + ex); - ex.rethrowFromSystemServer(); + ex.rethrowAsRuntimeException(); } } @@ -1587,7 +1593,6 @@ public final class SatelliteManager { * will return a {@link SatelliteException} with the {@link SatelliteResult}. * * @throws SecurityException if the caller doesn't have required permission. - * @throws IllegalStateException if the Telephony process is not currently available. */ @RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION) @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) @@ -1624,12 +1629,13 @@ public final class SatelliteManager { telephony.requestIsSatelliteCommunicationAllowedForCurrentLocation(mSubId, receiver); } else { - throw new IllegalStateException("telephony service is null."); + executor.execute(() -> Binder.withCleanCallingIdentity(() -> callback.onError( + new SatelliteException(SATELLITE_RESULT_ILLEGAL_STATE)))); } } catch (RemoteException ex) { loge("requestIsSatelliteCommunicationAllowedForCurrentLocation() RemoteException: " + ex); - ex.rethrowFromSystemServer(); + ex.rethrowAsRuntimeException(); } } @@ -1645,7 +1651,6 @@ public final class SatelliteManager { * will return a {@link SatelliteException} with the {@link SatelliteResult}. * * @throws SecurityException if the caller doesn't have required permission. - * @throws IllegalStateException if the Telephony process is not currently available. */ @RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION) @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) @@ -1681,11 +1686,12 @@ public final class SatelliteManager { }; telephony.requestTimeForNextSatelliteVisibility(mSubId, receiver); } else { - throw new IllegalStateException("telephony service is null."); + executor.execute(() -> Binder.withCleanCallingIdentity(() -> callback.onError( + new SatelliteException(SATELLITE_RESULT_ILLEGAL_STATE)))); } } catch (RemoteException ex) { loge("requestTimeForNextSatelliteVisibility() RemoteException: " + ex); - ex.rethrowFromSystemServer(); + ex.rethrowAsRuntimeException(); } } @@ -1713,7 +1719,7 @@ public final class SatelliteManager { } } catch (RemoteException ex) { loge("informDeviceAlignedToSatellite() RemoteException:" + ex); - ex.rethrowFromSystemServer(); + ex.rethrowAsRuntimeException(); } } @@ -1727,7 +1733,7 @@ public final class SatelliteManager { * <ul> * <li>Users want to enable it.</li> * <li>There is no satellite communication restriction, which is added by - * {@link #addSatelliteAttachRestrictionForCarrier(int, Executor, Consumer)}</li> + * {@link #addSatelliteAttachRestrictionForCarrier(int, int, Executor, Consumer)}</li> * <li>The carrier config {@link * android.telephony.CarrierConfigManager#KEY_SATELLITE_ATTACH_SUPPORTED_BOOL} is set to * {@code true}.</li> @@ -1739,7 +1745,6 @@ public final class SatelliteManager { * @param resultListener Listener for the {@link SatelliteResult} result of the operation. * * @throws SecurityException if the caller doesn't have required permission. - * @throws IllegalStateException if the Telephony process is not currently available. * @throws IllegalArgumentException if the subscription is invalid. */ @RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION) @@ -1799,7 +1804,6 @@ public final class SatelliteManager { * @param resultListener Listener for the {@link SatelliteResult} result of the operation. * * @throws SecurityException if the caller doesn't have required permission. - * @throws IllegalStateException if the Telephony process is not currently available. * @throws IllegalArgumentException if the subscription is invalid. */ @RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION) @@ -1824,11 +1828,12 @@ public final class SatelliteManager { }; telephony.addSatelliteAttachRestrictionForCarrier(subId, reason, errorCallback); } else { - throw new IllegalStateException("telephony service is null."); + executor.execute(() -> Binder.withCleanCallingIdentity( + () -> resultListener.accept(SATELLITE_RESULT_ILLEGAL_STATE))); } } catch (RemoteException ex) { loge("addSatelliteAttachRestrictionForCarrier() RemoteException:" + ex); - ex.rethrowFromSystemServer(); + ex.rethrowAsRuntimeException(); } } @@ -1842,7 +1847,6 @@ public final class SatelliteManager { * @param resultListener Listener for the {@link SatelliteResult} result of the operation. * * @throws SecurityException if the caller doesn't have required permission. - * @throws IllegalStateException if the Telephony process is not currently available. * @throws IllegalArgumentException if the subscription is invalid. */ @RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION) @@ -1867,17 +1871,18 @@ public final class SatelliteManager { }; telephony.removeSatelliteAttachRestrictionForCarrier(subId, reason, errorCallback); } else { - throw new IllegalStateException("telephony service is null."); + executor.execute(() -> Binder.withCleanCallingIdentity( + () -> resultListener.accept(SATELLITE_RESULT_ILLEGAL_STATE))); } } catch (RemoteException ex) { loge("removeSatelliteAttachRestrictionForCarrier() RemoteException:" + ex); - ex.rethrowFromSystemServer(); + ex.rethrowAsRuntimeException(); } } /** * Get reasons for disallowing satellite attach, as requested by - * {@link #addSatelliteAttachRestrictionForCarrier(int, Executor, Consumer)} + * {@link #addSatelliteAttachRestrictionForCarrier(int, int, Executor, Consumer)} * * @param subId The subscription ID of the carrier. * @return Set of reasons for disallowing satellite communication. @@ -1910,7 +1915,7 @@ public final class SatelliteManager { } } catch (RemoteException ex) { loge("getSatelliteAttachRestrictionReasonsForCarrier() RemoteException: " + ex); - ex.rethrowFromSystemServer(); + ex.rethrowAsRuntimeException(); } return new HashSet<>(); } @@ -1932,11 +1937,12 @@ public final class SatelliteManager { * The {@link NtnSignalStrength#NTN_SIGNAL_STRENGTH_NONE} will be returned if there is no * signal strength data available. * If the request is not successful, {@link OutcomeReceiver#onError(Throwable)} will return a - * {@link SatelliteException} with the {@link SatelliteResult}. + * {@link SatelliteException} with the {@link SatelliteResult}, or return a + * {@link IllegalStateException} if the Telephony process is not currently available or + * satellite is not supported, or return a {@link RuntimeException} when remote procedure call + * has failed. * * @throws SecurityException if the caller doesn't have required permission. - * @throws IllegalStateException if the Telephony process is not currently available or - * satellite is not supported. */ @RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION) @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) @@ -1972,11 +1978,12 @@ public final class SatelliteManager { }; telephony.requestNtnSignalStrength(mSubId, receiver); } else { - throw new IllegalStateException("Telephony service is null."); + executor.execute(() -> Binder.withCleanCallingIdentity(() -> callback.onError( + new SatelliteException(SATELLITE_RESULT_ILLEGAL_STATE)))); } } catch (RemoteException ex) { loge("requestNtnSignalStrength() RemoteException: " + ex); - ex.rethrowFromSystemServer(); + ex.rethrowAsRuntimeException(); } } @@ -1997,12 +2004,11 @@ public final class SatelliteManager { * * @throws SecurityException if the caller doesn't have required permission. * @throws IllegalStateException if the Telephony process is not currently available. - * @throws SatelliteException if the callback registration operation fails. */ @RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION) @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) public void registerForNtnSignalStrengthChanged(@NonNull @CallbackExecutor Executor executor, - @NonNull NtnSignalStrengthCallback callback) throws SatelliteException { + @NonNull NtnSignalStrengthCallback callback) { Objects.requireNonNull(executor); Objects.requireNonNull(callback); @@ -2024,12 +2030,9 @@ public final class SatelliteManager { } else { throw new IllegalStateException("Telephony service is null."); } - } catch (ServiceSpecificException ex) { - logd("registerForNtnSignalStrengthChanged() registration fails: " + ex.errorCode); - throw new SatelliteException(ex.errorCode); } catch (RemoteException ex) { loge("registerForNtnSignalStrengthChanged() RemoteException: " + ex); - ex.rethrowFromSystemServer(); + ex.rethrowAsRuntimeException(); } } @@ -2072,7 +2075,7 @@ public final class SatelliteManager { } } catch (RemoteException ex) { loge("unregisterForNtnSignalStrengthChanged() RemoteException: " + ex); - ex.rethrowFromSystemServer(); + ex.rethrowAsRuntimeException(); } } @@ -2113,7 +2116,7 @@ public final class SatelliteManager { } } catch (RemoteException ex) { loge("registerForSatelliteCapabilitiesChanged() RemoteException: " + ex); - ex.rethrowFromSystemServer(); + ex.rethrowAsRuntimeException(); } return SATELLITE_RESULT_REQUEST_FAILED; } @@ -2149,7 +2152,7 @@ public final class SatelliteManager { } } catch (RemoteException ex) { loge("unregisterForSatelliteCapabilitiesChanged() RemoteException: " + ex); - ex.rethrowFromSystemServer(); + ex.rethrowAsRuntimeException(); } } @@ -2177,7 +2180,7 @@ public final class SatelliteManager { } } catch (RemoteException ex) { loge("getAllSatellitePlmnsForCarrier() RemoteException: " + ex); - ex.rethrowFromSystemServer(); + ex.rethrowAsRuntimeException(); } return new ArrayList<>(); } diff --git a/telephony/java/android/telephony/satellite/stub/INtnSignalStrengthConsumer.aidl b/telephony/java/android/telephony/satellite/stub/INtnSignalStrengthConsumer.aidl index b7712bd83cf6..655da740012b 100644 --- a/telephony/java/android/telephony/satellite/stub/INtnSignalStrengthConsumer.aidl +++ b/telephony/java/android/telephony/satellite/stub/INtnSignalStrengthConsumer.aidl @@ -16,7 +16,7 @@ package android.telephony.satellite.stub; -import android.telephony.satellite.NtnSignalStrength; +import android.telephony.satellite.stub.NtnSignalStrength; /** * Consumer pattern for a request that receives the signal strength of non-terrestrial network from diff --git a/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt b/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt index 256a4696b763..566e51a9062a 100644 --- a/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt +++ b/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt @@ -24,6 +24,7 @@ import android.hardware.input.InputManager import android.hardware.input.InputManagerGlobal import android.os.test.TestLooper import android.platform.test.annotations.Presubmit +import android.platform.test.flag.junit.SetFlagsRule import android.provider.Settings import android.test.mock.MockContentResolver import android.view.Display @@ -72,6 +73,9 @@ class InputManagerServiceTests { @get:Rule val fakeSettingsProviderRule = FakeSettingsProvider.rule()!! + @get:Rule + val setFlagsRule = SetFlagsRule() + @Mock private lateinit var native: NativeInputManagerService @@ -170,6 +174,8 @@ class InputManagerServiceTests { @Test fun testSetVirtualMousePointerDisplayId() { + setFlagsRule.disableFlags(com.android.input.flags.Flags.FLAG_ENABLE_POINTER_CHOREOGRAPHER) + // Set the virtual mouse pointer displayId, and ensure that the calling thread is blocked // until the native callback happens. var countDownLatch = CountDownLatch(1) @@ -221,6 +227,8 @@ class InputManagerServiceTests { @Test fun testSetVirtualMousePointerDisplayId_unsuccessfulUpdate() { + setFlagsRule.disableFlags(com.android.input.flags.Flags.FLAG_ENABLE_POINTER_CHOREOGRAPHER) + // Set the virtual mouse pointer displayId, and ensure that the calling thread is blocked // until the native callback happens. val countDownLatch = CountDownLatch(1) @@ -246,6 +254,8 @@ class InputManagerServiceTests { @Test fun testSetVirtualMousePointerDisplayId_competingRequests() { + setFlagsRule.disableFlags(com.android.input.flags.Flags.FLAG_ENABLE_POINTER_CHOREOGRAPHER) + val firstRequestSyncLatch = CountDownLatch(1) doAnswer { firstRequestSyncLatch.countDown() @@ -289,6 +299,8 @@ class InputManagerServiceTests { @Test fun onDisplayRemoved_resetAllAdditionalInputProperties() { + setFlagsRule.disableFlags(com.android.input.flags.Flags.FLAG_ENABLE_POINTER_CHOREOGRAPHER) + setVirtualMousePointerDisplayIdAndVerify(10) localService.setPointerIconVisible(false, 10) @@ -313,6 +325,8 @@ class InputManagerServiceTests { @Test fun updateAdditionalInputPropertiesForOverrideDisplay() { + setFlagsRule.disableFlags(com.android.input.flags.Flags.FLAG_ENABLE_POINTER_CHOREOGRAPHER) + setVirtualMousePointerDisplayIdAndVerify(10) localService.setPointerIconVisible(false, 10) @@ -341,6 +355,8 @@ class InputManagerServiceTests { @Test fun setAdditionalInputPropertiesBeforeOverride() { + setFlagsRule.disableFlags(com.android.input.flags.Flags.FLAG_ENABLE_POINTER_CHOREOGRAPHER) + localService.setPointerIconVisible(false, 10) localService.setMousePointerAccelerationEnabled(false, 10) diff --git a/tests/SurfaceControlViewHostTest/src/com/android/test/viewembed/EmbeddedWindowService.java b/tests/SurfaceControlViewHostTest/src/com/android/test/viewembed/EmbeddedWindowService.java index 5aaf30a5b3a7..14230fe4c323 100644 --- a/tests/SurfaceControlViewHostTest/src/com/android/test/viewembed/EmbeddedWindowService.java +++ b/tests/SurfaceControlViewHostTest/src/com/android/test/viewembed/EmbeddedWindowService.java @@ -134,7 +134,7 @@ public class EmbeddedWindowService extends Service { c.drawText("Remote", 250, 250, paint); surface.unlockCanvasAndPost(c); WindowManager wm = getSystemService(WindowManager.class); - mInputToken = wm.registerBatchedSurfaceControlInputReceiver(displayId, hostToken, + wm.registerBatchedSurfaceControlInputReceiver(displayId, hostToken, mSurfaceControl, Choreographer.getInstance(), event -> { Log.d(TAG, "onInputEvent-remote " + event); @@ -147,11 +147,9 @@ public class EmbeddedWindowService extends Service { @Override public void tearDownEmbeddedSurfaceControl() { if (mSurfaceControl != null) { - new SurfaceControl.Transaction().remove(mSurfaceControl); - } - if (mInputToken != null) { WindowManager wm = getSystemService(WindowManager.class); - wm.unregisterSurfaceControlInputReceiver(mInputToken); + wm.unregisterSurfaceControlInputReceiver(mSurfaceControl); + new SurfaceControl.Transaction().remove(mSurfaceControl).apply(); } } } diff --git a/tests/SurfaceControlViewHostTest/src/com/android/test/viewembed/SurfaceInputTestActivity.java b/tests/SurfaceControlViewHostTest/src/com/android/test/viewembed/SurfaceInputTestActivity.java index e5f8f47aeecd..7330ec14011b 100644 --- a/tests/SurfaceControlViewHostTest/src/com/android/test/viewembed/SurfaceInputTestActivity.java +++ b/tests/SurfaceControlViewHostTest/src/com/android/test/viewembed/SurfaceInputTestActivity.java @@ -50,10 +50,11 @@ public class SurfaceInputTestActivity extends Activity { private static final String TAG = "SurfaceInputTestActivity"; private SurfaceView mLocalSurfaceView; private SurfaceView mRemoteSurfaceView; - private IBinder mInputToken; private IAttachEmbeddedWindow mIAttachEmbeddedWindow; private SurfaceControl mParentSurfaceControl; + private SurfaceControl mLocalSurfaceControl; + private final ServiceConnection mConnection = new ServiceConnection() { // Called when the connection with the service is established public void onServiceConnected(ComponentName className, IBinder service) { @@ -112,30 +113,33 @@ public class SurfaceInputTestActivity extends Activity { @Override protected void onDestroy() { super.onDestroy(); - getWindowManager().unregisterSurfaceControlInputReceiver(mInputToken); + if (mLocalSurfaceControl != null) { + getWindowManager().unregisterSurfaceControlInputReceiver(mLocalSurfaceControl); + new SurfaceControl.Transaction().remove(mLocalSurfaceControl).apply(); + } } private void addLocalChildSurfaceControl(AttachedSurfaceControl attachedSurfaceControl) { - SurfaceControl surfaceControl = new SurfaceControl.Builder().setName("LocalSC") + mLocalSurfaceControl = new SurfaceControl.Builder().setName("LocalSC") .setBufferSize(100, 100).build(); - attachedSurfaceControl.buildReparentTransaction(surfaceControl) - .setVisibility(surfaceControl, true) - .setCrop(surfaceControl, new Rect(0, 0, 100, 100)) - .setPosition(surfaceControl, 250, 1000) - .setLayer(surfaceControl, 1).apply(); + attachedSurfaceControl.buildReparentTransaction(mLocalSurfaceControl) + .setVisibility(mLocalSurfaceControl, true) + .setCrop(mLocalSurfaceControl, new Rect(0, 0, 100, 100)) + .setPosition(mLocalSurfaceControl, 250, 1000) + .setLayer(mLocalSurfaceControl, 1).apply(); Paint paint = new Paint(); paint.setColor(Color.WHITE); paint.setTextSize(20); - Surface surface = new Surface(surfaceControl); + Surface surface = new Surface(mLocalSurfaceControl); Canvas c = surface.lockCanvas(null); c.drawColor(Color.GREEN); c.drawText("Local SC", 0, 0, paint); surface.unlockCanvasAndPost(c); WindowManager wm = getSystemService(WindowManager.class); - mInputToken = wm.registerBatchedSurfaceControlInputReceiver(getDisplayId(), - attachedSurfaceControl.getHostToken(), surfaceControl, + wm.registerBatchedSurfaceControlInputReceiver(getDisplayId(), + attachedSurfaceControl.getHostToken(), mLocalSurfaceControl, Choreographer.getInstance(), event -> { Log.d(TAG, "onInputEvent-sc " + event); return false; @@ -143,8 +147,6 @@ public class SurfaceInputTestActivity extends Activity { } private final SurfaceHolder.Callback mLocalSurfaceViewCallback = new SurfaceHolder.Callback() { - private IBinder mInputToken; - @Override public void surfaceCreated(@NonNull SurfaceHolder holder) { Paint paint = new Paint(); @@ -157,7 +159,7 @@ public class SurfaceInputTestActivity extends Activity { holder.unlockCanvasAndPost(c); WindowManager wm = getSystemService(WindowManager.class); - mInputToken = wm.registerBatchedSurfaceControlInputReceiver(getDisplayId(), + wm.registerBatchedSurfaceControlInputReceiver(getDisplayId(), mLocalSurfaceView.getHostToken(), mLocalSurfaceView.getSurfaceControl(), Choreographer.getInstance(), event -> { Log.d(TAG, "onInputEvent-local " + event); @@ -173,9 +175,8 @@ public class SurfaceInputTestActivity extends Activity { @Override public void surfaceDestroyed(@NonNull SurfaceHolder holder) { - if (mInputToken != null) { - getWindowManager().unregisterSurfaceControlInputReceiver(mInputToken); - } + getWindowManager().unregisterSurfaceControlInputReceiver( + mLocalSurfaceView.getSurfaceControl()); } }; diff --git a/tools/aapt2/link/ManifestFixer.cpp b/tools/aapt2/link/ManifestFixer.cpp index 0b16e2c7efe4..d03f97e28156 100644 --- a/tools/aapt2/link/ManifestFixer.cpp +++ b/tools/aapt2/link/ManifestFixer.cpp @@ -415,6 +415,8 @@ bool ManifestFixer::BuildRules(xml::XmlActionExecutor* executor, android::IDiagn intent_filter_action["action"].Action(RequiredNameIsNotEmpty); intent_filter_action["category"].Action(RequiredNameIsNotEmpty); intent_filter_action["data"]; + intent_filter_action["uri-relative-filter-group"]; + intent_filter_action["uri-relative-filter-group"]["data"]; // Common <meta-data> actions. xml::XmlNodeAction meta_data_action; |