Merge "Skip OomAdjuster update in bindService for self-bindings" into main
diff --git a/AconfigFlags.bp b/AconfigFlags.bp
index f5bf437..98b62b3 100644
--- a/AconfigFlags.bp
+++ b/AconfigFlags.bp
@@ -26,6 +26,7 @@
":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 @@
":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 @@
"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 @@
"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 @@
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 @@
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 @@
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 9c56733..e12f74f 100644
--- a/Android.bp
+++ b/Android.bp
@@ -95,6 +95,7 @@
":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 0ea2daf..bc84708 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 @@
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 aa95dfd..44e8a67 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 @@
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 @@
}
}
+ 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 ecc5112..0b5345f 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 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 @@
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 @@
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 e73b434..788e824 100644
--- a/apex/jobscheduler/framework/aconfig/job.aconfig
+++ b/apex/jobscheduler/framework/aconfig/job.aconfig
@@ -13,3 +13,10 @@
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/DeviceIdleController.java b/apex/jobscheduler/service/java/com/android/server/DeviceIdleController.java
index 31214cb..696c317 100644
--- a/apex/jobscheduler/service/java/com/android/server/DeviceIdleController.java
+++ b/apex/jobscheduler/service/java/com/android/server/DeviceIdleController.java
@@ -3919,6 +3919,7 @@
if (locationManager != null
&& locationManager.getProvider(LocationManager.FUSED_PROVIDER)
!= null) {
+ mHasFusedLocation = true;
locationManager.requestLocationUpdates(LocationManager.FUSED_PROVIDER,
mLocationRequest,
AppSchedulingModuleThread.getExecutor(),
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 7a92cca..fc193d8 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 @@
/* 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 @@
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 @@
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 6f2393a..0cf6a7a 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 @@
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 fe55e27..8ab7d2f 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 @@
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 @@
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 14cce19..6883d18 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 @@
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 d39863c..a4df5d8 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.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.Objects;
import java.util.Random;
import java.util.function.Predicate;
+import java.util.regex.Pattern;
/**
* Uniquely identifies a job internally.
@@ -203,6 +205,17 @@
// 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 @@
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 @@
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 907916a..8d8a82b 100644
--- a/core/api/Android.bp
+++ b/core/api/Android.bp
@@ -96,21 +96,3 @@
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 c92b4ff..66feebc 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -277,6 +277,7 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
}
@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 @@
@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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 ff2279c..017de17 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -129,6 +129,7 @@
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 @@
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 @@
}
@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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 b8b98a3..7a3d320 100644
--- a/core/api/test-current.txt
+++ b/core/api/test-current.txt
@@ -3646,6 +3646,7 @@
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/AppOpsManager.java b/core/java/android/app/AppOpsManager.java
index ccd8456..00c4b0f 100644
--- a/core/java/android/app/AppOpsManager.java
+++ b/core/java/android/app/AppOpsManager.java
@@ -1548,9 +1548,16 @@
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 @@
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 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 @@
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 @@
// 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 0000000..664fceb
--- /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/Notification.java b/core/java/android/app/Notification.java
index ed0cfbe..a81ad3c 100644
--- a/core/java/android/app/Notification.java
+++ b/core/java/android/app/Notification.java
@@ -5487,6 +5487,15 @@
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 @@
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 @@
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 0760d4d..3b5bba2 100644
--- a/core/java/android/app/OWNERS
+++ b/core/java/android/app/OWNERS
@@ -90,8 +90,8 @@
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 350cf3d..06a0f5c 100644
--- a/core/java/android/companion/virtual/camera/VirtualCameraConfig.java
+++ b/core/java/android/companion/virtual/camera/VirtualCameraConfig.java
@@ -196,13 +196,12 @@
* <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 @@
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 ad3acd7..79af65a 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 @@
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 @@
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 @@
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 @@
}
/**
+ * 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 @@
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 @@
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 @@
}
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 @@
}
} 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 @@
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 @@
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 @@
} 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 @@
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 0000000..9866cd0
--- /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 0000000..72c396a
--- /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 4724e86..8744eae 100644
--- a/core/java/android/content/pm/PackageManager.java
+++ b/core/java/android/content/pm/PackageManager.java
@@ -128,6 +128,7 @@
* <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 @@
* application info.
* @hide
*/
+ @android.ravenwood.annotation.RavenwoodKeepWholeClass
public static class Flags {
final long mValue;
protected Flags(long value) {
@@ -5506,6 +5508,7 @@
* 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 @@
/**
* 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 @@
/**
* 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 @@
/**
* 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 8fd78bd..3e9f260 100644
--- a/core/java/android/content/pm/UserInfo.java
+++ b/core/java/android/content/pm/UserInfo.java
@@ -52,6 +52,7 @@
* @hide
*/
@TestApi
+@android.ravenwood.annotation.RavenwoodKeepWholeClass
public class UserInfo implements Parcelable {
/**
@@ -438,6 +439,7 @@
/**
* @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 @@
* @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 @@
* @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 269c6c2..1d0e2db 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 @@
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 @@
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 @@
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 @@
@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 @@
setShowInQuietMode(orig.getShowInQuietMode());
setShowInSharingSurfaces(orig.getShowInSharingSurfaces());
setCrossProfileContentSharingStrategy(orig.getCrossProfileContentSharingStrategy());
+ if (android.multiuser.Flags.supportHidingProfiles()) {
+ setProfileApiVisibility(orig.getProfileApiVisibility());
+ }
}
/**
@@ -951,9 +994,31 @@
}
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 @@
+ ", mDeleteAppWithParent=" + getDeleteAppWithParent()
+ ", mAlwaysVisible=" + getAlwaysVisible()
+ ", mCrossProfileContentSharingStrategy=" + getCrossProfileContentSharingStrategy()
+ + profileApiVisibility
+ "}";
}
@@ -1010,6 +1076,9 @@
pw.println(prefix + " mAlwaysVisible=" + getAlwaysVisible());
pw.println(prefix + " mCrossProfileContentSharingStrategy="
+ getCrossProfileContentSharingStrategy());
+ if (android.multiuser.Flags.supportHidingProfiles()) {
+ pw.println(prefix + " mProfileApiVisibility=" + getProfileApiVisibility());
+ }
}
/**
@@ -1093,6 +1162,12 @@
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 @@
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 @@
dest.writeBoolean(mDeleteAppWithParent);
dest.writeBoolean(mAlwaysVisible);
dest.writeInt(mCrossProfileContentSharingStrategy);
+ dest.writeInt(mProfileApiVisibility);
}
/**
@@ -1225,6 +1307,7 @@
mDeleteAppWithParent = source.readBoolean();
mAlwaysVisible = source.readBoolean();
mCrossProfileContentSharingStrategy = source.readInt();
+ mProfileApiVisibility = source.readInt();
}
@Override
@@ -1274,6 +1357,7 @@
private boolean mAlwaysVisible = false;
private @CrossProfileContentSharingStrategy int mCrossProfileContentSharingStrategy =
CROSS_PROFILE_CONTENT_SHARING_NO_DELEGATION;
+ private @ProfileApiVisibility int mProfileApiVisibility = 0;
/**
* @hide
@@ -1428,6 +1512,17 @@
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 @@
mAllowStoppingUserWithDelayedLocking,
mDeleteAppWithParent,
mAlwaysVisible,
- mCrossProfileContentSharingStrategy);
+ mCrossProfileContentSharingStrategy,
+ mProfileApiVisibility);
}
} // end Builder
@@ -1473,7 +1569,8 @@
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 @@
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 903875b..caff457 100644
--- a/core/java/android/content/pm/flags.aconfig
+++ b/core/java/android/content/pm/flags.aconfig
@@ -110,6 +110,14 @@
}
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 @@
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 9644d80..efb8607 100644
--- a/core/java/android/content/pm/multiuser.aconfig
+++ b/core/java/android/content/pm/multiuser.aconfig
@@ -85,6 +85,7 @@
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 @@
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 cbe4c62..625d7cb 100644
--- a/core/java/android/content/res/FontScaleConverterFactory.java
+++ b/core/java/android/content/res/FontScaleConverterFactory.java
@@ -58,6 +58,16 @@
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 b1a382c..9bd0871 100644
--- a/core/java/android/credentials/ui/AuthenticationEntry.java
+++ b/core/java/android/credentials/ui/AuthenticationEntry.java
@@ -34,15 +34,24 @@
/**
* 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 @@
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 @@
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 @@
}
/**
- * 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 @@
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 e8cf5ab..e985a46 100644
--- a/core/java/android/credentials/ui/BaseDialogResult.java
+++ b/core/java/android/credentials/ui/BaseDialogResult.java
@@ -24,8 +24,6 @@
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 @@
/**
* 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 @@
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 static final int RESULT_CODE_DATA_PARSING_FAILURE = 3;
@Nullable
+ @Deprecated
private final IBinder mRequestToken;
public BaseDialogResult(@Nullable IBinder requestToken) {
@@ -94,6 +94,7 @@
/** Returns the unique identifier for the request that launched the operation. */
@Nullable
+ @Deprecated
public IBinder getRequestToken() {
return mRequestToken;
}
@@ -115,14 +116,14 @@
public static final @NonNull Creator<BaseDialogResult> CREATOR =
new Creator<BaseDialogResult>() {
- @Override
- public BaseDialogResult createFromParcel(@NonNull Parcel in) {
- return new BaseDialogResult(in);
- }
+ @Override
+ public BaseDialogResult createFromParcel(@NonNull Parcel in) {
+ return new BaseDialogResult(in);
+ }
- @Override
- public BaseDialogResult[] newArray(int size) {
- return new BaseDialogResult[size];
- }
- };
+ @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 d4c249e..712424ce 100644
--- a/core/java/android/credentials/ui/CancelUiRequest.java
+++ b/core/java/android/credentials/ui/CancelUiRequest.java
@@ -24,7 +24,7 @@
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 @@
/**
* 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 @@
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 @@
return mShouldShowCancellationUi;
}
+ /** Constructs a {@link CancelUiRequest}. */
public CancelUiRequest(@NonNull IBinder token, boolean shouldShowCancellationUi,
@NonNull String appPackageName) {
mToken = token;
@@ -91,7 +99,8 @@
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 37f850b..68f28e7 100644
--- a/core/java/android/credentials/ui/Constants.java
+++ b/core/java/android/credentials/ui/Constants.java
@@ -36,7 +36,5 @@
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 2508d8e..d7a4f5b 100644
--- a/core/java/android/credentials/ui/CreateCredentialProviderData.java
+++ b/core/java/android/credentials/ui/CreateCredentialProviderData.java
@@ -47,6 +47,17 @@
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 0000000..41ca852
--- /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 c266fd5..8bccdc9 100644
--- a/core/java/android/credentials/ui/DisabledProviderData.java
+++ b/core/java/android/credentials/ui/DisabledProviderData.java
@@ -34,6 +34,16 @@
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 0000000..7ce6368
--- /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 55f2a3e..8469447 100644
--- a/core/java/android/credentials/ui/Entry.java
+++ b/core/java/android/credentials/ui/Entry.java
@@ -35,10 +35,14 @@
*/
@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 @@
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 @@
}
/**
- * 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 @@
/**
* 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 @@
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 0000000..abd5a92
--- /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 0000000..ec58417
--- /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 181475c..481419b 100644
--- a/core/java/android/credentials/ui/GetCredentialProviderData.java
+++ b/core/java/android/credentials/ui/GetCredentialProviderData.java
@@ -55,6 +55,17 @@
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 @@
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 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 @@
*/
@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 0000000..bac7147
--- /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 49321d5..5e1e0ef 100644
--- a/core/java/android/credentials/ui/IntentFactory.java
+++ b/core/java/android/credentials/ui/IntentFactory.java
@@ -113,25 +113,6 @@
}
/**
- * 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 0000000..c5f34c1
--- /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 53f1864..0000000
--- 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 47936c4..11cc21f9 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 @@
@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 @@
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 4fedc83..f651584 100644
--- a/core/java/android/credentials/ui/RequestInfo.java
+++ b/core/java/android/credentials/ui/RequestInfo.java
@@ -48,17 +48,24 @@
@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 0000000..7b9d5e8
--- /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/core/java/android/credentials/ui/UiResult.java b/core/java/android/credentials/ui/UiResult.java
new file mode 100644
index 0000000..692584d
--- /dev/null
+++ b/core/java/android/credentials/ui/UiResult.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.credentials.ui;
+
+/**
+ * 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 0000000..431dc63
--- /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 0047b7d..ce0f9f59 100644
--- a/core/java/android/hardware/HardwareBuffer.java
+++ b/core/java/android/hardware/HardwareBuffer.java
@@ -115,14 +115,16 @@
@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 c0424db..bdaf9d7 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 @@
* @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 @@
* @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 @@
/**
* 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 c5e5a80..25e5cca 100644
--- a/core/java/android/hardware/biometrics/PromptContentItemBulletedText.java
+++ b/core/java/android/hardware/biometrics/PromptContentItemBulletedText.java
@@ -28,14 +28,14 @@
*/
@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 @@
* @hide
*/
@NonNull
- public CharSequence getText() {
+ public String getText() {
return mText;
}
@@ -60,7 +60,7 @@
*/
@Override
public void writeToParcel(@NonNull Parcel dest, int flags) {
- dest.writeCharSequence(mText);
+ dest.writeString(mText);
}
/**
@@ -70,7 +70,7 @@
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 6434c59..7919256 100644
--- a/core/java/android/hardware/biometrics/PromptContentItemPlainText.java
+++ b/core/java/android/hardware/biometrics/PromptContentItemPlainText.java
@@ -28,14 +28,14 @@
*/
@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 @@
* @hide
*/
@NonNull
- public CharSequence getText() {
+ public String getText() {
return mText;
}
@@ -60,7 +60,7 @@
*/
@Override
public void writeToParcel(@NonNull Parcel dest, int flags) {
- dest.writeCharSequence(mText);
+ dest.writeString(mText);
}
/**
@@ -70,7 +70,7 @@
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 d788b37..0f9cadc 100644
--- a/core/java/android/hardware/biometrics/PromptInfo.java
+++ b/core/java/android/hardware/biometrics/PromptInfo.java
@@ -166,9 +166,9 @@
}
/**
- * 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 f3e6290..38d32dc 100644
--- a/core/java/android/hardware/biometrics/PromptVerticalListContentView.java
+++ b/core/java/android/hardware/biometrics/PromptVerticalListContentView.java
@@ -52,11 +52,11 @@
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 @@
mContentList = in.readArrayList(
PromptContentItemParcelable.class.getClassLoader(),
PromptContentItemParcelable.class);
- mDescription = in.readCharSequence();
+ mDescription = in.readString();
}
/**
@@ -84,12 +84,12 @@
/**
* 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 @@
@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 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 @@
* @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 d939532..fdbd319 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 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 @@
}
/**
+ * 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 362fe78..0ed6569 100644
--- a/core/java/android/hardware/input/input_framework.aconfig
+++ b/core/java/android/hardware/input/input_framework.aconfig
@@ -29,4 +29,11 @@
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 0000000..55c307b
--- /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 0000000..6e72f8e
--- /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 b5425b4..79a2c59 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 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 02704f5..236194d 100644
--- a/core/java/android/os/PersistableBundle.java
+++ b/core/java/android/os/PersistableBundle.java
@@ -294,6 +294,43 @@
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 dd0436c..1f3a162 100644
--- a/core/java/android/os/Process.java
+++ b/core/java/android/os/Process.java
@@ -1589,7 +1589,15 @@
@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 c280d13..d6df8d9 100644
--- a/core/java/android/os/UserManager.java
+++ b/core/java/android/os/UserManager.java
@@ -88,6 +88,7 @@
* 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 @@
/** 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 @@
"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 @@
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 @@
* {@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 @@
* 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 @@
* {@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 @@
* 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 @@
* 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 @@
* {@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 @@
*
* @hide
*/
+ @android.ravenwood.annotation.RavenwoodKeep
public static boolean isUserTypePrivateProfile(@Nullable String userType) {
return USER_TYPE_PROFILE_PRIVATE.equals(userType);
}
@@ -4423,15 +4471,15 @@
* 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 ecd6f22..11edcaf 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -444,6 +444,18 @@
"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 @@
/** @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 @@
}
}
+ 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 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 @@
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 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 @@
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 5ad2502..298bdb8 100644
--- a/core/java/android/service/autofill/AutofillService.java
+++ b/core/java/android/service/autofill/AutofillService.java
@@ -622,6 +622,15 @@
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 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 0000000..a39f011
--- /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 0000000..79681e2
--- /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 0000000..d2d7556
--- /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 0000000..98ac6f6
--- /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 0000000..5da4f63
--- /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 5d58120..98dda10 100644
--- a/core/java/android/service/autofill/FillEventHistory.java
+++ b/core/java/android/service/autofill/FillEventHistory.java
@@ -309,12 +309,19 @@
/** 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 @@
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 7ea74d3..09ec933 100644
--- a/core/java/android/service/autofill/FillResponse.java
+++ b/core/java/android/service/autofill/FillResponse.java
@@ -28,6 +28,7 @@
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 @@
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 @@
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 @@
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 @@
mShowFillDialogIcon = showFillDialogIcon;
mShowSaveDialogIcon = showSaveDialogIcon;
mDetectedFieldTypes = detectedFieldTypes;
+ mDialogPendingIntent = dialogPendingIntent;
}
private FillResponse(@NonNull Builder builder) {
@@ -219,6 +223,7 @@
mShowFillDialogIcon = builder.mShowFillDialogIcon;
mShowSaveDialogIcon = builder.mShowSaveDialogIcon;
mDetectedFieldTypes = builder.mDetectedFieldTypes;
+ mDialogPendingIntent = builder.mDialogPendingIntent;
}
/** @hide */
@@ -399,6 +404,7 @@
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 @@
}
/**
+ * 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 @@
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 @@
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 @@
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 03ead32..2c2feae 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 @@
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 0000000..9dfc294
--- /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 9895551..9d19ef6 100644
--- a/core/java/android/service/notification/ZenModeConfig.java
+++ b/core/java/android/service/notification/ZenModeConfig.java
@@ -1053,7 +1053,7 @@
out);
if (Flags.modesApi()) {
- writeZenPolicyState(ALLOW_ATT_CHANNELS, policy.getPriorityChannels(), out);
+ writeZenPolicyState(ALLOW_ATT_CHANNELS, policy.getPriorityChannelsAllowed(), out);
}
}
@@ -1381,7 +1381,7 @@
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 d8318a6..786d768 100644
--- a/core/java/android/service/notification/ZenPolicy.java
+++ b/core/java/android/service/notification/ZenPolicy.java
@@ -570,7 +570,7 @@
* 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 @@
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 92646b4..d27bef9 100644
--- a/core/java/android/util/Singleton.java
+++ b/core/java/android/util/Singleton.java
@@ -25,6 +25,7 @@
*
* @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 f28574e..27c509a 100644
--- a/core/java/android/view/AttachedSurfaceControl.java
+++ b/core/java/android/view/AttachedSurfaceControl.java
@@ -197,9 +197,28 @@
* 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 1908c64c..fbadef3 100644
--- a/core/java/android/view/Display.java
+++ b/core/java/android/view/Display.java
@@ -2079,6 +2079,7 @@
*
* @see Display#getSupportedModes()
*/
+ @android.ravenwood.annotation.RavenwoodKeepWholeClass
public static final class Mode implements Parcelable {
/**
* @hide
@@ -2467,6 +2468,7 @@
* <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 981911e..5654bc1 100644
--- a/core/java/android/view/DisplayInfo.java
+++ b/core/java/android/view/DisplayInfo.java
@@ -51,6 +51,7 @@
* 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 1d81be1..d2c25cd 100644
--- a/core/java/android/view/IWindowSession.aidl
+++ b/core/java/android/view/IWindowSession.aidl
@@ -235,7 +235,7 @@
*/
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 86ab213..bc33d5e 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 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 ba7874e..a1f44e4 100644
--- a/core/java/android/view/Surface.java
+++ b/core/java/android/view/Surface.java
@@ -1256,13 +1256,13 @@
}
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 42355bb..427d053 100644
--- a/core/java/android/view/WindowManager.java
+++ b/core/java/android/view/WindowManager.java
@@ -6022,8 +6022,8 @@
* 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 @@
* @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 @@
* 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 @@
* @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 @@
* {@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 8d40f9a..c49fce5 100644
--- a/core/java/android/view/WindowManagerGlobal.java
+++ b/core/java/android/view/WindowManagerGlobal.java
@@ -38,10 +38,12 @@
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.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 @@
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 @@
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 @@
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 @@
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 @@
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 aaf5fcc..41d181c 100644
--- a/core/java/android/view/WindowManagerImpl.java
+++ b/core/java/android/view/WindowManagerImpl.java
@@ -523,26 +523,30 @@
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 b95e459..c4d18c6 100644
--- a/core/java/android/view/WindowlessWindowManager.java
+++ b/core/java/android/view/WindowlessWindowManager.java
@@ -534,9 +534,8 @@
}
@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/autofill/AutofillManager.java b/core/java/android/view/autofill/AutofillManager.java
index dbeffc8..559ccfea7 100644
--- a/core/java/android/view/autofill/AutofillManager.java
+++ b/core/java/android/view/autofill/AutofillManager.java
@@ -298,6 +298,15 @@
"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 37c6f5b..898947a 100644
--- a/core/java/android/view/autofill/OWNERS
+++ b/core/java/android/view/autofill/OWNERS
@@ -4,6 +4,7 @@
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 befb002..5446428 100644
--- a/core/java/android/window/ScreenCapture.java
+++ b/core/java/android/window/ScreenCapture.java
@@ -30,6 +30,8 @@
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 @@
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 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 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 @@
* 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 86804c6..65075ae 100644
--- a/core/java/android/window/WindowOnBackInvokedDispatcher.java
+++ b/core/java/android/window/WindowOnBackInvokedDispatcher.java
@@ -226,9 +226,6 @@
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 @@
return WindowOnBackInvokedDispatcher
.isOnBackInvokedCallbackEnabled(activityInfo, applicationInfo,
- () -> originalContext.obtainStyledAttributes(
- new int[] {android.R.attr.windowSwipeToDismiss}), true);
+ () -> originalContext);
}
@Override
@@ -501,7 +497,7 @@
*/
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 @@
// 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 (context != null) {
+ final TypedArray array = context.obtainStyledAttributes(
+ new int[]{android.R.attr.windowSwipeToDismiss});
+ if (array.getIndexCount() > 0) {
+ windowSwipeToDismiss = array.getBoolean(0, true);
}
- if (recycleTypedArray) {
- windowAttr.recycle();
- }
+ 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 edfbea4..1de77f6 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 @@
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 751c1a8..069affb 100644
--- a/core/java/android/window/flags/window_surfaces.aconfig
+++ b/core/java/android/window/flags/window_surfaces.aconfig
@@ -88,3 +88,11 @@
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 2c5fbd7..f234637 100644
--- a/core/java/android/window/flags/windowing_frontend.aconfig
+++ b/core/java/android/window/flags/windowing_frontend.aconfig
@@ -38,14 +38,6 @@
}
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/app/ConfirmUserCreationActivity.java b/core/java/com/android/internal/app/ConfirmUserCreationActivity.java
index 0a28997..b4e8749 100644
--- a/core/java/com/android/internal/app/ConfirmUserCreationActivity.java
+++ b/core/java/com/android/internal/app/ConfirmUserCreationActivity.java
@@ -116,6 +116,14 @@
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 @@
}
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 37aaa72..0068490 100644
--- a/core/java/com/android/internal/display/BrightnessSynchronizer.java
+++ b/core/java/com/android/internal/display/BrightnessSynchronizer.java
@@ -47,6 +47,7 @@
* (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 @@
* @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 96740c5..7b3565b 100644
--- a/core/java/com/android/internal/jank/Cuj.java
+++ b/core/java/com/android/internal/jank/Cuj.java
@@ -121,10 +121,11 @@
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 @@
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 @@
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 @@
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 f3f16a0..d9cac12 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.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 @@
* @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 @@
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 @@
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 @@
@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 c6683cf..05728ee 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 @@
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 @@
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 @@
}
@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 7a1ac07..efa3697 100644
--- a/core/java/com/android/internal/policy/SystemBarUtils.java
+++ b/core/java/com/android/internal/policy/SystemBarUtils.java
@@ -19,8 +19,9 @@
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 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 42be784..a8d0d37 100644
--- a/core/java/com/android/internal/widget/ConversationLayout.java
+++ b/core/java/com/android/internal/widget/ConversationLayout.java
@@ -105,6 +105,9 @@
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 @@
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 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 @@
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 @@
/**
* Set conversation data
+ *
* @param extras Bundle contains conversation data
*/
@RemotableViewMethod(asyncImpl = "setDataAsync")
@@ -427,6 +440,7 @@
* 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 @@
/**
* enable/disable precomputed text usage
+ *
* @hide
*/
public void setPrecomputedTextEnabled(boolean precomputedTextEnabled) {
@@ -466,7 +481,9 @@
mImageResolver = resolver;
}
- /** @hide */
+ /**
+ * @hide
+ */
public void setUnreadCount(int unreadCount) {
mExpandButton.setNumber(unreadCount);
}
@@ -795,6 +812,10 @@
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 @@
}
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 @@
}
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 ae8f025..5da6435 100644
--- a/core/java/com/android/internal/widget/ImageFloatingTextView.java
+++ b/core/java/com/android/internal/widget/ImageFloatingTextView.java
@@ -251,7 +251,7 @@
*/
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 85cedc3..3f5b4a0 100644
--- a/core/java/com/android/internal/widget/PeopleHelper.java
+++ b/core/java/com/android/internal/widget/PeopleHelper.java
@@ -22,6 +22,8 @@
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 @@
}
/**
+ * 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 6a640a5..d2e58bb 100644
--- a/core/jni/android_util_Process.cpp
+++ b/core/jni/android_util_Process.cpp
@@ -1165,12 +1165,11 @@
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 @@
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 6e903b3..1031542 100644
--- a/core/jni/android_window_ScreenCapture.cpp
+++ b/core/jni/android_window_ScreenCapture.cpp
@@ -211,7 +211,7 @@
}
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 @@
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 @@
// 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 75e2908..1d1f88b 100644
--- a/core/proto/android/content/intent.proto
+++ b/core/proto/android/content/intent.proto
@@ -66,7 +66,7 @@
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 @@
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 @@
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 b63021d..c92435f 100644
--- a/core/proto/android/server/windowmanagerservice.proto
+++ b/core/proto/android/server/windowmanagerservice.proto
@@ -464,6 +464,7 @@
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 070e2cb..d972556 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 a794d53..0000000
--- 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 67b2c25..5a8594f 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 35276bf..6884fc0 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 0d1a987..23c78fd 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 b8fc052..830e99c 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 3a2e50a..9bb2499 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 0000000..ac64625
--- /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 0000000..8d00d0f
--- /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 0000000..fa585d8
--- /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 0000000..5deb2ce
--- /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
similarity index 100%
rename from core/tests/coretests/res/xml/ime_meta.xml
rename to 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
similarity index 100%
rename from core/tests/coretests/res/xml/ime_meta_inline_suggestions.xml
rename to 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
similarity index 100%
rename from core/tests/coretests/res/xml/ime_meta_inline_suggestions_with_touch_exploration.xml
rename to 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
similarity index 100%
rename from core/tests/coretests/res/xml/ime_meta_sw_next.xml
rename to 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
similarity index 100%
rename from core/tests/coretests/res/xml/ime_meta_virtual_device_only.xml
rename to 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
similarity index 100%
rename from core/tests/coretests/res/xml/ime_meta_vr_only.xml
rename to 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
similarity index 100%
rename from core/tests/coretests/src/android/view/inputmethod/BaseInputConnectionTest.java
rename to 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
similarity index 100%
rename from core/tests/coretests/src/android/view/inputmethod/CursorAnchorInfoTest.java
rename to 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
similarity index 100%
rename from core/tests/coretests/src/android/view/inputmethod/DeleteRangeGestureTest.java
rename to 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
similarity index 100%
rename from core/tests/coretests/src/android/view/inputmethod/EditorInfoTest.java
rename to 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
similarity index 98%
rename from core/tests/coretests/src/android/view/inputmethod/InputMethodInfoTest.java
rename to core/tests/InputMethodCoreTests/src/android/view/inputmethod/InputMethodInfoTest.java
index 909af7b..a3f537e 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.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
similarity index 100%
rename from core/tests/coretests/src/android/view/inputmethod/InputMethodManagerTest.java
rename to 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
similarity index 100%
rename from core/tests/coretests/src/android/view/inputmethod/InputMethodSubtypeArrayTest.java
rename to 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
similarity index 100%
rename from core/tests/coretests/src/android/view/inputmethod/InputMethodSubtypeTest.java
rename to 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
similarity index 100%
rename from core/tests/coretests/src/android/view/inputmethod/InsertGestureTest.java
rename to 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
similarity index 100%
rename from core/tests/coretests/src/android/view/inputmethod/InsertModeGestureTest.java
rename to 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
similarity index 100%
rename from core/tests/coretests/src/android/view/inputmethod/ParcelableHandwritingGestureTest.java
rename to 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
similarity index 100%
rename from core/tests/coretests/src/android/view/inputmethod/SelectGestureTest.java
rename to 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
similarity index 100%
rename from core/tests/coretests/src/android/view/inputmethod/SelectRangeGestureTest.java
rename to 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
similarity index 100%
rename from core/tests/coretests/src/android/view/inputmethod/SparseRectFArrayTest.java
rename to 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
similarity index 100%
rename from core/tests/coretests/src/android/view/inputmethod/SurroundingTextTest.java
rename to 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
similarity index 100%
rename from core/tests/coretests/src/android/view/inputmethod/TextAppearanceInfoTest.java
rename to 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
similarity index 100%
rename from core/tests/coretests/src/com/android/internal/inputmethod/CompletableFutureUtilTest.kt
rename to 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
similarity index 100%
rename from core/tests/coretests/src/com/android/internal/inputmethod/InputConnectionWrapperTest.java
rename to 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
similarity index 100%
rename from core/tests/coretests/src/com/android/internal/inputmethod/InputMethodDebugTest.java
rename to 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
similarity index 100%
rename from core/tests/coretests/src/com/android/internal/inputmethod/InputMethodSubtypeHandleTest.java
rename to 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
similarity index 100%
rename from core/tests/coretests/src/com/android/internal/inputmethod/SubtypeLocaleUtilsTest.java
rename to 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 4406302..f476799 100644
--- a/core/tests/coretests/Android.bp
+++ b/core/tests/coretests/Android.bp
@@ -209,9 +209,13 @@
"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 0000000..20421d1
--- /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 0000000..af36dbb
--- /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 e32a57b..a2a5433 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 @@
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 @@
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 0000000..c260807
--- /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 0000000..8c5a963
--- /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 803d38c..4c5b7e5 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.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 @@
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 a709d7b..52ff0d4 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_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 @@
}
@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 @@
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/data/etc/privapp-permissions-platform.xml b/data/etc/privapp-permissions-platform.xml
index 2de305f..2873428 100644
--- a/data/etc/privapp-permissions-platform.xml
+++ b/data/etc/privapp-permissions-platform.xml
@@ -450,7 +450,7 @@
<!-- 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 @@
<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 0000000..24c1a3a
--- /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
similarity index 92%
rename from libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipDoubleTapHelper.java
rename to libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipDoubleTapHelper.java
index 1b1ebc3..4cbb78f 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 @@
@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 @@
* @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 2dd2743..dbf7186 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 @@
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 @@
@Override
protected boolean eligibleToShowLayout() {
- return mHasSizeCompat || shouldShowCameraControl();
+ return (mHasSizeCompat && shouldShowSizeCompatRestartButton(getLastTaskInfo()))
+ || shouldShowCameraControl();
}
@Override
@@ -114,11 +114,6 @@
mLayout = inflateLayout();
mLayout.inject(this);
- final TaskInfo taskInfo = getLastTaskInfo();
- if (taskInfo != null) {
- mHasSizeCompat = mHasSizeCompat && shouldShowSizeCompatRestartButton(taskInfo);
- }
-
updateVisibilityOfViews();
if (mHasSizeCompat) {
@@ -139,8 +134,7 @@
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 180498c..0564c95 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 @@
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 3b48c67..7b98fa6 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 @@
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 04911c0..0e70736 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 java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;
+import java.util.function.Consumer;
/**
* Responsible supplying PiP Transitions.
@@ -116,6 +117,17 @@
}
/**
+ * 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 452a416..81705e2 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.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 0b8f60e..57b73b3 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.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.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 @@
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 @@
}
}
+ /**
+ * 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 3b0e7c1..f3d178a 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_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 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.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 @@
super(shellInit, shellTaskOrganizer, transitions, pipBoundsState, pipMenuController,
pipBoundsAlgorithm);
+ mContext = context;
mPipScheduler = pipScheduler;
mPipScheduler.setPipTransitionController(this);
}
@@ -87,7 +95,7 @@
@Override
public void startExitTransition(int type, WindowContainerTransaction out,
- @android.annotation.Nullable Rect destinationBounds) {
+ @Nullable Rect destinationBounds) {
if (out == null) {
return;
}
@@ -97,6 +105,16 @@
}
}
+ @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 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 @@
} 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 @@
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 253acc4..0ca244c 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 @@
* 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 2ec52bb..70cb2fc 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 @@
mStageCoordinator.onDroppedToSplit(position, dragSessionId);
}
+ void switchSplitPosition(String reason) {
+ if (isSplitScreenVisible()) {
+ mStageCoordinator.switchSplitPosition(reason);
+ }
+ }
+
/**
* Return the {@param exitReason} as a string.
*/
@@ -1473,5 +1479,11 @@
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 7fd03a9..7f16c5e 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 @@
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 @@
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 @@
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 3fb0dbf..67fc7e2 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 @@
/** 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 554b1fb..4ba05ce 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 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.window.WindowContainerToken;
import android.window.WindowContainerTransaction;
-import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.android.internal.annotations.VisibleForTesting;
@@ -544,12 +544,22 @@
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 4ddc539..dd358e7 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 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 @@
@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 @@
@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 @@
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 2acfd83..4f261cd 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_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 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 @@
@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 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 @@
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 @@
@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 @@
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 @@
// 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 @@
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 @@
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 0f8db85..b583acd 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 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.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 12a5594..7f3bfbb 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 @@
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 b40b73c..4e330da 100644
--- a/libs/hwui/Android.bp
+++ b/libs/hwui/Android.bp
@@ -229,15 +229,6 @@
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 @@
// 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 77a6820..0000000
--- 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 3dc024ef..6f88912 100644
--- a/location/java/android/location/altitude/AltitudeConverter.java
+++ b/location/java/android/location/altitude/AltitudeConverter.java
@@ -19,9 +19,11 @@
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 @@
* <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 @@
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 @@
/**
* 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 @@
// 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 @@
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 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 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
similarity index 65%
rename from location/java/com/android/internal/location/altitude/GeoidHeightMap.java
rename to location/java/com/android/internal/location/altitude/GeoidMap.java
index 8067050..9bf5689 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.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 @@
* 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 sParams;
+ private static MapParamsProto sGeoidHeightParams;
- /** Defines a cache large enough to hold all cache tiles needed for interpolation. */
- private final LruCache<Long, S2TileProto> mCacheTiles = new LruCache<>(4);
+ @GuardedBy("EXPIRATION_DISTANCE_PARAMS_LOCK")
+ @Nullable
+ private static MapParamsProto sExpirationDistanceParams;
/**
- * Returns the singleton parameter instance for a spherically projected geoid height map and its
- * corresponding tile management.
+ * 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 expiration distance cache tiles needed for
+ * interpolation.
+ */
+ private final LruCache<Long, S2TileProto> mExpirationDistanceCacheTiles = new LruCache<>(4);
+
+ /**
+ * 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 sParams;
+ return sGeoidHeightParams;
}
}
/**
- * Same as {@link #getParams(Context)} except that null is returned if the singleton parameter
- * instance is not yet initialized.
+ * 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 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 #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 @@
@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 @@
@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 @@
}
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 @@
* 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 @@
@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 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 @@
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 @@
"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 @@
};
}
- 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 @@
}
// 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 691aa77..425db06 100644
--- a/media/java/android/media/MediaRouter2.java
+++ b/media/java/android/media/MediaRouter2.java
@@ -183,23 +183,23 @@
* 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 @@
* {@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 @@
* @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 @@
})
@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 d28c26d..2202766 100644
--- a/media/java/android/media/RoutingSessionInfo.java
+++ b/media/java/android/media/RoutingSessionInfo.java
@@ -182,7 +182,7 @@
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 @@
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 0000000..c3997e9
--- /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 0000000..e099dea
--- /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 0000000..72e6db8
--- /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 2ddf623b..964e12c 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 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 @@
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 51b1cc2..e07ca67 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.os.PersistableBundle;
/**
- * Interface to the playback manager service.
+ * Interface to the media metrics manager service.
* @hide
*/
interface IMediaMetricsManager {
@@ -37,6 +38,8 @@
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 0898874..622b0c1 100644
--- a/media/java/android/media/metrics/MediaMetricsManager.java
+++ b/media/java/android/media/metrics/MediaMetricsManager.java
@@ -193,4 +193,18 @@
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 e3dba03..7b58531 100644
--- a/media/java/android/media/tv/interactive/ITvInteractiveAppClient.aidl
+++ b/media/java/android/media/tv/interactive/ITvInteractiveAppClient.aidl
@@ -61,7 +61,8 @@
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 0f58b29..1b9450b 100644
--- a/media/java/android/media/tv/interactive/ITvInteractiveAppManager.aidl
+++ b/media/java/android/media/tv/interactive/ITvInteractiveAppManager.aidl
@@ -58,6 +58,8 @@
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 06808c9..3969315 100644
--- a/media/java/android/media/tv/interactive/ITvInteractiveAppSession.aidl
+++ b/media/java/android/media/tv/interactive/ITvInteractiveAppSession.aidl
@@ -49,6 +49,7 @@
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 416b8f1..cb89181 100644
--- a/media/java/android/media/tv/interactive/ITvInteractiveAppSessionCallback.aidl
+++ b/media/java/android/media/tv/interactive/ITvInteractiveAppSessionCallback.aidl
@@ -61,5 +61,6 @@
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 77730aa..ec6c2bf 100644
--- a/media/java/android/media/tv/interactive/ITvInteractiveAppSessionWrapper.java
+++ b/media/java/android/media/tv/interactive/ITvInteractiveAppSessionWrapper.java
@@ -104,6 +104,7 @@
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 @@
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 @@
}
@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 8a340f6..011744f 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.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 @@
}
@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 @@
}
}
+ 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 @@
});
}
+ 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 @@
}
/**
+ * 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 5247a0e..054b272 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.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 @@
}
/**
+ * 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 @@
}
/**
+ * 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 @@
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 5bb61c2..3b29574 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.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 @@
}
/**
+ * 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/media/jni/soundpool/StreamManager.cpp b/media/jni/soundpool/StreamManager.cpp
index 52060f1..66fec1c 100644
--- a/media/jni/soundpool/StreamManager.cpp
+++ b/media/jni/soundpool/StreamManager.cpp
@@ -35,10 +35,9 @@
// In R, we change this to true, as it is the correct way per SoundPool documentation.
static constexpr bool kStealActiveStream_OldestFirst = true;
-// kPlayOnCallingThread = true prior to R.
// Changing to false means calls to play() are almost instantaneous instead of taking around
// ~10ms to launch the AudioTrack. It is perhaps 100x faster.
-static constexpr bool kPlayOnCallingThread = true;
+static constexpr bool kPlayOnCallingThread = false;
// Amount of time for a StreamManager thread to wait before closing.
static constexpr int64_t kWaitTimeBeforeCloseNs = 9 * NANOS_PER_SECOND;
diff --git a/media/jni/soundpool/StreamManager.h b/media/jni/soundpool/StreamManager.h
index adbab4b..340b49b 100644
--- a/media/jni/soundpool/StreamManager.h
+++ b/media/jni/soundpool/StreamManager.h
@@ -48,7 +48,7 @@
public:
JavaThread(std::function<void()> f, const char *name)
: mF{std::move(f)} {
- createThreadEtc(staticFunction, this, name);
+ createThreadEtc(staticFunction, this, name, ANDROID_PRIORITY_AUDIO);
}
JavaThread(JavaThread &&) = delete; // uses "this" ptr, not moveable.
diff --git a/native/graphics/jni/Android.bp b/native/graphics/jni/Android.bp
index 10c570b..8ea4632 100644
--- a/native/graphics/jni/Android.bp
+++ b/native/graphics/jni/Android.bp
@@ -72,6 +72,9 @@
],
},
},
+ stubs: {
+ symbol_file: "libjnigraphics.map.txt",
+ },
}
// The headers module is in frameworks/native/Android.bp.
@@ -93,15 +96,18 @@
],
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 4cd652d..99ae144 100644
--- a/nfc/jarjar-rules.txt
+++ b/nfc/jarjar-rules.txt
@@ -4,6 +4,7 @@
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 9d38e4c..1f41b81 100644
--- a/nfc/java/android/nfc/cardemulation/CardEmulation.java
+++ b/nfc/java/android/nfc/cardemulation/CardEmulation.java
@@ -338,8 +338,10 @@
}
}
/**
- * 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 0000000..5636266
--- /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/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/common/BroadcastReceiverFlow.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/common/BroadcastReceiverFlow.kt
index e099f11..0a424bc 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 @@
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 81a8b324..cea3d13 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 @@
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 eef5225..772f925 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 @@
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 0000000..de50de8
--- /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 cf4d6be..0613676 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 @@
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 3a15b71..9fd174d 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 @@
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 @@
}
}
- 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 @@
// 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 @@
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 14fab16..f2450de 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 @@
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 931a6f1..6be4336 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 @@
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 9348705..1d2f790 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 @@
}
/**
+ * 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 cf224dc..3de4933 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 @@
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 8a122fc..aef09ac 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.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 @@
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 0000000..3355fb3
--- /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 0000000..053c59b
--- /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 644b72c..ce3a7ba 100644
--- a/packages/SettingsLib/tests/integ/Android.bp
+++ b/packages/SettingsLib/tests/integ/Android.bp
@@ -57,6 +57,7 @@
"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 0000000..686362f
--- /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 0000000..3bc1edc
--- /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 e7487e8..aa5a298 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 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 @@
@Mock
private HearingAidProfile mHearingAidProfile;
@Mock
+ private LeAudioProfile mLeAudioProfile;
+ @Mock
+ private HapClientProfile mHapClientProfile;
+ @Mock
private AudioProductStrategy mAudioStrategy;
@Mock
private BluetoothDevice mDevice1;
@@ -123,6 +135,8 @@
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 @@
}
/**
- * 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 @@
}
/**
- * 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 @@
}
/**
- * 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 @@
}
/**
- * 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 @@
}
/**
- * 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 @@
}
/**
- * 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 @@
}
/**
+ * 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 89a8dd9..17d9f1b 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 8ae50eb..8ad5f24 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 @@
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 e7d7bb0..38ec931 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 @@
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 285c8c9..d854df38 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 @@
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 a8a659e..677c81a 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 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 572303a..98941c7 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_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.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 @@
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 3a46f4e..febce97 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 @@
}
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 @@
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 @@
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 d384542..cdb4aea 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/aconfig/accessibility.aconfig b/packages/SystemUI/aconfig/accessibility.aconfig
index 7ba889b..866aa89 100644
--- a/packages/SystemUI/aconfig/accessibility.aconfig
+++ b/packages/SystemUI/aconfig/accessibility.aconfig
@@ -17,6 +17,13 @@
}
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/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig
index 7eca04a..a2530d5 100644
--- a/packages/SystemUI/aconfig/systemui.aconfig
+++ b/packages/SystemUI/aconfig/systemui.aconfig
@@ -366,8 +366,8 @@
}
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 a22fecf..4fdcf75 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
@@ -84,13 +84,14 @@
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 +102,7 @@
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/keyguard/ui/composable/section/LockSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/LockSection.kt
index 2a6bea7..be6f022 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.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.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 @@
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 c35202c..9f9e1f5 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 @@
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 82d4239..b0dc3a1 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.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 90f46bd..9d4b69c 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Key.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Key.kt
@@ -44,7 +44,7 @@
class SceneKey(
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 @@
/** 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 3873878..8552aaf 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 @@
@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 @@
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 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 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 @@
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 @@
) = delegate.onPointerEvent(pointerEvent, pass, bounds)
private suspend fun PointerInputScope.pointerInput() {
- if (!enabled) {
+ if (!enabled()) {
return
}
@@ -163,8 +163,7 @@
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 @@
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 f67df54..af51cee 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 @@
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 ff05478..aed04f6 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.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 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 @@
}
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 @@
Orientation.Vertical -> SwipeDirection.Up
},
pointerCount = pointersDown,
- fromEdge = fromEdge,
+ fromSource = fromSource,
)
val downOrRight =
@@ -142,33 +153,31 @@
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 @@
findTargetSceneAndDistance(
fromScene,
swipeTransition.dragOffset,
- updateScenes = isNewFromScene,
+ updateSwipesResults = isNewFromScene,
)
?: run {
onDragStopped(delta, true)
@@ -200,9 +209,31 @@
}
}
- 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 @@
// 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 @@
* @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 @@
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 @@
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 80f8c1c..7e0aa9c3 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.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 @@
* 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 @@
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 @@
* 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 @@
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 @@
SceneTransitionLayout(
state,
modifier,
- edgeDetector,
+ swipeSourceDetector,
transitionInterceptionThreshold,
scenes,
)
@@ -113,7 +119,7 @@
*/
fun scene(
key: SceneKey,
- userActions: Map<UserAction, SceneKey> = emptyMap(),
+ userActions: Map<UserAction, UserActionResult> = emptyMap(),
content: @Composable SceneScope.() -> Unit,
)
}
@@ -335,7 +341,7 @@
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 @@
}
/**
+ * 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 @@
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 @@
SceneTransitionLayoutImpl(
state = state as BaseSceneTransitionLayoutState,
density = density,
- edgeDetector = edgeDetector,
+ swipeSourceDetector = swipeSourceDetector,
transitionInterceptionThreshold = transitionInterceptionThreshold,
builder = scenes,
coroutineScope = coroutineScope,
@@ -394,7 +489,7 @@
}
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 7cc9d26..8c5a472 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 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 @@
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 @@
// 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 0d3bc7d..b9c4ac0 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 }
+ return this.then(SwipeToSceneElement(gestureHandler))
+}
- 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) {
+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,
+ )
+ )
+
+ 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.
+ 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 multiPointerDraggable(
- orientation = orientation,
- enabled = gestureHandler.isDrivingTransition || canSwipe,
- // 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 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 dc8505c..a764a527 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 @@
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 2841bcf..ac11d30 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.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 @@
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 a68282a..cceaf57 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 @@
@Test
fun horizontalEdges() {
fun horizontalEdge(position: Int): Edge? =
- detector.edge(
+ detector.source(
layoutSize,
position = IntOffset(position, 0),
density,
@@ -53,7 +53,7 @@
@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 066a3e4..88363ad 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 @@
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 @@
SceneTransitionLayoutImpl(
state = layoutState,
density = Density(1f),
- edgeDetector = DefaultEdgeDetector,
+ swipeSourceDetector = DefaultEdgeDetector,
transitionInterceptionThreshold = transitionInterceptionThreshold,
builder = scenesBuilder,
coroutineScope = coroutineScope,
@@ -192,16 +192,14 @@
@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 @@
@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 @@
@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 @@
@Test
fun onDragStoppedAfterStarted_returnToIdle() = runGestureTest {
- draggable.onDragStarted()
+ draggable.onDragStarted(overSlop = down(0.1f))
assertTransition(currentScene = SceneA)
draggable.onDragStopped(velocity = 0f)
@@ -256,8 +248,7 @@
@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 @@
@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 @@
@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 @@
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 @@
@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 1ec3c8b..9403358 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 @@
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 @@
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 030d41d..c82688c 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 @@
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 @@
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 0000000..9287edf
--- /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 0000000..ce22e28
--- /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 bb3429e..c979ca6 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.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.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 @@
@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 @@
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
-
+ fakeWidgets = MutableStateFlow(emptyMap())
logBuffer = logcatLogBuffer(name = "CommunalWidgetRepoImplTest")
setAppWidgetIds(emptyList())
@@ -102,13 +93,11 @@
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 @@
}
@Test
- fun neverQueryDbForWidgets_whenHostIsInactive() =
+ fun communalWidgets_queryWidgetsFromDb() =
testScope.runTest {
- underTest.updateAppWidgetHostActive(false)
- underTest.communalWidgets.launchIn(testScope.backgroundScope)
- runCurrent()
-
- verify(communalWidgetDao, never()).getWidgets()
- }
-
- @Test
- fun communalWidgets_whenHostIsActive_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 @@
@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 @@
@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 @@
@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 @@
@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 @@
@Test
fun deleteWidget_removeWidgetId_andDeleteFromDb() =
testScope.runTest {
- underTest.updateAppWidgetHostActive(true)
-
val id = 1
underTest.deleteWidget(id)
runCurrent()
@@ -254,8 +211,6 @@
@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 @@
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 e821673..6a3fc2a 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 @@
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 1b7117f..a083e7c 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
@@ -172,20 +172,6 @@
}
@Test
- fun updateAppWidgetHostActive_uponStorageUnlockAsMainUser_true() =
- testScope.runTest {
- collectLastValue(underTest.isCommunalAvailable)
- assertThat(widgetRepository.isHostActive()).isFalse()
-
- keyguardRepository.setIsEncryptedOrLockdown(false)
- userRepository.setSelectedUserInfo(mainUser)
- keyguardRepository.setKeyguardShowing(true)
- runCurrent()
-
- assertThat(widgetRepository.isHostActive()).isTrue()
- }
-
- @Test
fun widget_tutorialCompletedAndWidgetsAvailable_showWidgetContent() =
testScope.runTest {
// Keyguard showing, and tutorial completed.
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 0000000..112b0c7
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartableTest.kt
@@ -0,0 +1,132 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.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.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(!available)
+ fakeUserRepository.setSelectedUserInfo(MAIN_USER_INFO)
+ fakeKeyguardRepository.setKeyguardShowing(true)
+ }
+
+ 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 6a14220..6808f5d 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 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.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 @@
biometricSettingsRepository = FakeBiometricSettingsRepository()
deviceEntryFingerprintAuthRepository = FakeDeviceEntryFingerprintAuthRepository()
trustRepository = FakeTrustRepository()
- featureFlags = FakeFeatureFlags().apply { set(KEYGUARD_WM_STATE_REFACTOR, false) }
+ featureFlags = FakeFeatureFlags()
powerRepository = FakePowerRepository()
powerInteractor =
@@ -252,6 +252,10 @@
.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 @@
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 0000000..88ad3f3
--- /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 0000000..d216fa0
--- /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 dc8b97a..78ae8b1 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 @@
}
@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()
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 eb845b2..d9f24b3 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.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.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.Mock
import org.mockito.MockitoAnnotations
+@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@RunWith(AndroidJUnit4::class)
@TestableLooper.RunWithLooper
@@ -82,7 +84,7 @@
underTest =
InstalledTilesComponentRepositoryImpl(
context,
- kosmos.testDispatcher,
+ testScope.backgroundScope,
kosmos.packageChangeRepository
)
}
@@ -103,6 +105,7 @@
.thenReturn(listOf(resolveInfo))
val componentNames by collectLastValue(underTest.getInstalledTilesComponents(userId))
+ runCurrent()
assertThat(componentNames).containsExactly(TEST_COMPONENT)
}
@@ -115,6 +118,8 @@
ResolveInfo(TEST_COMPONENT, hasPermission = true, defaultEnabled = true)
val componentNames by collectLastValue(underTest.getInstalledTilesComponents(userId))
+ runCurrent()
+
assertThat(componentNames).isEmpty()
whenever(
@@ -126,6 +131,7 @@
)
.thenReturn(listOf(resolveInfo))
kosmos.fakePackageChangeRepository.notifyChange(PackageChangeModel.Empty)
+ runCurrent()
assertThat(componentNames).containsExactly(TEST_COMPONENT)
}
@@ -146,6 +152,8 @@
.thenReturn(listOf(resolveInfo))
val componentNames by collectLastValue(underTest.getInstalledTilesComponents(userId))
+ runCurrent()
+
assertThat(componentNames).isEmpty()
}
@@ -165,6 +173,8 @@
.thenReturn(listOf(resolveInfo))
val componentNames by collectLastValue(underTest.getInstalledTilesComponents(userId))
+ runCurrent()
+
assertThat(componentNames).isEmpty()
}
@@ -210,10 +220,31 @@
.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/domain/autoaddable/A11yShortcutAutoAddableListTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/autoaddable/A11yShortcutAutoAddableListTest.kt
new file mode 100644
index 0000000..311122d
--- /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 0000000..3b33a43
--- /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/util/kotlin/BooleanFlowOperatorsTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/util/kotlin/BooleanFlowOperatorsTest.kt
new file mode 100644
index 0000000..b4a0a37
--- /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/res/values/colors.xml b/packages/SystemUI/res/values/colors.xml
index 3839dd9..307a619 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 8ec5ccd..2ab0813 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 2b43360..47ac96c 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -2560,6 +2560,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/src/com/android/keyguard/KeyguardSecurityContainerController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java
index ecce223..25d7713 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java
@@ -19,9 +19,9 @@
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.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.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 javax.inject.Inject;
import javax.inject.Provider;
+import dagger.Lazy;
import kotlinx.coroutines.Job;
/** Controller for {@link KeyguardSecurityContainer} */
@@ -330,7 +330,7 @@
}
}
- 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 d5dc85c..8e98150 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.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 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 @@
return mDownDetected;
}
+ @ExperimentalCoroutinesApi
@VisibleForTesting
protected void onLongPress() {
cancelTouches();
@@ -727,7 +731,8 @@
// 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 8c2d221..35f9344 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 @@
@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 0000000..401ac0f
--- /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 0000000..ed91f03
--- /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 568b24d..7fd72ec 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(DragToInteractView interactView, MenuView menuView) {
+ mDismissView = null;
+ mInteractView = interactView;
+ mInteractView.setPivotX(interactView.getWidth() / 2.0f);
+ mInteractView.setPivotY(interactView.getHeight() / 2.0f);
+ mMenuView = menuView;
+
+ updateResources();
+
+ mInteractMap = new ArrayMap<>();
+ interactView.getInteractMap().forEach((viewId, pair) -> {
+ DismissCircleView circleView = pair.getFirst();
+ createMagnetizedObjectAndAnimator(circleView);
+ });
+ }
+
DragToInteractAnimationController(DismissView dismissView, MenuView menuView) {
mDismissView = dismissView;
+ mInteractView = null;
mDismissView.setPivotX(dismissView.getWidth() / 2.0f);
mDismissView.setPivotY(dismissView.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));
- });
-
- mDismissAnimator.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(@NonNull Animator animation, boolean isReverse) {
- super.onAnimationEnd(animation, isReverse);
-
- if (isReverse) {
- mDismissView.getCircle().setScaleX(CIRCLE_VIEW_DEFAULT_SCALE);
- mDismissView.getCircle().setScaleY(CIRCLE_VIEW_DEFAULT_SCALE);
- mMenuView.setAlpha(COMPLETELY_OPAQUE);
- }
- }
- });
-
- 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 @@
* 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 0000000..0ef3d20
--- /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 a270558..d3e85e0 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.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 @@
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 @@
mSpringAnimationsEndAction = runnable;
}
- void setDismissCallback(
- DragToInteractAnimationController.DismissCallback dismissCallback) {
- mDismissCallback = dismissCallback;
- }
-
void moveToTopLeftPosition() {
mMenuView.updateMenuMoveToTucked(/* isMoveToTucked= */ false);
final Rect draggableBounds = mMenuView.getMenuDraggableBounds();
@@ -205,13 +198,6 @@
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 @@
moveToEdgeAndHide();
return true;
}
-
- fadeOutIfEnabled();
return false;
}
@@ -453,8 +437,6 @@
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/MenuItemAccessibilityDelegate.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuItemAccessibilityDelegate.java
index 9c22a77..975a602 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.core.view.accessibility.AccessibilityNodeInfoCompat;
import androidx.recyclerview.widget.RecyclerViewAccessibilityDelegate;
+import com.android.systemui.Flags;
import com.android.systemui.res.R;
/**
@@ -35,15 +36,18 @@
*/
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 @@
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 @@
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 52e7b91..7519168 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 @@
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 @@
final float endX = mMenuTranslationDown.x + dx;
mIsDragging = false;
- if (mMenuAnimationController.maybeMoveToEdgeAndHide(endX)) {
- mDragToInteractAnimationController.showDismissView(/* show= */ false);
- mMenuAnimationController.fadeOutIfEnabled();
+ mDragToInteractAnimationController.showInteractView(/* show= */ false);
+ if (mMenuAnimationController.maybeMoveToEdgeAndHide(endX)) {
+ 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 76808cb..334cc87 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 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 @@
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 @@
if (mFeaturesChangeListener != null) {
mFeaturesChangeListener.onChange(newTargetFeatures);
}
+
mMenuAnimationController.fadeOutIfEnabled();
}
@@ -306,6 +305,10 @@
return mMenuViewAppearance.getMenuPosition();
}
+ RecyclerView getTargetFeaturesView() {
+ return mTargetFeaturesView;
+ }
+
void persistPositionAndUpdateEdge(Position percentagePosition) {
mMenuViewModel.updateMenuSavingPosition(percentagePosition);
mMenuViewAppearance.setPercentagePosition(percentagePosition);
@@ -424,6 +427,35 @@
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 97999cc..bb5364d 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayer.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayer.java
@@ -59,7 +59,10 @@
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.dialog.AccessibilityTarget;
import com.android.internal.annotations.VisibleForTesting;
@@ -94,6 +97,8 @@
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;
@@ -178,7 +183,10 @@
};
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 @@
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 @@
});
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 @@
@Override
public void onConfigurationChanged(@NonNull Configuration newConfig) {
+ mDragToInteractView.updateResources();
mDismissView.updateResources();
mDragToInteractAnimationController.updateResources();
}
@@ -428,6 +450,23 @@
}
}
+ 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 @@
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 @@
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 1f54952..bc9d1ff 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 @@
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 71d0e7d..d2c6227 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.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.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 @@
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 @@
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 @@
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 @@
override fun onBiometricUnlockedWithKeyguardDismissal(
biometricSourceType: BiometricSourceType?
) {
+ DeviceEntryUdfpsRefactor.assertInLegacyMode()
if (biometricSourceType != null) {
- showUnlockRipple(biometricSourceType)
+ showUnlockRippleInternal(biometricSourceType)
} else {
logger.log(TAG,
LogLevel.ERROR,
@@ -143,7 +168,13 @@
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 @@
}
"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 @@
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 16e7f05..96582cb 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 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 c36e0e2..80d37b4 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 @@
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 @@
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 e78a7a9..0f1340a 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 @@
/** 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 3287ed4..f36547b 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.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 @@
* @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 @@
private val logger = Logger(logBuffer, TAG)
- override fun updateAppWidgetHostActive(active: Boolean) {
- if (active == isHostActive.value) {
- return
- }
+ private val appWidgetManager by appWidgetManagerOptional
- if (active) {
- appWidgetHost.startListening()
- } else {
- appWidgetHost.stopListening()
- }
- isHostActive.value = active
- }
-
- private val isHostActive = MutableStateFlow(false)
-
- @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 @@
}
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 @@
}
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 @@
@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 c36f7fa..28adb77 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
@@ -37,18 +37,22 @@
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.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
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. */
@@ -68,6 +72,11 @@
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
@@ -80,21 +89,17 @@
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)
+ if (enabled) {
+ val isMainUser = userRepository.selectedUserInfo.map { it.isMain }
+ and(
+ isMainUser,
+ not(keyguardInteractor.isEncryptedOrLockdown),
+ or(keyguardInteractor.isKeyguardVisible, keyguardInteractor.isDreaming),
+ )
+ } else {
+ flowOf(false)
+ }
}
- .distinctUntilChanged()
- .onEach { available -> widgetRepository.updateAppWidgetHostActive(available) }
.stateIn(
scope = applicationScope,
started = SharingStarted.WhileSubscribed(),
@@ -166,6 +171,10 @@
communalRepository.setDesiredScene(newScene)
}
+ fun setEditModeOpen(isOpen: Boolean) {
+ _editModeOpen.value = isOpen
+ }
+
/** Show the widget editor Activity. */
fun showWidgetEditor() {
editWidgetsActivityStarter.startActivity()
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 237a0c0..4b98f1a 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
@@ -75,4 +75,7 @@
_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/widgets/CommunalAppWidgetHostStartable.kt b/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartable.kt
new file mode 100644
index 0000000..586df32
--- /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 c7a14f9..a257543 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt
@@ -86,6 +86,8 @@
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
+ communalViewModel.setEditModeOpen(true)
+
val windowInsetsController = window.decorView.windowInsetsController
windowInsetsController?.hide(WindowInsets.Type.systemBars())
window.setDecorFitsSystemWindows(false)
@@ -138,13 +140,16 @@
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/dagger/SystemUICoreStartableModule.kt b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt
index 8d82b55..95233f7 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.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 @@
@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 7a70c4a..cf7d601 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.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.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.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 @@
@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 @@
// 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 08e8c2d..8283438 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.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 @@
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 0000000..337fe1e
--- /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 649a971..782bce4 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 @@
class DeviceEntryHapticsInteractor
@Inject
constructor(
- deviceEntryInteractor: DeviceEntryInteractor,
+ deviceEntrySourceInteractor: DeviceEntrySourceInteractor,
deviceEntryFingerprintAuthInteractor: DeviceEntryFingerprintAuthInteractor,
deviceEntryBiometricAuthInteractor: DeviceEntryBiometricAuthInteractor,
fingerprintPropertyRepository: FingerprintPropertyRepository,
@@ -80,7 +80,7 @@
}
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 0985357..73389cb 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.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 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 @@
@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 @@
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 0000000..d4f76a8
--- /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 d2883cc..c69c9ef 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 @@
@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/BluetoothDialog.java
deleted file mode 100644
index 5deea9b..0000000
--- a/packages/SystemUI/src/com/android/systemui/keyboard/BluetoothDialog.java
+++ /dev/null
@@ -1,32 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.keyboard;
-
-import android.content.Context;
-import android.view.WindowManager;
-
-import com.android.systemui.statusbar.phone.SystemUIDialog;
-
-public class BluetoothDialog extends SystemUIDialog {
-
- public BluetoothDialog(Context context) {
- super(context);
-
- getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_DIALOG);
- setShowForAllUsers(true);
- }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/BluetoothDialogDelegate.java b/packages/SystemUI/src/com/android/systemui/keyboard/BluetoothDialogDelegate.java
new file mode 100644
index 0000000..98642d7
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/BluetoothDialogDelegate.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyboard;
+
+import android.view.WindowManager;
+
+import com.android.systemui.statusbar.phone.SystemUIDialog;
+
+import javax.inject.Inject;
+
+public class BluetoothDialogDelegate implements SystemUIDialog.Delegate{
+
+ 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 1cdbe6f..17e3ca6 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.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 @@
private final Provider<LocalBluetoothManager> mBluetoothManagerProvider;
private final SecureSettings mSecureSettings;
+ private final BluetoothDialogDelegate mBluetoothDialogDelegate;
private boolean mEnabled;
private String mKeyboardName;
@@ -121,16 +123,20 @@
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 @@
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 4ed8120..5ef5ef1 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 @@
{ "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 34d2888..ec29bd6 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 @@
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.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 @@
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 @@
.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 @@
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 e2ab20e..f10b87e 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.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 @@
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 01ba0d2..53c81e5 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 @@
*/
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 @@
}
surfaceBehindRemoteAnimationTargets?.forEach { surfaceBehindRemoteAnimationTarget ->
- if (!featureFlags.isEnabled(Flags.KEYGUARD_WM_STATE_REFACTOR)) {
+ if (!KeyguardWmStateRefactor.isEnabled) {
val surfaceHeight: Int =
surfaceBehindRemoteAnimationTarget.screenSpaceBounds.height()
@@ -1005,7 +1005,7 @@
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 50caf17..8e3b196 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.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.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.concurrent.Executor;
import java.util.function.Consumer;
+import dagger.Lazy;
import kotlinx.coroutines.CoroutineDispatcher;
/**
@@ -1051,7 +1049,7 @@
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 @@
@Override // Binder interface
public void onAnimationCancelled() {
cancelKeyguardExitAnimation();
- if (mFeatureFlags.isEnabled(Flags.KEYGUARD_WM_STATE_REFACTOR)) {
+ if (KeyguardWmStateRefactor.isEnabled()) {
mWmLockscreenVisibilityManager.get().onKeyguardGoingAwayRemoteAnimationCancelled();
}
}
@@ -2757,7 +2755,7 @@
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 +2809,7 @@
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 +2886,7 @@
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 +2992,7 @@
} 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 +3028,7 @@
// 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 +3054,7 @@
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 +3086,7 @@
// 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 +3110,7 @@
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 +3129,7 @@
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 +3161,7 @@
@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 +3174,7 @@
@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 +3339,7 @@
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 0000000..ddccc5d
--- /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 1437194..1c6056c 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 @@
* 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 @@
}
.distinctUntilChanged()
- override val isKeyguardUnlocked: StateFlow<Boolean> =
+ override val isKeyguardDismissible: StateFlow<Boolean> =
conflatedCallbackFlow {
val callback =
object : KeyguardStateController.Callback {
@@ -396,7 +396,7 @@
trySendWithFailureLogging(
keyguardStateController.isUnlocked,
TAG,
- "updated isKeyguardUnlocked due to onUnlockedChanged"
+ "updated isKeyguardDismissible due to onUnlockedChanged"
)
}
@@ -404,7 +404,7 @@
trySendWithFailureLogging(
keyguardStateController.isUnlocked,
TAG,
- "updated isKeyguardUnlocked due to onKeyguardShowingChanged"
+ "updated isKeyguardDismissible due to onKeyguardShowingChanged"
)
}
}
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 8b2b45f..3965648 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.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 @@
combine(
startedKeyguardTransitionStep,
keyguardInteractor.statusBarState,
- keyguardInteractor.isKeyguardUnlocked,
+ keyguardInteractor.isKeyguardDismissible,
::Triple
),
::toQuad
@@ -307,7 +307,7 @@
}
private fun listenForLockscreenToGone() {
- if (flags.isEnabled(Flags.KEYGUARD_WM_STATE_REFACTOR)) {
+ if (KeyguardWmStateRefactor.isEnabled) {
return
}
@@ -324,7 +324,7 @@
}
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 33b6373..acbd9fb 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.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 @@
}
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 36bd905..22d11d0 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.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 @@
/** 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 @@
/** 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 a02e8ac..703bb87 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.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 @@
@SuppressLint("ClickableViewAccessibility")
@JvmStatic
fun bind(
+ applicationScope: CoroutineScope,
view: DeviceEntryIconView,
viewModel: DeviceEntryIconViewModel,
fgViewModel: DeviceEntryForegroundViewModel,
@@ -69,7 +71,7 @@
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 bc6c7cb..ad589df 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 @@
} 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 a1b3f27..fe4f07d 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.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 @@
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 @@
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 @@
}
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 0bf9ad0..3fc9b42 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.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.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 @@
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 @@
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 8c5e9b4..d75a72f 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 @@
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 37842a8..2f99719 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.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 @@
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 @@
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 @@
}
)
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 eacaa40..a3d5453 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.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 @@
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 @@
}
}
- 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/gestural/BackPanelController.kt b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanelController.kt
index fa03dc2..b3d848c 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.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 @@
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 @@
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 @@
vibratorHelper,
configurationController,
latencyTracker,
+ interactionJankMonitor
)
backPanelController.init()
return backPanelController
@@ -183,7 +188,7 @@
/* 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 @@
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 adea26e..e1ec338 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 @@
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 @@
)
.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 bcd09bd..dc39c97 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.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.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 @@
@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 0000000..2cebbe3
--- /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 0000000..08e3920
--- /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 d04e4f5..53f287b 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.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 @@
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 @@
}
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 @@
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 216d716..88863cb 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.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 @@
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 @@
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 cc53aab..f173900 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.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,9 +169,10 @@
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;
@@ -419,6 +422,21 @@
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,7 +604,8 @@
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)) {
@@ -615,8 +634,9 @@
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 +655,12 @@
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);
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
index c0ceba3..530c124 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 @@
}
@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/ShadeSurface.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeSurface.kt
index e54286f..4f970b3 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 @@
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 @@
/** 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 24ac70e..2a4753d 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 @@
} 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 6e3b15d..c643238 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 @@
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 @@
}
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 73decfc..639e23a 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 @@
}
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 4483599..c0b187b 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.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 @@
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 @@
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 ee0b008..e1d2cdc 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.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 @@
val isSnoozeEnabled: Boolean,
val isMinimized: Boolean,
val needsRedaction: Boolean,
+ val isChildInGroup: Boolean,
) {
companion object {
@JvmStatic
@@ -48,6 +50,11 @@
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 0585456..6f44c13 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.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 @@
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 @@
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 80ef14b..cd816ae 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.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.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 @@
@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 @@
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 @@
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 61e6f65..8021d8f 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 @@
}
}
+ /**
+ * 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 d626c18..8ae324f 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 @@
/**
* 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 43d99a0..6bc2b2f 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 @@
private ImageView mConversationIconView;
private TextView mConversationSenderName;
+ private ViewStub mConversationFacePileStub;
private View mConversationFacePile;
private int mSingleAvatarSize;
private int mFacePileSize;
@@ -65,7 +74,16 @@
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 @@
@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 @@
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 @@
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 ddd9bdd..09c0349 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 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 @@
private final Context mContext;
+ private static final String TAG = "HybridGroupManager";
+
private float mOverflowNumberSize;
private int mOverflowNumberPadding;
@@ -93,21 +96,34 @@
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 f186e66..913d5f6 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.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 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 @@
AsyncInflationTask task = new AsyncInflationTask(
mBgExecutor,
mInflateSynchronously,
- contentToBind,
+ /* reInflateFlags = */ contentToBind,
mRemoteViewCache,
entry,
mConversationProcessor,
@@ -145,7 +150,7 @@
bindParams.usesIncreasedHeadsUpHeight,
callback,
mRemoteInputManager.getRemoteViewsOnClickHandler(),
- mIsMediaInQS,
+ /* isMediaFlagEnabled = */ mIsMediaInQS,
mSmartReplyStateInflater,
mNotifLayoutInflaterFactoryProvider,
mLogger);
@@ -178,6 +183,29 @@
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 @@
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 @@
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 @@
}
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 @@
// 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 @@
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 @@
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 4f5455d..ee9462c 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.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 @@
)
}
+ 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 @@
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 a1718b9..402ea51 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.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 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 @@
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 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 @@
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 @@
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 @@
}
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 d7b7aa2..736140c 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 @@
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 @@
*/
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 a52f638..1494c27 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 @@
* @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 b70da00..f4f8374 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 @@
@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 0000000..d6118a0
--- /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 0000000..69284bd
--- /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 0000000..22e10c1
--- /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 0000000..d583fa5
--- /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 45b9c26..abf6c27 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 @@
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 04db653..dd04531 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 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 @@
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 1143481..49fde39 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 @@
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 f842e30..fe5bdd4 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 @@
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.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 @@
}
}
+ 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 @@
}
}
- 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 @@
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 99cd89b..4617ce4 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.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 @@
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 @@
* 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 266c19c..7952511 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 @@
// 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/ScrimState.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimState.java
index e3b65ab..61bd112 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.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 @@
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 @@
} 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 @@
// 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 @@
mBehindTint = previousState.mBehindTint;
mBehindAlpha = previousState.mBehindAlpha;
- mFrontTint = Color.BLACK;
+ mFrontTint = mBackgroundColor;
mFrontAlpha = .66f;
}
},
@@ -122,7 +123,7 @@
@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 @@
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 @@
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 @@
@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 @@
@Override
public float getMaxLightRevealScrimAlpha() {
return mWakeLockScreenSensorActive ? ScrimController.WAKE_SENSOR_SCRIM_ALPHA
- : AOD.getMaxLightRevealScrimAlpha();
+ : AOD.getMaxLightRevealScrimAlpha();
}
},
@@ -245,7 +246,6 @@
mBehindAlpha = mClipQsScrim ? 1 : 0;
mNotifAlpha = 0;
mFrontAlpha = 0;
-
mAnimationDuration = mKeyguardFadingAway
? mKeyguardFadingAwayDuration
: CentralSurfaces.FADE_KEYGUARD_DURATION;
@@ -259,22 +259,22 @@
&& !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 @@
@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 @@
mBlankScreen = false;
if (mClipQsScrim) {
- updateScrimColor(mScrimBehind, 1f /* alpha */, Color.BLACK);
+ updateScrimColor(mScrimBehind, 1f /* alpha */, mBackgroundColor);
}
}
};
@@ -327,9 +327,11 @@
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 88347ab..4c83ca2 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.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 @@
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 @@
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 0000000..693a835
--- /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 c587f2e..5150389 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 @@
* ```
*/
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 0000000..8d5e55a
--- /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 b1bfbe0..c842e5f 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 @@
/** 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 9bcab57..9087816 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 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 @@
@TestableLooper.RunWithLooper
public class DragToInteractAnimationControllerTest extends SysuiTestCase {
private DragToInteractAnimationController mDragToInteractAnimationController;
+ private DragToInteractView mInteractView;
private DismissView mDismissView;
@Rule
@@ -57,29 +62,72 @@
@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 215f93d..e0df1e0 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 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 @@
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 9c8de30..c2ed7d4 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 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.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.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
+import java.util.concurrent.atomic.AtomicBoolean;
+
/** Tests for {@link MenuItemAccessibilityDelegate}. */
@SmallTest
@TestableLooper.RunWithLooper
@@ -59,17 +66,16 @@
@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 @@
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 @@
}
@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 @@
@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 e1522f5..9e8c6b3 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.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.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 @@
private DragToInteractAnimationController mDragToInteractAnimationController;
private RecyclerView mStubListView;
private DismissView mDismissView;
+ private DragToInteractView mInteractView;
@Rule
public MockitoRule mockito = MockitoJUnit.rule();
@@ -81,19 +86,28 @@
@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 @@
@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 @@
}
@Test
+ @DisableFlags(Flags.FLAG_FLOATING_MENU_DRAG_TO_EDIT)
public void onActionMoveEvent_shouldShowDismissView() {
final int offset = 100;
final MotionEvent stubDownEvent =
@@ -154,6 +169,25 @@
}
@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 bc9a0a5..4a1bdbc 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerTest.java
@@ -30,6 +30,7 @@
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;
@@ -72,6 +73,8 @@
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 +84,7 @@
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 +126,17 @@
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 +148,16 @@
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();
@@ -236,6 +247,27 @@
}
@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 +339,13 @@
@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 +353,10 @@
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 +370,10 @@
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 +449,12 @@
});
}
- 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 8da6cf9..7c97f53 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 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 @@
@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 @@
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 10c8caa..8399fa8 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 android.os.SystemClock;
+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 @@
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 a47e288..7c03d78 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.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.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.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 @@
@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 @@
@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 @@
@Before
fun setUp() {
+ mSetFlagsRule.disableFlags(Flags.FLAG_DEVICE_ENTRY_UDFPS_REFACTOR)
MockitoAnnotations.initMocks(this)
staticMockSession = mockitoSession()
.mockStatic(RotationUtils::class.java)
@@ -128,6 +131,7 @@
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 8127bb1..6a9c881 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 @@
@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 @@
@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
similarity index 95%
rename from packages/SystemUI/tests/src/com/android/systemui/deviceentry/data/repository/DeviceEntryHapticsInteractorTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractorTest.kt
index 0dfdeca..bdf0e06 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.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 @@
}
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 0000000..ed80a86
--- /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 8a71368..6eebb6d 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 @@
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.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 @@
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 @@
val stickyKeysRepository = StickyKeysRepositoryImpl(
inputManager,
dispatcher,
+ secureSettings,
+ userRepository,
mock<StickyKeysLogger>()
)
+ setStickyKeySetting(enabled = false)
viewModel =
StickyKeysIndicatorViewModel(
stickyKeysRepository = stickyKeysRepository,
@@ -76,15 +85,26 @@
}
@Test
- fun startsListeningToStickyKeysOnlyWhenKeyboardIsConnected() {
+ fun doesntListenToStickyKeysOnlyWhenKeyboardIsConnected() {
testScope.runTest {
collectLastValue(viewModel.indicatorContent)
- runCurrent()
- verifyZeroInteractions(inputManager)
keyboardRepository.setIsAnyKeyboardConnected(true)
runCurrent()
+ verifyZeroInteractions(inputManager)
+ }
+ }
+
+ @Test
+ fun startsListeningToStickyKeysOnlyWhenKeyboardIsConnectedAndSettingIsOn() {
+ testScope.runTest {
+ collectLastValue(viewModel.indicatorContent)
+ keyboardRepository.setIsAnyKeyboardConnected(true)
+
+ setStickyKeySetting(enabled = true)
+ runCurrent()
+
verify(inputManager)
.registerStickyModifierStateListener(
any(),
@@ -93,11 +113,31 @@
}
}
+ 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 @@
fun emitsStickyKeysListWhenStickyKeyIsPressed() {
testScope.runTest {
val stickyKeys by collectLastValue(viewModel.indicatorContent)
- keyboardRepository.setIsAnyKeyboardConnected(true)
+ setStickyKeysActive()
setStickyKeys(mapOf(ALT to false))
@@ -123,7 +163,7 @@
fun emitsEmptyListWhenNoStickyKeysAreActive() {
testScope.runTest {
val stickyKeys by collectLastValue(viewModel.indicatorContent)
- keyboardRepository.setIsAnyKeyboardConnected(true)
+ setStickyKeysActive()
setStickyKeys(emptyMap())
@@ -135,7 +175,7 @@
fun passesAllStickyKeysToDialog() {
testScope.runTest {
val stickyKeys by collectLastValue(viewModel.indicatorContent)
- keyboardRepository.setIsAnyKeyboardConnected(true)
+ setStickyKeysActive()
setStickyKeys(mapOf(
ALT to false,
@@ -154,7 +194,7 @@
fun showsOnlyLockedStateIfKeyIsStickyAndLocked() {
testScope.runTest {
val stickyKeys by collectLastValue(viewModel.indicatorContent)
- keyboardRepository.setIsAnyKeyboardConnected(true)
+ setStickyKeysActive()
setStickyKeys(mapOf(
ALT to false,
@@ -168,7 +208,7 @@
fun doesNotChangeOrderOfKeysIfTheyBecomeLocked() {
testScope.runTest {
val stickyKeys by collectLastValue(viewModel.indicatorContent)
- keyboardRepository.setIsAnyKeyboardConnected(true)
+ setStickyKeysActive()
setStickyKeys(mapOf(
META to false,
@@ -186,6 +226,11 @@
}
}
+ 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 8a3a434..1183964 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.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 @@
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);
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 4f3a63d..e93ad0be3 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 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.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 @@
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 @@
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 c864704..699284e 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.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 @@
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/gestural/BackPanelControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/gestural/BackPanelControllerTest.kt
index f93d52b..aa54565 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.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.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 @@
@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 @@
vibratorHelper,
configurationController,
latencyTracker,
+ interactionJankMonitor,
)
mBackPanelController.setLayoutParams(layoutParams)
mBackPanelController.setBackCallback(backCallback)
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 0000000..e8aa8f0
--- /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 c7479fd5..1ed8c3c 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.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 @@
@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 @@
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 70a48f5..fdbba90 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/recents/OverviewProxyServiceTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/recents/OverviewProxyServiceTest.kt
@@ -27,6 +27,7 @@
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
@@ -109,6 +110,8 @@
@Mock
private lateinit var unfoldTransitionProgressForwarder:
Optional<UnfoldTransitionProgressForwarder>
+ @Mock
+ private lateinit var broadcastDispatcher: BroadcastDispatcher
@Before
fun setUp() {
@@ -131,7 +134,10 @@
whenever(packageManager.resolveServiceAsUser(any(), anyInt(), anyInt()))
.thenReturn(mock(ResolveInfo::class.java))
- featureFlags.set(Flags.KEYGUARD_WM_STATE_REFACTOR, false)
+ mSetFlagsRule.disableFlags(
+ com.android.systemui.Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR,
+ )
+
subject =
OverviewProxyService(
context,
@@ -155,7 +161,8 @@
featureFlags,
FakeSceneContainerFlags(),
dumpManager,
- unfoldTransitionProgressForwarder
+ unfoldTransitionProgressForwarder,
+ broadcastDispatcher
)
}
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 58eec2e..4519ba6 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.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 @@
@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 @@
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 f9f8d8a..73c49c0 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 @@
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.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.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 @@
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 @@
secureSettings,
lockscreenUserManager,
sectionStyleProvider,
- userTracker
+ userTracker,
+ groupMembershipManager,
)
@Before
@@ -127,4 +135,42 @@
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 b0996ad..a0d1075 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 @@
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 @@
.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 0000000..1c959af
--- /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 0000000..f0fc349
--- /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 0000000..b67153a
--- /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 1ab4c32..dbe63f2 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 @@
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 @@
/* 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 @@
/* 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 @@
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 06298b7..32c727c 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.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.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 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 @@
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 @@
}
@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 @@
@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/StatusBarKeyguardViewManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java
index 8dde935..cb45315 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 @@
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 0e0d489..5b5819d 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 @@
}
@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/accessibility/data/repository/FakeAccessibilityQsShortcutsRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/accessibility/data/repository/FakeAccessibilityQsShortcutsRepository.kt
new file mode 100644
index 0000000..e547da1
--- /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/communal/data/repository/FakeCommunalWidgetRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalWidgetRepository.kt
index e25e8c0..bc7e7af 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 @@
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/deviceentry/data/repository/FakeDeviceEntryRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/data/repository/FakeDeviceEntryRepository.kt
index 6436a38..77caeaa 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 @@
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 0000000..3070cf4
--- /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 de58ae5..878e385 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 @@
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 8dcdd3a..0d1a31f 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.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 0000000..0b9ec92
--- /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 5766f7a..793e2d7 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 @@
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 @@
_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 35cfa89..a8f45b0 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.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 5ceefde..73fd999 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.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 @@
setOf<DeviceEntryIconTransition>(fakeDeviceEntryIconViewModelTransition)
}
+@ExperimentalCoroutinesApi
val Kosmos.deviceEntryIconViewModel by Fixture {
DeviceEntryIconViewModel(
transitions = deviceEntryIconViewModelTransitionsMock,
@@ -46,5 +49,6 @@
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 db40509..7c398cd 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 @@
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 @@
occludedToLockscreenTransitionViewModel = occludedToLockscreenTransitionViewModel,
lockscreenToOccludedTransitionViewModel = lockscreenToOccludedTransitionViewModel,
glanceableHubToLockscreenTransitionViewModel = glanceableHubToLockscreenTransitionViewModel,
- lockscreenToGlanceableHubTransitionViewModel = lockscreenToGlanceableHubTransitionViewModel
+ lockscreenToGlanceableHubTransitionViewModel = lockscreenToGlanceableHubTransitionViewModel,
+ aodBurnInViewModel = aodBurnInViewModel,
)
}
diff --git a/ravenwood/framework-minus-apex-ravenwood-policies.txt b/ravenwood/framework-minus-apex-ravenwood-policies.txt
index e33fff1..16f99e9 100644
--- a/ravenwood/framework-minus-apex-ravenwood-policies.txt
+++ b/ravenwood/framework-minus-apex-ravenwood-policies.txt
@@ -138,6 +138,9 @@
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/ravenwood-annotation-allowed-classes.txt b/ravenwood/ravenwood-annotation-allowed-classes.txt
index 5700f00..eaf01a3 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.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.UidBatteryConsumer
android.os.UidBatteryConsumer$Builder
android.os.UserHandle
+android.os.UserManager
android.os.WorkSource
android.content.ClipData
@@ -89,16 +92,22 @@
android.content.IntentFilter
android.content.UriMatcher
-android.content.pm.PackageInfo
-android.content.pm.ApplicationInfo
-android.content.pm.PackageItemInfo
-android.content.pm.ComponentInfo
android.content.pm.ActivityInfo
-android.content.pm.ServiceInfo
+android.content.pm.ApplicationInfo
+android.content.pm.ComponentInfo
+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.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/autofill/java/com/android/server/autofill/RemoteFillService.java b/services/autofill/java/com/android/server/autofill/RemoteFillService.java
index 5c93991..f914ed5 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.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 @@
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 @@
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 @@
}
}
+ 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 @@
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 @@
}));
}
+ 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 1234703..0af703e 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.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 @@
}
+ @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 f3b74ea..049feee 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_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_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.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 @@
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 4022e33..1416c88 100644
--- a/services/backup/flags.aconfig
+++ b/services/backup/flags.aconfig
@@ -11,7 +11,7 @@
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 37c47ba..ae6d591 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 2e01ced..5019428 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 @@
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 3482863..aac628c 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 @@
* 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 @@
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 @@
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 7eca119..c514f3e 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.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 @@
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 3b9d92d..8962bf0 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 @@
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 @@
// 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 @@
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 @@
}
}
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 @@
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 @@
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 @@
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 @@
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 @@
/** 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 a54a48a..8e35b74 100644
--- a/services/core/Android.bp
+++ b/services/core/Android.bp
@@ -139,6 +139,7 @@
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 @@
],
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/AppBatteryExemptionTracker.java b/services/core/java/com/android/server/am/AppBatteryExemptionTracker.java
index b07d9a6..9c2e69b 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 @@
/**
* 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/audio/AudioDeviceInventory.java b/services/core/java/com/android/server/audio/AudioDeviceInventory.java
index 57b19cd..690c37a 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 @@
di.mDeviceCodecFormat = codec;
mConnectedDevices.replace(key, di);
codecChange = true;
- }
- final int res = mAudioSystem.handleDeviceConfigChange(
- btInfo.mAudioSystemDevice, address, BtHelper.getName(btDevice), codec);
+ 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));
- 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));
-
+ // 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 a30cdc4..9610034ca 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 @@
@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 21e6bac..3f3540e 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 @@
// 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 @@
if (promptInfo.containsPrivateApiConfigurations()) {
checkInternalPermission();
}
- if (promptInfo.containsManageBioApiConfigurations()) {
+ if (promptInfo.containsSetLogoApiConfigurations()) {
checkManageBiometricPermission();
}
@@ -997,8 +997,8 @@
}
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 9cf9119..245fcea 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 @@
}
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 380106b..b963a4b 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 @@
* 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 @@
*
* 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 67c23fc..687def0 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 @@
}
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 @@
}
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 @@
}
@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 @@
}
/**
+ * 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 572d844..165dfe4 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 @@
(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 @@
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 8aec8ca..bc82078 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 @@
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 @@
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 @@
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 @@
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 21b952b..ece236a 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.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 @@
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 0000000..e98a5a7
--- /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 50340d2..4767ebd 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.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.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 @@
@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 @@
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 @@
/**
* 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 @@
@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 @@
// 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 @@
+ "specified for cross-user startInputOrWindowGainedFocus()");
}
}
-
if (windowToken == null) {
Slog.e(TAG, "windowToken cannot be null.");
return InputBindResult.NULL;
@@ -3686,6 +3665,14 @@
"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 @@
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 5e38bca..2522f7b 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 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 @@
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 2cd3ab1..1d516e2 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 @@
}
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 @@
}
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 @@
}
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 df612e6..bbe6d3a 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.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 @@
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 c067fa0..923be56d 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 @@
!= 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 1660c3e..8452c0e 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 @@
@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 @@
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 0000000..baa41a5
--- /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 84324f2..c8bc56c 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 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 18caafd..f3b1464 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.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 @@
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 a97652c..7e3254d 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 @@
}
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 a6598d6..c0596bb 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 @@
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 @@
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 @@
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 @@
@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 @@
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 14db70e..23d0230 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 @@
* 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 @@
.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 f0ff85d..dd2b409 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 @@
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 d0fe964..6ed2d31 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 @@
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 0abf304..1fdcc64 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 @@
TalkbackShortcutController getTalkbackShortcutController() {
return new TalkbackShortcutController(mContext);
}
+
+ WindowWakeUpPolicy getWindowWakeUpPolicy() {
+ return new WindowWakeUpPolicy(mContext);
+ }
}
/** {@inheritDoc} */
@@ -2433,7 +2437,7 @@
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 0000000..e89ddfd
--- /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 0000000..599b840
--- /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 0000000..8d8d596
--- /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 0000000..0219645
--- /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 0000000..8a661bc
--- /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 d7b8495..b6d0ca1 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 @@
}
@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 @@
}
@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 19fd9a9..9e1b5d2 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 @@
}
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 2d584c4..f2d9bf8 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 @@
}
}
- // 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 @@
.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 @@
}
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 @@
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 @@
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 @@
@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 @@
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 @@
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 @@
@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 3d492bb..03d55d9 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.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 @@
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 @@
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 a4d15e0..83ccbdc 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 @@
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 @@
// 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 @@
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 @@
}
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 @@
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 9ac4a5c..5423c66 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 @@
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 @@
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 @@
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 e7ecf52..5dcd335 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.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.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.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.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 @@
}
/**
- * 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 @@
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 f279689..0e2d3d1 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 @@
@VisibleForTesting
boolean shouldShowLetterboxUi(WindowState mainWindow) {
- if (mIsRelaunchingAfterRequestedOrientationChanged || !isSurfaceReadyToShow(mainWindow)) {
+ if (mIsRelaunchingAfterRequestedOrientationChanged) {
return mLastShouldShowLetterboxUi;
}
@@ -1442,13 +1442,6 @@
}
@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 10405ec..e027eb6 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 @@
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 @@
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 f10a733..083872a 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 @@
}
@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 @@
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 0c6b174..b2b547e 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_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.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.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 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 @@
/** 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 @@
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 @@
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 @@
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 59e3350..d7b4a39 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 @@
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 @@
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 0fc62a7..6949a87 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 @@
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 @@
}
}
- 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 750fd50..b43a454 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 @@
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 9650b8bc..426694d 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 @@
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 @@
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 4ba52e4..3f889c0 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 @@
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 @@
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 @@
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 7ad87ed..24e50c5 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.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 @@
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 8bc41af..cbc301b 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 @@
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 @@
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 @@
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 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 @@
{"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 f288103..519c9bb 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_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.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 @@
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 86ad494..2b8bcc7 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.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 @@
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 @@
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 @@
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 24d4952..3284cf1 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 @@
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 @@
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 30afa72..b9f1ea0 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 @@
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 @@
}
@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 @@
}
@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 @@
}
@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 @@
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 438bea4..1c71a62 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 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 3aca1ca..f8accc3 100644
--- a/services/tests/PackageManagerServiceTests/server/Android.bp
+++ b/services/tests/PackageManagerServiceTests/server/Android.bp
@@ -103,6 +103,7 @@
":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 71f5c75..a0e0e1e 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 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.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.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 @@
@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 @@
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 @@
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/job/controllers/FlexibilityControllerTest.java b/services/tests/mockingservicestests/src/com/android/server/job/controllers/FlexibilityControllerTest.java
index 93a2eef..28471b3 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 @@
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 293391f..c6608e6 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.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 java.time.Clock;
import java.time.ZoneOffset;
+import java.util.Arrays;
@RunWith(AndroidJUnit4.class)
public class JobStatusTest {
@@ -138,6 +140,35 @@
}
@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 @@
}
@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 8d9a6c5..9a143d5 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 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 @@
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 4eba219..efab19c 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 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 @@
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 5bec903..656bc71 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 @@
@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 0000000..01c7fbe
--- /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 0000000..b36c9bd
--- /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 0000000..9758ea5
--- /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 ccbbaa5..5943832 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.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 @@
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 @@
@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 @@
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 @@
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 9ff29d2..5442af8 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 @@
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 @@
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 @@
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 @@
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 3e4f1df..81981e6 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 @@
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 dc1d2c5..1c6d36b 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.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 @@
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 @@
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 @@
new BugreportManagerServiceImpl.Injector(mContext, mAllowlistedPackages,
mMappingFile));
mBugreportFileManager = new BugreportManagerServiceImpl.BugreportFileManager(mMappingFile);
+ when(mPackageManager.getPackageUidAsUser(anyString(), anyInt())).thenReturn(mCallingUid);
}
@After
@@ -115,12 +125,13 @@
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 @@
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 @@
// 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 @@
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 d7ed7c2..8d8dc9c 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 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 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 @@
@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 @@
.setDeleteAppWithParent(false)
.setAlwaysVisible(false)
.setCrossProfileContentSharingStrategy(0)
+ .setProfileApiVisibility(34)
.build();
final UserProperties actualProps = new UserProperties(defaultProps);
actualProps.setShowInLauncher(14);
@@ -90,6 +96,7 @@
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 @@
/** 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 @@
/** 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 @@
.setAuthAlwaysRequiredToDisableQuietMode(false)
.setAllowStoppingUserWithDelayedLocking(false)
.setAlwaysVisible(true)
+ .setProfileApiVisibility(110)
.build();
final UserProperties orig = new UserProperties(defaultProps);
orig.setShowInLauncher(2841);
@@ -209,6 +219,8 @@
copy::isCredentialShareableWithParent, true);
assertEqualGetterOrThrows(orig::getCrossProfileContentSharingStrategy,
copy::getCrossProfileContentSharingStrategy, true);
+ assertEqualGetterOrThrows(orig::getProfileApiVisibility, copy::getProfileApiVisibility,
+ true);
}
/**
@@ -270,5 +282,6 @@
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 7083706..1ee604e 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.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 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 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 @@
.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 @@
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 @@
@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 @@
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 @@
/** 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 @@
.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 @@
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 @@
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 a743fff..db561c4 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 @@
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.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 org.junit.After;
import org.junit.Before;
+import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -95,6 +99,8 @@
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 @@
@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 @@
.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 @@
@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 @@
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 @@
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 3e78f9a..131b380 100644
--- a/services/tests/servicestests/test-apps/PackageParserApp/Android.bp
+++ b/services/tests/servicestests/test-apps/PackageParserApp/Android.bp
@@ -102,3 +102,17 @@
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 0000000..cb87a48
--- /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 0e20daf..99d5a6d 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 @@
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 @@
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 e523e79f..539bb37 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 @@
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 @@
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 25ad7db..227265a 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 @@
// 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 @@
// 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 57e1132..3a88294 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 @@
// 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 @@
// 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 @@
// 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 @@
// 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 @@
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 @@
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 @@
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 @@
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 7c2f7ee..c8abd8d 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 @@
@Mock
private PhoneWindowManager.ButtonOverridePermissionChecker mButtonOverridePermissionChecker;
+ @Mock private WindowWakeUpPolicy mWindowWakeUpPolicy;
@Mock private IBinder mInputToken;
@Mock private IBinder mImeTargetWindowToken;
@@ -230,6 +231,10 @@
TalkbackShortcutController getTalkbackShortcutController() {
return new TestTalkbackShortcutController(mContext);
}
+
+ WindowWakeUpPolicy getWindowWakeUpPolicy() {
+ return mWindowWakeUpPolicy;
+ }
}
TestPhoneWindowManager(Context context, boolean supportSettingsUpdate) {
@@ -620,7 +625,8 @@
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/SizeCompatTests.java b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
index 752dc5e8..0c1a9c3 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 @@
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 51df1d4..7d8eb90 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 @@
}
@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 d255271..e9ece5d 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.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 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 @@
@Presubmit
@RunWith(WindowTestRunner.class)
public class WindowContainerTransactionTests extends WindowTestsBase {
-
@Test
public void testRemoveTask() {
final Task rootTask = createTask(mDisplayContent);
@@ -72,6 +78,123 @@
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 @@
}
}
}
+
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 fe9d837..06afa38 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 @@
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 @@
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 a0bafb6..be83744 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.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 @@
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 @@
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 @@
}
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 @@
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/TelecomManager.java b/telecomm/java/android/telecom/TelecomManager.java
index e81f482..2c6e1e4 100644
--- a/telecomm/java/android/telecom/TelecomManager.java
+++ b/telecomm/java/android/telecom/TelecomManager.java
@@ -1360,7 +1360,7 @@
/**
* 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 @@
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/TelephonyManager.java b/telephony/java/android/telephony/TelephonyManager.java
index cbd5524..c1ceaef 100644
--- a/telephony/java/android/telephony/TelephonyManager.java
+++ b/telephony/java/android/telephony/TelephonyManager.java
@@ -25,6 +25,7 @@
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 @@
* 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;
- private boolean mCollectTelecomDumpSys;
- private boolean mCollectTelephonyDumpsys;
- private boolean mCollectLogcat;
+ // 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;
- //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
+ /**
+ * 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;
+ }
+
+ /**
+ * 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);
+ }
+ }
+
+ private boolean mCollectTelecomDumpSys;
+ private boolean mCollectTelephonyDumpsys;
+ private boolean mCollectLogcat;
private long mLogcatStartTimeMillis;
+ private static long sUnsetLogcatStartTime = -1L;
+
+ private EmergencyCallDiagnosticParams(boolean collectTelecomDumpSys,
+ boolean collectTelephonyDumpsys, long logcatStartTimeMillis) {
+ mCollectTelecomDumpSys = collectTelecomDumpSys;
+ mCollectTelephonyDumpsys = collectTelephonyDumpsys;
+ mLogcatStartTimeMillis = logcatStartTimeMillis;
+ mCollectLogcat = logcatStartTimeMillis != sUnsetLogcatStartTime;
+ }
public boolean isTelecomDumpSysCollectionEnabled() {
return mCollectTelecomDumpSys;
}
- public void setTelecomDumpSysCollection(boolean collectTelecomDumpSys) {
- mCollectTelecomDumpSys = collectTelecomDumpSys;
- }
-
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 @@
* 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 @@
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 70047a6..a1ac477 100644
--- a/telephony/java/android/telephony/satellite/SatelliteManager.java
+++ b/telephony/java/android/telephony/satellite/SatelliteManager.java
@@ -34,7 +34,6 @@
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 @@
@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 @@
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 @@
}
} catch (RemoteException ex) {
Rlog.e(TAG, "requestSatelliteEnabled() RemoteException: ", ex);
- ex.rethrowFromSystemServer();
+ ex.rethrowAsRuntimeException();
}
}
@@ -526,7 +532,6 @@
* 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 @@
};
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 @@
* 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 @@
};
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 @@
* 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 @@
};
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 @@
* 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 @@
};
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 @@
* @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 @@
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 @@
* @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 @@
() -> 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 @@
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 @@
* @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 @@
};
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 @@
}
} catch (RemoteException ex) {
loge("registerForSatelliteProvisionStateChanged() RemoteException: " + ex);
- ex.rethrowFromSystemServer();
+ ex.rethrowAsRuntimeException();
}
return SATELLITE_RESULT_REQUEST_FAILED;
}
@@ -1244,7 +1250,7 @@
}
} catch (RemoteException ex) {
loge("unregisterForSatelliteProvisionStateChanged() RemoteException: " + ex);
- ex.rethrowFromSystemServer();
+ ex.rethrowAsRuntimeException();
}
}
@@ -1260,7 +1266,6 @@
* 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 @@
};
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 @@
}
} catch (RemoteException ex) {
loge("registerForSatelliteModemStateChanged() RemoteException:" + ex);
- ex.rethrowFromSystemServer();
+ ex.rethrowAsRuntimeException();
}
return SATELLITE_RESULT_REQUEST_FAILED;
}
@@ -1376,7 +1382,7 @@
}
} catch (RemoteException ex) {
loge("unregisterForSatelliteModemStateChanged() RemoteException:" + ex);
- ex.rethrowFromSystemServer();
+ ex.rethrowAsRuntimeException();
}
}
@@ -1436,7 +1442,7 @@
}
} catch (RemoteException ex) {
loge("registerForSatelliteDatagram() RemoteException:" + ex);
- ex.rethrowFromSystemServer();
+ ex.rethrowAsRuntimeException();
}
return SATELLITE_RESULT_REQUEST_FAILED;
}
@@ -1471,7 +1477,7 @@
}
} catch (RemoteException ex) {
loge("unregisterForSatelliteDatagram() RemoteException:" + ex);
- ex.rethrowFromSystemServer();
+ ex.rethrowAsRuntimeException();
}
}
@@ -1488,7 +1494,6 @@
* @param resultListener Listener for the {@link SatelliteResult} result of the operation.
*
* @throws SecurityException if the caller doesn't have required permission.
- * @throws IllegalStateException if the Telephony process is not currently available.
*/
@RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION)
@FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
@@ -1509,11 +1514,12 @@
};
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 @@
* @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 @@
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 @@
* 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 @@
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 @@
* 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 @@
};
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 @@
}
} catch (RemoteException ex) {
loge("informDeviceAlignedToSatellite() RemoteException:" + ex);
- ex.rethrowFromSystemServer();
+ ex.rethrowAsRuntimeException();
}
}
@@ -1727,7 +1733,7 @@
* <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 @@
* @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 @@
* @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 @@
};
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 @@
* @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 @@
};
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 @@
}
} catch (RemoteException ex) {
loge("getSatelliteAttachRestrictionReasonsForCarrier() RemoteException: " + ex);
- ex.rethrowFromSystemServer();
+ ex.rethrowAsRuntimeException();
}
return new HashSet<>();
}
@@ -1932,11 +1937,12 @@
* 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 @@
};
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 @@
*
* @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 @@
} 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 @@
}
} catch (RemoteException ex) {
loge("unregisterForNtnSignalStrengthChanged() RemoteException: " + ex);
- ex.rethrowFromSystemServer();
+ ex.rethrowAsRuntimeException();
}
}
@@ -2113,7 +2116,7 @@
}
} catch (RemoteException ex) {
loge("registerForSatelliteCapabilitiesChanged() RemoteException: " + ex);
- ex.rethrowFromSystemServer();
+ ex.rethrowAsRuntimeException();
}
return SATELLITE_RESULT_REQUEST_FAILED;
}
@@ -2149,7 +2152,7 @@
}
} catch (RemoteException ex) {
loge("unregisterForSatelliteCapabilitiesChanged() RemoteException: " + ex);
- ex.rethrowFromSystemServer();
+ ex.rethrowAsRuntimeException();
}
}
@@ -2177,7 +2180,7 @@
}
} 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 b7712bd..655da740 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 256a469..566e51a 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.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 @@
@get:Rule
val fakeSettingsProviderRule = FakeSettingsProvider.rule()!!
+ @get:Rule
+ val setFlagsRule = SetFlagsRule()
+
@Mock
private lateinit var native: NativeInputManagerService
@@ -170,6 +174,8 @@
@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 @@
@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 @@
@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 @@
@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 @@
@Test
fun updateAdditionalInputPropertiesForOverrideDisplay() {
+ setFlagsRule.disableFlags(com.android.input.flags.Flags.FLAG_ENABLE_POINTER_CHOREOGRAPHER)
+
setVirtualMousePointerDisplayIdAndVerify(10)
localService.setPointerIconVisible(false, 10)
@@ -341,6 +355,8 @@
@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 5aaf30a..14230fe 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 @@
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 @@
@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 e5f8f47..7330ec1 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 @@
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 @@
@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 @@
}
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 @@
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 @@
@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 0b16e2c..d03f97e 100644
--- a/tools/aapt2/link/ManifestFixer.cpp
+++ b/tools/aapt2/link/ManifestFixer.cpp
@@ -415,6 +415,8 @@
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;