Merge "Sticky keys should not capture focus" into main
diff --git a/AconfigFlags.bp b/AconfigFlags.bp
index 98b62b3..0ee7ace 100644
--- a/AconfigFlags.bp
+++ b/AconfigFlags.bp
@@ -36,7 +36,9 @@
":android.location.flags-aconfig-java{.generated_srcjars}",
":android.media.tv.flags-aconfig-java{.generated_srcjars}",
":android.multiuser.flags-aconfig-java{.generated_srcjars}",
+ ":android.net.platform.flags-aconfig-java{.generated_srcjars}",
":android.net.vcn.flags-aconfig-java{.generated_srcjars}",
+ ":android.net.wifi.flags-aconfig-java{.generated_srcjars}",
":android.nfc.flags-aconfig-java{.generated_srcjars}",
":android.os.flags-aconfig-java{.generated_srcjars}",
":android.os.vibrator.flags-aconfig-java{.generated_srcjars}",
@@ -60,13 +62,13 @@
":android.webkit.flags-aconfig-java{.generated_srcjars}",
":android.widget.flags-aconfig-java{.generated_srcjars}",
":audio-framework-aconfig",
+ ":backup_flags_lib{.generated_srcjars}",
":camera_platform_flags_core_java_lib{.generated_srcjars}",
":com.android.hardware.input-aconfig-java{.generated_srcjars}",
":com.android.input.flags-aconfig-java{.generated_srcjars}",
":com.android.internal.foldables.flags-aconfig-java{.generated_srcjars}",
":com.android.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}",
@@ -108,7 +110,9 @@
"android.media.midi-aconfig",
"android.media.tv.flags-aconfig",
"android.multiuser.flags-aconfig",
+ "android.net.platform.flags-aconfig",
"android.net.vcn.flags-aconfig",
+ "android.net.wifi.flags-aconfig",
"android.nfc.flags-aconfig",
"android.os.flags-aconfig",
"android.os.vibrator.flags-aconfig",
@@ -136,7 +140,6 @@
"com.android.hardware.input.input-aconfig",
"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",
@@ -775,9 +778,10 @@
// Networking
aconfig_declarations {
- name: "com.android.net.flags-aconfig",
- package: "com.android.net.flags",
+ name: "android.net.platform.flags-aconfig",
+ package: "android.net.platform.flags",
srcs: ["core/java/android/net/flags.aconfig"],
+ visibility: [":__subpackages__"],
}
// Thread network
@@ -788,9 +792,10 @@
}
java_aconfig_library {
- name: "com.android.net.flags-aconfig-java",
- aconfig_declarations: "com.android.net.flags-aconfig",
+ name: "android.net.platform.flags-aconfig-java",
+ aconfig_declarations: "android.net.platform.flags-aconfig",
defaults: ["framework-minus-apex-aconfig-java-defaults"],
+ visibility: [":__subpackages__"],
}
java_aconfig_library {
@@ -1092,4 +1097,29 @@
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
+}
+
+// Backup
+java_aconfig_library {
+ name: "backup_flags_lib",
+ aconfig_declarations: "backup_flags",
+ defaults: ["framework-minus-apex-aconfig-java-defaults"],
+}
+
+// Wifi
+aconfig_declarations {
+ name: "android.net.wifi.flags-aconfig",
+ package: "android.net.wifi.flags",
+ srcs: ["wifi/*.aconfig"],
+}
+
+java_aconfig_library {
+ name: "android.net.wifi.flags-aconfig-java",
+ aconfig_declarations: "android.net.wifi.flags-aconfig",
+ min_sdk_version: "30",
+ apex_available: [
+ "//apex_available:platform",
+ "com.android.wifi",
+ ],
+ defaults: ["framework-minus-apex-aconfig-java-defaults"],
+}
diff --git a/OWNERS b/OWNERS
index 733157f..935b768 100644
--- a/OWNERS
+++ b/OWNERS
@@ -39,3 +39,5 @@
per-file *Ravenwood* = file:ravenwood/OWNERS
per-file PERFORMANCE_OWNERS = file:/PERFORMANCE_OWNERS
+
+per-file PACKAGE_MANAGER_OWNERS = file:/PACKAGE_MANAGER_OWNERS
\ No newline at end of file
diff --git a/PACKAGE_MANAGER_OWNERS b/PACKAGE_MANAGER_OWNERS
index e4549b4..eb5842b 100644
--- a/PACKAGE_MANAGER_OWNERS
+++ b/PACKAGE_MANAGER_OWNERS
@@ -1,3 +1,3 @@
-chiuwinson@google.com
+alexbuy@google.com
patb@google.com
schfan@google.com
\ No newline at end of file
diff --git a/apct-tests/perftests/core/src/android/input/MotionPredictorBenchmark.kt b/apct-tests/perftests/core/src/android/input/MotionPredictorBenchmark.kt
index aadbc23..add0a08 100644
--- a/apct-tests/perftests/core/src/android/input/MotionPredictorBenchmark.kt
+++ b/apct-tests/perftests/core/src/android/input/MotionPredictorBenchmark.kt
@@ -16,8 +16,6 @@
package android.input
-import android.content.Context
-import android.content.res.Resources
import android.os.SystemProperties
import android.perftests.utils.PerfStatusReporter
import android.view.InputDevice
@@ -38,8 +36,6 @@
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
-import org.mockito.Mockito.mock
-import org.mockito.Mockito.`when`
import java.time.Duration
@@ -68,18 +64,6 @@
InputDevice.SOURCE_STYLUS, /*flags=*/0)
}
-private fun getPredictionContext(offset: Duration, enablePrediction: Boolean): Context {
- val context = mock(Context::class.java)
- val resources: Resources = mock(Resources::class.java)
- `when`(context.getResources()).thenReturn(resources)
- `when`(resources.getInteger(
- com.android.internal.R.integer.config_motionPredictionOffsetNanos)).thenReturn(
- offset.toNanos().toInt())
- `when`(resources.getBoolean(
- com.android.internal.R.bool.config_enableMotionPrediction)).thenReturn(enablePrediction)
- return context
-}
-
@RunWith(AndroidJUnit4::class)
@LargeTest
class MotionPredictorBenchmark {
@@ -115,7 +99,7 @@
var eventPosition = 0f
val positionInterval = 10f
- val predictor = MotionPredictor(getPredictionContext(offset, /*enablePrediction=*/true))
+ val predictor = MotionPredictor(/*isPredictionEnabled=*/true, offset.toNanos().toInt())
// ACTION_DOWN t=0 x=0 y=0
predictor.record(getStylusMotionEvent(
eventTime, ACTION_DOWN, /*x=*/eventPosition, /*y=*/eventPosition))
@@ -141,12 +125,11 @@
*/
@Test
fun timeCreatePredictor() {
- val context = getPredictionContext(
- /*offset=*/Duration.ofMillis(20), /*enablePrediction=*/true)
+ val offsetNanos = Duration.ofMillis(20).toNanos().toInt()
val state = perfStatusReporter.getBenchmarkState()
while (state.keepRunning()) {
- MotionPredictor(context)
+ MotionPredictor(/*isPredictionEnabled=*/true, offsetNanos)
}
}
}
diff --git a/apex/jobscheduler/framework/java/com/android/server/job/JobSchedulerInternal.java b/apex/jobscheduler/framework/java/com/android/server/job/JobSchedulerInternal.java
index 6c8af39..ae98fe1 100644
--- a/apex/jobscheduler/framework/java/com/android/server/job/JobSchedulerInternal.java
+++ b/apex/jobscheduler/framework/java/com/android/server/job/JobSchedulerInternal.java
@@ -77,6 +77,12 @@
@NonNull String notificationChannel, int userId, @NonNull String packageName);
/**
+ * @return {@code true} if the given package holds the
+ * {@link android.Manifest.permission.RUN_BACKUP_JOBS} permission.
+ */
+ boolean hasRunBackupJobsPermission(@NonNull String packageName, int packageUid);
+
+ /**
* Report a snapshot of sync-related jobs back to the sync manager
*/
JobStorePersistStats getPersistStats();
diff --git a/apex/jobscheduler/service/java/com/android/server/AppStateTrackerImpl.java b/apex/jobscheduler/service/java/com/android/server/AppStateTrackerImpl.java
index e08200b..33f6899 100644
--- a/apex/jobscheduler/service/java/com/android/server/AppStateTrackerImpl.java
+++ b/apex/jobscheduler/service/java/com/android/server/AppStateTrackerImpl.java
@@ -743,7 +743,8 @@
private final class AppOpsWatcher extends IAppOpsCallback.Stub {
@Override
- public void opChanged(int op, int uid, String packageName) throws RemoteException {
+ public void opChanged(int op, int uid, String packageName,
+ String persistentDeviceId) throws RemoteException {
boolean restricted = false;
try {
restricted = mAppOpsService.checkOperation(TARGET_OP,
diff --git a/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java b/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java
index abf8008..39de0af 100644
--- a/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java
+++ b/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java
@@ -2026,8 +2026,8 @@
iAppOpsService.startWatchingMode(AppOpsManager.OP_SCHEDULE_EXACT_ALARM, null,
new IAppOpsCallback.Stub() {
@Override
- public void opChanged(int op, int uid, String packageName)
- throws RemoteException {
+ public void opChanged(int op, int uid, String packageName,
+ String persistentDeviceId) throws RemoteException {
final int userId = UserHandle.getUserId(uid);
if (op != AppOpsManager.OP_SCHEDULE_EXACT_ALARM
|| !isExactAlarmChangeEnabled(packageName, userId)) {
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 fc193d8..57467e3 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
@@ -4197,6 +4197,11 @@
}
@Override
+ public boolean hasRunBackupJobsPermission(@NonNull String packageName, int packageUid) {
+ return JobSchedulerService.this.hasRunBackupJobsPermission(packageName, packageUid);
+ }
+
+ @Override
public JobStorePersistStats getPersistStats() {
synchronized (mLock) {
return new JobStorePersistStats(mJobs.getPersistStats());
@@ -4359,6 +4364,22 @@
}
/**
+ * Returns whether the app holds the {@link Manifest.permission.RUN_BACKUP_JOBS} permission.
+ */
+ private boolean hasRunBackupJobsPermission(@NonNull String packageName, int packageUid) {
+ if (packageName == null) {
+ Slog.wtfStack(TAG,
+ "Expected a non-null package name when calling hasRunBackupJobsPermission");
+ return false;
+ }
+
+ return PermissionChecker.checkPermissionForPreflight(getTestableContext(),
+ android.Manifest.permission.RUN_BACKUP_JOBS,
+ PermissionChecker.PID_UNKNOWN, packageUid, packageName)
+ == PermissionChecker.PERMISSION_GRANTED;
+ }
+
+ /**
* Binder stub trampoline implementation
*/
final class JobSchedulerStub extends IJobScheduler.Stub {
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 a4df5d8..2ea980d 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
@@ -1222,21 +1222,25 @@
return ACTIVE_INDEX;
}
- final int bucketWithMediaExemption;
- if (actualBucket != RESTRICTED_INDEX && actualBucket != NEVER_INDEX
- && mHasMediaBackupExemption) {
+ final boolean isEligibleAsBackupJob = job.getTriggerContentUris() != null
+ && job.getRequiredNetwork() != null
+ && !job.hasLateConstraint()
+ && mJobSchedulerInternal.hasRunBackupJobsPermission(sourcePackageName, sourceUid);
+ final boolean isBackupExempt = mHasMediaBackupExemption || isEligibleAsBackupJob;
+ final int bucketWithBackupExemption;
+ if (actualBucket != RESTRICTED_INDEX && actualBucket != NEVER_INDEX && isBackupExempt) {
// Treat it as if it's at most WORKING_INDEX (lower index grants higher quota) since
// media backup jobs are important to the user, and the source package may not have
// been used directly in a while.
- bucketWithMediaExemption = Math.min(WORKING_INDEX, actualBucket);
+ bucketWithBackupExemption = Math.min(WORKING_INDEX, actualBucket);
} else {
- bucketWithMediaExemption = actualBucket;
+ bucketWithBackupExemption = actualBucket;
}
// If the app is considered buggy, but hasn't yet been put in the RESTRICTED bucket
// (potentially because it's used frequently by the user), limit its effective bucket
// so that it doesn't get to run as much as a normal ACTIVE app.
- if (isBuggy && bucketWithMediaExemption < WORKING_INDEX) {
+ if (isBuggy && bucketWithBackupExemption < WORKING_INDEX) {
if (!mIsDowngradedDueToBuggyApp) {
// Safety check to avoid logging multiple times for the same job.
Counter.logIncrementWithUid(
@@ -1246,7 +1250,7 @@
}
return WORKING_INDEX;
}
- return bucketWithMediaExemption;
+ return bucketWithBackupExemption;
}
/** Returns the real standby bucket of the job. */
diff --git a/apex/jobscheduler/service/java/com/android/server/tare/InternalResourceService.java b/apex/jobscheduler/service/java/com/android/server/tare/InternalResourceService.java
index 357e1396..6635484 100644
--- a/apex/jobscheduler/service/java/com/android/server/tare/InternalResourceService.java
+++ b/apex/jobscheduler/service/java/com/android/server/tare/InternalResourceService.java
@@ -227,7 +227,7 @@
private final IAppOpsCallback mApbListener = new IAppOpsCallback.Stub() {
@Override
- public void opChanged(int op, int uid, String packageName) {
+ public void opChanged(int op, int uid, String packageName, String persistentDeviceId) {
boolean restricted = false;
try {
restricted = mAppOpsService.checkOperation(
diff --git a/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java b/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java
index e589c21..19bc716 100644
--- a/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java
+++ b/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java
@@ -671,7 +671,8 @@
/*packageName=*/ null,
new IAppOpsCallback.Stub() {
@Override
- public void opChanged(int op, int uid, String packageName) {
+ public void opChanged(int op, int uid, String packageName,
+ String persistentDeviceId) {
final int userId = UserHandle.getUserId(uid);
synchronized (mSystemExemptionAppOpMode) {
mSystemExemptionAppOpMode.delete(uid);
diff --git a/core/TEST_MAPPING b/core/TEST_MAPPING
index 24ba5c4..f1e4d0ee 100644
--- a/core/TEST_MAPPING
+++ b/core/TEST_MAPPING
@@ -23,7 +23,7 @@
],
"postsubmit": [
{
- "name": "ContactKeysManagerTest",
+ "name": "CtsContactKeysManagerTestCases",
"options": [
{
"include-filter": "android.provider.cts.contactkeys."
diff --git a/core/api/current.txt b/core/api/current.txt
index 89c7728..0a62877 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -147,6 +147,7 @@
field public static final String MANAGE_DEVICE_POLICY_CAMERA = "android.permission.MANAGE_DEVICE_POLICY_CAMERA";
field public static final String MANAGE_DEVICE_POLICY_CERTIFICATES = "android.permission.MANAGE_DEVICE_POLICY_CERTIFICATES";
field public static final String MANAGE_DEVICE_POLICY_COMMON_CRITERIA_MODE = "android.permission.MANAGE_DEVICE_POLICY_COMMON_CRITERIA_MODE";
+ field @FlaggedApi("android.view.contentprotection.flags.manage_device_policy_enabled") public static final String MANAGE_DEVICE_POLICY_CONTENT_PROTECTION = "android.permission.MANAGE_DEVICE_POLICY_CONTENT_PROTECTION";
field public static final String MANAGE_DEVICE_POLICY_DEBUGGING_FEATURES = "android.permission.MANAGE_DEVICE_POLICY_DEBUGGING_FEATURES";
field public static final String MANAGE_DEVICE_POLICY_DEFAULT_SMS = "android.permission.MANAGE_DEVICE_POLICY_DEFAULT_SMS";
field public static final String MANAGE_DEVICE_POLICY_DEVICE_IDENTIFIERS = "android.permission.MANAGE_DEVICE_POLICY_DEVICE_IDENTIFIERS";
@@ -5094,10 +5095,12 @@
public static interface AppOpsManager.OnOpActiveChangedListener {
method public void onOpActiveChanged(@NonNull String, int, @NonNull String, boolean);
+ method @FlaggedApi("android.permission.flags.device_aware_permission_apis_enabled") public default void onOpActiveChanged(@NonNull String, int, @NonNull String, @Nullable String, int, boolean, int, int);
}
public static interface AppOpsManager.OnOpChangedListener {
method public void onOpChanged(String, String);
+ method @FlaggedApi("android.permission.flags.device_aware_permission_apis_enabled") public default void onOpChanged(@NonNull String, @NonNull String, int, @NonNull String);
}
public abstract static class AppOpsManager.OnOpNotedCallback {
@@ -8194,6 +8197,9 @@
field public static final String ACTION_SET_NEW_PASSWORD = "android.app.action.SET_NEW_PASSWORD";
field public static final String ACTION_START_ENCRYPTION = "android.app.action.START_ENCRYPTION";
field public static final String ACTION_SYSTEM_UPDATE_POLICY_CHANGED = "android.app.action.SYSTEM_UPDATE_POLICY_CHANGED";
+ field @FlaggedApi("android.view.contentprotection.flags.manage_device_policy_enabled") public static final int CONTENT_PROTECTION_DISABLED = 1; // 0x1
+ field @FlaggedApi("android.view.contentprotection.flags.manage_device_policy_enabled") public static final int CONTENT_PROTECTION_ENABLED = 2; // 0x2
+ field @FlaggedApi("android.view.contentprotection.flags.manage_device_policy_enabled") public static final int CONTENT_PROTECTION_NOT_CONTROLLED_BY_POLICY = 0; // 0x0
field public static final String DELEGATION_APP_RESTRICTIONS = "delegation-app-restrictions";
field public static final String DELEGATION_BLOCK_UNINSTALL = "delegation-block-uninstall";
field public static final String DELEGATION_CERT_INSTALL = "delegation-cert-install";
@@ -12387,6 +12393,7 @@
method public void registerCallback(android.content.pm.LauncherApps.Callback, android.os.Handler);
method public void registerPackageInstallerSessionCallback(@NonNull java.util.concurrent.Executor, @NonNull android.content.pm.PackageInstaller.SessionCallback);
method public android.content.pm.LauncherActivityInfo resolveActivity(android.content.Intent, android.os.UserHandle);
+ method @FlaggedApi("android.content.pm.archiving") public void setArchiveCompatibilityOptions(boolean, boolean);
method public boolean shouldHideFromSuggestions(@NonNull String, @NonNull android.os.UserHandle);
method public void startAppDetailsActivity(android.content.ComponentName, android.os.UserHandle, android.graphics.Rect, android.os.Bundle);
method public void startMainActivity(android.content.ComponentName, android.os.UserHandle, android.graphics.Rect, android.os.Bundle);
@@ -22371,6 +22378,7 @@
method @NonNull public java.util.List<java.lang.String> getSupportedVendorParameters();
method @Nullable public static android.media.Image mapHardwareBuffer(@NonNull android.hardware.HardwareBuffer);
method public void queueInputBuffer(int, int, int, long, int) throws android.media.MediaCodec.CryptoException;
+ method @FlaggedApi("com.android.media.codec.flags.large_audio_frame") public void queueInputBuffers(int, @NonNull java.util.ArrayDeque<android.media.MediaCodec.BufferInfo>);
method public void queueSecureInputBuffer(int, int, @NonNull android.media.MediaCodec.CryptoInfo, long, int) throws android.media.MediaCodec.CryptoException;
method public void release();
method public void releaseOutputBuffer(int, boolean);
@@ -22432,6 +22440,7 @@
method public abstract void onError(@NonNull android.media.MediaCodec, @NonNull android.media.MediaCodec.CodecException);
method public abstract void onInputBufferAvailable(@NonNull android.media.MediaCodec, int);
method public abstract void onOutputBufferAvailable(@NonNull android.media.MediaCodec, int, @NonNull android.media.MediaCodec.BufferInfo);
+ method @FlaggedApi("com.android.media.codec.flags.large_audio_frame") public void onOutputBuffersAvailable(@NonNull android.media.MediaCodec, int, @NonNull java.util.ArrayDeque<android.media.MediaCodec.BufferInfo>);
method public abstract void onOutputFormatChanged(@NonNull android.media.MediaCodec, @NonNull android.media.MediaFormat);
}
@@ -22519,6 +22528,7 @@
}
public static final class MediaCodec.OutputFrame {
+ method @FlaggedApi("com.android.media.codec.flags.large_audio_frame") @NonNull public java.util.ArrayDeque<android.media.MediaCodec.BufferInfo> getBufferInfos();
method @NonNull public java.util.Set<java.lang.String> getChangedKeys();
method public int getFlags();
method @NonNull public android.media.MediaFormat getFormat();
@@ -22534,6 +22544,7 @@
public final class MediaCodec.QueueRequest {
method public void queue();
+ method @FlaggedApi("com.android.media.codec.flags.large_audio_frame") @NonNull public android.media.MediaCodec.QueueRequest setBufferInfos(@NonNull java.util.ArrayDeque<android.media.MediaCodec.BufferInfo>);
method @NonNull public android.media.MediaCodec.QueueRequest setByteBufferParameter(@NonNull String, @NonNull java.nio.ByteBuffer);
method @NonNull public android.media.MediaCodec.QueueRequest setEncryptedLinearBlock(@NonNull android.media.MediaCodec.LinearBlock, int, int, @NonNull android.media.MediaCodec.CryptoInfo);
method @NonNull public android.media.MediaCodec.QueueRequest setFlags(int);
@@ -25781,6 +25792,7 @@
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
+ field public static final int TIME_SINCE_CREATED_UNKNOWN = -1; // 0xffffffff
}
@FlaggedApi("com.android.media.editing.flags.add_media_metrics_editing") public static final class EditingEndedEvent.Builder {
@@ -25788,7 +25800,7 @@
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);
+ method @NonNull public android.media.metrics.EditingEndedEvent.Builder setTimeSinceCreatedMillis(@IntRange(from=android.media.metrics.EditingEndedEvent.TIME_SINCE_CREATED_UNKNOWN) long);
}
public final class EditingSession implements java.lang.AutoCloseable {
@@ -33136,7 +33148,7 @@
method public static long getStartRequestedElapsedRealtime();
method public static long getStartRequestedUptimeMillis();
method public static long getStartUptimeMillis();
- method public static final int getThreadPriority(int) throws java.lang.IllegalArgumentException;
+ method @IntRange(from=0xffffffec, to=android.os.Process.THREAD_PRIORITY_LOWEST) public static final int getThreadPriority(int) throws java.lang.IllegalArgumentException;
method public static final int getUidForName(String);
method public static final boolean is64Bit();
method public static boolean isApplicationUid(int);
@@ -33151,8 +33163,8 @@
method public static final int myUid();
method public static android.os.UserHandle myUserHandle();
method public static final void sendSignal(int, int);
- method public static final void setThreadPriority(int, int) throws java.lang.IllegalArgumentException, java.lang.SecurityException;
- method public static final void setThreadPriority(int) throws java.lang.IllegalArgumentException, java.lang.SecurityException;
+ method public static final void setThreadPriority(int, @IntRange(from=0xffffffec, to=android.os.Process.THREAD_PRIORITY_LOWEST) int) throws java.lang.IllegalArgumentException, java.lang.SecurityException;
+ method public static final void setThreadPriority(@IntRange(from=0xffffffec, to=android.os.Process.THREAD_PRIORITY_LOWEST) int) throws java.lang.IllegalArgumentException, java.lang.SecurityException;
method @Deprecated public static final boolean supportsProcesses();
field public static final int BLUETOOTH_UID = 1002; // 0x3ea
field public static final int FIRST_APPLICATION_UID = 10000; // 0x2710
@@ -50640,6 +50652,7 @@
field public static final int KEYCODE_DVR = 173; // 0xad
field public static final int KEYCODE_E = 33; // 0x21
field public static final int KEYCODE_EISU = 212; // 0xd4
+ field @FlaggedApi("com.android.hardware.input.emoji_and_screenshot_keycodes_available") public static final int KEYCODE_EMOJI_PICKER = 317; // 0x13d
field public static final int KEYCODE_ENDCALL = 6; // 0x6
field public static final int KEYCODE_ENTER = 66; // 0x42
field public static final int KEYCODE_ENVELOPE = 65; // 0x41
@@ -50772,6 +50785,7 @@
field public static final int KEYCODE_RIGHT_BRACKET = 72; // 0x48
field public static final int KEYCODE_RO = 217; // 0xd9
field public static final int KEYCODE_S = 47; // 0x2f
+ field @FlaggedApi("com.android.hardware.input.emoji_and_screenshot_keycodes_available") public static final int KEYCODE_SCREENSHOT = 318; // 0x13e
field public static final int KEYCODE_SCROLL_LOCK = 116; // 0x74
field public static final int KEYCODE_SEARCH = 84; // 0x54
field public static final int KEYCODE_SEMICOLON = 74; // 0x4a
@@ -59533,7 +59547,6 @@
}
@FlaggedApi("android.appwidget.flags.draw_data_parcel") public static final class RemoteViews.DrawInstructions {
- method @FlaggedApi("android.appwidget.flags.draw_data_parcel") public void appendInstructions(@NonNull byte[]);
}
@FlaggedApi("android.appwidget.flags.draw_data_parcel") public static final class RemoteViews.DrawInstructions.Builder {
diff --git a/core/api/module-lib-current.txt b/core/api/module-lib-current.txt
index 24b9233..55ed1f5 100644
--- a/core/api/module-lib-current.txt
+++ b/core/api/module-lib-current.txt
@@ -66,6 +66,11 @@
method @RequiresPermission(android.Manifest.permission.STATUS_BAR) public void setExpansionDisabledForSimNetworkLock(boolean);
}
+ public final class SystemServiceRegistry {
+ method @FlaggedApi("android.webkit.update_service_ipc_wrapper") @Nullable public static Object getSystemServiceWithNoContext(@NonNull String);
+ method @FlaggedApi("android.webkit.update_service_ipc_wrapper") public static <TServiceClass> void registerForeverStaticService(@NonNull String, @NonNull Class<TServiceClass>, @NonNull android.app.SystemServiceRegistry.StaticServiceProducerWithBinder<TServiceClass>);
+ }
+
}
package android.app.admin {
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index 94cde98..3b18dac 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -561,7 +561,7 @@
public class ActivityManager {
method @RequiresPermission(android.Manifest.permission.PACKAGE_USAGE_STATS) public void addOnUidImportanceListener(android.app.ActivityManager.OnUidImportanceListener, int);
- method @FlaggedApi("android.app.uid_importance_listener_for_uids") @RequiresPermission(android.Manifest.permission.PACKAGE_USAGE_STATS) public void addOnUidImportanceListener(@NonNull android.app.ActivityManager.OnUidImportanceListener, int, @Nullable int[]);
+ method @FlaggedApi("android.app.uid_importance_listener_for_uids") @RequiresPermission(android.Manifest.permission.PACKAGE_USAGE_STATS) public void addOnUidImportanceListener(@NonNull android.app.ActivityManager.OnUidImportanceListener, int, @NonNull int[]);
method @RequiresPermission(android.Manifest.permission.FORCE_STOP_PACKAGES) public void forceStopPackage(String);
method @FlaggedApi("android.app.get_binding_uid_importance") @RequiresPermission(android.Manifest.permission.GET_BINDING_UID_IMPORTANCE) public int getBindingUidImportance(int);
method @RequiresPermission(anyOf={"android.permission.INTERACT_ACROSS_USERS", "android.permission.INTERACT_ACROSS_USERS_FULL"}) public static int getCurrentUser();
@@ -822,6 +822,7 @@
public static interface AppOpsManager.OnOpNotedListener {
method public void onOpNoted(@NonNull String, int, @NonNull String, @Nullable String, int, int);
+ method @FlaggedApi("android.permission.flags.device_aware_permission_apis_enabled") public default void onOpNoted(@NonNull String, int, @NonNull String, @Nullable String, int, int, int);
}
public static final class AppOpsManager.OpEntry implements android.os.Parcelable {
@@ -3210,6 +3211,7 @@
public final class VirtualDeviceManager {
method @NonNull @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public android.companion.virtual.VirtualDeviceManager.VirtualDevice createVirtualDevice(int, @NonNull android.companion.virtual.VirtualDeviceParams);
+ method @FlaggedApi("android.companion.virtual.flags.persistent_device_id_api") @NonNull public java.util.Set<java.lang.String> getAllPersistentDeviceIds();
method @FlaggedApi("android.companion.virtual.flags.persistent_device_id_api") @Nullable public CharSequence getDisplayNameForPersistentDeviceId(@NonNull String);
field public static final int LAUNCH_FAILURE_NO_ACTIVITY = 2; // 0x2
field public static final int LAUNCH_FAILURE_PENDING_INTENT_CANCELED = 1; // 0x1
@@ -6165,6 +6167,7 @@
method public boolean containsKey(String);
method public int describeContents();
method @Deprecated public android.graphics.Bitmap getBitmap(String);
+ method @FlaggedApi("android.hardware.radio.hd_radio_improved") public int getBitmapId(@NonNull String);
method public android.hardware.radio.RadioMetadata.Clock getClock(String);
method public int getInt(String);
method public String getString(String);
@@ -6228,6 +6231,7 @@
method @RequiresPermission(android.Manifest.permission.ACCESS_BROADCAST_RADIO) public abstract void close();
method @Deprecated @RequiresPermission(android.Manifest.permission.ACCESS_BROADCAST_RADIO) public abstract int getConfiguration(android.hardware.radio.RadioManager.BandConfig[]);
method @Nullable @RequiresPermission(android.Manifest.permission.ACCESS_BROADCAST_RADIO) public android.hardware.radio.ProgramList getDynamicProgramList(@Nullable android.hardware.radio.ProgramList.Filter);
+ method @FlaggedApi("android.hardware.radio.hd_radio_improved") @NonNull @RequiresPermission(android.Manifest.permission.ACCESS_BROADCAST_RADIO) public android.graphics.Bitmap getMetadataImage(int);
method @RequiresPermission(android.Manifest.permission.ACCESS_BROADCAST_RADIO) public abstract boolean getMute();
method @NonNull @RequiresPermission(android.Manifest.permission.ACCESS_BROADCAST_RADIO) public java.util.Map<java.lang.String,java.lang.String> getParameters(@NonNull java.util.List<java.lang.String>);
method @Deprecated @RequiresPermission(android.Manifest.permission.ACCESS_BROADCAST_RADIO) public abstract int getProgramInformation(android.hardware.radio.RadioManager.ProgramInfo[]);
@@ -9655,6 +9659,7 @@
public final class DeviceWiphyCapabilities implements android.os.Parcelable {
ctor public DeviceWiphyCapabilities();
method public int describeContents();
+ method @FlaggedApi("android.net.wifi.flags.get_device_cross_akm_roaming_support") public int getMaxNumberAkms();
method public int getMaxNumberRxSpatialStreams();
method public int getMaxNumberTxSpatialStreams();
method public boolean isChannelWidthSupported(int);
@@ -14846,11 +14851,11 @@
method @Deprecated @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public void setDataEnabled(int, boolean);
method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public void setDataRoamingEnabled(boolean);
method @FlaggedApi("com.android.internal.telephony.flags.enable_identifier_disclosure_transparency") @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public void setEnableCellularIdentifierDisclosureNotifications(boolean);
- method @FlaggedApi("com.android.internal.telephony.flags.enable_modem_cipher_transparency") @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public void setEnableNullCipherNotifications(boolean);
method @NonNull @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public android.telephony.PinResult setIccLockEnabled(boolean, @NonNull String);
method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public void setMobileDataPolicyEnabled(int, boolean);
method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public void setMultiSimCarrierRestriction(boolean);
method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public int setNrDualConnectivityState(int);
+ method @FlaggedApi("com.android.internal.telephony.flags.enable_modem_cipher_transparency") @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public void setNullCipherNotificationsEnabled(boolean);
method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public boolean setOpportunisticNetworkState(boolean);
method @Deprecated @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public boolean setPreferredNetworkTypeBitmask(long);
method @Deprecated @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public boolean setRadio(boolean);
diff --git a/core/api/test-current.txt b/core/api/test-current.txt
index 42cf08f..77add41 100644
--- a/core/api/test-current.txt
+++ b/core/api/test-current.txt
@@ -195,8 +195,11 @@
method public void setTaskOverlay(boolean, boolean);
}
- public static final class ActivityOptions.LaunchCookie {
+ public static final class ActivityOptions.LaunchCookie implements android.os.Parcelable {
ctor public ActivityOptions.LaunchCookie();
+ method public int describeContents();
+ method public void writeToParcel(@NonNull android.os.Parcel, int);
+ field @NonNull public static final android.os.Parcelable.Creator<android.app.ActivityOptions.LaunchCookie> CREATOR;
}
public static interface ActivityOptions.OnAnimationFinishedListener {
@@ -2108,6 +2111,14 @@
}
+package android.media.projection {
+
+ public final class MediaProjectionManager {
+ method @NonNull public android.content.Intent createScreenCaptureIntent(@Nullable android.app.ActivityOptions.LaunchCookie);
+ }
+
+}
+
package android.media.soundtrigger {
public final class SoundTriggerInstrumentation {
@@ -2643,7 +2654,6 @@
method @NonNull public static String convert(@NonNull java.util.UUID);
method @Nullable public String getCloudMediaProvider();
method public boolean isAppIoBlocked(@NonNull java.util.UUID, int, int, int);
- method public static boolean isUserKeyUnlocked(int);
field public static final String CACHE_RESERVE_PERCENT_HIGH_KEY = "cache_reserve_percent_high";
field public static final String CACHE_RESERVE_PERCENT_LOW_KEY = "cache_reserve_percent_low";
field public static final String STORAGE_THRESHOLD_PERCENT_HIGH_KEY = "storage_threshold_percent_high";
@@ -3606,7 +3616,7 @@
method public final int getDisplayId();
method public final void setDisplayId(int);
field public static final int FLAG_IS_ACCESSIBILITY_EVENT = 2048; // 0x800
- field public static final int LAST_KEYCODE = 316; // 0x13c
+ field public static final int LAST_KEYCODE = 318; // 0x13e
}
public final class KeyboardShortcutGroup implements android.os.Parcelable {
diff --git a/core/java/android/app/ActivityManager.java b/core/java/android/app/ActivityManager.java
index 9d20f3c..6285eb3 100644
--- a/core/java/android/app/ActivityManager.java
+++ b/core/java/android/app/ActivityManager.java
@@ -107,6 +107,7 @@
import java.util.Collections;
import java.util.List;
import java.util.Locale;
+import java.util.Objects;
import java.util.concurrent.Executor;
import java.util.function.Consumer;
@@ -4440,8 +4441,7 @@
* is used here, you will receive a call each time a uids importance transitions between
* being <= {@link RunningAppProcessInfo#IMPORTANCE_PERCEPTIBLE} and
* > {@link RunningAppProcessInfo#IMPORTANCE_PERCEPTIBLE}.
- * @param uids The UIDs that this listener is interested with. A {@code null} value means
- * all UIDs will be monitored by this listener, this will be equivalent to the
+ * @param uids The UIDs that this listener is interested with.
* {@link #addOnUidImportanceListener(OnUidImportanceListener, int)} in this case.
*
* <p>Calling this API with the same instance of {@code listener} without
@@ -4456,7 +4456,9 @@
@SuppressLint("SamShouldBeLast")
@RequiresPermission(Manifest.permission.PACKAGE_USAGE_STATS)
public void addOnUidImportanceListener(@NonNull OnUidImportanceListener listener,
- @RunningAppProcessInfo.Importance int importanceCutpoint, @Nullable int[] uids) {
+ @RunningAppProcessInfo.Importance int importanceCutpoint, @NonNull int[] uids) {
+ Objects.requireNonNull(listener);
+ Objects.requireNonNull(uids);
addOnUidImportanceListenerInternal(listener, importanceCutpoint, uids);
}
diff --git a/core/java/android/app/ActivityOptions.aidl b/core/java/android/app/ActivityOptions.aidl
index bd5cd88..2d4a85f 100644
--- a/core/java/android/app/ActivityOptions.aidl
+++ b/core/java/android/app/ActivityOptions.aidl
@@ -17,4 +17,7 @@
package android.app;
/** @hide */
-parcelable ActivityOptions.SceneTransitionInfo;
\ No newline at end of file
+parcelable ActivityOptions.SceneTransitionInfo;
+
+/** @hide */
+parcelable ActivityOptions.LaunchCookie;
\ No newline at end of file
diff --git a/core/java/android/app/ActivityOptions.java b/core/java/android/app/ActivityOptions.java
index 4a566db..111895e 100644
--- a/core/java/android/app/ActivityOptions.java
+++ b/core/java/android/app/ActivityOptions.java
@@ -1958,14 +1958,87 @@
*/
@SuppressLint("UnflaggedApi")
@TestApi
- public static final class LaunchCookie {
+ public static final class LaunchCookie implements Parcelable {
/** @hide */
- public final IBinder binder = new Binder();
+ public final IBinder binder;
/** @hide */
@SuppressLint("UnflaggedApi")
@TestApi
- public LaunchCookie() {}
+ public LaunchCookie() {
+ binder = new Binder();
+ }
+
+ /** @hide */
+ public LaunchCookie(@Nullable String descriptor) {
+ binder = new Binder(descriptor);
+ }
+
+ private LaunchCookie(Parcel in) {
+ this.binder = in.readStrongBinder();
+ }
+
+ /** @hide */
+ @SuppressLint("UnflaggedApi")
+ @TestApi
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ /** @hide */
+ @SuppressLint("UnflaggedApi")
+ @TestApi
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ dest.writeStrongBinder(binder);
+ }
+
+ /** @hide */
+ public static LaunchCookie readFromParcel(@NonNull Parcel in) {
+ return new LaunchCookie(in);
+ }
+
+ /** @hide */
+ public static void writeToParcel(@Nullable LaunchCookie launchCookie, Parcel out) {
+ if (launchCookie != null) {
+ launchCookie.writeToParcel(out, 0);
+ } else {
+ out.writeStrongBinder(null);
+ }
+ }
+
+ /** @hide */
+ @SuppressLint("UnflaggedApi")
+ @TestApi
+ @NonNull
+ public static final Parcelable.Creator<LaunchCookie> CREATOR =
+ new Parcelable.Creator<LaunchCookie>() {
+
+ @Override
+ public LaunchCookie createFromParcel(Parcel source) {
+ return new LaunchCookie(source);
+ }
+
+ @Override
+ public LaunchCookie[] newArray(int size) {
+ return new LaunchCookie[size];
+ }
+ };
+
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (obj instanceof LaunchCookie) {
+ LaunchCookie other = (LaunchCookie) obj;
+ return binder == other.binder;
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return binder.hashCode();
+ }
}
/**
diff --git a/core/java/android/app/AppOpsManager.java b/core/java/android/app/AppOpsManager.java
index 00c4b0f..7907059 100644
--- a/core/java/android/app/AppOpsManager.java
+++ b/core/java/android/app/AppOpsManager.java
@@ -36,6 +36,7 @@
import android.annotation.SystemService;
import android.annotation.TestApi;
import android.app.usage.UsageStatsManager;
+import android.companion.virtual.VirtualDeviceManager;
import android.compat.Compatibility;
import android.compat.annotation.ChangeId;
import android.compat.annotation.EnabledAfter;
@@ -1555,9 +1556,24 @@
*/
public static final int OP_RUN_BACKUP_JOBS = AppProtoEnums.APP_OP_RUN_BACKUP_JOBS;
+ /**
+ * Whether the app has enabled to receive the icon overlay for fetching archived apps.
+ *
+ * @hide
+ */
+ public static final int OP_ARCHIVE_ICON_OVERLAY = AppProtoEnums.APP_OP_ARCHIVE_ICON_OVERLAY;
+
+ /**
+ * Whether the app has enabled compatibility support for unarchival.
+ *
+ * @hide
+ */
+ public static final int OP_UNARCHIVAL_CONFIRMATION =
+ AppProtoEnums.APP_OP_UNARCHIVAL_CONFIRMATION;
+
/** @hide */
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
- public static final int _NUM_OP = 145;
+ public static final int _NUM_OP = 147;
/**
* All app ops represented as strings.
@@ -1708,6 +1724,8 @@
OPSTR_RESERVED_FOR_TESTING,
OPSTR_RAPID_CLEAR_NOTIFICATIONS_BY_LISTENER,
OPSTR_RUN_BACKUP_JOBS,
+ OPSTR_ARCHIVE_ICON_OVERLAY,
+ OPSTR_UNARCHIVAL_CONFIRMATION,
})
public @interface AppOpString {}
@@ -2048,6 +2066,20 @@
public static final String OPSTR_MEDIA_ROUTING_CONTROL = "android:media_routing_control";
/**
+ * Whether the app has enabled to receive the icon overlay for fetching archived apps.
+ *
+ * @hide
+ */
+ public static final String OPSTR_ARCHIVE_ICON_OVERLAY = "android:archive_icon_overlay";
+
+ /**
+ * Whether the app has enabled compatibility support for unarchival.
+ *
+ * @hide
+ */
+ public static final String OPSTR_UNARCHIVAL_CONFIRMATION = "android:unarchival_support";
+
+ /**
* AppOp granted to apps that we are started via {@code am instrument -e --no-isolated-storage}
*
* <p>MediaProvider is the only component (outside of system server) that should care about this
@@ -2520,6 +2552,8 @@
OP_MEDIA_ROUTING_CONTROL,
OP_READ_SYSTEM_GRAMMATICAL_GENDER,
OP_RUN_BACKUP_JOBS,
+ OP_ARCHIVE_ICON_OVERLAY,
+ OP_UNARCHIVAL_CONFIRMATION,
};
static final AppOpInfo[] sAppOpInfos = new AppOpInfo[]{
@@ -2979,6 +3013,12 @@
.build(),
new AppOpInfo.Builder(OP_RUN_BACKUP_JOBS, OPSTR_RUN_BACKUP_JOBS, "RUN_BACKUP_JOBS")
.setPermission(Manifest.permission.RUN_BACKUP_JOBS).build(),
+ new AppOpInfo.Builder(OP_ARCHIVE_ICON_OVERLAY, OPSTR_ARCHIVE_ICON_OVERLAY,
+ "ARCHIVE_ICON_OVERLAY")
+ .setDefaultMode(MODE_ALLOWED).build(),
+ new AppOpInfo.Builder(OP_UNARCHIVAL_CONFIRMATION, OPSTR_UNARCHIVAL_CONFIRMATION,
+ "UNARCHIVAL_CONFIRMATION")
+ .setDefaultMode(MODE_ALLOWED).build(),
};
// The number of longs needed to form a full bitmask of app ops
@@ -3113,7 +3153,7 @@
/**
* Retrieve the permission associated with an operation, or null if there is not one.
- *
+
* @param op The operation name.
*
* @hide
@@ -7307,6 +7347,12 @@
* The default impl is to fallback onto {@link #onOpChanged(String, String)
*
+ * <p> Implement this method and not
+ * {@link #onOpChanged(String, String, int, String)} if
+ * callbacks are
+ * required only on op state changes for the default device
+ * {@link VirtualDeviceManager#PERSISTENT_DEVICE_ID_DEFAULT}.
+ *
* @param op The Op that changed.
* @param packageName Package of the app whose Op changed.
* @param userId User Space of the app whose Op changed.
@@ -7315,6 +7361,31 @@
default void onOpChanged(@NonNull String op, @NonNull String packageName, int userId) {
onOpChanged(op, packageName);
}
+
+ /**
+ * Similar to {@link #onOpChanged(String, String, int)} but includes the device for which
+ * the op mode has changed.
+ *
+ * <p> Implement this method if callbacks are required on all devices.
+ * If not implemented explicitly, the default implementation will notify for op changes
+ * on the default device {@link VirtualDeviceManager#PERSISTENT_DEVICE_ID_DEFAULT} only.
+ *
+ * <p> If implemented, {@link #onOpChanged(String, String, int)}
+ * will not be called automatically.
+ *
+ * @param op The Op that changed.
+ * @param packageName Package of the app whose Op changed.
+ * @param userId User id of the app whose Op changed.
+ * @param persistentDeviceId persistent device id whose Op changed.
+ */
+ @FlaggedApi(android.permission.flags.Flags.FLAG_DEVICE_AWARE_PERMISSION_APIS_ENABLED)
+ default void onOpChanged(@NonNull String op, @NonNull String packageName, int userId,
+ @NonNull String persistentDeviceId) {
+ if (Objects.equals(persistentDeviceId,
+ VirtualDeviceManager.PERSISTENT_DEVICE_ID_DEFAULT)) {
+ onOpChanged(op, packageName, userId);
+ }
+ }
}
/**
@@ -7334,6 +7405,12 @@
/**
* Called when the active state of an app-op changes.
*
+ * <p> Implement this method and not
+ * {@link #onOpActiveChanged(String, int, String, String, int, boolean, int, int)} if
+ * callbacks are
+ * required only on op state changes for the default device
+ * {@link Context#DEVICE_ID_DEFAULT}.
+ *
* @param op The operation that changed.
* @param uid The UID performing the operation.
* @param packageName The package performing the operation.
@@ -7349,6 +7426,37 @@
int attributionFlags, int attributionChainId) {
onOpActiveChanged(op, uid, packageName, active);
}
+
+ /**
+ * Similar to {@link #onOpActiveChanged(String, int, String, String, boolean, int, int)},
+ * but also includes the virtual device id of the op is now active or inactive.
+ *
+ * <p> Implement this method if callbacks are required on all devices.
+ * If not implemented explicitly, the default implementation will notify for op state
+ * changes on the default device {@link Context#DEVICE_ID_DEFAULT} only.
+ *
+ * <p> If implemented,
+ * {@link #onOpActiveChanged(String, int, String, String, boolean, int, int)}
+ * will not be called automatically.
+ *
+ * @param op The operation that changed.
+ * @param uid The UID performing the operation.
+ * @param packageName The package performing the operation.
+ * @param attributionTag The operation's attribution tag.
+ * @param virtualDeviceId the virtual device id whose operation has changed
+ * @param active Whether the operation became active or inactive.
+ * @param attributionFlags the attribution flags for this operation.
+ * @param attributionChainId the unique id of the attribution chain this op is a part of.
+ */
+ @FlaggedApi(android.permission.flags.Flags.FLAG_DEVICE_AWARE_PERMISSION_APIS_ENABLED)
+ default void onOpActiveChanged(@NonNull String op, int uid, @NonNull String packageName,
+ @Nullable String attributionTag, int virtualDeviceId, boolean active,
+ @AttributionFlags int attributionFlags, int attributionChainId) {
+ if (virtualDeviceId == Context.DEVICE_ID_DEFAULT) {
+ onOpActiveChanged(op, uid, packageName, attributionTag, active, attributionFlags,
+ attributionChainId);
+ }
+ }
}
/**
@@ -7361,6 +7469,10 @@
/**
* Called when an app-op is noted.
*
+ * <p> Implement this method and not
+ * {@link #onOpNoted(String, int, String, String, int, int, int)} if callbacks are
+ * required only on op notes for the default device {@link Context#DEVICE_ID_DEFAULT}.
+ *
* @param op The operation that was noted.
* @param uid The UID performing the operation.
* @param packageName The package performing the operation.
@@ -7370,6 +7482,34 @@
*/
void onOpNoted(@NonNull String op, int uid, @NonNull String packageName,
@Nullable String attributionTag, @OpFlags int flags, @Mode int result);
+
+ /**
+ * Similar to {@link #onOpNoted(String, int, String, String, int, int, int)},
+ * but also includes the virtual device id of the op is now active or inactive.
+ *
+ * <p> Implement this method if callbacks are required for op notes on all devices.
+ * If not implemented explicitly, the default implementation will notify for the
+ * default device {@link Context#DEVICE_ID_DEFAULT} only.
+ *
+ * <p> If implemented, {@link #onOpNoted(String, int, String, String, int, int)}
+ * will not be called automatically.
+ *
+ * @param op The operation that was noted.
+ * @param uid The UID performing the operation.
+ * @param packageName The package performing the operation.
+ * @param attributionTag The attribution tag performing the operation.
+ * @param virtualDeviceId the device that noted the operation
+ * @param flags The flags of this op
+ * @param result The result of the note.
+ */
+ @FlaggedApi(android.permission.flags.Flags.FLAG_DEVICE_AWARE_PERMISSION_APIS_ENABLED)
+ default void onOpNoted(@NonNull String op, int uid, @NonNull String packageName,
+ @Nullable String attributionTag, int virtualDeviceId, @OpFlags int flags,
+ @Mode int result) {
+ if (virtualDeviceId == Context.DEVICE_ID_DEFAULT) {
+ onOpNoted(op, uid, packageName, attributionTag, flags, result);
+ }
+ }
}
/**
@@ -7408,6 +7548,9 @@
public static class OnOpChangedInternalListener implements OnOpChangedListener {
public void onOpChanged(String op, String packageName) { }
public void onOpChanged(int op, String packageName) { }
+ public void onOpChanged(int op, String packageName, String persistentDeviceId) {
+ onOpChanged(op, packageName);
+ }
}
/**
@@ -7418,6 +7561,8 @@
public interface OnOpActiveChangedInternalListener extends OnOpActiveChangedListener {
default void onOpActiveChanged(String op, int uid, String packageName, boolean active) { }
default void onOpActiveChanged(int op, int uid, String packageName, boolean active) { }
+ default void onOpActiveChanged(int op, int uid, String packageName, int virtualDeviceId,
+ boolean active) { }
}
/**
@@ -7471,6 +7616,12 @@
/**
* Called when an op was started.
*
+ * <p> Implement this method and not
+ * {@link #onOpStarted(int, int, String, String, int, int, int, int, int, int)} if
+ * callbacks are
+ * required only on op starts for the default device
+ * {@link Context#DEVICE_ID_DEFAULT}.
+ *
* Note: This is only for op starts. It is not called when an op is noted or stopped.
* By default, unless this method is overridden, no code will be executed for resume
* events.
@@ -7491,6 +7642,37 @@
onOpStarted(op, uid, packageName, attributionTag, flags, result);
}
}
+
+ /**
+ * Similar to {@link #onOpStarted(int, int, String, String, int, int)},
+ * but also includes the virtual device id that started the op.
+ *
+ * <p> Implement this method if callbacks are required on all devices.
+ * If not implemented explicitly, the default implementation will notify for op starts on
+ * the default device {@link Context#DEVICE_ID_DEFAULT} only.
+ *
+ * <p> If implemented, {@link #onOpStarted(int, int, String, String, int, int)}
+ * will not be called automatically.
+ *
+ * @param op The op code.
+ * @param uid The UID performing the operation.
+ * @param packageName The package performing the operation.
+ * @param attributionTag The attribution tag performing the operation.
+ * @param virtualDeviceId the device that started the operation
+ * @param flags The flags of this op.
+ * @param result The result of the start.
+ * @param startType The start type of this start event. Either failed, resumed, or started.
+ * @param attributionFlags The location of this started op in an attribution chain.
+ */
+ default void onOpStarted(int op, int uid, @NonNull String packageName,
+ @Nullable String attributionTag, int virtualDeviceId, @OpFlags int flags,
+ @Mode int result, @StartedType int startType,
+ @AttributionFlags int attributionFlags, int attributionChainId) {
+ if (virtualDeviceId == Context.DEVICE_ID_DEFAULT) {
+ onOpStarted(op, uid, packageName, attributionTag, flags, result, startType,
+ attributionFlags, attributionChainId);
+ }
+ }
}
AppOpsManager(Context context, IAppOpsService service) {
@@ -8015,14 +8197,16 @@
IAppOpsCallback cb = mModeWatchers.get(callback);
if (cb == null) {
cb = new IAppOpsCallback.Stub() {
- public void opChanged(int op, int uid, String packageName) {
+ public void opChanged(int op, int uid, String packageName,
+ String persistentDeviceId) {
if (callback instanceof OnOpChangedInternalListener) {
- ((OnOpChangedInternalListener)callback).onOpChanged(op, packageName);
+ ((OnOpChangedInternalListener)callback).onOpChanged(op, packageName,
+ persistentDeviceId);
}
if (sAppOpInfos[op].name != null) {
callback.onOpChanged(sAppOpInfos[op].name, packageName,
- UserHandle.getUserId(uid));
+ UserHandle.getUserId(uid), persistentDeviceId);
}
}
};
@@ -8103,16 +8287,17 @@
cb = new IAppOpsActiveCallback.Stub() {
@Override
public void opActiveChanged(int op, int uid, String packageName,
- String attributionTag, boolean active, @AttributionFlags
- int attributionFlags, int attributionChainId) {
+ String attributionTag, int virtualDeviceId, boolean active,
+ @AttributionFlags int attributionFlags, int attributionChainId) {
executor.execute(() -> {
if (callback instanceof OnOpActiveChangedInternalListener) {
((OnOpActiveChangedInternalListener) callback).onOpActiveChanged(op,
- uid, packageName, active);
+ uid, packageName, virtualDeviceId, active);
}
if (sAppOpInfos[op].name != null) {
callback.onOpActiveChanged(sAppOpInfos[op].name, uid, packageName,
- attributionTag, active, attributionFlags, attributionChainId);
+ attributionTag, virtualDeviceId, active, attributionFlags,
+ attributionChainId);
}
});
}
@@ -8181,10 +8366,10 @@
cb = new IAppOpsStartedCallback.Stub() {
@Override
public void opStarted(int op, int uid, String packageName, String attributionTag,
- int flags, int mode, int startType, int attributionFlags,
- int attributionChainId) {
- callback.onOpStarted(op, uid, packageName, attributionTag, flags, mode,
- startType, attributionFlags, attributionChainId);
+ int virtualDeviceId, int flags, int mode, int startType,
+ int attributionFlags, int attributionChainId) {
+ callback.onOpStarted(op, uid, packageName, attributionTag, virtualDeviceId,
+ flags, mode, startType, attributionFlags, attributionChainId);
}
};
mStartedWatchers.put(callback, cb);
@@ -8352,13 +8537,13 @@
cb = new IAppOpsNotedCallback.Stub() {
@Override
public void opNoted(int op, int uid, String packageName, String attributionTag,
- int flags, int mode) {
+ int virtualDeviceId, int flags, int mode) {
final long identity = Binder.clearCallingIdentity();
try {
executor.execute(() -> {
if (sAppOpInfos[op].name != null) {
listener.onOpNoted(sAppOpInfos[op].name, uid, packageName,
- attributionTag,
+ attributionTag, virtualDeviceId,
flags, mode);
}
});
diff --git a/core/java/android/app/ApplicationPackageManager.java b/core/java/android/app/ApplicationPackageManager.java
index 34c44f9..4f1db7d 100644
--- a/core/java/android/app/ApplicationPackageManager.java
+++ b/core/java/android/app/ApplicationPackageManager.java
@@ -4032,7 +4032,8 @@
private Drawable getArchivedAppIcon(String packageName) {
try {
return new BitmapDrawable(null,
- mPM.getArchivedAppIcon(packageName, new UserHandle(getUserId())));
+ mPM.getArchivedAppIcon(packageName, new UserHandle(getUserId()),
+ mContext.getPackageName()));
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
diff --git a/core/java/android/app/Instrumentation.java b/core/java/android/app/Instrumentation.java
index 2162e3a..68512b8 100644
--- a/core/java/android/app/Instrumentation.java
+++ b/core/java/android/app/Instrumentation.java
@@ -79,6 +79,7 @@
* implementation is described to the system through an AndroidManifest.xml's
* <instrumentation> tag.
*/
+@android.ravenwood.annotation.RavenwoodKeepPartialClass
public class Instrumentation {
/**
@@ -132,6 +133,7 @@
private UiAutomation mUiAutomation;
private final Object mAnimationCompleteLock = new Object();
+ @android.ravenwood.annotation.RavenwoodKeep
public Instrumentation() {
}
@@ -142,6 +144,7 @@
* reflection, but it will serve as noticeable discouragement from
* doing such a thing.
*/
+ @android.ravenwood.annotation.RavenwoodReplace
private void checkInstrumenting(String method) {
// Check if we have an instrumentation context, as init should only get called by
// the system in startup processes that are being instrumented.
@@ -151,6 +154,11 @@
}
}
+ private void checkInstrumenting$ravenwood(String method) {
+ // At the moment, Ravenwood doesn't attach a Context, but we're only ever
+ // running code as part of tests, so we continue quietly
+ }
+
/**
* Returns if it is being called in an instrumentation environment.
*
@@ -2504,6 +2512,7 @@
* Takes control of the execution of messages on the specified looper until
* {@link TestLooperManager#release} is called.
*/
+ @android.ravenwood.annotation.RavenwoodKeep
public TestLooperManager acquireLooperManager(Looper looper) {
checkInstrumenting("acquireLooperManager");
return new TestLooperManager(looper);
diff --git a/core/java/android/app/OWNERS b/core/java/android/app/OWNERS
index 3b5bba2..729f92a 100644
--- a/core/java/android/app/OWNERS
+++ b/core/java/android/app/OWNERS
@@ -111,5 +111,9 @@
per-file ConfigurationController.java = file:/services/core/java/com/android/server/wm/OWNERS
per-file *ScreenCapture* = file:/services/core/java/com/android/server/wm/OWNERS
+# Multitasking
+per-file multitasking.aconfig = file:/services/core/java/com/android/server/wm/OWNERS
+per-file multitasking.aconfig = file:/libs/WindowManager/Shell/OWNERS
+
# Zygote
per-file *Zygote* = file:/ZYGOTE_OWNERS
diff --git a/core/java/android/app/PendingIntent.java b/core/java/android/app/PendingIntent.java
index 0261f0a..1ac08ac 100644
--- a/core/java/android/app/PendingIntent.java
+++ b/core/java/android/app/PendingIntent.java
@@ -179,6 +179,14 @@
@Overridable
public static final long BLOCK_MUTABLE_IMPLICIT_PENDING_INTENT = 236704164L;
+ /**
+ * Validate options passed in as bundle.
+ * @hide
+ */
+ @ChangeId
+ @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ public static final long PENDING_INTENT_OPTIONS_CHECK = 320664730L;
+
/** @hide */
@IntDef(flag = true,
value = {
diff --git a/core/java/android/app/SystemServiceRegistry.java b/core/java/android/app/SystemServiceRegistry.java
index 397b63f..b21b0f3 100644
--- a/core/java/android/app/SystemServiceRegistry.java
+++ b/core/java/android/app/SystemServiceRegistry.java
@@ -19,6 +19,7 @@
import android.accounts.AccountManager;
import android.accounts.IAccountManager;
import android.adservices.AdServicesFrameworkInitializer;
+import android.annotation.FlaggedApi;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SystemApi;
@@ -1668,11 +1669,7 @@
return new Object[sServiceCacheSize];
}
- /**
- * Gets a system service from a given context.
- * @hide
- */
- public static Object getSystemService(ContextImpl ctx, String name) {
+ private static ServiceFetcher<?> getSystemServiceFetcher(String name) {
if (name == null) {
return null;
}
@@ -1683,6 +1680,18 @@
}
return null;
}
+ return fetcher;
+ }
+
+ /**
+ * Gets a system service from a given context.
+ * @hide
+ */
+ public static Object getSystemService(@NonNull ContextImpl ctx, String name) {
+ final ServiceFetcher<?> fetcher = getSystemServiceFetcher(name);
+ if (fetcher == null) {
+ return null;
+ }
final Object ret = fetcher.getService(ctx);
if (sEnableServiceNotFoundWtf && ret == null) {
@@ -1710,6 +1719,26 @@
}
/**
+ * Gets a system service which has opted-in to being fetched without a context.
+ * @hide
+ */
+ @FlaggedApi(android.webkit.Flags.FLAG_UPDATE_SERVICE_IPC_WRAPPER)
+ @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+ public static @Nullable Object getSystemServiceWithNoContext(@NonNull String name) {
+ final ServiceFetcher<?> fetcher = getSystemServiceFetcher(name);
+ if (fetcher == null) {
+ return null;
+ }
+
+ if (!fetcher.supportsFetchWithoutContext()) {
+ throw new IllegalArgumentException(
+ "Manager cannot be fetched without a context: " + name);
+ }
+
+ return fetcher.getService(null);
+ }
+
+ /**
* Gets the name of the system-level service that is represented by the specified class.
* @hide
*/
@@ -1863,6 +1892,50 @@
}
/**
+ * Used by apex modules to register a "service wrapper" that is not tied to any {@link Context}
+ * and will never require a context in the future.
+ *
+ * Services registered in this way can be fetched via
+ * {@link #getSystemServiceWithNoContext(String)}, so cannot require a context in future without
+ * a breaking change.
+ *
+ * <p>This can only be called from the methods called by the static initializer of
+ * {@link SystemServiceRegistry}. (Otherwise it throws a {@link IllegalStateException}.)
+ *
+ * @param serviceName the name of the binder object, such as
+ * {@link Context#JOB_SCHEDULER_SERVICE}.
+ * @param serviceWrapperClass the wrapper class, such as the class of
+ * {@link android.app.job.JobScheduler}.
+ * @param serviceProducer Callback that takes the service binder object with the name
+ * {@code serviceName} and returns an actual service wrapper instance.
+ *
+ * @hide
+ */
+ @FlaggedApi(android.webkit.Flags.FLAG_UPDATE_SERVICE_IPC_WRAPPER)
+ @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+ public static <TServiceClass> void registerForeverStaticService(
+ @NonNull String serviceName, @NonNull Class<TServiceClass> serviceWrapperClass,
+ @NonNull StaticServiceProducerWithBinder<TServiceClass> serviceProducer) {
+ ensureInitializing("registerStaticService");
+ Preconditions.checkStringNotEmpty(serviceName);
+ Objects.requireNonNull(serviceWrapperClass);
+ Objects.requireNonNull(serviceProducer);
+
+ registerService(serviceName, serviceWrapperClass,
+ new StaticServiceFetcher<TServiceClass>() {
+ @Override
+ public TServiceClass createService() throws ServiceNotFoundException {
+ return serviceProducer.createService(
+ ServiceManager.getServiceOrThrow(serviceName));
+ }
+
+ @Override
+ public boolean supportsFetchWithoutContext() {
+ return true;
+ }});
+ }
+
+ /**
* Similar to {@link #registerStaticService(String, Class, StaticServiceProducerWithBinder)},
* but used for a "service wrapper" that doesn't take a service binder in its constructor.
*
@@ -1952,6 +2025,18 @@
*/
static abstract interface ServiceFetcher<T> {
T getService(ContextImpl ctx);
+
+ /**
+ * Should this service fetcher support being fetched via {@link #getSystemService(String)},
+ * without a Context?
+ *
+ * This means that the service cannot depend on a Context in future!
+ *
+ * @return true if this is supported for this service.
+ */
+ default boolean supportsFetchWithoutContext() {
+ return false;
+ }
}
/**
@@ -2059,6 +2144,11 @@
}
public abstract T createService(ContextImpl ctx) throws ServiceNotFoundException;
+
+ // Services that explicitly use a Context can never be fetched without one.
+ public final boolean supportsFetchWithoutContext() {
+ return false;
+ }
}
/**
@@ -2083,6 +2173,13 @@
}
public abstract T createService() throws ServiceNotFoundException;
+
+ // Services that do not need a Context can potentially be fetched without one, but the
+ // default is false, so that the service can require one in future without this being a
+ // breaking change.
+ public boolean supportsFetchWithoutContext() {
+ return false;
+ }
}
/** @hide */
diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java
index 5c42b0e..86d0125 100644
--- a/core/java/android/app/admin/DevicePolicyManager.java
+++ b/core/java/android/app/admin/DevicePolicyManager.java
@@ -53,6 +53,7 @@
import static android.content.Intent.LOCAL_FLAG_FROM_SYSTEM;
import static android.net.NetworkCapabilities.NET_ENTERPRISE_ID_1;
import static android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE;
+import static android.view.contentprotection.flags.Flags.FLAG_MANAGE_DEVICE_POLICY_ENABLED;
import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage;
@@ -61,6 +62,7 @@
import android.annotation.BroadcastBehavior;
import android.annotation.CallbackExecutor;
import android.annotation.ColorInt;
+import android.annotation.FlaggedApi;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -4092,6 +4094,29 @@
return MTE_NOT_CONTROLLED_BY_POLICY;
}
+ /** Indicates that content protection is not controlled by policy, allowing user to choose. */
+ @FlaggedApi(FLAG_MANAGE_DEVICE_POLICY_ENABLED)
+ public static final int CONTENT_PROTECTION_NOT_CONTROLLED_BY_POLICY = 0;
+
+ /** Indicates that content protection is controlled and disabled by a policy. */
+ @FlaggedApi(FLAG_MANAGE_DEVICE_POLICY_ENABLED)
+ public static final int CONTENT_PROTECTION_DISABLED = 1;
+
+ /** Indicates that content protection is controlled and enabled by a policy. */
+ @FlaggedApi(FLAG_MANAGE_DEVICE_POLICY_ENABLED)
+ public static final int CONTENT_PROTECTION_ENABLED = 2;
+
+ /** @hide */
+ @IntDef(
+ prefix = {"CONTENT_PROTECTION_"},
+ value = {
+ CONTENT_PROTECTION_NOT_CONTROLLED_BY_POLICY,
+ CONTENT_PROTECTION_DISABLED,
+ CONTENT_PROTECTION_ENABLED,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface ContentProtectionPolicy {}
+
/**
* This object is a single place to tack on invalidation and disable calls. All
* binder caches in this class derive from this Config, so all can be invalidated or
diff --git a/core/java/android/app/background_install_control_manager.aconfig b/core/java/android/app/background_install_control_manager.aconfig
index 029b93a..4473b95 100644
--- a/core/java/android/app/background_install_control_manager.aconfig
+++ b/core/java/android/app/background_install_control_manager.aconfig
@@ -1,7 +1,7 @@
package: "android.app"
flag {
- namespace: "background_install_control"
+ namespace: "preload_safety"
name: "bic_client"
description: "System API for background install control."
is_fixed_read_only: true
diff --git a/core/java/android/app/backup/BackupAgent.java b/core/java/android/app/backup/BackupAgent.java
index 6b558d0..ffbd80c 100644
--- a/core/java/android/app/backup/BackupAgent.java
+++ b/core/java/android/app/backup/BackupAgent.java
@@ -43,12 +43,14 @@
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.infra.AndroidFuture;
+import com.android.server.backup.Flags;
import libcore.io.IoUtils;
import org.xmlpull.v1.XmlPullParserException;
import java.io.File;
+import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.annotation.Retention;
@@ -1283,6 +1285,14 @@
// And bring live SharedPreferences instances up to date
reloadSharedPreferences();
+ // It's possible that onRestoreFile was overridden and that the agent did not
+ // consume all the data for this file from the pipe. We need to clear the pipe,
+ // otherwise the framework can get stuck trying to write to a full pipe or
+ // onRestoreFile could be called with the previous file's data left in the pipe.
+ if (Flags.enableClearPipeAfterRestoreFile()) {
+ clearUnconsumedDataFromPipe(data, size);
+ }
+
Binder.restoreCallingIdentity(ident);
try {
callbackBinder.opCompleteForUser(getBackupUserId(), token, 0);
@@ -1296,6 +1306,16 @@
}
}
+ private static void clearUnconsumedDataFromPipe(ParcelFileDescriptor data, long size) {
+ try (FileInputStream in = new FileInputStream(data.getFileDescriptor())) {
+ if (in.available() > 0) {
+ in.skip(size);
+ }
+ } catch (IOException e) {
+ Log.w(TAG, "Failed to clear unconsumed data from pipe.", e);
+ }
+ }
+
@Override
public void doRestoreFinished(int token, IBackupManager callbackBinder) {
final long ident = Binder.clearCallingIdentity();
diff --git a/core/java/android/app/multitasking.aconfig b/core/java/android/app/multitasking.aconfig
new file mode 100644
index 0000000..ab00891
--- /dev/null
+++ b/core/java/android/app/multitasking.aconfig
@@ -0,0 +1,8 @@
+package: "android.app"
+
+flag {
+ name: "enable_pip_ui_state_callback_on_entering"
+ namespace: "multitasking"
+ description: "Enables PiP UI state callback on entering"
+ bug: "303718131"
+}
diff --git a/core/java/android/appwidget/AppWidgetHostView.java b/core/java/android/appwidget/AppWidgetHostView.java
index ec181da..1f19f81 100644
--- a/core/java/android/appwidget/AppWidgetHostView.java
+++ b/core/java/android/appwidget/AppWidgetHostView.java
@@ -907,7 +907,10 @@
private InteractionHandler getHandler(InteractionHandler handler) {
return (view, pendingIntent, response) -> {
- AppWidgetManager.getInstance(mContext).noteAppWidgetTapped(mAppWidgetId);
+ AppWidgetManager manager = AppWidgetManager.getInstance(mContext);
+ if (manager != null) {
+ manager.noteAppWidgetTapped(mAppWidgetId);
+ }
if (handler != null) {
return handler.onInteraction(view, pendingIntent, response);
} else {
diff --git a/core/java/android/companion/virtual/VirtualDeviceManager.java b/core/java/android/companion/virtual/VirtualDeviceManager.java
index 90d251b..a16e94a 100644
--- a/core/java/android/companion/virtual/VirtualDeviceManager.java
+++ b/core/java/android/companion/virtual/VirtualDeviceManager.java
@@ -63,6 +63,7 @@
import android.os.Binder;
import android.os.Looper;
import android.os.RemoteException;
+import android.util.ArraySet;
import android.util.Log;
import android.view.Surface;
import android.view.WindowManager;
@@ -74,9 +75,11 @@
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.ArrayList;
+import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;
+import java.util.Set;
import java.util.concurrent.Executor;
import java.util.function.IntConsumer;
@@ -389,6 +392,28 @@
}
/**
+ * Returns all current persistent device IDs, including the ones for which no virtual device
+ * exists, as long as one may have existed or can be created.
+ *
+ * @hide
+ */
+ // TODO(b/315481938): Link @see VirtualDevice#getPersistentDeviceId()
+ @FlaggedApi(Flags.FLAG_PERSISTENT_DEVICE_ID_API)
+ @SystemApi
+ @NonNull
+ public Set<String> getAllPersistentDeviceIds() {
+ if (mService == null) {
+ Log.w(TAG, "Failed to retrieve persistent ids; no virtual device manager service.");
+ return Collections.emptySet();
+ }
+ try {
+ return new ArraySet<>(mService.getAllPersistentDeviceIds());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
* Checks whether the passed {@code deviceId} is a valid virtual device ID or not.
* {@link Context#DEVICE_ID_DEFAULT} is not valid as it is the ID of the default
* device which is not a virtual device. {@code deviceId} must correspond to a virtual device
diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java
index b1173a2..b8d7543 100644
--- a/core/java/android/content/Context.java
+++ b/core/java/android/content/Context.java
@@ -3657,8 +3657,8 @@
* On Android {@link android.os.Build.VERSION_CODES#S} and later,
* if the application is in a state where the service
* can not be started (such as not in the foreground in a state when services are allowed),
- * {@link android.app.BackgroundServiceStartNotAllowedException} is thrown
- * This excemption extends {@link IllegalStateException}, so apps can
+ * {@link android.app.BackgroundServiceStartNotAllowedException} is thrown.
+ * This exception extends {@link IllegalStateException}, so apps can
* use {@code catch (IllegalStateException)} to catch both.
*
* @see #startForegroundService(Intent)
diff --git a/core/java/android/content/pm/ILauncherApps.aidl b/core/java/android/content/pm/ILauncherApps.aidl
index a97de63..62db65f 100644
--- a/core/java/android/content/pm/ILauncherApps.aidl
+++ b/core/java/android/content/pm/ILauncherApps.aidl
@@ -128,4 +128,6 @@
/** Unregister a callback, so that it won't be called when LauncherApps dumps. */
void unRegisterDumpCallback(IDumpCallback cb);
+
+ void setArchiveCompatibilityOptions(boolean enableIconOverlay, boolean enableUnarchivalConfirmation);
}
diff --git a/core/java/android/content/pm/IPackageManager.aidl b/core/java/android/content/pm/IPackageManager.aidl
index 6dc8d47..380de96 100644
--- a/core/java/android/content/pm/IPackageManager.aidl
+++ b/core/java/android/content/pm/IPackageManager.aidl
@@ -840,7 +840,7 @@
ArchivedPackageParcel getArchivedPackage(in String packageName, int userId);
- Bitmap getArchivedAppIcon(String packageName, in UserHandle user);
+ Bitmap getArchivedAppIcon(String packageName, in UserHandle user, String callingPackageName);
boolean isAppArchivable(String packageName, in UserHandle user);
}
diff --git a/core/java/android/content/pm/LauncherApps.java b/core/java/android/content/pm/LauncherApps.java
index 1d2b1af..50be983 100644
--- a/core/java/android/content/pm/LauncherApps.java
+++ b/core/java/android/content/pm/LauncherApps.java
@@ -1801,6 +1801,31 @@
}
}
+ /**
+ * Enable or disable different archive compatibility options of the launcher.
+ *
+ * @param enableIconOverlay Provides a cloud overlay for archived apps to ensure users are aware
+ * that a certain app is archived. True by default.
+ * Launchers might want to disable this operation if they want to provide custom user experience
+ * to differentiate archived apps.
+ * @param enableUnarchivalConfirmation If true, the user is shown a confirmation dialog when
+ * they click an archived app, which explains that the app will be downloaded and restored in
+ * the background. True by default.
+ * Launchers might want to disable this operation if they provide sufficient, alternative user
+ * guidance to highlight that an unarchival is starting and ongoing once an archived app is
+ * tapped. E.g., this could be achieved by showing the unarchival progress around the icon.
+ */
+ @FlaggedApi(android.content.pm.Flags.FLAG_ARCHIVING)
+ public void setArchiveCompatibilityOptions(boolean enableIconOverlay,
+ boolean enableUnarchivalConfirmation) {
+ try {
+ mService.setArchiveCompatibilityOptions(enableIconOverlay,
+ enableUnarchivalConfirmation);
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+
/** @return position in mCallbacks for callback or -1 if not present. */
private int findCallbackLocked(Callback callback) {
if (callback == null) {
diff --git a/core/java/android/content/pm/verify/domain/OWNERS b/core/java/android/content/pm/verify/domain/OWNERS
index c669112..b451fe4 100644
--- a/core/java/android/content/pm/verify/domain/OWNERS
+++ b/core/java/android/content/pm/verify/domain/OWNERS
@@ -1,5 +1,4 @@
# Bug component: 36137
+include /PACKAGE_MANAGER_OWNERS
-chiuwinson@google.com
-patb@google.com
-toddke@google.com
\ No newline at end of file
+wloh@google.com
\ No newline at end of file
diff --git a/core/java/android/content/res/Element.java b/core/java/android/content/res/Element.java
index 89f4985..6ff96f4 100644
--- a/core/java/android/content/res/Element.java
+++ b/core/java/android/content/res/Element.java
@@ -93,6 +93,7 @@
protected static final String TAG_SUPPORTS_GL_TEXTURE = "supports-gl-texture";
protected static final String TAG_SUPPORTS_INPUT = "supports-input";
protected static final String TAG_SUPPORTS_SCREENS = "supports-screens";
+ protected static final String TAG_URI_RELATIVE_FILTER_GROUP = "uri-relative-filter-group";
protected static final String TAG_USES_CONFIGURATION = "uses-configuration";
protected static final String TAG_USES_FEATURE = "uses-feature";
protected static final String TAG_USES_GL_TEXTURE = "uses-gl-texture";
@@ -106,6 +107,11 @@
protected static final String TAG_ATTR_BACKUP_AGENT = "backupAgent";
protected static final String TAG_ATTR_CATEGORY = "category";
+ protected static final String TAG_ATTR_FRAGMENT = "fragment";
+ protected static final String TAG_ATTR_FRAGMENT_ADVANCED_PATTERN = "fragmentAdvancedPattern";
+ protected static final String TAG_ATTR_FRAGMENT_PATTERN = "fragmentPattern";
+ protected static final String TAG_ATTR_FRAGMENT_PREFIX = "fragmentPrefix";
+ protected static final String TAG_ATTR_FRAGMENT_SUFFIX = "fragmentSuffix";
protected static final String TAG_ATTR_HOST = "host";
protected static final String TAG_ATTR_MANAGE_SPACE_ACTIVITY = "manageSpaceActivity";
protected static final String TAG_ATTR_MIMETYPE = "mimeType";
@@ -122,6 +128,11 @@
protected static final String TAG_ATTR_PERMISSION_GROUP = "permissionGroup";
protected static final String TAG_ATTR_PORT = "port";
protected static final String TAG_ATTR_PROCESS = "process";
+ protected static final String TAG_ATTR_QUERY = "query";
+ protected static final String TAG_ATTR_QUERY_ADVANCED_PATTERN = "queryAdvancedPattern";
+ protected static final String TAG_ATTR_QUERY_PATTERN = "queryPattern";
+ protected static final String TAG_ATTR_QUERY_PREFIX = "queryPrefix";
+ protected static final String TAG_ATTR_QUERY_SUFFIX = "querySuffix";
protected static final String TAG_ATTR_READ_PERMISSION = "readPermission";
protected static final String TAG_ATTR_REQUIRED_ACCOUNT_TYPE = "requiredAccountType";
protected static final String TAG_ATTR_REQUIRED_SYSTEM_PROPERTY_NAME =
@@ -143,7 +154,7 @@
// The length of mTagCounters corresponds to the number of tags defined in getCounterIdx. If new
// tags are added then the size here should be increased to match.
- private final TagCounter[] mTagCounters = new TagCounter[34];
+ private final TagCounter[] mTagCounters = new TagCounter[35];
String mTag;
@@ -238,9 +249,11 @@
return 31;
case TAG_INTENT:
return 32;
+ case TAG_URI_RELATIVE_FILTER_GROUP:
+ return 33;
default:
// The size of the mTagCounters array should be equal to this value+1
- return 33;
+ return 34;
}
}
@@ -276,6 +289,7 @@
case TAG_SERVICE:
case TAG_SUPPORTS_GL_TEXTURE:
case TAG_SUPPORTS_SCREENS:
+ case TAG_URI_RELATIVE_FILTER_GROUP:
case TAG_USES_CONFIGURATION:
case TAG_USES_FEATURE:
case TAG_USES_LIBRARY:
@@ -322,6 +336,7 @@
break;
case TAG_INTENT:
case TAG_INTENT_FILTER:
+ initializeCounter(TAG_URI_RELATIVE_FILTER_GROUP, 100);
initializeCounter(TAG_ACTION, 20000);
initializeCounter(TAG_CATEGORY, 40000);
initializeCounter(TAG_DATA, 40000);
@@ -354,6 +369,9 @@
initializeCounter(TAG_INTENT, 2000);
initializeCounter(TAG_PROVIDER, 8000);
break;
+ case TAG_URI_RELATIVE_FILTER_GROUP:
+ initializeCounter(TAG_DATA, 100);
+ break;
}
}
@@ -391,11 +409,21 @@
case TAG_ATTR_VERSION_NAME:
case TAG_ATTR_ZYGOTE_PRELOAD_NAME:
return MAX_ATTR_LEN_NAME;
+ case TAG_ATTR_FRAGMENT:
+ case TAG_ATTR_FRAGMENT_ADVANCED_PATTERN:
+ case TAG_ATTR_FRAGMENT_PATTERN:
+ case TAG_ATTR_FRAGMENT_PREFIX:
+ case TAG_ATTR_FRAGMENT_SUFFIX:
case TAG_ATTR_PATH:
case TAG_ATTR_PATH_ADVANCED_PATTERN:
case TAG_ATTR_PATH_PATTERN:
case TAG_ATTR_PATH_PREFIX:
case TAG_ATTR_PATH_SUFFIX:
+ case TAG_ATTR_QUERY:
+ case TAG_ATTR_QUERY_ADVANCED_PATTERN:
+ case TAG_ATTR_QUERY_PATTERN:
+ case TAG_ATTR_QUERY_PREFIX:
+ case TAG_ATTR_QUERY_SUFFIX:
return MAX_ATTR_LEN_PATH;
case TAG_ATTR_VALUE:
return MAX_ATTR_LEN_VALUE;
@@ -535,6 +563,16 @@
case R.styleable.AndroidManifestData_pathPrefix:
case R.styleable.AndroidManifestData_pathSuffix:
case R.styleable.AndroidManifestData_pathAdvancedPattern:
+ case R.styleable.AndroidManifestData_query:
+ case R.styleable.AndroidManifestData_queryPattern:
+ case R.styleable.AndroidManifestData_queryPrefix:
+ case R.styleable.AndroidManifestData_querySuffix:
+ case R.styleable.AndroidManifestData_queryAdvancedPattern:
+ case R.styleable.AndroidManifestData_fragment:
+ case R.styleable.AndroidManifestData_fragmentPattern:
+ case R.styleable.AndroidManifestData_fragmentPrefix:
+ case R.styleable.AndroidManifestData_fragmentSuffix:
+ case R.styleable.AndroidManifestData_fragmentAdvancedPattern:
return MAX_ATTR_LEN_PATH;
default:
return DEFAULT_MAX_STRING_ATTR_LENGTH;
diff --git a/core/java/android/hardware/Camera.java b/core/java/android/hardware/Camera.java
index 0396443..1c36a88 100644
--- a/core/java/android/hardware/Camera.java
+++ b/core/java/android/hardware/Camera.java
@@ -1575,7 +1575,7 @@
}
@Override
- public void opChanged(int op, int uid, String packageName) {
+ public void opChanged(int op, int uid, String packageName, String persistentDeviceId) {
if (op == AppOpsManager.OP_PLAY_AUDIO) {
final Camera camera = mWeakCamera.get();
if (camera != null) {
diff --git a/core/java/android/hardware/camera2/extension/CameraOutputConfig.aidl b/core/java/android/hardware/camera2/extension/CameraOutputConfig.aidl
index 7c54a9b..509bcb8 100644
--- a/core/java/android/hardware/camera2/extension/CameraOutputConfig.aidl
+++ b/core/java/android/hardware/camera2/extension/CameraOutputConfig.aidl
@@ -26,6 +26,7 @@
Surface surface;
int imageFormat;
int capacity;
+ long usage;
const int TYPE_SURFACE = 0;
const int TYPE_IMAGEREADER = 1;
diff --git a/core/java/android/hardware/camera2/impl/CameraAdvancedExtensionSessionImpl.java b/core/java/android/hardware/camera2/impl/CameraAdvancedExtensionSessionImpl.java
index 98bc311..f6c8f36 100644
--- a/core/java/android/hardware/camera2/impl/CameraAdvancedExtensionSessionImpl.java
+++ b/core/java/android/hardware/camera2/impl/CameraAdvancedExtensionSessionImpl.java
@@ -1182,7 +1182,8 @@
return null;
}
ImageReader reader = ImageReader.newInstance(output.size.width,
- output.size.height, output.imageFormat, output.capacity);
+ output.size.height, output.imageFormat, output.capacity,
+ output.usage);
mReaderMap.put(output.outputId.id, reader);
return reader.getSurface();
case CameraOutputConfig.TYPE_MULTIRES_IMAGEREADER:
diff --git a/core/java/android/hardware/input/InputSettings.java b/core/java/android/hardware/input/InputSettings.java
index fdbd319..89fa5fb 100644
--- a/core/java/android/hardware/input/InputSettings.java
+++ b/core/java/android/hardware/input/InputSettings.java
@@ -19,6 +19,7 @@
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.hardware.input.Flags.touchpadTapDragging;
import static com.android.input.flags.Flags.enableInputFilterRustImpl;
import android.Manifest;
@@ -303,6 +304,53 @@
}
/**
+ * Returns true if the feature flag for touchpad tap dragging is enabled.
+ *
+ * @hide
+ */
+ public static boolean isTouchpadTapDraggingFeatureFlagEnabled() {
+ return touchpadTapDragging();
+ }
+
+ /**
+ * Returns true if the touchpad should allow tap dragging.
+ *
+ * The returned value only applies to gesture-compatible touchpads.
+ *
+ * @param context The application context.
+ * @return Whether the touchpad should allow tap dragging.
+ *
+ * @hide
+ */
+ public static boolean useTouchpadTapDragging(@NonNull Context context) {
+ if (!isTouchpadTapDraggingFeatureFlagEnabled()) {
+ return false;
+ }
+ return Settings.System.getIntForUser(context.getContentResolver(),
+ Settings.System.TOUCHPAD_TAP_DRAGGING, 0, UserHandle.USER_CURRENT) == 1;
+ }
+
+ /**
+ * Sets the tap dragging behavior for the touchpad.
+ *
+ * The new behavior is only applied to gesture-compatible touchpads.
+ *
+ * @param context The application context.
+ * @param enabled Will enable tap dragging if true, disable it if false
+ *
+ * @hide
+ */
+ @RequiresPermission(Manifest.permission.WRITE_SETTINGS)
+ public static void setTouchpadTapDragging(@NonNull Context context, boolean enabled) {
+ if (!isTouchpadTapDraggingFeatureFlagEnabled()) {
+ return;
+ }
+ Settings.System.putIntForUser(context.getContentResolver(),
+ Settings.System.TOUCHPAD_TAP_DRAGGING, enabled ? 1 : 0,
+ UserHandle.USER_CURRENT);
+ }
+
+ /**
* Returns true if the touchpad should use the right click zone.
*
* The returned value only applies to gesture-compatible touchpads.
diff --git a/core/java/android/hardware/input/input_framework.aconfig b/core/java/android/hardware/input/input_framework.aconfig
index 0ed6569..e070fe5 100644
--- a/core/java/android/hardware/input/input_framework.aconfig
+++ b/core/java/android/hardware/input/input_framework.aconfig
@@ -33,7 +33,21 @@
flag {
namespace: "input_native"
+ name: "emoji_and_screenshot_keycodes_available"
+ description: "Add new KeyEvent keycodes for opening Emoji Picker and Taking Screenshots"
+ bug: "315307777"
+}
+
+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"
+}
+
+flag {
+ namespace: "input_native"
+ name: "touchpad_tap_dragging"
+ description: "Offers a setting to enable touchpad tap dragging"
+ bug: "321978150"
}
\ No newline at end of file
diff --git a/core/java/android/hardware/radio/RadioMetadata.java b/core/java/android/hardware/radio/RadioMetadata.java
index db14c08..da6b9c2 100644
--- a/core/java/android/hardware/radio/RadioMetadata.java
+++ b/core/java/android/hardware/radio/RadioMetadata.java
@@ -507,10 +507,16 @@
*
* @param key The key the value is stored under.
* @return a bitmap identifier or 0 if it's missing.
- * @hide This API is not thoroughly elaborated yet
+ * @throws NullPointerException if metadata key is {@code null}
+ * @throws IllegalArgumentException if the metadata with the key is not found in
+ * metadata or the key is not of bitmap-key type
*/
+ @FlaggedApi(Flags.FLAG_HD_RADIO_IMPROVED)
public int getBitmapId(@NonNull String key) {
- if (!METADATA_KEY_ICON.equals(key) && !METADATA_KEY_ART.equals(key)) return 0;
+ Objects.requireNonNull(key, "Metadata key can not be null");
+ if (!METADATA_KEY_ICON.equals(key) && !METADATA_KEY_ART.equals(key)) {
+ throw new IllegalArgumentException("Failed to retrieve key " + key + " as bitmap key");
+ }
return getInt(key);
}
diff --git a/core/java/android/hardware/radio/RadioTuner.java b/core/java/android/hardware/radio/RadioTuner.java
index 9b2bcde..7c5c003 100644
--- a/core/java/android/hardware/radio/RadioTuner.java
+++ b/core/java/android/hardware/radio/RadioTuner.java
@@ -17,6 +17,7 @@
package android.hardware.radio;
import android.Manifest;
+import android.annotation.FlaggedApi;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -332,29 +333,30 @@
public abstract int getProgramInformation(RadioManager.ProgramInfo[] info);
/**
- * Retrieves a {@link Bitmap} for the given image ID or null,
+ * Retrieves a {@link Bitmap} for the given image ID or throw {@link IllegalArgumentException},
* if the image was missing from the tuner.
*
* <p>This involves doing a call to the tuner, so the bitmap should be cached
* on the application side.
*
- * <p>If the method returns null for non-zero ID, it means the image was
- * updated on the tuner side. There is a race conditon between fetching
- * image for an old ID and tuner updating the image (and cleaning up the
+ * <p>If the method throws {@link IllegalArgumentException} for non-zero ID, it
+ * means the image was updated on the tuner side. There is a race condition between
+ * fetching image for an old ID and tuner updating the image (and cleaning up the
* old image). In such case, a new ProgramInfo with updated image id will
* be sent with a {@link Callback#onProgramInfoChanged(RadioManager.ProgramInfo)}
* callback.
*
* @param id The image identifier, retrieved with
* {@link RadioMetadata#getBitmapId(String)}.
- * @return A {@link Bitmap} or null.
- * @throws IllegalArgumentException if id==0
- * @hide This API is not thoroughly elaborated yet
+ * @return A {@link Bitmap} for the given image ID.
+ * @throws IllegalArgumentException if id is 0 or the referenced image id no longer exists.
*/
- @SuppressWarnings("HiddenAbstractMethod")
+ @FlaggedApi(Flags.FLAG_HD_RADIO_IMPROVED)
@RequiresPermission(Manifest.permission.ACCESS_BROADCAST_RADIO)
- public abstract @Nullable Bitmap getMetadataImage(int id);
-
+ public @NonNull Bitmap getMetadataImage(int id) {
+ throw new UnsupportedOperationException(
+ "Getting metadata image must be implemented in child classes");
+ }
/**
* Initiates a background scan to update internally cached program list.
*
diff --git a/core/java/android/hardware/radio/TunerAdapter.java b/core/java/android/hardware/radio/TunerAdapter.java
index ba31ca3..63b2d4c 100644
--- a/core/java/android/hardware/radio/TunerAdapter.java
+++ b/core/java/android/hardware/radio/TunerAdapter.java
@@ -16,6 +16,7 @@
package android.hardware.radio;
+import android.annotation.NonNull;
import android.annotation.Nullable;
import android.graphics.Bitmap;
import android.os.RemoteException;
@@ -251,10 +252,18 @@
}
@Override
- @Nullable
+ @NonNull
public Bitmap getMetadataImage(int id) {
+ if (id == 0) {
+ throw new IllegalArgumentException("Invalid metadata image id 0");
+ }
try {
- return mTuner.getImage(id);
+ Bitmap bitmap = mTuner.getImage(id);
+ if (bitmap == null) {
+ throw new IllegalArgumentException("Metadata image with id " + id
+ + " is not available");
+ }
+ return bitmap;
} catch (RemoteException e) {
throw new RuntimeException("Service died", e);
}
diff --git a/core/java/android/net/flags.aconfig b/core/java/android/net/flags.aconfig
index 0ad1804..311dc09 100644
--- a/core/java/android/net/flags.aconfig
+++ b/core/java/android/net/flags.aconfig
@@ -1,50 +1,11 @@
-package: "com.android.net.flags"
+package: "android.net.platform.flags"
-flag {
- name: "track_multiple_network_activities"
- namespace: "android_core_networking"
- description: "NetworkActivityTracker tracks multiple networks including non default networks"
- bug: "267870186"
-}
-
-flag {
- name: "forbidden_capability"
- namespace: "android_core_networking"
- description: "This flag controls the forbidden capability API"
- bug: "302997505"
-}
-
-flag {
- name: "set_data_saver_via_cm"
- namespace: "android_core_networking"
- description: "Set data saver through ConnectivityManager API"
- bug: "297836825"
-}
-
-flag {
- name: "support_is_uid_networking_blocked"
- namespace: "android_core_networking"
- description: "This flag controls whether isUidNetworkingBlocked is supported"
- bug: "297836825"
-}
-
-flag {
- name: "basic_background_restrictions_enabled"
- namespace: "android_core_networking"
- description: "Block network access for apps in a low importance background state"
- bug: "304347838"
-}
-
-flag {
- name: "register_nsd_offload_engine"
- namespace: "android_core_networking"
- description: "The flag controls the access for registerOffloadEngine API in NsdManager"
- bug: "294777050"
-}
+# This file contains aconfig flags used from platform code
+# Flags used for module APIs must be in aconfig files under each modules
flag {
name: "ipsec_transform_state"
- namespace: "android_core_networking_ipsec"
+ namespace: "core_networking_ipsec"
description: "The flag controls the access for getIpSecTransformState and IpSecTransformState"
bug: "308011229"
}
diff --git a/core/java/android/os/Binder.java b/core/java/android/os/Binder.java
index 05b7827f..b7556df 100644
--- a/core/java/android/os/Binder.java
+++ b/core/java/android/os/Binder.java
@@ -292,7 +292,7 @@
sWarnOnBlockingOnCurrentThread.set(sWarnOnBlocking);
}
- private static ThreadLocal<SomeArgs> sIdentity$ravenwood;
+ private static volatile ThreadLocal<SomeArgs> sIdentity$ravenwood;
@android.ravenwood.annotation.RavenwoodKeepWholeClass
private static class IdentitySupplier implements Supplier<SomeArgs> {
diff --git a/core/java/android/os/HandlerThread.java b/core/java/android/os/HandlerThread.java
index fcd5731..36730cb 100644
--- a/core/java/android/os/HandlerThread.java
+++ b/core/java/android/os/HandlerThread.java
@@ -35,6 +35,7 @@
public HandlerThread(String name) {
super(name);
mPriority = Process.THREAD_PRIORITY_DEFAULT;
+ onCreated();
}
/**
@@ -46,8 +47,21 @@
public HandlerThread(String name, int priority) {
super(name);
mPriority = priority;
+ onCreated();
}
-
+
+ /** @hide */
+ @android.ravenwood.annotation.RavenwoodReplace
+ protected void onCreated() {
+ }
+
+ /** @hide */
+ protected void onCreated$ravenwood() {
+ // Mark ourselves as daemon to enable tests to terminate quickly when finished, despite
+ // any HandlerThread instances that may be lingering around
+ setDaemon(true);
+ }
+
/**
* Call back method that can be explicitly overridden if needed to execute some
* setup before Looper loops.
diff --git a/core/java/android/os/Process.java b/core/java/android/os/Process.java
index 1f3a162..7020a38 100644
--- a/core/java/android/os/Process.java
+++ b/core/java/android/os/Process.java
@@ -20,6 +20,7 @@
import android.annotation.ElapsedRealtimeLong;
import android.annotation.FlaggedApi;
+import android.annotation.IntRange;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SuppressLint;
@@ -844,7 +845,7 @@
return "amd64".equals(System.getProperty("os.arch"));
}
- private static ThreadLocal<SomeArgs> sIdentity$ravenwood;
+ private static volatile ThreadLocal<SomeArgs> sIdentity$ravenwood;
/** @hide */
@android.ravenwood.annotation.RavenwoodKeep
@@ -1122,7 +1123,8 @@
* priority.
*/
@android.ravenwood.annotation.RavenwoodReplace
- public static final native void setThreadPriority(int tid, int priority)
+ public static final native void setThreadPriority(int tid,
+ @IntRange(from = -20, to = THREAD_PRIORITY_LOWEST) int priority)
throws IllegalArgumentException, SecurityException;
/** @hide */
@@ -1288,7 +1290,8 @@
* @see #setThreadPriority(int, int)
*/
@android.ravenwood.annotation.RavenwoodReplace
- public static final native void setThreadPriority(int priority)
+ public static final native void setThreadPriority(
+ @IntRange(from = -20, to = THREAD_PRIORITY_LOWEST) int priority)
throws IllegalArgumentException, SecurityException;
/** @hide */
@@ -1310,6 +1313,7 @@
* <var>tid</var> does not exist.
*/
@android.ravenwood.annotation.RavenwoodReplace
+ @IntRange(from = -20, to = THREAD_PRIORITY_LOWEST)
public static final native int getThreadPriority(int tid)
throws IllegalArgumentException;
diff --git a/core/java/android/os/TestLooperManager.java b/core/java/android/os/TestLooperManager.java
index 5e7549f..4b16c1d 100644
--- a/core/java/android/os/TestLooperManager.java
+++ b/core/java/android/os/TestLooperManager.java
@@ -28,6 +28,7 @@
* The test code may use {@link #next()} to acquire messages that have been queued to this
* {@link Looper}/{@link MessageQueue} and then {@link #execute} to run any that desires.
*/
+@android.ravenwood.annotation.RavenwoodKeepWholeClass
public class TestLooperManager {
private static final ArraySet<Looper> sHeldLoopers = new ArraySet<>();
diff --git a/core/java/android/os/incremental/OWNERS b/core/java/android/os/incremental/OWNERS
index 47eee64..a925564 100644
--- a/core/java/android/os/incremental/OWNERS
+++ b/core/java/android/os/incremental/OWNERS
@@ -1,6 +1,4 @@
# Bug component: 554432
-alexbuy@google.com
-schfan@google.com
-toddke@google.com
+include /PACKAGE_MANAGER_OWNERS
+
zyy@google.com
-patb@google.com
diff --git a/core/java/android/os/storage/StorageManager.java b/core/java/android/os/storage/StorageManager.java
index 9587db1..3a57e84 100644
--- a/core/java/android/os/storage/StorageManager.java
+++ b/core/java/android/os/storage/StorageManager.java
@@ -1669,12 +1669,6 @@
}
}
- /** {@hide} */
- @TestApi
- public static boolean isUserKeyUnlocked(int userId) {
- return isCeStorageUnlocked(userId);
- }
-
/**
* Returns true if the user's credential-encrypted (CE) storage is unlocked.
*
diff --git a/core/java/android/os/vibrator/VibrationConfig.java b/core/java/android/os/vibrator/VibrationConfig.java
index 92e4967..bcdb982 100644
--- a/core/java/android/os/vibrator/VibrationConfig.java
+++ b/core/java/android/os/vibrator/VibrationConfig.java
@@ -35,6 +35,7 @@
import android.util.IndentingPrintWriter;
import java.io.PrintWriter;
+import java.util.Arrays;
/**
* List of device-specific internal vibration configuration loaded from platform config.xml.
@@ -51,6 +52,8 @@
private final float mHapticChannelMaxVibrationAmplitude;
private final int mRampStepDurationMs;
private final int mRampDownDurationMs;
+ private final int mRequestVibrationParamsTimeoutMs;
+ private final int[] mRequestVibrationParamsForUsages;
private final boolean mIgnoreVibrationsOnWirelessCharger;
@@ -75,6 +78,10 @@
com.android.internal.R.integer.config_vibrationWaveformRampDownDuration, 0);
mRampStepDurationMs = loadInteger(resources,
com.android.internal.R.integer.config_vibrationWaveformRampStepDuration, 0);
+ mRequestVibrationParamsTimeoutMs = loadInteger(resources,
+ com.android.internal.R.integer.config_requestVibrationParamsTimeout, 0);
+ mRequestVibrationParamsForUsages = loadIntArray(resources,
+ com.android.internal.R.array.config_requestVibrationParamsForUsages);
mIgnoreVibrationsOnWirelessCharger = loadBoolean(resources,
com.android.internal.R.bool.config_ignoreVibrationsOnWirelessCharger, false);
@@ -115,6 +122,10 @@
return res != null ? res.getBoolean(resId) : defaultValue;
}
+ private static int[] loadIntArray(@Nullable Resources res, int resId) {
+ return res != null ? res.getIntArray(resId) : new int[0];
+ }
+
/**
* Return the maximum amplitude the vibrator can play using the audio haptic channels.
*
@@ -140,6 +151,23 @@
}
/**
+ * The duration, in milliseconds, that the vibrator control service will wait for new
+ * vibration params.
+ */
+ public int getRequestVibrationParamsTimeoutMs() {
+ return Math.max(mRequestVibrationParamsTimeoutMs, 0);
+ }
+
+ /**
+ * The list of usages that should request vibration params before they are played. These
+ * usages don't have strong latency requirements, e.g. ringtone and notification, and can be
+ * slightly delayed.
+ */
+ public int[] getRequestVibrationParamsForUsages() {
+ return mRequestVibrationParamsForUsages;
+ }
+
+ /**
* The duration, in milliseconds, that should be applied to convert vibration effect's
* {@link android.os.vibrator.RampSegment} to a {@link android.os.vibrator.StepSegment} on
* devices without PWLE support.
@@ -204,6 +232,9 @@
+ ", mDefaultMediaIntensity=" + mDefaultMediaVibrationIntensity
+ ", mDefaultNotificationIntensity=" + mDefaultNotificationVibrationIntensity
+ ", mDefaultRingIntensity=" + mDefaultRingVibrationIntensity
+ + ", mRequestVibrationParamsTimeoutMs=" + mRequestVibrationParamsTimeoutMs
+ + ", mRequestVibrationParamsForUsages=" + Arrays.toString(
+ getRequestVibrationParamsForUsagesNames())
+ "}";
}
@@ -220,4 +251,14 @@
pw.println("rampDownDurationMs = " + mRampDownDurationMs);
pw.decreaseIndent();
}
+
+ private String[] getRequestVibrationParamsForUsagesNames() {
+ int usagesCount = mRequestVibrationParamsForUsages.length;
+ String[] names = new String[usagesCount];
+ for (int i = 0; i < usagesCount; i++) {
+ names[i] = VibrationAttributes.usageToString(mRequestVibrationParamsForUsages[i]);
+ }
+
+ return names;
+ }
}
diff --git a/core/java/android/provider/CallLog.java b/core/java/android/provider/CallLog.java
index 5d6dfc7..7d127ad 100644
--- a/core/java/android/provider/CallLog.java
+++ b/core/java/android/provider/CallLog.java
@@ -2102,7 +2102,7 @@
PhoneAccountHandle accountHandle) {
TelecomManager tm = null;
try {
- tm = TelecomManager.from(context);
+ tm = context.getSystemService(TelecomManager.class);
} catch (UnsupportedOperationException e) {
if (VERBOSE_LOG) {
Log.v(LOG_TAG, "No TelecomManager found to get account address.");
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index e238080..524b733 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -6049,6 +6049,13 @@
public static final String TOUCHPAD_TAP_TO_CLICK = "touchpad_tap_to_click";
/**
+ * Whether to enable tap dragging on touchpads.
+ *
+ * @hide
+ */
+ public static final String TOUCHPAD_TAP_DRAGGING = "touchpad_tap_dragging";
+
+ /**
* Whether to enable a right-click zone on touchpads.
*
* When set to 1, pressing to click in a section on the right-hand side of the touchpad will
@@ -6270,6 +6277,7 @@
PRIVATE_SETTINGS.add(TOUCHPAD_POINTER_SPEED);
PRIVATE_SETTINGS.add(TOUCHPAD_NATURAL_SCROLLING);
PRIVATE_SETTINGS.add(TOUCHPAD_TAP_TO_CLICK);
+ PRIVATE_SETTINGS.add(TOUCHPAD_TAP_DRAGGING);
PRIVATE_SETTINGS.add(TOUCHPAD_RIGHT_CLICK_ZONE);
PRIVATE_SETTINGS.add(CAMERA_FLASH_NOTIFICATION);
PRIVATE_SETTINGS.add(SCREEN_FLASH_NOTIFICATION);
diff --git a/core/java/android/provider/Telephony.java b/core/java/android/provider/Telephony.java
index d47ff2e..2841dc0 100644
--- a/core/java/android/provider/Telephony.java
+++ b/core/java/android/provider/Telephony.java
@@ -3202,7 +3202,7 @@
/**
* The infrastructure bitmask which the APN can be used on. For example, some APNs can only
* be used when the device is on cellular, on satellite, or both. The default value is
- * 1 (INFRASTRUCTURE_CELLULAR).
+ * 3 (INFRASTRUCTURE_CELLULAR | INFRASTRUCTURE_SATELLITE).
*
* <P>Type: INTEGER</P>
* @hide
@@ -4927,7 +4927,7 @@
/**
* TelephonyProvider column name for satellite attach enabled for carrier. The value of this
* column is set based on user settings.
- * By default, it's disabled.
+ * By default, it's enabled.
*
* @hide
*/
diff --git a/core/java/android/service/notification/ZenModeConfig.java b/core/java/android/service/notification/ZenModeConfig.java
index 9d19ef6..d4a5356 100644
--- a/core/java/android/service/notification/ZenModeConfig.java
+++ b/core/java/android/service/notification/ZenModeConfig.java
@@ -2297,6 +2297,18 @@
component, configurationActivity, pkg, id, enabler, zenPolicy, modified);
}
+ /** Returns a deep copy of the {@link ZenRule}. */
+ public ZenRule copy() {
+ final Parcel parcel = Parcel.obtain();
+ try {
+ writeToParcel(parcel, 0);
+ parcel.setDataPosition(0);
+ return new ZenRule(parcel);
+ } finally {
+ parcel.recycle();
+ }
+ }
+
public boolean isAutomaticActive() {
return enabled && !snoozing && getPkg() != null && isTrueOrUnknown();
}
diff --git a/core/java/android/service/notification/flags.aconfig b/core/java/android/service/notification/flags.aconfig
index 3008b8d..446fe3d 100644
--- a/core/java/android/service/notification/flags.aconfig
+++ b/core/java/android/service/notification/flags.aconfig
@@ -8,14 +8,6 @@
}
flag {
- name: "notification_lifetime_extension_refactor"
- namespace: "systemui"
- description: "Enables moving notification lifetime extension management from SystemUI to "
- "Notification Manager Service"
- bug: "299448097"
-}
-
-flag {
name: "redact_sensitive_notifications_from_untrusted_listeners"
namespace: "systemui"
description: "This flag controls the redacting of sensitive notifications from untrusted NotificationListenerServices"
diff --git a/core/java/android/text/FontConfig.java b/core/java/android/text/FontConfig.java
index 94c8eaf..783f3b7 100644
--- a/core/java/android/text/FontConfig.java
+++ b/core/java/android/text/FontConfig.java
@@ -26,7 +26,6 @@
import android.annotation.SystemApi;
import android.annotation.TestApi;
import android.compat.annotation.UnsupportedAppUsage;
-import android.graphics.fonts.FontFamily.Builder.VariableFontFamilyType;
import android.graphics.fonts.FontStyle;
import android.graphics.fonts.FontVariationAxis;
import android.icu.util.ULocale;
@@ -240,6 +239,23 @@
private final @IntRange(from = 0) int mIndex;
private final @NonNull String mFontVariationSettings;
private final @Nullable String mFontFamilyName;
+ private final @VarTypeAxes int mVarTypeAxes;
+
+ /** @hide */
+ @Retention(SOURCE)
+ @IntDef(prefix = { "VAR_TYPE_AXES_" }, value = {
+ VAR_TYPE_AXES_NONE,
+ VAR_TYPE_AXES_WGHT,
+ VAR_TYPE_AXES_ITAL,
+ })
+ public @interface VarTypeAxes {}
+
+ /** @hide */
+ public static final int VAR_TYPE_AXES_NONE = 0;
+ /** @hide */
+ public static final int VAR_TYPE_AXES_WGHT = 1;
+ /** @hide */
+ public static final int VAR_TYPE_AXES_ITAL = 2;
/**
* Construct a Font instance.
@@ -248,7 +264,8 @@
*/
public Font(@NonNull File file, @Nullable File originalFile, @NonNull String postScriptName,
@NonNull FontStyle style, @IntRange(from = 0) int index,
- @NonNull String fontVariationSettings, @Nullable String fontFamilyName) {
+ @NonNull String fontVariationSettings, @Nullable String fontFamilyName,
+ @VarTypeAxes int varTypeAxes) {
mFile = file;
mOriginalFile = originalFile;
mPostScriptName = postScriptName;
@@ -256,6 +273,7 @@
mIndex = index;
mFontVariationSettings = fontVariationSettings;
mFontFamilyName = fontFamilyName;
+ mVarTypeAxes = varTypeAxes;
}
@Override
@@ -273,6 +291,7 @@
dest.writeInt(mIndex);
dest.writeString8(mFontVariationSettings);
dest.writeString8(mFontFamilyName);
+ dest.writeInt(mVarTypeAxes);
}
public static final @NonNull Creator<Font> CREATOR = new Creator<Font>() {
@@ -288,9 +307,10 @@
int index = source.readInt();
String varSettings = source.readString8();
String fallback = source.readString8();
+ int varTypeAxes = source.readInt();
return new Font(path, originalPath, postScriptName, new FontStyle(weight, slant),
- index, varSettings, fallback);
+ index, varSettings, fallback, varTypeAxes);
}
@Override
@@ -366,6 +386,15 @@
}
/**
+ * Returns the list of supported axes tags for variable family type resolution.
+ *
+ * @hide
+ */
+ public @VarTypeAxes int getVarTypeAxes() {
+ return mVarTypeAxes;
+ }
+
+ /**
* Returns the list of axes associated to this font.
* @deprecated Use getFontVariationSettings
* @hide
@@ -408,13 +437,14 @@
&& Objects.equals(mOriginalFile, font.mOriginalFile)
&& Objects.equals(mStyle, font.mStyle)
&& Objects.equals(mFontVariationSettings, font.mFontVariationSettings)
- && Objects.equals(mFontFamilyName, font.mFontFamilyName);
+ && Objects.equals(mFontFamilyName, font.mFontFamilyName)
+ && mVarTypeAxes == font.mVarTypeAxes;
}
@Override
public int hashCode() {
return Objects.hash(mFile, mOriginalFile, mStyle, mIndex, mFontVariationSettings,
- mFontFamilyName);
+ mFontFamilyName, mVarTypeAxes);
}
@Override
@@ -426,6 +456,7 @@
+ ", mIndex=" + mIndex
+ ", mFontVariationSettings='" + mFontVariationSettings + '\''
+ ", mFontFamilyName='" + mFontFamilyName + '\''
+ + ", mVarTypeAxes='" + mVarTypeAxes + '\''
+ '}';
}
}
@@ -549,7 +580,6 @@
private final @NonNull List<Font> mFonts;
private final @NonNull LocaleList mLocaleList;
private final @Variant int mVariant;
- private final int mVariableFontFamilyType;
/** @hide */
@Retention(SOURCE)
@@ -589,11 +619,10 @@
* @hide Only system server can create this instance and passed via IPC.
*/
public FontFamily(@NonNull List<Font> fonts, @NonNull LocaleList localeList,
- @Variant int variant, int variableFontFamilyType) {
+ @Variant int variant) {
mFonts = fonts;
mLocaleList = localeList;
mVariant = variant;
- mVariableFontFamilyType = variableFontFamilyType;
}
/**
@@ -644,20 +673,6 @@
return mVariant;
}
- /**
- * Returns the font family type.
- *
- * @see Builder#VARIABLE_FONT_FAMILY_TYPE_NONE
- * @see Builder#VARIABLE_FONT_FAMILY_TYPE_SINGLE_FONT_WGHT_ITAL
- * @see Builder#VARIABLE_FONT_FAMILY_TYPE_SINGLE_FONT_WGHT_ONLY
- * @see Builder#VARIABLE_FONT_FAMILY_TYPE_TWO_FONTS_WGHT
- * @hide
- * @return variable font family type.
- */
- public @VariableFontFamilyType int getVariableFontFamilyType() {
- return mVariableFontFamilyType;
- }
-
@Override
public int describeContents() {
return 0;
@@ -668,7 +683,6 @@
dest.writeTypedList(mFonts, flags);
dest.writeString8(mLocaleList.toLanguageTags());
dest.writeInt(mVariant);
- dest.writeInt(mVariableFontFamilyType);
}
public static final @NonNull Creator<FontFamily> CREATOR = new Creator<FontFamily>() {
@@ -679,10 +693,8 @@
source.readTypedList(fonts, Font.CREATOR);
String langTags = source.readString8();
int variant = source.readInt();
- int varFamilyType = source.readInt();
- return new FontFamily(fonts, LocaleList.forLanguageTags(langTags), variant,
- varFamilyType);
+ return new FontFamily(fonts, LocaleList.forLanguageTags(langTags), variant);
}
@Override
diff --git a/core/java/android/text/flags/flags.aconfig b/core/java/android/text/flags/flags.aconfig
index 6e45fea..f3e0ea7 100644
--- a/core/java/android/text/flags/flags.aconfig
+++ b/core/java/android/text/flags/flags.aconfig
@@ -96,3 +96,10 @@
description: "A feature flag for fixing the crash while delete text in insert mode."
bug: "314254153"
}
+
+flag {
+ name: "insert_mode_not_update_selection"
+ namespace: "text"
+ description: "Fix that InputService#onUpdateSelection is not called when insert mode gesture is performed."
+ bug: "300850862"
+}
diff --git a/core/java/android/util/Slog.java b/core/java/android/util/Slog.java
index c0ceb9ea..9ecb4cb 100644
--- a/core/java/android/util/Slog.java
+++ b/core/java/android/util/Slog.java
@@ -217,7 +217,6 @@
* @see Log#wtf(String, String)
*/
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
- @android.ravenwood.annotation.RavenwoodThrow
public static int wtf(@Nullable String tag, @NonNull String msg) {
return Log.wtf(Log.LOG_ID_SYSTEM, tag, msg, null, false, true);
}
@@ -263,7 +262,6 @@
*
* @see Log#wtf(String, Throwable)
*/
- @android.ravenwood.annotation.RavenwoodThrow
public static int wtf(@Nullable String tag, @Nullable Throwable tr) {
return Log.wtf(Log.LOG_ID_SYSTEM, tag, tr.getMessage(), tr, false, true);
}
@@ -284,7 +282,6 @@
* @see Log#wtf(String, String, Throwable)
*/
@UnsupportedAppUsage
- @android.ravenwood.annotation.RavenwoodThrow
public static int wtf(@Nullable String tag, @NonNull String msg, @Nullable Throwable tr) {
return Log.wtf(Log.LOG_ID_SYSTEM, tag, msg, tr, false, true);
}
diff --git a/core/java/android/view/HdrRenderState.java b/core/java/android/view/HdrRenderState.java
index 2fbbf48..eadc507 100644
--- a/core/java/android/view/HdrRenderState.java
+++ b/core/java/android/view/HdrRenderState.java
@@ -31,9 +31,11 @@
private final ViewRootImpl mViewRoot;
+ private boolean mIsHdrEnabled = false;
private boolean mIsListenerRegistered = false;
private boolean mUpdateHdrSdrRatioInfo = false;
private float mDesiredHdrSdrRatio = 1f;
+ private float mTargetDesiredHdrSdrRatio = 1f;
private float mTargetHdrSdrRatio = 1f;
private float mRenderHdrSdrRatio = 1f;
private float mPreviousRenderRatio = 1f;
@@ -50,7 +52,7 @@
}
boolean isHdrEnabled() {
- return mDesiredHdrSdrRatio >= 1.01f;
+ return mIsHdrEnabled;
}
void stopListening() {
@@ -75,9 +77,7 @@
final float maxStep = timeDelta * TRANSITION_PER_MS;
mLastUpdateMillis = frameTimeMillis;
if (hasUpdate && FLAG_ANIMATE_ENABLED) {
- if (mTargetHdrSdrRatio == 1.0f) {
- mPreviousRenderRatio = mTargetHdrSdrRatio;
- } else {
+ if (isHdrEnabled()) {
float delta = mTargetHdrSdrRatio - mPreviousRenderRatio;
if (delta > maxStep) {
mRenderHdrSdrRatio = mPreviousRenderRatio + maxStep;
@@ -85,6 +85,19 @@
mViewRoot.invalidate();
}
mPreviousRenderRatio = mRenderHdrSdrRatio;
+
+ if (mTargetDesiredHdrSdrRatio < mDesiredHdrSdrRatio) {
+ mDesiredHdrSdrRatio = Math.max(mTargetDesiredHdrSdrRatio,
+ mDesiredHdrSdrRatio - maxStep);
+ if (mDesiredHdrSdrRatio != mTargetDesiredHdrSdrRatio) {
+ mUpdateHdrSdrRatioInfo = true;
+ mViewRoot.invalidate();
+ }
+ }
+
+ } else {
+ mPreviousRenderRatio = mTargetHdrSdrRatio;
+ mDesiredHdrSdrRatio = mTargetDesiredHdrSdrRatio;
}
}
return hasUpdate;
@@ -99,15 +112,23 @@
}
void forceUpdateHdrSdrRatio() {
- mTargetHdrSdrRatio = Math.min(mDesiredHdrSdrRatio, mViewRoot.mDisplay.getHdrSdrRatio());
+ if (isHdrEnabled()) {
+ mTargetHdrSdrRatio = Math.min(mDesiredHdrSdrRatio,
+ mViewRoot.mDisplay.getHdrSdrRatio());
+ } else {
+ mTargetHdrSdrRatio = 1.0f;
+ }
mUpdateHdrSdrRatioInfo = true;
}
- void setDesiredHdrSdrRatio(float desiredRatio) {
+ void setDesiredHdrSdrRatio(boolean isHdrEnabled, float desiredRatio) {
+ mIsHdrEnabled = isHdrEnabled;
mLastUpdateMillis = SystemClock.uptimeMillis();
- // TODO: When decreasing the desired ratio we need to animate it downwards
- if (desiredRatio != mDesiredHdrSdrRatio) {
- mDesiredHdrSdrRatio = desiredRatio;
+ if (desiredRatio != mTargetDesiredHdrSdrRatio) {
+ mTargetDesiredHdrSdrRatio = desiredRatio;
+ if (mTargetDesiredHdrSdrRatio > mDesiredHdrSdrRatio || !FLAG_ANIMATE_ENABLED) {
+ mDesiredHdrSdrRatio = mTargetDesiredHdrSdrRatio;
+ }
forceUpdateHdrSdrRatio();
mViewRoot.invalidate();
diff --git a/core/java/android/view/KeyEvent.java b/core/java/android/view/KeyEvent.java
index c6601e8d..1ee9509 100644
--- a/core/java/android/view/KeyEvent.java
+++ b/core/java/android/view/KeyEvent.java
@@ -19,6 +19,7 @@
import static android.os.IInputConstants.INPUT_EVENT_FLAG_IS_ACCESSIBILITY_EVENT;
import static android.view.Display.INVALID_DISPLAY;
+import android.annotation.FlaggedApi;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.TestApi;
@@ -31,6 +32,8 @@
import android.util.SparseIntArray;
import android.view.KeyCharacterMap.KeyData;
+import com.android.hardware.input.Flags;
+
import java.util.concurrent.TimeUnit;
/**
@@ -384,7 +387,13 @@
public static final int KEYCODE_META_RIGHT = 118;
/** Key code constant: Function modifier key. */
public static final int KEYCODE_FUNCTION = 119;
- /** Key code constant: System Request / Print Screen key. */
+ /**
+ * Key code constant: System Request / Print Screen key.
+ *
+ * This key is sent to the app first and only if the app doesn't handle it, the framework
+ * handles it (to take a screenshot), unlike {@code KEYCODE_TAKE_SCREENSHOT} which is
+ * fully handled by the framework.
+ */
public static final int KEYCODE_SYSRQ = 120;
/** Key code constant: Break / Pause key. */
public static final int KEYCODE_BREAK = 121;
@@ -921,14 +930,25 @@
* User customizable key #4.
*/
public static final int KEYCODE_MACRO_4 = 316;
-
+ /** Key code constant: To open emoji picker */
+ @FlaggedApi(Flags.FLAG_EMOJI_AND_SCREENSHOT_KEYCODES_AVAILABLE)
+ public static final int KEYCODE_EMOJI_PICKER = 317;
+ /**
+ * Key code constant: To take a screenshot
+ *
+ * This key is fully handled by the framework and will not be sent to the foreground app,
+ * unlike {@code KEYCODE_SYSRQ} which is sent to the app first and only if the app
+ * doesn't handle it, the framework handles it (to take a screenshot).
+ */
+ @FlaggedApi(Flags.FLAG_EMOJI_AND_SCREENSHOT_KEYCODES_AVAILABLE)
+ public static final int KEYCODE_SCREENSHOT = 318;
/**
* Integer value of the last KEYCODE. Increases as new keycodes are added to KeyEvent.
* @hide
*/
@TestApi
- public static final int LAST_KEYCODE = KEYCODE_MACRO_4;
+ public static final int LAST_KEYCODE = KEYCODE_SCREENSHOT;
// NOTE: If you add a new keycode here you must also add it to:
// isSystem()
diff --git a/core/java/android/view/MotionPredictor.java b/core/java/android/view/MotionPredictor.java
index 27af300..db2efaa 100644
--- a/core/java/android/view/MotionPredictor.java
+++ b/core/java/android/view/MotionPredictor.java
@@ -20,6 +20,8 @@
import android.annotation.Nullable;
import android.content.Context;
+import com.android.internal.annotations.VisibleForTesting;
+
import libcore.util.NativeAllocationRegistry;
/**
@@ -57,11 +59,21 @@
* @param context The context for the predictions
*/
public MotionPredictor(@NonNull Context context) {
- mIsPredictionEnabled = context.getResources().getBoolean(
- com.android.internal.R.bool.config_enableMotionPrediction);
- final int offsetNanos = context.getResources().getInteger(
- com.android.internal.R.integer.config_motionPredictionOffsetNanos);
- mPtr = nativeInitialize(offsetNanos);
+ this(
+ context.getResources().getBoolean(
+ com.android.internal.R.bool.config_enableMotionPrediction),
+ context.getResources().getInteger(
+ com.android.internal.R.integer.config_motionPredictionOffsetNanos));
+ }
+
+ /**
+ * Internal constructor for testing.
+ * @hide
+ */
+ @VisibleForTesting
+ public MotionPredictor(boolean isPredictionEnabled, int motionPredictionOffsetNanos) {
+ mIsPredictionEnabled = isPredictionEnabled;
+ mPtr = nativeInitialize(motionPredictionOffsetNanos);
RegistryHolder.REGISTRY.registerNativeAllocation(this, mPtr);
}
diff --git a/core/java/android/view/SurfaceControlViewHost.java b/core/java/android/view/SurfaceControlViewHost.java
index 4840f00..5249fd5 100644
--- a/core/java/android/view/SurfaceControlViewHost.java
+++ b/core/java/android/view/SurfaceControlViewHost.java
@@ -538,8 +538,8 @@
}
private void addWindowToken(WindowManager.LayoutParams attrs) {
- final WindowManagerImpl wm =
- (WindowManagerImpl) mViewRoot.mContext.getSystemService(Context.WINDOW_SERVICE);
+ final WindowManager wm =
+ (WindowManager) mViewRoot.mContext.getSystemService(Context.WINDOW_SERVICE);
attrs.token = wm.getDefaultToken();
}
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index 7bc832e..c18aeee 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -399,6 +399,8 @@
@Nullable
private ContentObserver mForceInvertObserver;
+ private static final int INVALID_VALUE = Integer.MIN_VALUE;
+ private int mForceInvertEnabled = INVALID_VALUE;
/**
* Callback for notifying about global configuration changes.
*/
@@ -1604,6 +1606,23 @@
}
}
+ private boolean isForceInvertEnabled() {
+ if (mForceInvertEnabled == INVALID_VALUE) {
+ reloadForceInvertEnabled();
+ }
+ return mForceInvertEnabled == 1;
+ }
+
+ private void reloadForceInvertEnabled() {
+ if (forceInvertColor()) {
+ mForceInvertEnabled = Settings.Secure.getIntForUser(
+ mContext.getContentResolver(),
+ Settings.Secure.ACCESSIBILITY_FORCE_INVERT_COLOR_ENABLED,
+ /* def= */ 0,
+ UserHandle.myUserId());
+ }
+ }
+
/**
* Register any kind of listeners if setView was success.
*/
@@ -1630,6 +1649,7 @@
mForceInvertObserver = new ContentObserver(mHandler) {
@Override
public void onChange(boolean selfChange) {
+ reloadForceInvertEnabled();
updateForceDarkMode();
}
};
@@ -1850,16 +1870,11 @@
@VisibleForTesting
public @ForceDarkType.ForceDarkTypeDef int determineForceDarkType() {
if (forceInvertColor()) {
- boolean isForceInvertEnabled = Settings.Secure.getIntForUser(
- mContext.getContentResolver(),
- Settings.Secure.ACCESSIBILITY_FORCE_INVERT_COLOR_ENABLED,
- /* def= */ 0,
- UserHandle.myUserId()) == 1;
// Force invert ignores all developer opt-outs.
// We also ignore dark theme, since the app developer can override the user's preference
// for dark mode in configuration.uiMode. Instead, we assume that the force invert
// setting will be enabled at the same time dark theme is in the Settings app.
- if (isForceInvertEnabled) {
+ if (isForceInvertEnabled()) {
return ForceDarkType.FORCE_INVERT_COLOR_DARK;
}
}
@@ -5809,9 +5824,11 @@
if (mAttachInfo.mThreadedRenderer == null) {
return;
}
- if ((colorMode == ActivityInfo.COLOR_MODE_HDR || colorMode == ActivityInfo.COLOR_MODE_HDR10)
- && !mDisplay.isHdrSdrRatioAvailable()) {
+ boolean isHdr = colorMode == ActivityInfo.COLOR_MODE_HDR
+ || colorMode == ActivityInfo.COLOR_MODE_HDR10;
+ if (isHdr && !mDisplay.isHdrSdrRatioAvailable()) {
colorMode = ActivityInfo.COLOR_MODE_WIDE_COLOR_GAMUT;
+ isHdr = false;
}
// TODO: Centralize this sanitization? Why do we let setting bad modes?
// Alternatively, can we just let HWUI figure it out? Do we need to care here?
@@ -5824,7 +5841,7 @@
desiredRatio = automaticRatio;
}
- mHdrRenderState.setDesiredHdrSdrRatio(desiredRatio);
+ mHdrRenderState.setDesiredHdrSdrRatio(isHdr, desiredRatio);
}
@Override
diff --git a/core/java/android/view/WindowManager.java b/core/java/android/view/WindowManager.java
index 427d053..c788261 100644
--- a/core/java/android/view/WindowManager.java
+++ b/core/java/android/view/WindowManager.java
@@ -6109,4 +6109,12 @@
throw new UnsupportedOperationException(
"getSurfaceControlInputClientToken is not implemented");
}
+
+ /**
+ * @hide
+ */
+ default @NonNull IBinder getDefaultToken() {
+ throw new UnsupportedOperationException(
+ "getDefaultToken is not implemented");
+ }
}
diff --git a/core/java/android/view/WindowManagerImpl.java b/core/java/android/view/WindowManagerImpl.java
index 41d181c..5072ad7 100644
--- a/core/java/android/view/WindowManagerImpl.java
+++ b/core/java/android/view/WindowManagerImpl.java
@@ -458,7 +458,9 @@
return null;
}
- IBinder getDefaultToken() {
+ @Override
+ @NonNull
+ public IBinder getDefaultToken() {
return mDefaultToken;
}
diff --git a/core/java/android/view/accessibility/AccessibilityManager.java b/core/java/android/view/accessibility/AccessibilityManager.java
index 7782fd7..ef1bf5a 100644
--- a/core/java/android/view/accessibility/AccessibilityManager.java
+++ b/core/java/android/view/accessibility/AccessibilityManager.java
@@ -62,6 +62,7 @@
import android.util.Log;
import android.util.SparseArray;
import android.view.IWindow;
+import android.view.SurfaceControl;
import android.view.View;
import android.view.accessibility.AccessibilityEvent.EventType;
@@ -2404,4 +2405,29 @@
throw re.rethrowFromSystemServer();
}
}
+
+
+ /**
+ * Attaches a {@link android.view.SurfaceControl} containing an accessibility overlay to the
+ * specified display.
+ *
+ * @hide
+ */
+ @RequiresPermission(android.Manifest.permission.INTERNAL_SYSTEM_WINDOW)
+ public void attachAccessibilityOverlayToDisplay(
+ int displayId, @NonNull SurfaceControl surfaceControl) {
+ final IAccessibilityManager service;
+ synchronized (mLock) {
+ service = getServiceLocked();
+ if (service == null) {
+ return;
+ }
+ }
+ try {
+ service.attachAccessibilityOverlayToDisplay_enforcePermission(
+ displayId, surfaceControl);
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
}
diff --git a/core/java/android/view/accessibility/IAccessibilityManager.aidl b/core/java/android/view/accessibility/IAccessibilityManager.aidl
index 9c04c27..1c5d29e 100644
--- a/core/java/android/view/accessibility/IAccessibilityManager.aidl
+++ b/core/java/android/view/accessibility/IAccessibilityManager.aidl
@@ -31,6 +31,7 @@
import android.view.InputEvent;
import android.view.IWindow;
import android.view.MagnificationSpec;
+import android.view.SurfaceControl;
/**
* Interface implemented by the AccessibilityManagerService called by
@@ -136,4 +137,7 @@
MagnificationSpec magnificationSpec;
}
WindowTransformationSpec getWindowTransformationSpec(int windowId);
+
+ @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.INTERNAL_SYSTEM_WINDOW)")
+ void attachAccessibilityOverlayToDisplay_enforcePermission(int displayId, in SurfaceControl surfaceControl);
}
diff --git a/core/java/android/view/contentprotection/flags/content_protection_flags.aconfig b/core/java/android/view/contentprotection/flags/content_protection_flags.aconfig
index 2a3008a..5d3153c 100644
--- a/core/java/android/view/contentprotection/flags/content_protection_flags.aconfig
+++ b/core/java/android/view/contentprotection/flags/content_protection_flags.aconfig
@@ -34,3 +34,10 @@
description: "If true, an appop is logged when a notification is rapidly cleared by a notification listener."
bug: "289080543"
}
+
+flag {
+ name: "manage_device_policy_enabled"
+ namespace: "content_protection"
+ description: "If true, the APIs to manage content protection device policy will be enabled."
+ bug: "319477846"
+}
diff --git a/core/java/android/webkit/flags.aconfig b/core/java/android/webkit/flags.aconfig
new file mode 100644
index 0000000..6938b29
--- /dev/null
+++ b/core/java/android/webkit/flags.aconfig
@@ -0,0 +1,9 @@
+package: "android.webkit"
+
+flag {
+ name: "update_service_ipc_wrapper"
+ namespace: "webview"
+ description: "New API: proper wrapper for IWebViewUpdateService"
+ bug: "319292658"
+ is_fixed_read_only: true
+}
diff --git a/core/java/android/widget/RemoteViews.java b/core/java/android/widget/RemoteViews.java
index 0654add..2433bd8 100644
--- a/core/java/android/widget/RemoteViews.java
+++ b/core/java/android/widget/RemoteViews.java
@@ -7597,14 +7597,6 @@
}
/**
- * Append additional instructions to this {@link DrawInstructions} object.
- */
- @FlaggedApi(FLAG_DRAW_DATA_PARCEL)
- public void appendInstructions(@NonNull final byte[] instructions) {
- mInstructions.add(instructions);
- }
-
- /**
* Builder class for {@link DrawInstructions} objects.
*/
@FlaggedApi(FLAG_DRAW_DATA_PARCEL)
diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java
index e812f85..90d5140 100644
--- a/core/java/android/widget/TextView.java
+++ b/core/java/android/widget/TextView.java
@@ -243,6 +243,7 @@
import com.android.internal.util.ArrayUtils;
import com.android.internal.util.FastMath;
import com.android.internal.util.Preconditions;
+import com.android.text.flags.Flags;
import libcore.util.EmptyArray;
@@ -539,7 +540,6 @@
// System wide time for last cut, copy or text changed action.
static long sLastCutCopyOrTextChangedTime;
-
private ColorStateList mTextColor;
private ColorStateList mHintTextColor;
private ColorStateList mLinkTextColor;
@@ -2857,7 +2857,38 @@
}
if (updateText) {
- setText(mText);
+ if (Flags.insertModeNotUpdateSelection()) {
+ // Update the transformation text.
+ if (mTransformation == null) {
+ mTransformed = mText;
+ } else {
+ mTransformed = mTransformation.getTransformation(mText, this);
+ }
+ if (mTransformed == null) {
+ // Should not happen if the transformation method follows the non-null
+ // postcondition.
+ mTransformed = "";
+ }
+ final boolean isOffsetMapping = mTransformed instanceof OffsetMapping;
+
+ // If the mText is a Spannable and the new TransformationMethod needs to listen to
+ // its updates, apply the watcher on it.
+ if (mTransformation != null && mText instanceof Spannable
+ && (!mAllowTransformationLengthChange || isOffsetMapping)) {
+ Spannable sp = (Spannable) mText;
+ final int priority = isOffsetMapping ? OFFSET_MAPPING_SPAN_PRIORITY : 0;
+ sp.setSpan(mTransformation, 0, mText.length(),
+ Spanned.SPAN_INCLUSIVE_INCLUSIVE
+ | (priority << Spanned.SPAN_PRIORITY_SHIFT));
+ }
+ if (mLayout != null) {
+ nullLayouts();
+ requestLayout();
+ invalidate();
+ }
+ } else {
+ setText(mText);
+ }
}
if (hasPasswordTransformationMethod()) {
diff --git a/core/java/com/android/internal/app/IAppOpsActiveCallback.aidl b/core/java/com/android/internal/app/IAppOpsActiveCallback.aidl
index ae6ad32..94bbd72 100644
--- a/core/java/com/android/internal/app/IAppOpsActiveCallback.aidl
+++ b/core/java/com/android/internal/app/IAppOpsActiveCallback.aidl
@@ -19,5 +19,5 @@
// Iterface to observe op active changes
oneway interface IAppOpsActiveCallback {
void opActiveChanged(int op, int uid, String packageName, String attributionTag,
- boolean active, int attributionFlags, int attributionChainId);
+ int virtualDeviceId, boolean active, int attributionFlags, int attributionChainId);
}
diff --git a/core/java/com/android/internal/app/IAppOpsCallback.aidl b/core/java/com/android/internal/app/IAppOpsCallback.aidl
index 024ff66..3a9525c 100644
--- a/core/java/com/android/internal/app/IAppOpsCallback.aidl
+++ b/core/java/com/android/internal/app/IAppOpsCallback.aidl
@@ -19,5 +19,5 @@
// This interface is also used by native code, so must
// be kept in sync with frameworks/native/libs/permission/include/binder/IAppOpsCallback.h
oneway interface IAppOpsCallback {
- void opChanged(int op, int uid, String packageName);
+ void opChanged(int op, int uid, String packageName, String persistentDeviceId);
}
diff --git a/core/java/com/android/internal/app/IAppOpsNotedCallback.aidl b/core/java/com/android/internal/app/IAppOpsNotedCallback.aidl
index f3759e0..123f4f4 100644
--- a/core/java/com/android/internal/app/IAppOpsNotedCallback.aidl
+++ b/core/java/com/android/internal/app/IAppOpsNotedCallback.aidl
@@ -18,5 +18,6 @@
// Iterface to observe op note/checks of ops
oneway interface IAppOpsNotedCallback {
- void opNoted(int op, int uid, String packageName, String attributionTag, int flags, int mode);
+ void opNoted(int op, int uid, String packageName, String attributionTag, int virtualDeviceId,
+ int flags, int mode);
}
diff --git a/core/java/com/android/internal/app/IAppOpsStartedCallback.aidl b/core/java/com/android/internal/app/IAppOpsStartedCallback.aidl
index 06640cb..fdae37a 100644
--- a/core/java/com/android/internal/app/IAppOpsStartedCallback.aidl
+++ b/core/java/com/android/internal/app/IAppOpsStartedCallback.aidl
@@ -18,6 +18,6 @@
// Iterface to observe op starts
oneway interface IAppOpsStartedCallback {
- void opStarted(int op, int uid, String packageName, String attributionTag, int flags, int mode,
- int startedType, int attributionFlags, int attributionChainId);
+ void opStarted(int op, int uid, String packageName, String attributionTag, int virtualDeviceId,
+ int flags, int mode, int startedType, int attributionFlags, int attributionChainId);
}
diff --git a/core/java/com/android/internal/os/AndroidPrintStream.java b/core/java/com/android/internal/os/AndroidPrintStream.java
index a6e41ff..bb388bb 100644
--- a/core/java/com/android/internal/os/AndroidPrintStream.java
+++ b/core/java/com/android/internal/os/AndroidPrintStream.java
@@ -24,6 +24,7 @@
*
* {@hide}
*/
+@android.ravenwood.annotation.RavenwoodKeepWholeClass
class AndroidPrintStream extends LoggingPrintStream {
private final int priority;
diff --git a/core/java/com/android/internal/os/BatteryStatsHistory.java b/core/java/com/android/internal/os/BatteryStatsHistory.java
index 7a79e0f..aa60cc9 100644
--- a/core/java/com/android/internal/os/BatteryStatsHistory.java
+++ b/core/java/com/android/internal/os/BatteryStatsHistory.java
@@ -490,7 +490,7 @@
* Returns true if this instance only supports reading history.
*/
public boolean isReadOnly() {
- return mActiveFile == null || mHistoryDir == null;
+ return !mMutable || mActiveFile == null || mHistoryDir == null;
}
/**
@@ -508,6 +508,13 @@
* create next history file.
*/
public void startNextFile(long elapsedRealtimeMs) {
+ synchronized (this) {
+ startNextFileLocked(elapsedRealtimeMs);
+ }
+ }
+
+ @GuardedBy("this")
+ private void startNextFileLocked(long elapsedRealtimeMs) {
if (mMaxHistoryFiles == 0) {
Slog.wtf(TAG, "mMaxHistoryFiles should not be zero when writing history");
return;
@@ -548,10 +555,7 @@
}
mWrittenPowerStatsDescriptors.clear();
-
- synchronized (this) {
- cleanupLocked();
- }
+ cleanupLocked();
}
@GuardedBy("this")
@@ -599,27 +603,31 @@
* number 0 again.
*/
public void reset() {
- if (DEBUG) Slog.i(TAG, "********** CLEARING HISTORY!");
- for (BatteryHistoryFile file : mHistoryFiles) {
- file.atomicFile.delete();
+ synchronized (this) {
+ if (DEBUG) Slog.i(TAG, "********** CLEARING HISTORY!");
+ for (BatteryHistoryFile file : mHistoryFiles) {
+ file.atomicFile.delete();
+ }
+ mHistoryFiles.clear();
+
+ BatteryHistoryFile name = makeBatteryHistoryFile();
+ mHistoryFiles.add(name);
+ setActiveFile(name);
+
+ initHistoryBuffer();
}
- mHistoryFiles.clear();
-
- BatteryHistoryFile name = makeBatteryHistoryFile();
- mHistoryFiles.add(name);
- setActiveFile(name);
-
- initHistoryBuffer();
}
/**
* Returns the monotonic clock time when the available battery history collection started.
*/
public long getStartTime() {
- if (!mHistoryFiles.isEmpty()) {
- return mHistoryFiles.get(0).monotonicTimeMs;
- } else {
- return mHistoryBufferStartTime;
+ synchronized (this) {
+ if (!mHistoryFiles.isEmpty()) {
+ return mHistoryFiles.get(0).monotonicTimeMs;
+ } else {
+ return mHistoryBufferStartTime;
+ }
}
}
@@ -633,11 +641,14 @@
*/
@NonNull
public BatteryStatsHistoryIterator iterate(long startTimeMs, long endTimeMs) {
+ if (mMutable) {
+ return copy().iterate(startTimeMs, endTimeMs);
+ }
+
mCurrentFileIndex = 0;
mCurrentParcel = null;
mCurrentParcelEnd = 0;
mParcelIndex = 0;
- mMutable = false;
if (mWritableHistory != null) {
synchronized (mWritableHistory) {
mWritableHistory.setCleanupEnabledLocked(false);
@@ -650,14 +661,11 @@
* Finish iterating history files and history buffer.
*/
void iteratorFinished() {
- // setDataPosition so mHistoryBuffer Parcel can be written.
mHistoryBuffer.setDataPosition(mHistoryBuffer.dataSize());
if (mWritableHistory != null) {
synchronized (mWritableHistory) {
mWritableHistory.setCleanupEnabledLocked(true);
}
- } else {
- mMutable = true;
}
}
@@ -671,6 +679,8 @@
*/
@Nullable
public Parcel getNextParcel(long startTimeMs, long endTimeMs) {
+ checkImmutable();
+
// First iterate through all records in current parcel.
if (mCurrentParcel != null) {
if (mCurrentParcel.dataPosition() < mCurrentParcelEnd) {
@@ -754,6 +764,12 @@
return mCurrentParcel;
}
+ private void checkImmutable() {
+ if (mMutable) {
+ throw new IllegalStateException("Iterating over a mutable battery history");
+ }
+ }
+
/**
* Read history file into a parcel.
*
@@ -863,8 +879,10 @@
* @param out the output parcel
*/
public void writeToParcel(Parcel out) {
- writeHistoryBuffer(out);
- writeToParcel(out, false /* useBlobs */);
+ synchronized (this) {
+ writeHistoryBuffer(out);
+ writeToParcel(out, false /* useBlobs */);
+ }
}
/**
@@ -874,8 +892,10 @@
* @param out the output parcel
*/
public void writeToBatteryUsageStatsParcel(Parcel out) {
- out.writeBlob(mHistoryBuffer.marshall());
- writeToParcel(out, true /* useBlobs */);
+ synchronized (this) {
+ out.writeBlob(mHistoryBuffer.marshall());
+ writeToParcel(out, true /* useBlobs */);
+ }
}
private void writeToParcel(Parcel out, boolean useBlobs) {
@@ -1022,14 +1042,18 @@
* Enables/disables recording of history. When disabled, all "record*" calls are a no-op.
*/
public void setHistoryRecordingEnabled(boolean enabled) {
- mRecordingHistory = enabled;
+ synchronized (this) {
+ mRecordingHistory = enabled;
+ }
}
/**
* Returns true if history recording is enabled.
*/
public boolean isRecordingHistory() {
- return mRecordingHistory;
+ synchronized (this) {
+ return mRecordingHistory;
+ }
}
/**
@@ -1037,8 +1061,10 @@
*/
@VisibleForTesting
public void forceRecordAllHistory() {
- mHaveBatteryLevel = true;
- mRecordingHistory = true;
+ synchronized (this) {
+ mHaveBatteryLevel = true;
+ mRecordingHistory = true;
+ }
}
/**
@@ -1046,37 +1072,43 @@
*/
public void startRecordingHistory(final long elapsedRealtimeMs, final long uptimeMs,
boolean reset) {
- mRecordingHistory = true;
- mHistoryCur.currentTime = mClock.currentTimeMillis();
- writeHistoryItem(elapsedRealtimeMs, uptimeMs, mHistoryCur,
- reset ? HistoryItem.CMD_RESET : HistoryItem.CMD_CURRENT_TIME);
- mHistoryCur.currentTime = 0;
+ synchronized (this) {
+ mRecordingHistory = true;
+ mHistoryCur.currentTime = mClock.currentTimeMillis();
+ writeHistoryItem(elapsedRealtimeMs, uptimeMs, mHistoryCur,
+ reset ? HistoryItem.CMD_RESET : HistoryItem.CMD_CURRENT_TIME);
+ mHistoryCur.currentTime = 0;
+ }
}
/**
* Prepares to continue recording after restoring previous history from persistent storage.
*/
public void continueRecordingHistory() {
- if (mHistoryBuffer.dataPosition() <= 0 && mHistoryFiles.size() <= 1) {
- return;
- }
+ synchronized (this) {
+ if (mHistoryBuffer.dataPosition() <= 0 && mHistoryFiles.size() <= 1) {
+ return;
+ }
- mRecordingHistory = true;
- final long elapsedRealtimeMs = mClock.elapsedRealtime();
- final long uptimeMs = mClock.uptimeMillis();
- writeHistoryItem(elapsedRealtimeMs, uptimeMs, mHistoryCur, HistoryItem.CMD_START);
- startRecordingHistory(elapsedRealtimeMs, uptimeMs, false);
+ mRecordingHistory = true;
+ final long elapsedRealtimeMs = mClock.elapsedRealtime();
+ final long uptimeMs = mClock.uptimeMillis();
+ writeHistoryItem(elapsedRealtimeMs, uptimeMs, mHistoryCur, HistoryItem.CMD_START);
+ startRecordingHistory(elapsedRealtimeMs, uptimeMs, false);
+ }
}
/**
* Notes the current battery state to be reflected in the next written history item.
*/
public void setBatteryState(boolean charging, int status, int level, int chargeUah) {
- mHaveBatteryLevel = true;
- setChargingState(charging);
- mHistoryCur.batteryStatus = (byte) status;
- mHistoryCur.batteryLevel = (byte) level;
- mHistoryCur.batteryChargeUah = chargeUah;
+ synchronized (this) {
+ mHaveBatteryLevel = true;
+ setChargingState(charging);
+ mHistoryCur.batteryStatus = (byte) status;
+ mHistoryCur.batteryLevel = (byte) level;
+ mHistoryCur.batteryChargeUah = chargeUah;
+ }
}
/**
@@ -1084,24 +1116,28 @@
*/
public void setBatteryState(int status, int level, int health, int plugType, int temperature,
int voltageMv, int chargeUah) {
- mHaveBatteryLevel = true;
- mHistoryCur.batteryStatus = (byte) status;
- mHistoryCur.batteryLevel = (byte) level;
- mHistoryCur.batteryHealth = (byte) health;
- mHistoryCur.batteryPlugType = (byte) plugType;
- mHistoryCur.batteryTemperature = (short) temperature;
- mHistoryCur.batteryVoltage = (char) voltageMv;
- mHistoryCur.batteryChargeUah = chargeUah;
+ synchronized (this) {
+ mHaveBatteryLevel = true;
+ mHistoryCur.batteryStatus = (byte) status;
+ mHistoryCur.batteryLevel = (byte) level;
+ mHistoryCur.batteryHealth = (byte) health;
+ mHistoryCur.batteryPlugType = (byte) plugType;
+ mHistoryCur.batteryTemperature = (short) temperature;
+ mHistoryCur.batteryVoltage = (char) voltageMv;
+ mHistoryCur.batteryChargeUah = chargeUah;
+ }
}
/**
* Notes the current power plugged-in state to be reflected in the next written history item.
*/
public void setPluggedInState(boolean pluggedIn) {
- if (pluggedIn) {
- mHistoryCur.states |= HistoryItem.STATE_BATTERY_PLUGGED_FLAG;
- } else {
- mHistoryCur.states &= ~HistoryItem.STATE_BATTERY_PLUGGED_FLAG;
+ synchronized (this) {
+ if (pluggedIn) {
+ mHistoryCur.states |= HistoryItem.STATE_BATTERY_PLUGGED_FLAG;
+ } else {
+ mHistoryCur.states &= ~HistoryItem.STATE_BATTERY_PLUGGED_FLAG;
+ }
}
}
@@ -1109,10 +1145,12 @@
* Notes the current battery charging state to be reflected in the next written history item.
*/
public void setChargingState(boolean charging) {
- if (charging) {
- mHistoryCur.states2 |= HistoryItem.STATE2_CHARGING_FLAG;
- } else {
- mHistoryCur.states2 &= ~HistoryItem.STATE2_CHARGING_FLAG;
+ synchronized (this) {
+ if (charging) {
+ mHistoryCur.states2 |= HistoryItem.STATE2_CHARGING_FLAG;
+ } else {
+ mHistoryCur.states2 &= ~HistoryItem.STATE2_CHARGING_FLAG;
+ }
}
}
@@ -1121,38 +1159,44 @@
*/
public void recordEvent(long elapsedRealtimeMs, long uptimeMs, int code, String name,
int uid) {
- mHistoryCur.eventCode = code;
- mHistoryCur.eventTag = mHistoryCur.localEventTag;
- mHistoryCur.eventTag.string = name;
- mHistoryCur.eventTag.uid = uid;
- writeHistoryItem(elapsedRealtimeMs, uptimeMs);
+ synchronized (this) {
+ mHistoryCur.eventCode = code;
+ mHistoryCur.eventTag = mHistoryCur.localEventTag;
+ mHistoryCur.eventTag.string = name;
+ mHistoryCur.eventTag.uid = uid;
+ writeHistoryItem(elapsedRealtimeMs, uptimeMs);
+ }
}
/**
* Records a time change event.
*/
public void recordCurrentTimeChange(long elapsedRealtimeMs, long uptimeMs, long currentTimeMs) {
- if (!mRecordingHistory) {
- return;
- }
+ synchronized (this) {
+ if (!mRecordingHistory) {
+ return;
+ }
- mHistoryCur.currentTime = currentTimeMs;
- writeHistoryItem(elapsedRealtimeMs, uptimeMs, mHistoryCur,
- HistoryItem.CMD_CURRENT_TIME);
- mHistoryCur.currentTime = 0;
+ mHistoryCur.currentTime = currentTimeMs;
+ writeHistoryItem(elapsedRealtimeMs, uptimeMs, mHistoryCur,
+ HistoryItem.CMD_CURRENT_TIME);
+ mHistoryCur.currentTime = 0;
+ }
}
/**
* Records a system shutdown event.
*/
public void recordShutdownEvent(long elapsedRealtimeMs, long uptimeMs, long currentTimeMs) {
- if (!mRecordingHistory) {
- return;
- }
+ synchronized (this) {
+ if (!mRecordingHistory) {
+ return;
+ }
- mHistoryCur.currentTime = currentTimeMs;
- writeHistoryItem(elapsedRealtimeMs, uptimeMs, mHistoryCur, HistoryItem.CMD_SHUTDOWN);
- mHistoryCur.currentTime = 0;
+ mHistoryCur.currentTime = currentTimeMs;
+ writeHistoryItem(elapsedRealtimeMs, uptimeMs, mHistoryCur, HistoryItem.CMD_SHUTDOWN);
+ mHistoryCur.currentTime = 0;
+ }
}
/**
@@ -1160,13 +1204,15 @@
*/
public void recordBatteryState(long elapsedRealtimeMs, long uptimeMs, int batteryLevel,
boolean isPlugged) {
- mHistoryCur.batteryLevel = (byte) batteryLevel;
- setPluggedInState(isPlugged);
- if (DEBUG) {
- Slog.v(TAG, "Battery unplugged to: "
- + Integer.toHexString(mHistoryCur.states));
+ synchronized (this) {
+ mHistoryCur.batteryLevel = (byte) batteryLevel;
+ setPluggedInState(isPlugged);
+ if (DEBUG) {
+ Slog.v(TAG, "Battery unplugged to: "
+ + Integer.toHexString(mHistoryCur.states));
+ }
+ writeHistoryItem(elapsedRealtimeMs, uptimeMs);
}
- writeHistoryItem(elapsedRealtimeMs, uptimeMs);
}
/**
@@ -1174,9 +1220,11 @@
*/
public void recordPowerStats(long elapsedRealtimeMs, long uptimeMs,
PowerStats powerStats) {
- mHistoryCur.powerStats = powerStats;
- mHistoryCur.states2 |= HistoryItem.STATE2_EXTENSIONS_FLAG;
- writeHistoryItem(elapsedRealtimeMs, uptimeMs);
+ synchronized (this) {
+ mHistoryCur.powerStats = powerStats;
+ mHistoryCur.states2 |= HistoryItem.STATE2_EXTENSIONS_FLAG;
+ writeHistoryItem(elapsedRealtimeMs, uptimeMs);
+ }
}
/**
@@ -1184,11 +1232,13 @@
*/
public void recordProcessStateChange(long elapsedRealtimeMs, long uptimeMs,
int uid, @BatteryConsumer.ProcessState int processState) {
- mHistoryCur.processStateChange = mHistoryCur.localProcessStateChange;
- mHistoryCur.processStateChange.uid = uid;
- mHistoryCur.processStateChange.processState = processState;
- mHistoryCur.states2 |= HistoryItem.STATE2_EXTENSIONS_FLAG;
- writeHistoryItem(elapsedRealtimeMs, uptimeMs);
+ synchronized (this) {
+ mHistoryCur.processStateChange = mHistoryCur.localProcessStateChange;
+ mHistoryCur.processStateChange.uid = uid;
+ mHistoryCur.processStateChange.processState = processState;
+ mHistoryCur.states2 |= HistoryItem.STATE2_EXTENSIONS_FLAG;
+ writeHistoryItem(elapsedRealtimeMs, uptimeMs);
+ }
}
/**
@@ -1197,8 +1247,10 @@
*/
public void recordWifiConsumedCharge(long elapsedRealtimeMs, long uptimeMs,
double monitoredRailChargeMah) {
- mHistoryCur.wifiRailChargeMah += monitoredRailChargeMah;
- writeHistoryItem(elapsedRealtimeMs, uptimeMs);
+ synchronized (this) {
+ mHistoryCur.wifiRailChargeMah += monitoredRailChargeMah;
+ writeHistoryItem(elapsedRealtimeMs, uptimeMs);
+ }
}
/**
@@ -1206,10 +1258,12 @@
*/
public void recordWakelockStartEvent(long elapsedRealtimeMs, long uptimeMs, String historyName,
int uid) {
- mHistoryCur.wakelockTag = mHistoryCur.localWakelockTag;
- mHistoryCur.wakelockTag.string = historyName;
- mHistoryCur.wakelockTag.uid = uid;
- recordStateStartEvent(elapsedRealtimeMs, uptimeMs, HistoryItem.STATE_WAKE_LOCK_FLAG);
+ synchronized (this) {
+ mHistoryCur.wakelockTag = mHistoryCur.localWakelockTag;
+ mHistoryCur.wakelockTag.string = historyName;
+ mHistoryCur.wakelockTag.uid = uid;
+ recordStateStartEvent(elapsedRealtimeMs, uptimeMs, HistoryItem.STATE_WAKE_LOCK_FLAG);
+ }
}
/**
@@ -1217,18 +1271,20 @@
*/
public boolean maybeUpdateWakelockTag(long elapsedRealtimeMs, long uptimeMs, String historyName,
int uid) {
- if (mHistoryLastWritten.cmd != HistoryItem.CMD_UPDATE) {
- return false;
+ synchronized (this) {
+ if (mHistoryLastWritten.cmd != HistoryItem.CMD_UPDATE) {
+ return false;
+ }
+ if (mHistoryLastWritten.wakelockTag != null) {
+ // We'll try to update the last tag.
+ mHistoryLastWritten.wakelockTag = null;
+ mHistoryCur.wakelockTag = mHistoryCur.localWakelockTag;
+ mHistoryCur.wakelockTag.string = historyName;
+ mHistoryCur.wakelockTag.uid = uid;
+ writeHistoryItem(elapsedRealtimeMs, uptimeMs);
+ }
+ return true;
}
- if (mHistoryLastWritten.wakelockTag != null) {
- // We'll try to update the last tag.
- mHistoryLastWritten.wakelockTag = null;
- mHistoryCur.wakelockTag = mHistoryCur.localWakelockTag;
- mHistoryCur.wakelockTag.string = historyName;
- mHistoryCur.wakelockTag.uid = uid;
- writeHistoryItem(elapsedRealtimeMs, uptimeMs);
- }
- return true;
}
/**
@@ -1236,26 +1292,32 @@
*/
public void recordWakelockStopEvent(long elapsedRealtimeMs, long uptimeMs, String historyName,
int uid) {
- mHistoryCur.wakelockTag = mHistoryCur.localWakelockTag;
- mHistoryCur.wakelockTag.string = historyName != null ? historyName : "";
- mHistoryCur.wakelockTag.uid = uid;
- recordStateStopEvent(elapsedRealtimeMs, uptimeMs, HistoryItem.STATE_WAKE_LOCK_FLAG);
+ synchronized (this) {
+ mHistoryCur.wakelockTag = mHistoryCur.localWakelockTag;
+ mHistoryCur.wakelockTag.string = historyName != null ? historyName : "";
+ mHistoryCur.wakelockTag.uid = uid;
+ recordStateStopEvent(elapsedRealtimeMs, uptimeMs, HistoryItem.STATE_WAKE_LOCK_FLAG);
+ }
}
/**
* Records an event when some state flag changes to true.
*/
public void recordStateStartEvent(long elapsedRealtimeMs, long uptimeMs, int stateFlags) {
- mHistoryCur.states |= stateFlags;
- writeHistoryItem(elapsedRealtimeMs, uptimeMs);
+ synchronized (this) {
+ mHistoryCur.states |= stateFlags;
+ writeHistoryItem(elapsedRealtimeMs, uptimeMs);
+ }
}
/**
* Records an event when some state flag changes to false.
*/
public void recordStateStopEvent(long elapsedRealtimeMs, long uptimeMs, int stateFlags) {
- mHistoryCur.states &= ~stateFlags;
- writeHistoryItem(elapsedRealtimeMs, uptimeMs);
+ synchronized (this) {
+ mHistoryCur.states &= ~stateFlags;
+ writeHistoryItem(elapsedRealtimeMs, uptimeMs);
+ }
}
/**
@@ -1263,34 +1325,42 @@
*/
public void recordStateChangeEvent(long elapsedRealtimeMs, long uptimeMs, int stateStartFlags,
int stateStopFlags) {
- mHistoryCur.states = (mHistoryCur.states | stateStartFlags) & ~stateStopFlags;
- writeHistoryItem(elapsedRealtimeMs, uptimeMs);
+ synchronized (this) {
+ mHistoryCur.states = (mHistoryCur.states | stateStartFlags) & ~stateStopFlags;
+ writeHistoryItem(elapsedRealtimeMs, uptimeMs);
+ }
}
/**
* Records an event when some state2 flag changes to true.
*/
public void recordState2StartEvent(long elapsedRealtimeMs, long uptimeMs, int stateFlags) {
- mHistoryCur.states2 |= stateFlags;
- writeHistoryItem(elapsedRealtimeMs, uptimeMs);
+ synchronized (this) {
+ mHistoryCur.states2 |= stateFlags;
+ writeHistoryItem(elapsedRealtimeMs, uptimeMs);
+ }
}
/**
* Records an event when some state2 flag changes to false.
*/
public void recordState2StopEvent(long elapsedRealtimeMs, long uptimeMs, int stateFlags) {
- mHistoryCur.states2 &= ~stateFlags;
- writeHistoryItem(elapsedRealtimeMs, uptimeMs);
+ synchronized (this) {
+ mHistoryCur.states2 &= ~stateFlags;
+ writeHistoryItem(elapsedRealtimeMs, uptimeMs);
+ }
}
/**
* Records an wakeup event.
*/
public void recordWakeupEvent(long elapsedRealtimeMs, long uptimeMs, String reason) {
- mHistoryCur.wakeReasonTag = mHistoryCur.localWakeReasonTag;
- mHistoryCur.wakeReasonTag.string = reason;
- mHistoryCur.wakeReasonTag.uid = 0;
- writeHistoryItem(elapsedRealtimeMs, uptimeMs);
+ synchronized (this) {
+ mHistoryCur.wakeReasonTag = mHistoryCur.localWakeReasonTag;
+ mHistoryCur.wakeReasonTag.string = reason;
+ mHistoryCur.wakeReasonTag.uid = 0;
+ writeHistoryItem(elapsedRealtimeMs, uptimeMs);
+ }
}
/**
@@ -1298,10 +1368,12 @@
*/
public void recordScreenBrightnessEvent(long elapsedRealtimeMs, long uptimeMs,
int brightnessBin) {
- mHistoryCur.states = setBitField(mHistoryCur.states, brightnessBin,
- HistoryItem.STATE_BRIGHTNESS_SHIFT,
- HistoryItem.STATE_BRIGHTNESS_MASK);
- writeHistoryItem(elapsedRealtimeMs, uptimeMs);
+ synchronized (this) {
+ mHistoryCur.states = setBitField(mHistoryCur.states, brightnessBin,
+ HistoryItem.STATE_BRIGHTNESS_SHIFT,
+ HistoryItem.STATE_BRIGHTNESS_MASK);
+ writeHistoryItem(elapsedRealtimeMs, uptimeMs);
+ }
}
/**
@@ -1309,20 +1381,24 @@
*/
public void recordGpsSignalQualityEvent(long elapsedRealtimeMs, long uptimeMs,
int signalLevel) {
- mHistoryCur.states2 = setBitField(mHistoryCur.states2, signalLevel,
- HistoryItem.STATE2_GPS_SIGNAL_QUALITY_SHIFT,
- HistoryItem.STATE2_GPS_SIGNAL_QUALITY_MASK);
- writeHistoryItem(elapsedRealtimeMs, uptimeMs);
+ synchronized (this) {
+ mHistoryCur.states2 = setBitField(mHistoryCur.states2, signalLevel,
+ HistoryItem.STATE2_GPS_SIGNAL_QUALITY_SHIFT,
+ HistoryItem.STATE2_GPS_SIGNAL_QUALITY_MASK);
+ writeHistoryItem(elapsedRealtimeMs, uptimeMs);
+ }
}
/**
* Records a device idle mode change event.
*/
public void recordDeviceIdleEvent(long elapsedRealtimeMs, long uptimeMs, int mode) {
- mHistoryCur.states2 = setBitField(mHistoryCur.states2, mode,
- HistoryItem.STATE2_DEVICE_IDLE_SHIFT,
- HistoryItem.STATE2_DEVICE_IDLE_MASK);
- writeHistoryItem(elapsedRealtimeMs, uptimeMs);
+ synchronized (this) {
+ mHistoryCur.states2 = setBitField(mHistoryCur.states2, mode,
+ HistoryItem.STATE2_DEVICE_IDLE_SHIFT,
+ HistoryItem.STATE2_DEVICE_IDLE_MASK);
+ writeHistoryItem(elapsedRealtimeMs, uptimeMs);
+ }
}
/**
@@ -1330,20 +1406,22 @@
*/
public void recordPhoneStateChangeEvent(long elapsedRealtimeMs, long uptimeMs, int addStateFlag,
int removeStateFlag, int state, int signalStrength) {
- mHistoryCur.states = (mHistoryCur.states | addStateFlag) & ~removeStateFlag;
- if (state != -1) {
- mHistoryCur.states =
- setBitField(mHistoryCur.states, state,
- HistoryItem.STATE_PHONE_STATE_SHIFT,
- HistoryItem.STATE_PHONE_STATE_MASK);
+ synchronized (this) {
+ mHistoryCur.states = (mHistoryCur.states | addStateFlag) & ~removeStateFlag;
+ if (state != -1) {
+ mHistoryCur.states =
+ setBitField(mHistoryCur.states, state,
+ HistoryItem.STATE_PHONE_STATE_SHIFT,
+ HistoryItem.STATE_PHONE_STATE_MASK);
+ }
+ if (signalStrength != -1) {
+ mHistoryCur.states =
+ setBitField(mHistoryCur.states, signalStrength,
+ HistoryItem.STATE_PHONE_SIGNAL_STRENGTH_SHIFT,
+ HistoryItem.STATE_PHONE_SIGNAL_STRENGTH_MASK);
+ }
+ writeHistoryItem(elapsedRealtimeMs, uptimeMs);
}
- if (signalStrength != -1) {
- mHistoryCur.states =
- setBitField(mHistoryCur.states, signalStrength,
- HistoryItem.STATE_PHONE_SIGNAL_STRENGTH_SHIFT,
- HistoryItem.STATE_PHONE_SIGNAL_STRENGTH_MASK);
- }
- writeHistoryItem(elapsedRealtimeMs, uptimeMs);
}
/**
@@ -1351,10 +1429,12 @@
*/
public void recordDataConnectionTypeChangeEvent(long elapsedRealtimeMs, long uptimeMs,
int dataConnectionType) {
- mHistoryCur.states = setBitField(mHistoryCur.states, dataConnectionType,
- HistoryItem.STATE_DATA_CONNECTION_SHIFT,
- HistoryItem.STATE_DATA_CONNECTION_MASK);
- writeHistoryItem(elapsedRealtimeMs, uptimeMs);
+ synchronized (this) {
+ mHistoryCur.states = setBitField(mHistoryCur.states, dataConnectionType,
+ HistoryItem.STATE_DATA_CONNECTION_SHIFT,
+ HistoryItem.STATE_DATA_CONNECTION_MASK);
+ writeHistoryItem(elapsedRealtimeMs, uptimeMs);
+ }
}
/**
@@ -1362,10 +1442,12 @@
*/
public void recordNrStateChangeEvent(long elapsedRealtimeMs, long uptimeMs,
int nrState) {
- mHistoryCur.states2 = setBitField(mHistoryCur.states2, nrState,
- HistoryItem.STATE2_NR_STATE_SHIFT,
- HistoryItem.STATE2_NR_STATE_MASK);
- writeHistoryItem(elapsedRealtimeMs, uptimeMs);
+ synchronized (this) {
+ mHistoryCur.states2 = setBitField(mHistoryCur.states2, nrState,
+ HistoryItem.STATE2_NR_STATE_SHIFT,
+ HistoryItem.STATE2_NR_STATE_MASK);
+ writeHistoryItem(elapsedRealtimeMs, uptimeMs);
+ }
}
/**
@@ -1373,11 +1455,13 @@
*/
public void recordWifiSupplicantStateChangeEvent(long elapsedRealtimeMs, long uptimeMs,
int supplState) {
- mHistoryCur.states2 =
- setBitField(mHistoryCur.states2, supplState,
- HistoryItem.STATE2_WIFI_SUPPL_STATE_SHIFT,
- HistoryItem.STATE2_WIFI_SUPPL_STATE_MASK);
- writeHistoryItem(elapsedRealtimeMs, uptimeMs);
+ synchronized (this) {
+ mHistoryCur.states2 =
+ setBitField(mHistoryCur.states2, supplState,
+ HistoryItem.STATE2_WIFI_SUPPL_STATE_SHIFT,
+ HistoryItem.STATE2_WIFI_SUPPL_STATE_MASK);
+ writeHistoryItem(elapsedRealtimeMs, uptimeMs);
+ }
}
/**
@@ -1385,11 +1469,13 @@
*/
public void recordWifiSignalStrengthChangeEvent(long elapsedRealtimeMs, long uptimeMs,
int strengthBin) {
- mHistoryCur.states2 =
- setBitField(mHistoryCur.states2, strengthBin,
- HistoryItem.STATE2_WIFI_SIGNAL_STRENGTH_SHIFT,
- HistoryItem.STATE2_WIFI_SIGNAL_STRENGTH_MASK);
- writeHistoryItem(elapsedRealtimeMs, uptimeMs);
+ synchronized (this) {
+ mHistoryCur.states2 =
+ setBitField(mHistoryCur.states2, strengthBin,
+ HistoryItem.STATE2_WIFI_SIGNAL_STRENGTH_SHIFT,
+ HistoryItem.STATE2_WIFI_SIGNAL_STRENGTH_MASK);
+ writeHistoryItem(elapsedRealtimeMs, uptimeMs);
+ }
}
/**
@@ -1446,25 +1532,30 @@
* Writes the current history item to history.
*/
public void writeHistoryItem(long elapsedRealtimeMs, long uptimeMs) {
- if (mTrackRunningHistoryElapsedRealtimeMs != 0) {
- final long diffElapsedMs = elapsedRealtimeMs - mTrackRunningHistoryElapsedRealtimeMs;
- final long diffUptimeMs = uptimeMs - mTrackRunningHistoryUptimeMs;
- if (diffUptimeMs < (diffElapsedMs - 20)) {
- final long wakeElapsedTimeMs = elapsedRealtimeMs - (diffElapsedMs - diffUptimeMs);
- mHistoryAddTmp.setTo(mHistoryLastWritten);
- mHistoryAddTmp.wakelockTag = null;
- mHistoryAddTmp.wakeReasonTag = null;
- mHistoryAddTmp.eventCode = HistoryItem.EVENT_NONE;
- mHistoryAddTmp.states &= ~HistoryItem.STATE_CPU_RUNNING_FLAG;
- writeHistoryItem(wakeElapsedTimeMs, uptimeMs, mHistoryAddTmp);
+ synchronized (this) {
+ if (mTrackRunningHistoryElapsedRealtimeMs != 0) {
+ final long diffElapsedMs =
+ elapsedRealtimeMs - mTrackRunningHistoryElapsedRealtimeMs;
+ final long diffUptimeMs = uptimeMs - mTrackRunningHistoryUptimeMs;
+ if (diffUptimeMs < (diffElapsedMs - 20)) {
+ final long wakeElapsedTimeMs =
+ elapsedRealtimeMs - (diffElapsedMs - diffUptimeMs);
+ mHistoryAddTmp.setTo(mHistoryLastWritten);
+ mHistoryAddTmp.wakelockTag = null;
+ mHistoryAddTmp.wakeReasonTag = null;
+ mHistoryAddTmp.eventCode = HistoryItem.EVENT_NONE;
+ mHistoryAddTmp.states &= ~HistoryItem.STATE_CPU_RUNNING_FLAG;
+ writeHistoryItem(wakeElapsedTimeMs, uptimeMs, mHistoryAddTmp);
+ }
}
+ mHistoryCur.states |= HistoryItem.STATE_CPU_RUNNING_FLAG;
+ mTrackRunningHistoryElapsedRealtimeMs = elapsedRealtimeMs;
+ mTrackRunningHistoryUptimeMs = uptimeMs;
+ writeHistoryItem(elapsedRealtimeMs, uptimeMs, mHistoryCur);
}
- mHistoryCur.states |= HistoryItem.STATE_CPU_RUNNING_FLAG;
- mTrackRunningHistoryElapsedRealtimeMs = elapsedRealtimeMs;
- mTrackRunningHistoryUptimeMs = uptimeMs;
- writeHistoryItem(elapsedRealtimeMs, uptimeMs, mHistoryCur);
}
+ @GuardedBy("this")
private void writeHistoryItem(long elapsedRealtimeMs, long uptimeMs, HistoryItem cur) {
if (mTracer != null && mTracer.tracingEnabled()) {
recordTraceEvents(cur.eventCode, cur.eventTag);
@@ -1591,6 +1682,7 @@
writeHistoryItem(elapsedRealtimeMs, uptimeMs, cur, HistoryItem.CMD_UPDATE);
}
+ @GuardedBy("this")
private void writeHistoryItem(long elapsedRealtimeMs,
@SuppressWarnings("UnusedVariable") long uptimeMs, HistoryItem cur, byte cmd) {
if (!mMutable) {
@@ -1701,7 +1793,8 @@
/**
* Writes the delta between the previous and current history items into history buffer.
*/
- public void writeHistoryDelta(Parcel dest, HistoryItem cur, HistoryItem last) {
+ @GuardedBy("this")
+ private void writeHistoryDelta(Parcel dest, HistoryItem cur, HistoryItem last) {
if (last == null || cur.cmd != HistoryItem.CMD_UPDATE) {
dest.writeInt(BatteryStatsHistory.DELTA_TIME_ABS);
cur.writeToParcel(dest, 0);
@@ -1921,6 +2014,7 @@
* while writing the current history buffer, the method returns
* <code>(index | TAG_FIRST_OCCURRENCE_FLAG)</code>
*/
+ @GuardedBy("this")
private int writeHistoryTag(HistoryTag tag) {
if (tag.string == null) {
Slog.wtfStack(TAG, "writeHistoryTag called with null name");
@@ -1964,33 +2058,37 @@
* Don't allow any more batching in to the current history event.
*/
public void commitCurrentHistoryBatchLocked() {
- mHistoryLastWritten.cmd = HistoryItem.CMD_NULL;
+ synchronized (this) {
+ mHistoryLastWritten.cmd = HistoryItem.CMD_NULL;
+ }
}
/**
* Saves the accumulated history buffer in the active file, see {@link #getActiveFile()} .
*/
public void writeHistory() {
- if (isReadOnly()) {
- Slog.w(TAG, "writeHistory: this instance instance is read-only");
- return;
- }
-
- // Save the monotonic time first, so that even if the history write below fails,
- // we still wouldn't end up with overlapping history timelines.
- mMonotonicClock.write();
-
- Parcel p = Parcel.obtain();
- try {
- final long start = SystemClock.uptimeMillis();
- writeHistoryBuffer(p);
- if (DEBUG) {
- Slog.d(TAG, "writeHistoryBuffer duration ms:"
- + (SystemClock.uptimeMillis() - start) + " bytes:" + p.dataSize());
+ synchronized (this) {
+ if (isReadOnly()) {
+ Slog.w(TAG, "writeHistory: this instance instance is read-only");
+ return;
}
- writeParcelToFileLocked(p, mActiveFile);
- } finally {
- p.recycle();
+
+ // Save the monotonic time first, so that even if the history write below fails,
+ // we still wouldn't end up with overlapping history timelines.
+ mMonotonicClock.write();
+
+ Parcel p = Parcel.obtain();
+ try {
+ final long start = SystemClock.uptimeMillis();
+ writeHistoryBuffer(p);
+ if (DEBUG) {
+ Slog.d(TAG, "writeHistoryBuffer duration ms:"
+ + (SystemClock.uptimeMillis() - start) + " bytes:" + p.dataSize());
+ }
+ writeParcelToFileLocked(p, mActiveFile);
+ } finally {
+ p.recycle();
+ }
}
}
@@ -1998,35 +2096,38 @@
* Reads history buffer from a persisted Parcel.
*/
public void readHistoryBuffer(Parcel in) throws ParcelFormatException {
- final int version = in.readInt();
- if (version != BatteryStatsHistory.VERSION) {
- Slog.w("BatteryStats", "readHistoryBuffer: version got " + version
- + ", expected " + BatteryStatsHistory.VERSION + "; erasing old stats");
- return;
- }
-
- mHistoryBufferStartTime = in.readLong();
- mHistoryBuffer.setDataSize(0);
- mHistoryBuffer.setDataPosition(0);
-
- int bufSize = in.readInt();
- int curPos = in.dataPosition();
- if (bufSize >= (mMaxHistoryBufferSize * 100)) {
- throw new ParcelFormatException(
- "File corrupt: history data buffer too large " + bufSize);
- } else if ((bufSize & ~3) != bufSize) {
- throw new ParcelFormatException(
- "File corrupt: history data buffer not aligned " + bufSize);
- } else {
- if (DEBUG) {
- Slog.i(TAG, "***************** READING NEW HISTORY: " + bufSize
- + " bytes at " + curPos);
+ synchronized (this) {
+ final int version = in.readInt();
+ if (version != BatteryStatsHistory.VERSION) {
+ Slog.w("BatteryStats", "readHistoryBuffer: version got " + version
+ + ", expected " + BatteryStatsHistory.VERSION + "; erasing old stats");
+ return;
}
- mHistoryBuffer.appendFrom(in, curPos, bufSize);
- in.setDataPosition(curPos + bufSize);
+
+ mHistoryBufferStartTime = in.readLong();
+ mHistoryBuffer.setDataSize(0);
+ mHistoryBuffer.setDataPosition(0);
+
+ int bufSize = in.readInt();
+ int curPos = in.dataPosition();
+ if (bufSize >= (mMaxHistoryBufferSize * 100)) {
+ throw new ParcelFormatException(
+ "File corrupt: history data buffer too large " + bufSize);
+ } else if ((bufSize & ~3) != bufSize) {
+ throw new ParcelFormatException(
+ "File corrupt: history data buffer not aligned " + bufSize);
+ } else {
+ if (DEBUG) {
+ Slog.i(TAG, "***************** READING NEW HISTORY: " + bufSize
+ + " bytes at " + curPos);
+ }
+ mHistoryBuffer.appendFrom(in, curPos, bufSize);
+ in.setDataPosition(curPos + bufSize);
+ }
}
}
+ @GuardedBy("this")
private void writeHistoryBuffer(Parcel out) {
out.writeInt(BatteryStatsHistory.VERSION);
out.writeLong(mHistoryBufferStartTime);
@@ -2038,6 +2139,7 @@
out.appendFrom(mHistoryBuffer, 0, mHistoryBuffer.dataSize());
}
+ @GuardedBy("this")
private void writeParcelToFileLocked(Parcel p, AtomicFile file) {
FileOutputStream fos = null;
mWriteLock.lock();
@@ -2066,34 +2168,43 @@
* Returns the total number of history tags in the tag pool.
*/
public int getHistoryStringPoolSize() {
- return mHistoryTagPool.size();
+ synchronized (this) {
+ return mHistoryTagPool.size();
+ }
}
/**
* Returns the total number of bytes occupied by the history tag pool.
*/
public int getHistoryStringPoolBytes() {
- return mNumHistoryTagChars;
+ synchronized (this) {
+ return mNumHistoryTagChars;
+ }
}
/**
* Returns the string held by the requested history tag.
*/
public String getHistoryTagPoolString(int index) {
- ensureHistoryTagArray();
- HistoryTag historyTag = mHistoryTags.get(index);
- return historyTag != null ? historyTag.string : null;
+ synchronized (this) {
+ ensureHistoryTagArray();
+ HistoryTag historyTag = mHistoryTags.get(index);
+ return historyTag != null ? historyTag.string : null;
+ }
}
/**
* Returns the UID held by the requested history tag.
*/
public int getHistoryTagPoolUid(int index) {
- ensureHistoryTagArray();
- HistoryTag historyTag = mHistoryTags.get(index);
- return historyTag != null ? historyTag.uid : Process.INVALID_UID;
+ synchronized (this) {
+ ensureHistoryTagArray();
+ HistoryTag historyTag = mHistoryTags.get(index);
+ return historyTag != null ? historyTag.uid : Process.INVALID_UID;
+ }
}
+ @GuardedBy("this")
private void ensureHistoryTagArray() {
if (mHistoryTags != null) {
return;
diff --git a/core/java/com/android/internal/os/RuntimeInit.java b/core/java/com/android/internal/os/RuntimeInit.java
index 28b98d6..cdac097 100644
--- a/core/java/com/android/internal/os/RuntimeInit.java
+++ b/core/java/com/android/internal/os/RuntimeInit.java
@@ -39,6 +39,7 @@
import libcore.content.type.MimeMap;
+import java.io.PrintStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
@@ -50,6 +51,7 @@
* public consumption.
* @hide
*/
+@android.ravenwood.annotation.RavenwoodKeepPartialClass
public class RuntimeInit {
final static String TAG = "AndroidRuntime";
final static boolean DEBUG = false;
@@ -67,7 +69,15 @@
private static volatile ApplicationWtfHandler sDefaultApplicationWtfHandler;
+ /**
+ * Stored values of System.out and System.err before they've been replaced by
+ * redirectLogStreams(). Kept open here for other Ravenwood internals to use.
+ */
+ public static PrintStream sOut$ravenwood;
+ public static PrintStream sErr$ravenwood;
+
private static final native void nativeFinishInit();
+
private static final native void nativeSetExitWithoutCleanup(boolean exitWithoutCleanup);
private static int Clog_e(String tag, String msg, Throwable tr) {
@@ -385,6 +395,7 @@
/**
* Redirect System.out and System.err to the Android log.
*/
+ @android.ravenwood.annotation.RavenwoodReplace
public static void redirectLogStreams() {
System.out.close();
System.setOut(new AndroidPrintStream(Log.INFO, "System.out"));
@@ -392,6 +403,17 @@
System.setErr(new AndroidPrintStream(Log.WARN, "System.err"));
}
+ public static void redirectLogStreams$ravenwood() {
+ if (sOut$ravenwood == null) {
+ sOut$ravenwood = System.out;
+ System.setOut(new AndroidPrintStream(Log.INFO, "System.out"));
+ }
+ if (sErr$ravenwood == null) {
+ sErr$ravenwood = System.err;
+ System.setErr(new AndroidPrintStream(Log.WARN, "System.err"));
+ }
+ }
+
/**
* Report a serious error in the current process. May or may not cause
* the process to terminate (depends on system settings).
@@ -399,6 +421,7 @@
* @param tag to record with the error
* @param t exception describing the error site and conditions
*/
+ @android.ravenwood.annotation.RavenwoodReplace
public static void wtf(String tag, Throwable t, boolean system) {
try {
boolean exit = false;
@@ -436,6 +459,11 @@
}
}
+ public static void wtf$ravenwood(String tag, Throwable t, boolean system) {
+ // We've already emitted to logs, so there's nothing more to do here,
+ // as we don't have a DropBox pipeline configured
+ }
+
/**
* Set the default {@link ApplicationWtfHandler}, in case the ActivityManager is not ready yet.
*/
diff --git a/core/java/com/android/internal/policy/PhoneWindow.java b/core/java/com/android/internal/policy/PhoneWindow.java
index 4e3b64c..b4f9ee3 100644
--- a/core/java/com/android/internal/policy/PhoneWindow.java
+++ b/core/java/com/android/internal/policy/PhoneWindow.java
@@ -2583,7 +2583,7 @@
mNavigationBarDividerColor = a.getColor(R.styleable.Window_navigationBarDividerColor,
Color.TRANSPARENT);
}
- if (!targetPreQ && !mEdgeToEdgeEnforced) {
+ if (!targetPreQ) {
mEnsureStatusBarContrastWhenTransparent = a.getBoolean(
R.styleable.Window_enforceStatusBarContrast, false);
mEnsureNavigationBarContrastWhenTransparent = a.getBoolean(
@@ -3966,9 +3966,6 @@
@Override
public void setStatusBarContrastEnforced(boolean ensureContrast) {
- if (mEdgeToEdgeEnforced) {
- return;
- }
mEnsureStatusBarContrastWhenTransparent = ensureContrast;
if (mDecor != null) {
mDecor.updateColorViews(null, false /* animate */);
@@ -3982,9 +3979,6 @@
@Override
public void setNavigationBarContrastEnforced(boolean enforceContrast) {
- if (mEdgeToEdgeEnforced) {
- return;
- }
mEnsureNavigationBarContrastWhenTransparent = enforceContrast;
if (mDecor != null) {
mDecor.updateColorViews(null, false /* animate */);
diff --git a/core/java/com/android/internal/statusbar/IStatusBar.aidl b/core/java/com/android/internal/statusbar/IStatusBar.aidl
index 5351c6d..ed43b81 100644
--- a/core/java/com/android/internal/statusbar/IStatusBar.aidl
+++ b/core/java/com/android/internal/statusbar/IStatusBar.aidl
@@ -151,8 +151,17 @@
* @param hidesStatusBar whether it is being hidden
*/
void setTopAppHidesStatusBar(boolean hidesStatusBar);
-
+ /**
+ * Add a tile to the Quick Settings Panel to the first item in the QS Panel
+ * @param tile the ComponentName of the {@link android.service.quicksettings.TileService}
+ */
void addQsTile(in ComponentName tile);
+ /**
+ * Add a tile to the Quick Settings Panel
+ * @param tile the ComponentName of the {@link android.service.quicksettings.TileService}
+ * @param end if true, the tile will be added at the end. If false, at the beginning.
+ */
+ void addQsTileToFrontOrEnd(in ComponentName tile, boolean end);
void remQsTile(in ComponentName tile);
void setQsTiles(in String[] tiles);
void clickQsTile(in ComponentName tile);
diff --git a/core/jni/Android.bp b/core/jni/Android.bp
index 656cc3e..3fc1683 100644
--- a/core/jni/Android.bp
+++ b/core/jni/Android.bp
@@ -284,7 +284,6 @@
"libscrypt_static",
"libstatssocket_lazy",
"libskia",
- "libperfetto_client_experimental",
],
shared_libs: [
diff --git a/core/jni/android_tracing_PerfettoDataSource.cpp b/core/jni/android_tracing_PerfettoDataSource.cpp
index d710698..16c3ca9 100644
--- a/core/jni/android_tracing_PerfettoDataSource.cpp
+++ b/core/jni/android_tracing_PerfettoDataSource.cpp
@@ -25,7 +25,6 @@
#include <perfetto/public/producer.h>
#include <perfetto/public/protos/trace/test_event.pzc.h>
#include <perfetto/public/protos/trace/trace_packet.pzc.h>
-#include <perfetto/tracing.h>
#include <utils/Log.h>
#include <utils/RefBase.h>
diff --git a/core/jni/android_tracing_PerfettoDataSource.h b/core/jni/android_tracing_PerfettoDataSource.h
index 4ddf1d8..61a7654 100644
--- a/core/jni/android_tracing_PerfettoDataSource.h
+++ b/core/jni/android_tracing_PerfettoDataSource.h
@@ -23,10 +23,10 @@
#include <perfetto/public/producer.h>
#include <perfetto/public/protos/trace/test_event.pzc.h>
#include <perfetto/public/protos/trace/trace_packet.pzc.h>
-#include <perfetto/tracing.h>
#include <utils/Log.h>
#include <utils/RefBase.h>
+#include <map>
#include <sstream>
#include <thread>
diff --git a/core/jni/android_tracing_PerfettoDataSourceInstance.cpp b/core/jni/android_tracing_PerfettoDataSourceInstance.cpp
index e659bf1..3fbd5b3 100644
--- a/core/jni/android_tracing_PerfettoDataSourceInstance.cpp
+++ b/core/jni/android_tracing_PerfettoDataSourceInstance.cpp
@@ -25,7 +25,6 @@
#include <perfetto/public/producer.h>
#include <perfetto/public/protos/trace/test_event.pzc.h>
#include <perfetto/public/protos/trace/trace_packet.pzc.h>
-#include <perfetto/tracing.h>
#include <utils/Log.h>
#include <utils/RefBase.h>
diff --git a/core/jni/android_tracing_PerfettoDataSourceInstance.h b/core/jni/android_tracing_PerfettoDataSourceInstance.h
index d577655..ebb5259 100644
--- a/core/jni/android_tracing_PerfettoDataSourceInstance.h
+++ b/core/jni/android_tracing_PerfettoDataSourceInstance.h
@@ -23,7 +23,6 @@
#include <perfetto/public/producer.h>
#include <perfetto/public/protos/trace/test_event.pzc.h>
#include <perfetto/public/protos/trace/trace_packet.pzc.h>
-#include <perfetto/tracing.h>
#include <utils/Log.h>
#include <utils/RefBase.h>
diff --git a/core/jni/android_tracing_PerfettoProducer.cpp b/core/jni/android_tracing_PerfettoProducer.cpp
index ce72f58..f8c63c8 100644
--- a/core/jni/android_tracing_PerfettoProducer.cpp
+++ b/core/jni/android_tracing_PerfettoProducer.cpp
@@ -23,7 +23,6 @@
#include <perfetto/public/producer.h>
#include <perfetto/public/protos/trace/test_event.pzc.h>
#include <perfetto/public/protos/trace/trace_packet.pzc.h>
-#include <perfetto/tracing.h>
#include <utils/Log.h>
#include <utils/RefBase.h>
diff --git a/core/jni/android_view_InputChannel.cpp b/core/jni/android_view_InputChannel.cpp
index d4a7462..d11166f 100644
--- a/core/jni/android_view_InputChannel.cpp
+++ b/core/jni/android_view_InputChannel.cpp
@@ -203,8 +203,10 @@
if (parcel) {
bool isInitialized = parcel->readInt32();
if (isInitialized) {
- std::unique_ptr<InputChannel> inputChannel = std::make_unique<InputChannel>();
- inputChannel->readFromParcel(parcel);
+ android::os::InputChannelCore parcelableChannel;
+ parcelableChannel.readFromParcel(parcel);
+ std::unique_ptr<InputChannel> inputChannel =
+ InputChannel::create(std::move(parcelableChannel));
NativeInputChannel* nativeInputChannel =
new NativeInputChannel(std::move(inputChannel));
return reinterpret_cast<jlong>(nativeInputChannel);
@@ -228,7 +230,9 @@
return;
}
parcel->writeInt32(1); // initialized
- nativeInputChannel->getInputChannel()->writeToParcel(parcel);
+ android::os::InputChannelCore parcelableChannel;
+ nativeInputChannel->getInputChannel()->copyTo(parcelableChannel);
+ parcelableChannel.writeToParcel(parcel);
}
static jstring android_view_InputChannel_nativeGetName(JNIEnv* env, jobject obj, jlong channel) {
diff --git a/core/jni/android_view_InputEventReceiver.cpp b/core/jni/android_view_InputEventReceiver.cpp
index f7d8152..f93b306 100644
--- a/core/jni/android_view_InputEventReceiver.cpp
+++ b/core/jni/android_view_InputEventReceiver.cpp
@@ -189,11 +189,11 @@
void NativeInputEventReceiver::setFdEvents(int events) {
if (mFdEvents != events) {
mFdEvents = events;
- auto&& fd = mInputConsumer.getChannel()->getFd();
+ const int fd = mInputConsumer.getChannel()->getFd();
if (events) {
- mMessageQueue->getLooper()->addFd(fd.get(), 0, events, this, nullptr);
+ mMessageQueue->getLooper()->addFd(fd, 0, events, this, nullptr);
} else {
- mMessageQueue->getLooper()->removeFd(fd.get());
+ mMessageQueue->getLooper()->removeFd(fd);
}
}
}
diff --git a/core/jni/android_view_InputEventSender.cpp b/core/jni/android_view_InputEventSender.cpp
index 6bdf821..323f7b6 100644
--- a/core/jni/android_view_InputEventSender.cpp
+++ b/core/jni/android_view_InputEventSender.cpp
@@ -102,8 +102,8 @@
}
status_t NativeInputEventSender::initialize() {
- auto&& receiveFd = mInputPublisher.getChannel()->getFd();
- mMessageQueue->getLooper()->addFd(receiveFd.get(), 0, ALOOPER_EVENT_INPUT, this, NULL);
+ const int receiveFd = mInputPublisher.getChannel()->getFd();
+ mMessageQueue->getLooper()->addFd(receiveFd, 0, ALOOPER_EVENT_INPUT, this, NULL);
return OK;
}
@@ -112,7 +112,7 @@
LOG(DEBUG) << "channel '" << getInputChannelName() << "' ~ Disposing input event sender.";
}
- mMessageQueue->getLooper()->removeFd(mInputPublisher.getChannel()->getFd().get());
+ mMessageQueue->getLooper()->removeFd(mInputPublisher.getChannel()->getFd());
}
status_t NativeInputEventSender::sendKeyEvent(uint32_t seq, const KeyEvent* event) {
diff --git a/core/proto/OWNERS b/core/proto/OWNERS
index a854e36..381580b 100644
--- a/core/proto/OWNERS
+++ b/core/proto/OWNERS
@@ -13,7 +13,7 @@
jjaggi@google.com
kwekua@google.com
roosa@google.com
-per-file package_item_info.proto = toddke@google.com,patb@google.com
+per-file package_item_info.proto = file:/PACKAGE_MANAGER_OWNERS
per-file usagestatsservice.proto, usagestatsservice_v2.proto = file:/core/java/android/app/usage/OWNERS
per-file apphibernationservice.proto = file:/core/java/android/apphibernation/OWNERS
per-file android/hardware/sensorprivacy.proto = ntmyren@google.com,evanseverson@google.com
diff --git a/core/proto/android/providers/settings/system.proto b/core/proto/android/providers/settings/system.proto
index 48243f2..5fc2a59 100644
--- a/core/proto/android/providers/settings/system.proto
+++ b/core/proto/android/providers/settings/system.proto
@@ -207,6 +207,7 @@
optional SettingProto pointer_speed = 2 [ (android.privacy).dest = DEST_AUTOMATIC ];
optional SettingProto right_click_zone = 3 [ (android.privacy).dest = DEST_AUTOMATIC ];
optional SettingProto tap_to_click = 4 [ (android.privacy).dest = DEST_AUTOMATIC ];
+ optional SettingProto tap_dragging = 5 [ (android.privacy).dest = DEST_AUTOMATIC ];
}
optional Touchpad touchpad = 36;
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index c4b5d81..0171f58 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -3751,6 +3751,13 @@
<permission android:name="android.permission.MANAGE_DEVICE_POLICY_DEVICE_IDENTIFIERS"
android:protectionLevel="internal|role" />
+ <!-- Allows an application to manage policy related to content protection.
+ <p>Protection level: internal|role
+ @FlaggedApi("android.view.contentprotection.flags.manage_device_policy_enabled")
+ -->
+ <permission android:name="android.permission.MANAGE_DEVICE_POLICY_CONTENT_PROTECTION"
+ android:protectionLevel="internal|role" />
+
<!-- Allows an application to set device policies outside the current user
that are critical for securing data within the current user.
<p>Holding this permission allows the use of other held MANAGE_DEVICE_POLICY_*
diff --git a/core/res/res/drawable-nodpi/platlogo.xml b/core/res/res/drawable-nodpi/platlogo.xml
index fe12f6e..f3acab0 100644
--- a/core/res/res/drawable-nodpi/platlogo.xml
+++ b/core/res/res/drawable-nodpi/platlogo.xml
@@ -1,5 +1,5 @@
<!--
-Copyright (C) 2024 The Android Open Source Project
+Copyright (C) 2021 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -20,103 +20,179 @@
android:viewportWidth="512"
android:viewportHeight="512">
<path
- android:pathData="M256.22,437.7c-26.38,0 -43.81,-18.3 -61.2,-48.42 -17.39,-30.12 -103.26,-178.86 -120.65,-208.98s-24.52,-54.37 -11.33,-77.21c13.19,-22.84 37.75,-28.79 72.53,-28.79 34.78,0 206.53,0 241.31,0 34.78,0 59.35,5.95 72.53,28.79 13.19,22.84 6.06,47.09 -11.33,77.21s-103.26,178.86 -120.65,208.98c-17.39,30.12 -34.83,48.42 -61.2,48.42Z"
- android:strokeWidth="0">
+ android:pathData="M256,256m-200,0a200,200 0,1 1,400 0a200,200 0,1 1,-400 0">
<aapt:attr name="android:fillColor">
<gradient
- android:startX="56.22"
- android:startY="256"
- android:endX="456.22"
- android:endY="256"
+ android:startX="256"
+ android:startY="21.81"
+ android:endX="256"
+ android:endY="350.42"
android:type="linear">
<item android:offset="0" android:color="#FF073042"/>
<item android:offset="1" android:color="#FF073042"/>
</gradient>
</aapt:attr>
</path>
+ <group>
+ <clip-path
+ android:pathData="M256,256m-200,0a200,200 0,1 1,400 0a200,200 0,1 1,-400 0"/>
+ <path
+ android:pathData="m195.27,187.64l2.25,-6.69c13.91,78.13 50.84,284.39 50.84,50.33 0,-0.97 0.72,-1.81 1.62,-1.81h12.69c0.9,0 1.62,0.83 1.62,1.8 -0.2,409.91 -69.03,-43.64 -69.03,-43.64Z"
+ android:fillColor="#3ddc84"/>
+ </group>
+ <group>
+ <clip-path
+ android:pathData="M256,256m-200,0a200,200 0,1 1,400 0a200,200 0,1 1,-400 0"/>
+ <path
+ android:pathData="m158.77,180.68l-33.17,57.45c-1.9,3.3 -0.77,7.52 2.53,9.42 3.3,1.9 7.52,0.77 9.42,-2.53l33.59,-58.17c54.27,24.33 116.34,24.33 170.61,0l33.59,58.17c1.97,3.26 6.21,4.3 9.47,2.33 3.17,-1.91 4.26,-5.99 2.47,-9.23l-33.16,-57.45c56.95,-30.97 95.91,-88.64 101.61,-156.76H57.17c5.7,68.12 44.65,125.79 101.61,156.76Z"
+ android:fillColor="#fff"/>
+ </group>
+ <group>
+ <clip-path
+ android:pathData="M256,256m-200,0a200,200 0,1 1,400 0a200,200 0,1 1,-400 0"/>
+ <path
+ android:pathData="M250.26,187.66L262.17,187.66A2.13,2.13 0,0 1,264.3 189.78L264.3,217.85A2.13,2.13 0,0 1,262.17 219.98L250.26,219.98A2.13,2.13 0,0 1,248.14 217.85L248.14,189.78A2.13,2.13 0,0 1,250.26 187.66z"
+ android:fillColor="#3ddc84"/>
+ </group>
+ <group>
+ <clip-path
+ android:pathData="M256,256m-200,0a200,200 0,1 1,400 0a200,200 0,1 1,-400 0"/>
+ <path
+ android:pathData="M250.12,170.29L262.32,170.29A1.98,1.98 0,0 1,264.3 172.26L264.3,176.39A1.98,1.98 0,0 1,262.32 178.37L250.12,178.37A1.98,1.98 0,0 1,248.14 176.39L248.14,172.26A1.98,1.98 0,0 1,250.12 170.29z"
+ android:fillColor="#3ddc84"/>
+ </group>
+ <group>
+ <clip-path
+ android:pathData="M256,256m-200,0a200,200 0,1 1,400 0a200,200 0,1 1,-400 0"/>
+ <path
+ android:pathData="M171.92,216.82h4.04v4.04h-4.04z"
+ android:fillColor="#fff"/>
+ </group>
+ <group>
+ <clip-path
+ android:pathData="M256,256m-200,0a200,200 0,1 1,400 0a200,200 0,1 1,-400 0"/>
+ <path
+ android:pathData="M188.8,275.73h4.04v4.04h-4.04z"
+ android:fillColor="#fff"/>
+ </group>
+ <group>
+ <clip-path
+ android:pathData="M256,256m-200,0a200,200 0,1 1,400 0a200,200 0,1 1,-400 0"/>
+ <path
+ android:pathData="M369.04,337.63h4.04v4.04h-4.04z"
+ android:fillColor="#fff"/>
+ </group>
+ <group>
+ <clip-path
+ android:pathData="M256,256m-200,0a200,200 0,1 1,400 0a200,200 0,1 1,-400 0"/>
+ <path
+ android:pathData="M285.93,252.22h4.04v4.04h-4.04z"
+ android:fillColor="#fff"/>
+ </group>
+ <group>
+ <clip-path
+ android:pathData="M256,256m-200,0a200,200 0,1 1,400 0a200,200 0,1 1,-400 0"/>
+ <path
+ android:pathData="M318.96,218.84h4.04v4.04h-4.04z"
+ android:fillColor="#fff"/>
+ </group>
+ <group>
+ <clip-path
+ android:pathData="M256,256m-200,0a200,200 0,1 1,400 0a200,200 0,1 1,-400 0"/>
+ <path
+ android:pathData="M294.05,288.55h4.04v4.04h-4.04z"
+ android:fillColor="#fff"/>
+ </group>
+ <group>
+ <clip-path
+ android:pathData="M256,256m-200,0a200,200 0,1 1,400 0a200,200 0,1 1,-400 0"/>
+ <path
+ android:pathData="M330.82,273.31h8.08v8.08h-8.08z"
+ android:fillColor="#fff"/>
+ </group>
+ <group>
+ <clip-path
+ android:pathData="M256,256m-200,0a200,200 0,1 1,400 0a200,200 0,1 1,-400 0"/>
+ <path
+ android:pathData="M188.8,298.95h4.04v4.04h-4.04z"
+ android:fillColor="#fff"/>
+ </group>
+ <group>
+ <clip-path
+ android:pathData="M256,256m-200,0a200,200 0,1 1,400 0a200,200 0,1 1,-400 0"/>
+ <path
+ android:pathData="M220.14,238.94h8.08v8.08h-8.08z"
+ android:fillColor="#fff"/>
+ </group>
+ <group>
+ <clip-path
+ android:pathData="M256,256m-200,0a200,200 0,1 1,400 0a200,200 0,1 1,-400 0"/>
+ <path
+ android:pathData="M272.1,318.9h8.08v8.08h-8.08z"
+ android:fillColor="#fff"/>
+ </group>
+ <group>
+ <clip-path
+ android:pathData="M256,256m-200,0a200,200 0,1 1,400 0a200,200 0,1 1,-400 0"/>
+ <path
+ android:pathData="M293.34,349.25h8.08v8.08h-8.08z"
+ android:fillColor="#fff"/>
+ </group>
+ <group>
+ <clip-path
+ android:pathData="M256,256m-200,0a200,200 0,1 1,400 0a200,200 0,1 1,-400 0"/>
+ <path
+ android:pathData="M161.05,254.24h8.08v8.08h-8.08z"
+ android:fillColor="#fff"/>
+ </group>
+ <group>
+ <clip-path
+ android:pathData="M256,256m-200,0a200,200 0,1 1,400 0a200,200 0,1 1,-400 0"/>
+ <path
+ android:pathData="M378.92,192h8.08v8.08h-8.08z"
+ android:fillColor="#fff"/>
+ </group>
+ <group>
+ <clip-path
+ android:pathData="M256,256m-200,0a200,200 0,1 1,400 0a200,200 0,1 1,-400 0"/>
+ <path
+ android:pathData="M137.87,323.7h8.08v8.08h-8.08z"
+ android:fillColor="#fff"/>
+ </group>
<path
- android:pathData="M198.85,168.57l2.03,-6.03c12.55,70.48 45.87,256.56 45.87,45.41 0,-0.88 0.65,-1.63 1.46,-1.63h11.45c0.81,0 1.46,0.75 1.46,1.63 -0.18,369.8 -62.28,-39.37 -62.28,-39.37Z"
- android:strokeWidth="0"
- android:fillColor="#3ddc84"/>
- <path
- android:pathData="M186.69,167.97l-23.69,41.03c-1.36,2.36 -0.55,5.37 1.8,6.73 2.36,1.36 5.37,0.55 6.73,-1.8l23.99,-41.55c38.76,17.38 83.1,17.38 121.86,0l23.99,41.55c1.41,2.33 4.44,3.07 6.77,1.66 2.26,-1.37 3.04,-4.28 1.76,-6.59l-23.69,-41.03c40.68,-22.12 68.5,-63.31 72.57,-111.97H114.11c4.07,48.65 31.89,89.85 72.57,111.97Z"
- android:strokeWidth="0"
- android:fillColor="#fff"/>
- <path
- android:pathData="M248.46,168.59L259.2,168.59A1.92,1.92 0,0 1,261.12 170.5L261.12,195.83A1.92,1.92 0,0 1,259.2 197.75L248.46,197.75A1.92,1.92 0,0 1,246.54 195.83L246.54,170.5A1.92,1.92 0,0 1,248.46 168.59z"
- android:strokeWidth="0"
- android:fillColor="#3ddc84"/>
- <path
- android:pathData="M248.32,152.92L259.34,152.92A1.78,1.78 0,0 1,261.12 154.7L261.12,158.43A1.78,1.78 0,0 1,259.34 160.21L248.32,160.21A1.78,1.78 0,0 1,246.54 158.43L246.54,154.7A1.78,1.78 0,0 1,248.32 152.92z"
- android:strokeWidth="0"
- android:fillColor="#3ddc84"/>
- <path
- android:pathData="M159.03,176.91h4.04v4.04h-4.04z"
- android:strokeWidth="0"
- android:fillColor="#fff"/>
- <path
- android:pathData="M188.8,275.73h4.04v4.04h-4.04z"
- android:strokeWidth="0"
- android:fillColor="#fff"/>
- <path
- android:pathData="M373.41,158.93h4.04v4.04h-4.04z"
- android:strokeWidth="0"
- android:fillColor="#fff"/>
- <path
- android:pathData="M112.1,129.34h4.04v4.04h-4.04z"
- android:strokeWidth="0"
- android:fillColor="#fff"/>
- <path
- android:pathData="M285.93,252.22h4.04v4.04h-4.04z"
- android:strokeWidth="0"
- android:fillColor="#fff"/>
- <path
- android:pathData="M318.96,218.84h4.04v4.04h-4.04z"
- android:strokeWidth="0"
- android:fillColor="#fff"/>
- <path
- android:pathData="M294.05,288.55h4.04v4.04h-4.04z"
- android:strokeWidth="0"
- android:fillColor="#fff"/>
- <path
- android:pathData="M330.82,263.31h8.08v8.08h-8.08z"
- android:strokeWidth="0"
- android:fillColor="#fff"/>
- <path
- android:pathData="M188.8,298.95h4.04v4.04h-4.04z"
- android:strokeWidth="0"
- android:fillColor="#fff"/>
- <path
- android:pathData="M224.18,216.82h8.08v8.08h-8.08z"
- android:strokeWidth="0"
- android:fillColor="#fff"/>
- <path
- android:pathData="M272.1,318.9h8.08v8.08h-8.08z"
- android:strokeWidth="0"
- android:fillColor="#fff"/>
- <path
- android:pathData="M293.34,339.25h8.08v8.08h-8.08z"
- android:strokeWidth="0"
- android:fillColor="#fff"/>
- <path
- android:pathData="M165.09,236.28h8.08v8.08h-8.08z"
- android:strokeWidth="0"
- android:fillColor="#fff"/>
- <path
- android:pathData="M378.92,192h8.08v8.08h-8.08z"
- android:strokeWidth="0"
- android:fillColor="#fff"/>
- <path
- android:pathData="M204.28,314.86h8.08v8.08h-8.08z"
- android:strokeWidth="0"
- android:fillColor="#fff"/>
- <path
- android:pathData="M253.83,118.47c-6.04,0 -10.93,4.76 -10.93,10.62v13.1c0,1.13 0.92,2.05 2.05,2.05h0c1.13,0 2.05,-0.92 2.05,-2.05v-3.53c0,-2.27 1.84,-4.1 4.1,-4.1h5.47c2.27,0 4.1,1.84 4.1,4.1v3.53c0,1.13 0.92,2.05 2.05,2.05s2.05,-0.92 2.05,-2.05v-13.1c0,-5.86 -4.9,-10.62 -10.93,-10.62Z"
- android:strokeWidth="0"
- android:fillColor="#3ddc84"/>
- <path
- android:pathData="M256,437.7c-26.38,0 -43.81,-18.3 -61.2,-48.42 -17.39,-30.12 -103.26,-178.86 -120.65,-208.98 -17.39,-30.12 -24.52,-54.37 -11.33,-77.21 13.19,-22.84 37.75,-28.79 72.53,-28.79 34.78,0 206.53,0 241.31,0 34.78,0 59.35,5.95 72.53,28.79 13.19,22.84 6.06,47.09 -11.33,77.21 -17.39,30.12 -103.26,178.86 -120.65,208.98 -17.39,30.12 -34.83,48.42 -61.2,48.42Z"
- android:strokeWidth="55"
+ android:pathData="M256,256m-200,0a200,200 0,1 1,400 0a200,200 0,1 1,-400 0"
+ android:strokeWidth="56.561"
android:fillColor="#00000000"
- android:strokeColor="#f86733"/>
+ android:strokeColor="#f86734"/>
+ <path
+ android:pathData="m256.22,126.57c-6.69,0 -12.12,5.27 -12.12,11.77v14.52c0,1.25 1.02,2.27 2.27,2.27h0c1.25,0 2.27,-1.02 2.27,-2.27v-3.91c0,-2.51 2.04,-4.55 4.55,-4.55h6.06c2.51,0 4.55,2.04 4.55,4.55v3.91c0,1.25 1.02,2.27 2.27,2.27s2.27,-1.02 2.27,-2.27v-14.52c0,-6.5 -5.43,-11.77 -12.12,-11.77Z"
+ android:fillColor="#3ddc84"/>
+ <path
+ android:pathData="m93.34,116.36l3.85,-4.36 29.64,9.76 -4.44,5.03 -6.23,-2.1 -7.86,8.91 2.86,5.92 -4.43,5.03 -13.39,-28.18ZM110.43,122.76l-8.86,-3.02 4.11,8.41 4.76,-5.39Z"
+ android:fillColor="#fff"/>
+ <path
+ android:pathData="m153.62,100.85l-21.71,-6.2 10.38,14.38 -5.21,3.76 -16.78,-23.26 4.49,-3.24 21.65,6.19 -10.35,-14.35 5.24,-3.78 16.78,23.26 -4.49,3.24Z"
+ android:fillColor="#fff"/>
+ <path
+ android:pathData="m161.46,63.15l8.99,-3.84c7.43,-3.18 15.96,0.12 19.09,7.44 3.13,7.32 -0.38,15.76 -7.81,18.94l-8.99,3.84 -11.28,-26.38ZM179.41,80.26c4.46,-1.91 5.96,-6.72 4.15,-10.96 -1.81,-4.24 -6.33,-6.48 -10.79,-4.57l-3.08,1.32 6.64,15.53 3.08,-1.32Z"
+ android:fillColor="#fff"/>
+ <path
+ android:pathData="m204.23,47.57l11.1,-2.2c5.31,-1.05 9.47,2.08 10.4,6.76 0.72,3.65 -0.76,6.37 -4.07,8.34l12.4,10.44 -7.57,1.5 -11.65,-9.76 -1.03,0.2 2.3,11.61 -6.3,1.25 -5.57,-28.14ZM216.78,56.7c1.86,-0.37 3,-1.71 2.68,-3.33 -0.34,-1.7 -1.88,-2.43 -3.74,-2.06l-4.04,0.8 1.07,5.39 4.04,-0.8Z"
+ android:fillColor="#fff"/>
+ <path
+ android:pathData="m244.29,55.6c0.13,-8.16 6.86,-14.72 15.06,-14.58 8.16,0.13 14.72,6.9 14.58,15.06s-6.91,14.72 -15.06,14.58c-8.2,-0.13 -14.71,-6.9 -14.58,-15.06ZM267.44,55.98c0.08,-4.64 -3.54,-8.66 -8.18,-8.74 -4.68,-0.08 -8.42,3.82 -8.5,8.47 -0.08,4.65 3.54,8.66 8.22,8.74 4.64,0.08 8.39,-3.82 8.46,-8.47Z"
+ android:fillColor="#fff"/>
+ <path
+ android:pathData="m294.39,44.84l6.31,1.23 -5.49,28.16 -6.31,-1.23 5.49,-28.16Z"
+ android:fillColor="#fff"/>
+ <path
+ android:pathData="m321.94,51.41l9.14,3.48c7.55,2.88 11.39,11.17 8.56,18.61 -2.83,7.44 -11.22,11.07 -18.77,8.19l-9.14,-3.48 10.22,-26.8ZM322.96,76.19c4.53,1.73 8.95,-0.69 10.6,-5 1.64,-4.3 -0.05,-9.06 -4.58,-10.78l-3.13,-1.19 -6.01,15.78 3.13,1.19Z"
+ android:fillColor="#fff"/>
+ <path
+ android:pathData="m381.41,89.24l-4.21,-3.21 3.65,-4.78 9.06,6.91 -17.4,22.81 -4.85,-3.7 13.75,-18.02Z"
+ android:fillColor="#fff"/>
+ <path
+ android:pathData="m397.96,126.37l-9.56,-10.26 3.61,-3.36 22.8,-1.25 3.74,4.02 -12.35,11.51 2.51,2.69 -4.08,3.8 -2.51,-2.69 -4.55,4.24 -4.16,-4.46 4.55,-4.24ZM407.83,117.17l-10.28,0.58 4.49,4.82 5.79,-5.4Z"
+ android:fillColor="#fff"/>
</vector>
+
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index 23c78fd..238f242 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -283,6 +283,20 @@
when there's no network connection. If the scan doesn't timeout, use zero -->
<integer name="config_radioScanningTimeout">0</integer>
+ <!-- The duration (in milliseconds) that the vibrator control service will wait for new
+ vibration params. -->
+ <integer name="config_requestVibrationParamsTimeout">50</integer>
+
+ <!-- Array containing the usages that should request vibration params before they are played.
+ These usages don't have strong latency requirements, e.g. ringtone and notification, and
+ can be slightly delayed. -->
+ <integer-array name="config_requestVibrationParamsForUsages">
+ <item>17</item> <!-- USAGE_ALARM -->
+ <item>33</item> <!-- USAGE_RINGTONE -->
+ <item>49</item> <!-- USAGE_NOTIFICATION -->
+ <item>65</item> <!-- USAGE_COMMUNICATION_REQUEST -->
+ </integer-array>
+
<!-- XXXXX NOTE THE FOLLOWING RESOURCES USE THE WRONG NAMING CONVENTION.
Please don't copy them, copy anything else. -->
@@ -3327,9 +3341,27 @@
<string name="config_carrierAppInstallDialogComponent" translatable="false"
>com.android.simappdialog/com.android.simappdialog.InstallCarrierAppActivity</string>
- <!-- Name of the dialog that is used to get or save an app credential -->
+ <!-- Name of the default framework dialog that is used to get or save an app credential.
+
+ This UI should be always launch-able and is used as a fallback when an oem replacement activity
+ (defined at config_oemCredentialManagerDialogComponent) is undefined / not found. -->
<string name="config_credentialManagerDialogComponent" translatable="false"
>com.android.credentialmanager/com.android.credentialmanager.CredentialSelectorActivity</string>
+ <!-- Whether to allow the credential selector activity to be replaced by an activity at
+ run-time (restricted to the privileged activity specified by
+ config_credentialSelectorActivityName).
+
+ When disabled, the fallback activity defined at
+ config_credentialManagerDialogComponent will be used instead. -->
+ <bool name="config_enableOemCredentialManagerDialogComponent" translatable="false">true</bool>
+ <!-- Fully qualified activity name providing the credential selector UI, that serves the
+ CredentialManager APIs.
+
+ Used only when config_enableOemCredentialManagerDialogComponent is true.
+
+ If the activity specified cannot be found or launched, then the fallback activity defined at
+ config_credentialManagerDialogComponent will be used instead. -->
+ <string name="config_oemCredentialManagerDialogComponent" translatable="false"></string>
<!-- Name of the broadcast receiver that is used to receive provider change events -->
<string name="config_credentialManagerReceiverComponent" translatable="false"
@@ -6908,4 +6940,7 @@
<!-- Defines the minimum interval (in ms) between two input-based user-activity poke events. -->
<integer name="config_minMillisBetweenInputUserActivityEvents">100</integer>
+
+ <!-- Name of the starting activity for DisplayCompat host. specific to automotive.-->
+ <string name="config_defaultDisplayCompatHostActivity" translatable="false"></string>
</resources>
diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml
index bd3a5e4..558bae7 100644
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -5307,6 +5307,12 @@
<!-- Content description of the expand button icon in the notification when expanded.-->
<string name="expand_button_content_description_expanded">Collapse</string>
+ <!-- A11y announcement when a view is collapsed. -->
+ <string name="content_description_collapsed">Collapsed</string>
+
+ <!-- A11y announcement when a view is expanded. -->
+ <string name="content_description_expanded">Expanded</string>
+
<!-- Accessibility action description on the expand button. -->
<string name="expand_action_accessibility">toggle expansion</string>
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 3c1a42f..8b74cbf 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -2080,6 +2080,8 @@
<java-symbol type="bool" name="config_ignoreVibrationsOnWirelessCharger" />
<java-symbol type="integer" name="config_vibrationWaveformRampDownDuration" />
<java-symbol type="integer" name="config_radioScanningTimeout" />
+ <java-symbol type="integer" name="config_requestVibrationParamsTimeout" />
+ <java-symbol type="array" name="config_requestVibrationParamsForUsages" />
<java-symbol type="integer" name="config_screenBrightnessSettingMinimum" />
<java-symbol type="integer" name="config_screenBrightnessSettingMaximum" />
<java-symbol type="integer" name="config_screenBrightnessSettingDefault" />
@@ -2281,6 +2283,8 @@
<java-symbol type="string" name="config_platformVpnConfirmDialogComponent" />
<java-symbol type="string" name="config_carrierAppInstallDialogComponent" />
<java-symbol type="string" name="config_credentialManagerDialogComponent" />
+ <java-symbol type="bool" name="config_enableOemCredentialManagerDialogComponent" />
+ <java-symbol type="string" name="config_oemCredentialManagerDialogComponent" />
<java-symbol type="string" name="config_credentialManagerReceiverComponent" />
<java-symbol type="string" name="config_defaultNetworkScorerPackageName" />
<java-symbol type="string" name="config_persistentDataPackageName" />
@@ -3826,6 +3830,9 @@
<java-symbol type="string" name="expand_button_content_description_collapsed" />
<java-symbol type="string" name="expand_button_content_description_expanded" />
+ <java-symbol type="string" name="content_description_collapsed" />
+ <java-symbol type="string" name="content_description_expanded" />
+
<!-- Colon separated list of package names that should be granted Notification Listener access -->
<java-symbol type="string" name="config_defaultListenerAccessPackages" />
diff --git a/core/tests/BroadcastRadioTests/src/android/hardware/radio/DefaultRadioTunerTest.java b/core/tests/BroadcastRadioTests/src/android/hardware/radio/DefaultRadioTunerTest.java
index 63de759..ddf3615 100644
--- a/core/tests/BroadcastRadioTests/src/android/hardware/radio/DefaultRadioTunerTest.java
+++ b/core/tests/BroadcastRadioTests/src/android/hardware/radio/DefaultRadioTunerTest.java
@@ -20,8 +20,6 @@
import static org.junit.Assert.assertThrows;
-import android.graphics.Bitmap;
-
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -88,12 +86,6 @@
return 0;
}
- @Nullable
- @Override
- public Bitmap getMetadataImage(int id) {
- return null;
- }
-
@Override
public boolean startBackgroundScan() {
return false;
@@ -138,6 +130,16 @@
}
@Test
+ public void getMetadataImage_forRadioTuner_throwsException() {
+ UnsupportedOperationException thrown = assertThrows(UnsupportedOperationException.class,
+ () -> DEFAULT_RADIO_TUNER.getMetadataImage(/* id= */ 1));
+
+ assertWithMessage("Exception for getting metadata image from default radio tuner")
+ .that(thrown).hasMessageThat()
+ .contains("Getting metadata image must be implemented in child classes");
+ }
+
+ @Test
public void getDynamicProgramList_forRadioTuner_returnsNull() {
assertWithMessage("Dynamic program list obtained from default radio tuner")
.that(DEFAULT_RADIO_TUNER.getDynamicProgramList(new ProgramList.Filter())).isNull();
diff --git a/core/tests/BroadcastRadioTests/src/android/hardware/radio/RadioMetadataTest.java b/core/tests/BroadcastRadioTests/src/android/hardware/radio/RadioMetadataTest.java
index fddfd39..b3a0aba 100644
--- a/core/tests/BroadcastRadioTests/src/android/hardware/radio/RadioMetadataTest.java
+++ b/core/tests/BroadcastRadioTests/src/android/hardware/radio/RadioMetadataTest.java
@@ -290,13 +290,29 @@
}
@Test
- public void getBitmapId_withIllegalKey() {
+ public void getBitmapId_withIllegalKey_fails() {
String illegalKey = RadioMetadata.METADATA_KEY_ARTIST;
RadioMetadata metadata = mBuilder.putInt(RadioMetadata.METADATA_KEY_ART, INT_KEY_VALUE)
.build();
- mExpect.withMessage("Bitmap id value with non-bitmap-id-type key %s", illegalKey)
- .that(metadata.getBitmapId(illegalKey)).isEqualTo(0);
+ IllegalArgumentException thrown = assertThrows(IllegalArgumentException.class, () -> {
+ metadata.getBitmapId(illegalKey);
+ });
+
+ mExpect.withMessage("Exception for getting string array for non-bitmap-id type key %s",
+ illegalKey).that(thrown).hasMessageThat().contains("bitmap key");
+ }
+
+ @Test
+ public void getBitmapId_withNullKey_fails() {
+ RadioMetadata metadata = mBuilder.build();
+
+ NullPointerException thrown = assertThrows(NullPointerException.class, () -> {
+ metadata.getBitmapId(/* key= */ null);
+ });
+
+ mExpect.withMessage("Exception for getting bitmap identifier with null key")
+ .that(thrown).hasMessageThat().contains("can not be null");
}
@Test
diff --git a/core/tests/BroadcastRadioTests/src/android/hardware/radio/TunerAdapterTest.java b/core/tests/BroadcastRadioTests/src/android/hardware/radio/TunerAdapterTest.java
index 4cda26d..5aace81 100644
--- a/core/tests/BroadcastRadioTests/src/android/hardware/radio/TunerAdapterTest.java
+++ b/core/tests/BroadcastRadioTests/src/android/hardware/radio/TunerAdapterTest.java
@@ -514,6 +514,26 @@
}
@Test
+ public void getMetadataImage_withImageIdUnavailable_fails() throws Exception {
+ int nonExistImageId = 2;
+ when(mTunerMock.getImage(nonExistImageId)).thenReturn(null);
+ IllegalArgumentException thrown = assertThrows(IllegalArgumentException.class,
+ () -> mRadioTuner.getMetadataImage(nonExistImageId));
+
+ assertWithMessage("Exception for getting metadata image with non-existing id")
+ .that(thrown).hasMessageThat().contains("is not available");
+ }
+
+ @Test
+ public void getMetadataImage_withInvalidId_fails() {
+ IllegalArgumentException thrown = assertThrows(IllegalArgumentException.class,
+ () -> mRadioTuner.getMetadataImage(/* id= */ 0));
+
+ assertWithMessage("Exception for getting metadata image for id 0").that(thrown)
+ .hasMessageThat().contains("Invalid metadata image id 0");
+ }
+
+ @Test
public void getMetadataImage_whenServiceDied_fails() throws Exception {
when(mTunerMock.getImage(anyInt())).thenThrow(new RemoteException());
diff --git a/core/tests/coretests/src/android/app/backup/BackupAgentTest.java b/core/tests/coretests/src/android/app/backup/BackupAgentTest.java
index 9e8e2d6..cd5deb6 100644
--- a/core/tests/coretests/src/android/app/backup/BackupAgentTest.java
+++ b/core/tests/coretests/src/android/app/backup/BackupAgentTest.java
@@ -20,24 +20,33 @@
import static org.mockito.Mockito.when;
+import android.app.IBackupAgent;
import android.app.backup.BackupAgent.IncludeExcludeRules;
import android.app.backup.BackupAnnotations.BackupDestination;
import android.app.backup.BackupAnnotations.OperationType;
import android.app.backup.FullBackup.BackupScheme.PathWithRequiredFlags;
+import android.content.Context;
import android.os.ParcelFileDescriptor;
import android.os.UserHandle;
import android.platform.test.annotations.Presubmit;
+import android.platform.test.flag.junit.SetFlagsRule;
import android.util.ArraySet;
import androidx.test.runner.AndroidJUnit4;
+import com.android.server.backup.Flags;
+
import org.junit.Before;
+import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
import java.io.IOException;
+import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.Map;
import java.util.Set;
@@ -49,9 +58,12 @@
private static final UserHandle USER_HANDLE = new UserHandle(15);
private static final String DATA_TYPE_BACKED_UP = "test data type";
+ @Mock IBackupManager mIBackupManager;
@Mock FullBackup.BackupScheme mBackupScheme;
+ @Mock Context mContext;
- private BackupAgent mBackupAgent;
+ @Rule
+ public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
@Before
public void setUp() {
@@ -67,11 +79,11 @@
excludePaths.add(path);
IncludeExcludeRules expectedRules = new IncludeExcludeRules(includePaths, excludePaths);
- mBackupAgent = getAgentForBackupDestination(BackupDestination.CLOUD);
+ BackupAgent backupAgent = getAgentForBackupDestination(BackupDestination.CLOUD);
when(mBackupScheme.maybeParseAndGetCanonicalExcludePaths()).thenReturn(excludePaths);
when(mBackupScheme.maybeParseAndGetCanonicalIncludePaths()).thenReturn(includePaths);
- IncludeExcludeRules rules = mBackupAgent.getIncludeExcludeRules(mBackupScheme);
+ IncludeExcludeRules rules = backupAgent.getIncludeExcludeRules(mBackupScheme);
assertThat(rules).isEqualTo(expectedRules);
}
@@ -137,6 +149,31 @@
0).getSuccessCount()).isEqualTo(1);
}
+ @Test
+ public void doRestoreFile_agentOverrideIgnoresFile_consumesAllBytesInBuffer() throws Exception {
+ mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_CLEAR_PIPE_AFTER_RESTORE_FILE);
+ BackupAgent agent = new TestRestoreIgnoringFullBackupAgent();
+ agent.attach(mContext);
+ agent.onCreate(USER_HANDLE, BackupDestination.CLOUD, OperationType.RESTORE);
+ IBackupAgent agentBinder = (IBackupAgent) agent.onBind();
+
+ ParcelFileDescriptor[] pipes = ParcelFileDescriptor.createPipe();
+ FileOutputStream writeSide = new FileOutputStream(
+ pipes[1].getFileDescriptor());
+ writeSide.write("Hello".getBytes(StandardCharsets.UTF_8));
+
+ agentBinder.doRestoreFile(pipes[0], /* length= */ 5, BackupAgent.TYPE_FILE,
+ FullBackup.FILES_TREE_TOKEN, /* path= */ "hello_file", /* mode= */
+ 0666, /* mtime= */ 12345, /* token= */ 6789, mIBackupManager);
+
+ try (FileInputStream in = new FileInputStream(pipes[0].getFileDescriptor())) {
+ assertThat(in.available()).isEqualTo(0);
+ } finally {
+ pipes[0].close();
+ pipes[1].close();
+ }
+ }
+
private BackupAgent getAgentForBackupDestination(@BackupDestination int backupDestination) {
BackupAgent agent = new TestFullBackupAgent();
agent.onCreate(USER_HANDLE, backupDestination);
@@ -144,7 +181,6 @@
}
private static class TestFullBackupAgent extends BackupAgent {
-
@Override
public void onBackup(ParcelFileDescriptor oldState, BackupDataOutput data,
ParcelFileDescriptor newState) throws IOException {
@@ -162,4 +198,14 @@
// Left empty as this is a full backup agent.
}
}
+
+ private static class TestRestoreIgnoringFullBackupAgent extends TestFullBackupAgent {
+
+ @Override
+ protected void onRestoreFile(ParcelFileDescriptor data, long size,
+ int type, String domain, String path, long mode, long mtime)
+ throws IOException {
+ // Ignore the file and don't consume any data.
+ }
+ }
}
diff --git a/core/tests/coretests/src/android/graphics/FontListParserTest.java b/core/tests/coretests/src/android/graphics/FontListParserTest.java
index 4dd5889..5f96c17 100644
--- a/core/tests/coretests/src/android/graphics/FontListParserTest.java
+++ b/core/tests/coretests/src/android/graphics/FontListParserTest.java
@@ -22,14 +22,18 @@
import static android.text.FontConfig.FontFamily.VARIANT_COMPACT;
import static android.text.FontConfig.FontFamily.VARIANT_DEFAULT;
import static android.text.FontConfig.FontFamily.VARIANT_ELEGANT;
+import static android.graphics.fonts.FontFamily.Builder.VARIABLE_FONT_FAMILY_TYPE_NONE;
+import static android.graphics.fonts.FontFamily.Builder.VARIABLE_FONT_FAMILY_TYPE_SINGLE_FONT_WGHT_ONLY;
+import static android.graphics.fonts.FontFamily.Builder.VARIABLE_FONT_FAMILY_TYPE_SINGLE_FONT_WGHT_ITAL;
+import static android.graphics.fonts.FontFamily.Builder.VARIABLE_FONT_FAMILY_TYPE_TWO_FONTS_WGHT;
import static com.google.common.truth.Truth.assertThat;
import static junit.framework.Assert.fail;
import android.graphics.fonts.FontCustomizationParser;
-import android.graphics.fonts.FontFamily;
import android.graphics.fonts.FontStyle;
+import android.graphics.fonts.SystemFonts;
import android.os.LocaleList;
import android.text.FontConfig;
import android.util.Xml;
@@ -64,9 +68,9 @@
FontConfig.NamedFamilyList expected = new FontConfig.NamedFamilyList(
Collections.singletonList(new FontConfig.FontFamily(
Arrays.asList(new FontConfig.Font(new File("test.ttf"), null, "test",
- new FontStyle(FONT_WEIGHT_NORMAL, FONT_SLANT_UPRIGHT), 0, "", null)),
- LocaleList.getEmptyLocaleList(), VARIANT_DEFAULT,
- FontFamily.Builder.VARIABLE_FONT_FAMILY_TYPE_NONE)), "sans-serif");
+ new FontStyle(FONT_WEIGHT_NORMAL, FONT_SLANT_UPRIGHT), 0, "", null,
+ FontConfig.Font.VAR_TYPE_AXES_NONE)),
+ LocaleList.getEmptyLocaleList(), VARIANT_DEFAULT)), "sans-serif");
FontConfig.NamedFamilyList family = readNamedFamily(xml);
assertThat(family).isEqualTo(expected);
}
@@ -82,12 +86,11 @@
Arrays.asList(
new FontConfig.Font(new File("test.ttf"), null, "test",
new FontStyle(FONT_WEIGHT_NORMAL, FONT_SLANT_UPRIGHT),
- 0, "", null),
+ 0, "", null, FontConfig.Font.VAR_TYPE_AXES_NONE),
new FontConfig.Font(new File("test.ttf"), null, "test",
new FontStyle(FONT_WEIGHT_NORMAL, FONT_SLANT_UPRIGHT),
- 0, "", "serif")),
- LocaleList.forLanguageTags("en"), VARIANT_DEFAULT,
- FontFamily.Builder.VARIABLE_FONT_FAMILY_TYPE_NONE);
+ 0, "", "serif", FontConfig.Font.VAR_TYPE_AXES_NONE)),
+ LocaleList.forLanguageTags("en"), VARIANT_DEFAULT);
FontConfig.FontFamily family = readFamily(xml);
assertThat(family).isEqualTo(expected);
@@ -103,9 +106,8 @@
Arrays.asList(
new FontConfig.Font(new File("test.ttf"), null, "test",
new FontStyle(FONT_WEIGHT_NORMAL, FONT_SLANT_UPRIGHT),
- 0, "", null)),
- LocaleList.forLanguageTags("en"), VARIANT_COMPACT,
- FontFamily.Builder.VARIABLE_FONT_FAMILY_TYPE_NONE);
+ 0, "", null, FontConfig.Font.VAR_TYPE_AXES_NONE)),
+ LocaleList.forLanguageTags("en"), VARIANT_COMPACT);
FontConfig.FontFamily family = readFamily(xml);
assertThat(family).isEqualTo(expected);
@@ -121,9 +123,8 @@
Arrays.asList(
new FontConfig.Font(new File("test.ttf"), null, "test",
new FontStyle(FONT_WEIGHT_NORMAL, FONT_SLANT_UPRIGHT),
- 0, "", null)),
- LocaleList.forLanguageTags("en"), VARIANT_ELEGANT,
- FontFamily.Builder.VARIABLE_FONT_FAMILY_TYPE_NONE);
+ 0, "", null, FontConfig.Font.VAR_TYPE_AXES_NONE)),
+ LocaleList.forLanguageTags("en"), VARIANT_ELEGANT);
FontConfig.FontFamily family = readFamily(xml);
assertThat(family).isEqualTo(expected);
@@ -140,13 +141,15 @@
FontConfig.NamedFamilyList expected = new FontConfig.NamedFamilyList(
Collections.singletonList(new FontConfig.FontFamily(Arrays.asList(
new FontConfig.Font(new File("normal.ttf"), null, "test",
- new FontStyle(FONT_WEIGHT_NORMAL, FONT_SLANT_UPRIGHT), 0, "", null),
+ new FontStyle(FONT_WEIGHT_NORMAL, FONT_SLANT_UPRIGHT), 0, "", null,
+ FontConfig.Font.VAR_TYPE_AXES_NONE),
new FontConfig.Font(new File("weight.ttf"), null, "test",
- new FontStyle(100, FONT_SLANT_UPRIGHT), 0, "", null),
+ new FontStyle(100, FONT_SLANT_UPRIGHT), 0, "", null,
+ FontConfig.Font.VAR_TYPE_AXES_NONE),
new FontConfig.Font(new File("italic.ttf"), null, "test",
- new FontStyle(FONT_WEIGHT_NORMAL, FONT_SLANT_ITALIC), 0, "", null)),
- LocaleList.getEmptyLocaleList(), VARIANT_DEFAULT,
- FontFamily.Builder.VARIABLE_FONT_FAMILY_TYPE_NONE)), "sans-serif");
+ new FontStyle(FONT_WEIGHT_NORMAL, FONT_SLANT_ITALIC), 0, "", null,
+ FontConfig.Font.VAR_TYPE_AXES_NONE)),
+ LocaleList.getEmptyLocaleList(), VARIANT_DEFAULT)), "sans-serif");
FontConfig.NamedFamilyList family = readNamedFamily(xml);
assertThat(family).isEqualTo(expected);
}
@@ -168,12 +171,13 @@
Collections.singletonList(new FontConfig.FontFamily(Arrays.asList(
new FontConfig.Font(new File("test-VF.ttf"), null, "test",
new FontStyle(FONT_WEIGHT_NORMAL, FONT_SLANT_UPRIGHT),
- 0, "'wdth' 100.0,'wght' 200.0", null),
+ 0, "'wdth' 100.0,'wght' 200.0", null,
+ FontConfig.Font.VAR_TYPE_AXES_NONE),
new FontConfig.Font(new File("test-VF.ttf"), null, "test",
new FontStyle(FONT_WEIGHT_NORMAL, FONT_SLANT_UPRIGHT),
- 0, "'wdth' 400.0,'wght' 700.0", null)),
- LocaleList.getEmptyLocaleList(), VARIANT_DEFAULT,
- FontFamily.Builder.VARIABLE_FONT_FAMILY_TYPE_NONE)),
+ 0, "'wdth' 400.0,'wght' 700.0", null,
+ FontConfig.Font.VAR_TYPE_AXES_NONE)),
+ LocaleList.getEmptyLocaleList(), VARIANT_DEFAULT)),
"sans-serif");
FontConfig.NamedFamilyList family = readNamedFamily(xml);
assertThat(family).isEqualTo(expected);
@@ -190,12 +194,11 @@
Collections.singletonList(new FontConfig.FontFamily(Arrays.asList(
new FontConfig.Font(new File("test.ttc"), null, "test",
new FontStyle(FONT_WEIGHT_NORMAL, FONT_SLANT_UPRIGHT),
- 0, "", null),
+ 0, "", null, FontConfig.Font.VAR_TYPE_AXES_NONE),
new FontConfig.Font(new File("test.ttc"), null, "test",
new FontStyle(FONT_WEIGHT_NORMAL, FONT_SLANT_UPRIGHT),
- 1, "", null)),
- LocaleList.getEmptyLocaleList(), VARIANT_DEFAULT,
- FontFamily.Builder.VARIABLE_FONT_FAMILY_TYPE_NONE)),
+ 1, "", null, FontConfig.Font.VAR_TYPE_AXES_NONE)),
+ LocaleList.getEmptyLocaleList(), VARIANT_DEFAULT)),
"sans-serif");
FontConfig.NamedFamilyList family = readNamedFamily(xml);
assertThat(family).isEqualTo(expected);
@@ -211,11 +214,12 @@
FontConfig.NamedFamilyList expected = new FontConfig.NamedFamilyList(
Collections.singletonList(new FontConfig.FontFamily(Arrays.asList(
new FontConfig.Font(new File("test.ttc"), null, "foo",
- new FontStyle(FONT_WEIGHT_NORMAL, FONT_SLANT_UPRIGHT), 0, "", null),
+ new FontStyle(FONT_WEIGHT_NORMAL, FONT_SLANT_UPRIGHT), 0, "", null,
+ FontConfig.Font.VAR_TYPE_AXES_NONE),
new FontConfig.Font(new File("test.ttc"), null, "test",
- new FontStyle(FONT_WEIGHT_NORMAL, FONT_SLANT_UPRIGHT), 1, "", null)),
- LocaleList.getEmptyLocaleList(), VARIANT_DEFAULT,
- FontFamily.Builder.VARIABLE_FONT_FAMILY_TYPE_NONE)), "sans-serif");
+ new FontStyle(FONT_WEIGHT_NORMAL, FONT_SLANT_UPRIGHT), 1, "", null,
+ FontConfig.Font.VAR_TYPE_AXES_NONE)),
+ LocaleList.getEmptyLocaleList(), VARIANT_DEFAULT)), "sans-serif");
FontConfig.NamedFamilyList family = readNamedFamily(xml);
assertThat(family).isEqualTo(expected);
}
@@ -385,14 +389,81 @@
public void varFamilyType() throws Exception {
String xml = "<?xml version='1.0' encoding='UTF-8'?>"
+ "<familyset>"
- + " <family name='sans-serif' varFamilyType='1'>"
- + " <font>test.ttf</font>"
+ + " <family name='sans-serif'>"
+ + " <font supportedAxes='wght'>test.ttf</font>"
+ + " <font supportedAxes='ital'>test.ttf</font>"
+ + " <font supportedAxes='wght,ital'>test.ttf</font>"
+ " </family>"
+ "</familyset>";
FontConfig config = readFamilies(xml, true /* include non-existing font files */);
List<FontConfig.FontFamily> families = config.getFontFamilies();
assertThat(families.size()).isEqualTo(1); // legacy one should be ignored.
- assertThat(families.get(0).getVariableFontFamilyType()).isEqualTo(1);
+ assertThat(families.get(0).getFontList().get(0).getVarTypeAxes())
+ .isEqualTo(FontConfig.Font.VAR_TYPE_AXES_WGHT);
+ assertThat(families.get(0).getFontList().get(1).getVarTypeAxes())
+ .isEqualTo(FontConfig.Font.VAR_TYPE_AXES_ITAL);
+ assertThat(families.get(0).getFontList().get(2).getVarTypeAxes())
+ .isEqualTo(FontConfig.Font.VAR_TYPE_AXES_WGHT | FontConfig.Font.VAR_TYPE_AXES_ITAL);
+ }
+
+ @Test
+ public void varFamilyTypeRsolve() throws Exception {
+ String xml = "<?xml version='1.0' encoding='UTF-8'?>"
+ + "<familyset>"
+ + " <family name='sans-serif'>"
+ + " <font style='normal' supportedAxes='wght'>test.ttf</font>"
+ + " </family>"
+ + " <family>"
+ + " <font style='normal' supportedAxes='wght'>test.ttf</font>"
+ + " <font style='italic' supportedAxes='wght'>test.ttf</font>"
+ + " </family>"
+ + " <family>"
+ + " <font supportedAxes='wght,ital'>test.ttf</font>"
+ + " </family>"
+ + " <family>"
+ + " <font supportedAxes='ital'>test.ttf</font>"
+ + " </family>"
+ + " <family>"
+ + " <font>test.ttf</font>"
+ + " </family>"
+ + "</familyset>";
+ FontConfig config = readFamilies(xml, true /* include non-existing font files */);
+ List<FontConfig.FontFamily> families = config.getFontFamilies();
+ assertThat(families.size()).isEqualTo(5);
+ assertThat(SystemFonts.resolveVarFamilyType(families.get(0), null))
+ .isEqualTo(VARIABLE_FONT_FAMILY_TYPE_SINGLE_FONT_WGHT_ONLY);
+ assertThat(SystemFonts.resolveVarFamilyType(families.get(1), null))
+ .isEqualTo(VARIABLE_FONT_FAMILY_TYPE_TWO_FONTS_WGHT);
+ assertThat(SystemFonts.resolveVarFamilyType(families.get(2), null))
+ .isEqualTo(VARIABLE_FONT_FAMILY_TYPE_SINGLE_FONT_WGHT_ITAL);
+ assertThat(SystemFonts.resolveVarFamilyType(families.get(3), null))
+ .isEqualTo(VARIABLE_FONT_FAMILY_TYPE_NONE);
+ assertThat(SystemFonts.resolveVarFamilyType(families.get(4), null))
+ .isEqualTo(VARIABLE_FONT_FAMILY_TYPE_NONE);
+ }
+
+ @Test
+ public void varFamilyTypeRsolve_ForName() throws Exception {
+ String xml = "<?xml version='1.0' encoding='UTF-8'?>"
+ + "<familyset>"
+ + " <family name='sans-serif'>"
+ + " <font style='normal' supportedAxes='wght'>test.ttf</font>"
+ + " </family>"
+ + " <family name='serif'>"
+ + " <font style='normal' supportedAxes='wght'>test.ttf</font>"
+ + " </family>"
+ + " <family>"
+ + " <font style='normal' supportedAxes='wght'>test.ttf</font>"
+ + " <font fallbackFor='serif' supportedAxes='wght,ital'>test.ttf</font>"
+ + " </family>"
+ + "</familyset>";
+ FontConfig config = readFamilies(xml, true /* include non-existing font files */);
+ List<FontConfig.FontFamily> families = config.getFontFamilies();
+ assertThat(families.size()).isEqualTo(2);
+ assertThat(SystemFonts.resolveVarFamilyType(families.get(1), null))
+ .isEqualTo(VARIABLE_FONT_FAMILY_TYPE_SINGLE_FONT_WGHT_ONLY);
+ assertThat(SystemFonts.resolveVarFamilyType(families.get(1), "serif"))
+ .isEqualTo(VARIABLE_FONT_FAMILY_TYPE_SINGLE_FONT_WGHT_ITAL);
}
private FontConfig readFamilies(String xml, boolean allowNonExisting)
diff --git a/core/tests/coretests/src/android/os/BundleTest.java b/core/tests/coretests/src/android/os/BundleTest.java
index e7b5dff6..93c2e0e 100644
--- a/core/tests/coretests/src/android/os/BundleTest.java
+++ b/core/tests/coretests/src/android/os/BundleTest.java
@@ -20,6 +20,7 @@
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
@@ -121,6 +122,14 @@
}
@Test
+ public void testEmpty() throws Exception {
+ assertNotNull(Bundle.EMPTY);
+ assertEquals(0, Bundle.EMPTY.size());
+
+ new Bundle(Bundle.EMPTY);
+ }
+
+ @Test
@IgnoreUnderRavenwood(blockedBy = ParcelFileDescriptor.class)
public void testCreateFromParcel() throws Exception {
boolean withFd;
diff --git a/core/tests/coretests/src/android/os/TestLooperManagerTest.java b/core/tests/coretests/src/android/os/TestLooperManagerTest.java
new file mode 100644
index 0000000..5959444
--- /dev/null
+++ b/core/tests/coretests/src/android/os/TestLooperManagerTest.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.os;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import android.platform.test.ravenwood.RavenwoodRule;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+@RunWith(AndroidJUnit4.class)
+public class TestLooperManagerTest {
+ private static final String TAG = "TestLooperManagerTest";
+
+ @Rule
+ public final RavenwoodRule mRavenwood = new RavenwoodRule.Builder()
+ .setProvideMainThread(true)
+ .build();
+
+ @Test
+ public void testMainThread() throws Exception {
+ doTest(Looper.getMainLooper());
+ }
+
+ @Test
+ public void testCustomThread() throws Exception {
+ final HandlerThread thread = new HandlerThread(TAG);
+ thread.start();
+ doTest(thread.getLooper());
+ }
+
+ private void doTest(Looper looper) throws Exception {
+ final TestLooperManager tlm =
+ InstrumentationRegistry.getInstrumentation().acquireLooperManager(looper);
+
+ final Handler handler = new Handler(looper);
+ final CountDownLatch latch = new CountDownLatch(1);
+
+ assertFalse(tlm.hasMessages(handler, null, 42));
+
+ handler.sendEmptyMessage(42);
+ handler.post(() -> {
+ latch.countDown();
+ });
+ assertTrue(tlm.hasMessages(handler, null, 42));
+ assertFalse(latch.await(100, TimeUnit.MILLISECONDS));
+
+ final Message first = tlm.next();
+ assertEquals(42, first.what);
+ assertNull(first.callback);
+ tlm.execute(first);
+ assertFalse(tlm.hasMessages(handler, null, 42));
+ assertFalse(latch.await(100, TimeUnit.MILLISECONDS));
+ tlm.recycle(first);
+
+ final Message second = tlm.next();
+ assertNotNull(second.callback);
+ tlm.execute(second);
+ assertFalse(tlm.hasMessages(handler, null, 42));
+ assertTrue(latch.await(100, TimeUnit.MILLISECONDS));
+ tlm.recycle(second);
+
+ tlm.release();
+ }
+}
diff --git a/core/tests/coretests/src/android/util/LogTest.java b/core/tests/coretests/src/android/util/LogTest.java
index f9966a1..15caac9 100644
--- a/core/tests/coretests/src/android/util/LogTest.java
+++ b/core/tests/coretests/src/android/util/LogTest.java
@@ -37,6 +37,13 @@
private static final String LOG_TAG = "LogTest";
@Test
+ public void testWtf() {
+ Log.wtf(LOG_TAG, "Message");
+ Log.wtf(LOG_TAG, "Message", new Throwable("Throwable"));
+ Log.wtf(LOG_TAG, new Throwable("Throwable"));
+ }
+
+ @Test
@Ignore
public void testIsLoggable() {
// First clear any SystemProperty setting for our test key.
diff --git a/core/tests/coretests/src/android/widget/RemoteViewsTest.java b/core/tests/coretests/src/android/widget/RemoteViewsTest.java
index 543d73b..5862711 100644
--- a/core/tests/coretests/src/android/widget/RemoteViewsTest.java
+++ b/core/tests/coretests/src/android/widget/RemoteViewsTest.java
@@ -450,13 +450,8 @@
}
private RemoteViews.DrawInstructions getDrawInstructions() {
- final byte[] first = new byte[] {'f', 'i', 'r', 's', 't'};
- final byte[] second = new byte[] {'s', 'e', 'c', 'o', 'n', 'd'};
- final RemoteViews.DrawInstructions drawInstructions =
- new RemoteViews.DrawInstructions.Builder(
- Collections.singletonList(first)).build();
- drawInstructions.appendInstructions(second);
- return drawInstructions;
+ final byte[] bytes = new byte[] {'h', 'e', 'l', 'l', 'o'};
+ return new RemoteViews.DrawInstructions.Builder(Collections.singletonList(bytes)).build();
}
private RemoteViews createViewChained(int depth, String... texts) {
diff --git a/core/tests/utiltests/src/android/util/SlogTest.java b/core/tests/utiltests/src/android/util/SlogTest.java
index 6f761e3..738c668 100644
--- a/core/tests/utiltests/src/android/util/SlogTest.java
+++ b/core/tests/utiltests/src/android/util/SlogTest.java
@@ -44,4 +44,11 @@
Slog.w(TAG, MSG, THROWABLE);
Slog.e(TAG, MSG, THROWABLE);
}
+
+ @Test
+ public void testWtf() {
+ Slog.wtf(TAG, MSG);
+ Slog.wtf(TAG, MSG, THROWABLE);
+ Slog.wtf(TAG, THROWABLE);
+ }
}
diff --git a/data/etc/OWNERS b/data/etc/OWNERS
index ea23aba..245f216 100644
--- a/data/etc/OWNERS
+++ b/data/etc/OWNERS
@@ -1,3 +1,5 @@
+include /PACKAGE_MANAGER_OWNERS
+
alanstokes@google.com
cbrubaker@google.com
hackbod@android.com
@@ -6,11 +8,6 @@
jsharkey@android.com
jsharkey@google.com
lorenzo@google.com
-svetoslavganov@android.com
-svetoslavganov@google.com
-toddke@android.com
-toddke@google.com
-patb@google.com
yamasani@google.com
per-file preinstalled-packages* = file:/MULTIUSER_OWNERS
diff --git a/data/fonts/font_fallback.xml b/data/fonts/font_fallback.xml
index 1bd182f..15ea15a 100644
--- a/data/fonts/font_fallback.xml
+++ b/data/fonts/font_fallback.xml
@@ -11,12 +11,84 @@
effectively add 300 to the weight, this ensures that 900 is the bold
paired with the 500 weight, ensuring adequate contrast.
- TODO(rsheeter) update comment; ordering to match 800 to 900 is no longer required
+
+ The font_fallback.xml defines the list of font used by the system.
+
+ `familyset` node:
+ A `familyset` element must be a root node of the font_fallback.xml. No attributes are allowed
+ to `familyset` node.
+ The `familyset` node can contains `family` and `alias` nodes. Any other nodes will be ignored.
+
+ `family` node:
+ A `family` node defines a single font family definition.
+ A font family is a set of fonts for drawing text in various styles such as weight, slant.
+ There are three types of families, default family, named family and locale fallback family.
+
+ The default family is a special family node appeared the first node of the `familyset` node.
+ The default family is used as first priority fallback.
+ Only `name` attribute can be used for default family node. If the `name` attribute is
+ specified, This family will also works as named family.
+
+ The named family is a family that has name attribute. The named family defines a new fallback.
+ For example, if the name attribute is "serif", it creates serif fallback. Developers can
+ access the fallback by using Typeface#create API.
+ The named family can not have attribute other than `name` attribute. The `name` attribute
+ cannot be empty.
+
+ The locale fallback family is a font family that is used for fallback. The fallback family is
+ used when the named family or default family cannot be used. The locale fallback family can
+ have `lang` attribute and `variant` attribute. The `lang` attribute is an optional comma
+ separated BCP-47i language tag. The `variant` is an optional attribute that can be one one
+ `element`, `compact`. If a `variant` attribute is not specified, it is treated as default.
+
+ `alias` node:
+ An `alias` node defines a alias of named family with changing weight offset. An `alias` node
+ can have mandatory `name` and `to` attribute and optional `weight` attribute. This `alias`
+ defines new fallback that has the name of specified `name` attribute. The fallback list is
+ the same to the fallback that of the name specified with `to` attribute. If `weight` attribute
+ is specified, the base weight offset is shifted to the specified value. For example, if the
+ `weight` is 500, the output text is drawn with 500 of weight.
+
+ `font` node:
+ A `font` node defines a single font definition. There are two types of fonts, static font and
+ variable font.
+
+ A static font can have `weight`, `style`, `index` and `postScriptName` attributes. A `weight`
+ is a mandatory attribute that defines the weight of the font. Any number between 0 to 1000 is
+ valid. A `style` is a mandatory attribute that defines the style of the font. A 'style'
+ attribute can be `normal` or `italic`. An `index` is an optional attribute that defines the
+ index of the font collection. If this is not specified, it is treated as 0. If the font file
+ is not a font collection, this attribute is ignored. A `postScriptName` attribute is an
+ optional attribute. A PostScript name is used for identifying target of system font update.
+ If this is not specified, the system assumes the filename is same to PostScript name of the
+ font file. For example, if the font file is "Roboto-Regular.ttf", the system assume the
+ PostScript name of this font is "Roboto-Regular".
+
+ A variable font can be only defined for the variable font file. A variable font can have
+ `axis` child nodes for specifying axis values. A variable font can have all attribute of
+ static font and can have additional `supportedAxes` attribute. A `supportedAxes` attribute
+ is a comma separated supported axis tags. As of Android V, only `wght` and `ital` axes can
+ be specified.
+
+ If `supportedAxes` attribute is not specified, this `font` node works as static font of the
+ single instance of variable font specified with `axis` children.
+
+ If `supportedAxes` attribute is specified, the system dynamically create font instance for the
+ given weight and style value. If `wght` is specified in `supportedAxes` attribute the `weight`
+ attribute and `axis` child that has `wght` tag become optional and ignored because it is
+ determined by system at runtime. Similarly, if `ital` is specified in `supportedAxes`
+ attribute, the `style` attribute and `axis` child that has `ital` tag become optional and
+ ignored.
+
+ `axis` node:
+ An `axis` node defines a font variation value for a tag. An `axis` node can have two mandatory
+ attributes, `tag` and `value`. If the font is variable font and the same tag `axis` node is
+ specified in `supportedAxes` attribute, the style value works like a default instance.
-->
-<familyset version="23">
+<familyset>
<!-- first font is default -->
- <family name="sans-serif" varFamilyType="2">
- <font>Roboto-Regular.ttf
+ <family name="sans-serif">
+ <font supportedAxes="wght,ital">Roboto-Regular.ttf
<axis tag="wdth" stylevalue="100" />
</font>
</family>
@@ -32,8 +104,8 @@
<alias name="tahoma" to="sans-serif" />
<alias name="verdana" to="sans-serif" />
- <family name="sans-serif-condensed" varFamilyType="2">
- <font>Roboto-Regular.ttf
+ <family name="sans-serif-condensed">
+ <font supportedAxes="wght,ital">Roboto-Regular.ttf
<axis tag="wdth" stylevalue="75" />
</font>
</family>
@@ -72,8 +144,8 @@
<font weight="400" style="normal" postScriptName="ComingSoon-Regular">ComingSoon.ttf</font>
</family>
- <family name="cursive" varFamilyType="1">
- <font>DancingScript-Regular.ttf</font>
+ <family name="cursive">
+ <font supportedAxes="wght">DancingScript-Regular.ttf</font>
</family>
<family name="sans-serif-smallcaps">
@@ -90,8 +162,8 @@
</family>
<alias name="source-sans-pro-semi-bold" to="source-sans-pro" weight="600"/>
- <family name="roboto-flex" varFamilyType="2">
- <font>RobotoFlex-Regular.ttf
+ <family name="roboto-flex">
+ <font supportedAxes="wght">RobotoFlex-Regular.ttf
<axis tag="wdth" stylevalue="100" />
</font>
</family>
@@ -109,11 +181,11 @@
</font>
<font weight="700" style="normal">NotoNaskhArabicUI-Bold.ttf</font>
</family>
- <family lang="und-Ethi" varFamilyType="1">
- <font postScriptName="NotoSansEthiopic-Regular">
+ <family lang="und-Ethi">
+ <font postScriptName="NotoSansEthiopic-Regular" supportedAxes="wght">
NotoSansEthiopic-VF.ttf
</font>
- <font fallbackFor="serif" postScriptName="NotoSerifEthiopic-Regular">
+ <font fallbackFor="serif" postScriptName="NotoSerifEthiopic-Regular" supportedAxes="wght">
NotoSerifEthiopic-VF.ttf
</font>
</family>
@@ -140,32 +212,32 @@
</font>
<font weight="700" style="normal">NotoSansThaiUI-Bold.ttf</font>
</family>
- <family lang="und-Armn" varFamilyType="1">
- <font postScriptName="NotoSansArmenian-Regular">
+ <family lang="und-Armn">
+ <font postScriptName="NotoSansArmenian-Regular" supportedAxes="wght">
NotoSansArmenian-VF.ttf
</font>
- <font fallbackFor="serif" postScriptName="NotoSerifArmenian-Regular">
+ <font fallbackFor="serif" postScriptName="NotoSerifArmenian-Regular" supportedAxes="wght">
NotoSerifArmenian-VF.ttf
</font>
</family>
- <family lang="und-Geor,und-Geok" varFamilyType="1">
- <font postScriptName="NotoSansGeorgian-Regular">
+ <family lang="und-Geor,und-Geok">
+ <font postScriptName="NotoSansGeorgian-Regular" supportedAxes="wght">
NotoSansGeorgian-VF.ttf
</font>
- <font fallbackFor="serif" postScriptName="NotoSerifGeorgian-Regular">
+ <font fallbackFor="serif" postScriptName="NotoSerifGeorgian-Regular" supportedAxes="wght">
NotoSerifGeorgian-VF.ttf
</font>
</family>
- <family lang="und-Deva" variant="elegant" varFamilyType="1">
- <font postScriptName="NotoSansDevanagari-Regular">
+ <family lang="und-Deva" variant="elegant">
+ <font postScriptName="NotoSansDevanagari-Regular" supportedAxes="wght">
NotoSansDevanagari-VF.ttf
</font>
- <font fallbackFor="serif" postScriptName="NotoSerifDevanagari-Regular">
+ <font fallbackFor="serif" postScriptName="NotoSerifDevanagari-Regular" supportedAxes="wght">
NotoSerifDevanagari-VF.ttf
</font>
</family>
- <family lang="und-Deva" variant="compact" varFamilyType="1">
- <font postScriptName="NotoSansDevanagariUI-Regular">
+ <family lang="und-Deva" variant="compact">
+ <font postScriptName="NotoSansDevanagariUI-Regular" supportedAxes="wght">
NotoSansDevanagariUI-VF.ttf
</font>
</family>
@@ -178,21 +250,9 @@
NotoSansGujarati-Regular.ttf
</font>
<font weight="700" style="normal">NotoSansGujarati-Bold.ttf</font>
- <font weight="400" style="normal" fallbackFor="serif"
- postScriptName="NotoSerifGujarati-Regular">NotoSerifGujarati-VF.ttf
- <axis tag="wght" stylevalue="400"/>
- </font>
- <font weight="500" style="normal" fallbackFor="serif"
- postScriptName="NotoSerifGujarati-Regular">NotoSerifGujarati-VF.ttf
- <axis tag="wght" stylevalue="500"/>
- </font>
- <font weight="600" style="normal" fallbackFor="serif"
- postScriptName="NotoSerifGujarati-Regular">NotoSerifGujarati-VF.ttf
- <axis tag="wght" stylevalue="600"/>
- </font>
- <font weight="700" style="normal" fallbackFor="serif"
- postScriptName="NotoSerifGujarati-Regular">NotoSerifGujarati-VF.ttf
- <axis tag="wght" stylevalue="700"/>
+ <font style="normal" fallbackFor="serif" postScriptName="NotoSerifGujarati-Regular"
+ supportedAxes="wght">
+ NotoSerifGujarati-VF.ttf
</font>
</family>
<family lang="und-Gujr" variant="compact">
@@ -201,81 +261,81 @@
</font>
<font weight="700" style="normal">NotoSansGujaratiUI-Bold.ttf</font>
</family>
- <family lang="und-Guru" variant="elegant" varFamilyType="1">
- <font postScriptName="NotoSansGurmukhi-Regular">
+ <family lang="und-Guru" variant="elegant">
+ <font postScriptName="NotoSansGurmukhi-Regular" supportedAxes="wght">
NotoSansGurmukhi-VF.ttf
</font>
- <font fallbackFor="serif" postScriptName="NotoSerifGurmukhi-Regular">
+ <font fallbackFor="serif" postScriptName="NotoSerifGurmukhi-Regular" supportedAxes="wght">
NotoSerifGurmukhi-VF.ttf
</font>
</family>
- <family lang="und-Guru" variant="compact" varFamilyType="1">
- <font postScriptName="NotoSansGurmukhiUI-Regular">
+ <family lang="und-Guru" variant="compact">
+ <font postScriptName="NotoSansGurmukhiUI-Regular" supportedAxes="wght">
NotoSansGurmukhiUI-VF.ttf
</font>
</family>
- <family lang="und-Taml" variant="elegant" varFamilyType="1">
- <font postScriptName="NotoSansTamil-Regular">
+ <family lang="und-Taml" variant="elegant">
+ <font postScriptName="NotoSansTamil-Regular" supportedAxes="wght">
NotoSansTamil-VF.ttf
</font>
- <font fallbackFor="serif" postScriptName="NotoSerifTamil-Regular">
+ <font fallbackFor="serif" postScriptName="NotoSerifTamil-Regular" supportedAxes="wght">
NotoSerifTamil-VF.ttf
</font>
</family>
- <family lang="und-Taml" variant="compact" varFamilyType="1">
- <font postScriptName="NotoSansTamilUI-Regular">
+ <family lang="und-Taml" variant="compact">
+ <font postScriptName="NotoSansTamilUI-Regular" supportedAxes="wght">
NotoSansTamilUI-VF.ttf
</font>
</family>
- <family lang="und-Mlym" variant="elegant" varFamilyType="1">
- <font postScriptName="NotoSansMalayalam-Regular">
+ <family lang="und-Mlym" variant="elegant">
+ <font postScriptName="NotoSansMalayalam-Regular" supportedAxes="wght">
NotoSansMalayalam-VF.ttf
</font>
- <font fallbackFor="serif" postScriptName="NotoSerifMalayalam-Regular">
+ <font fallbackFor="serif" postScriptName="NotoSerifMalayalam-Regular" supportedAxes="wght">
NotoSerifMalayalam-VF.ttf
</font>
</family>
- <family lang="und-Mlym" variant="compact" varFamilyType="1">
- <font postScriptName="NotoSansMalayalamUI-Regular">
+ <family lang="und-Mlym" variant="compact">
+ <font postScriptName="NotoSansMalayalamUI-Regular" supportedAxes="wght">
NotoSansMalayalamUI-VF.ttf
</font>
</family>
- <family lang="und-Beng" variant="elegant" varFamilyType="1">
- <font postScriptName="NotoSansBengali-Regular">
+ <family lang="und-Beng" variant="elegant">
+ <font postScriptName="NotoSansBengali-Regular" supportedAxes="wght">
NotoSansBengali-VF.ttf
</font>
<font fallbackFor="serif" postScriptName="NotoSerifBengali-Regular">
NotoSerifBengali-VF.ttf
</font>
</family>
- <family lang="und-Beng" variant="compact" varFamilyType="1">
- <font postScriptName="NotoSansBengaliUI-Regular">
+ <family lang="und-Beng" variant="compact">
+ <font postScriptName="NotoSansBengaliUI-Regular" supportedAxes="wght">
NotoSansBengaliUI-VF.ttf
</font>
</family>
- <family lang="und-Telu" variant="elegant" varFamilyType="1">
- <font postScriptName="NotoSansTelugu-Regular">
+ <family lang="und-Telu" variant="elegant">
+ <font postScriptName="NotoSansTelugu-Regular" supportedAxes="wght">
NotoSansTelugu-VF.ttf
</font>
- <font fallbackFor="serif" postScriptName="NotoSerifTelugu-Regular">
+ <font fallbackFor="serif" postScriptName="NotoSerifTelugu-Regular" supportedAxes="wght">
NotoSerifTelugu-VF.ttf
</font>
</family>
- <family lang="und-Telu" variant="compact" varFamilyType="1">
- <font postScriptName="NotoSansTeluguUI-Regular">
+ <family lang="und-Telu" variant="compact">
+ <font postScriptName="NotoSansTeluguUI-Regular" supportedAxes="wght">
NotoSansTeluguUI-VF.ttf
</font>
</family>
- <family lang="und-Knda" variant="elegant" varFamilyType="1">
- <font postScriptName="NotoSansKannada-Regular">
+ <family lang="und-Knda" variant="elegant">
+ <font postScriptName="NotoSansKannada-Regular" supportedAxes="wght">
NotoSansKannada-VF.ttf
</font>
- <font fallbackFor="serif" postScriptName="NotoSerifKannada-Regular">
+ <font fallbackFor="serif" postScriptName="NotoSerifKannada-Regular" supportedAxes="wght">
NotoSerifKannada-VF.ttf
</font>
</family>
- <family lang="und-Knda" variant="compact" varFamilyType="1">
- <font postScriptName="NotoSansKannadaUI-Regular">
+ <family lang="und-Knda" variant="compact">
+ <font postScriptName="NotoSansKannadaUI-Regular" supportedAxes="wght">
NotoSansKannadaUI-VF.ttf
</font>
</family>
@@ -290,19 +350,20 @@
</font>
<font weight="700" style="normal">NotoSansOriyaUI-Bold.ttf</font>
</family>
- <family lang="und-Sinh" variant="elegant" varFamilyType="1">
- <font postScriptName="NotoSansSinhala-Regular">
+ <family lang="und-Sinh" variant="elegant">
+ <font postScriptName="NotoSansSinhala-Regular" supportedAxes="wght">
NotoSansSinhala-VF.ttf
</font>
<font fallbackFor="serif" postScriptName="NotoSerifSinhala-Regular">
NotoSerifSinhala-VF.ttf
</font>
</family>
- <family lang="und-Sinh" variant="compact" varFamilyType="1">
- <font postScriptName="NotoSansSinhalaUI-Regular">
+ <family lang="und-Sinh" variant="compact">
+ <font postScriptName="NotoSansSinhalaUI-Regular" supportedAxes="wght">
NotoSansSinhalaUI-VF.ttf
</font>
</family>
+ <!-- TODO: NotoSansKhmer uses non-standard wght value, so cannot use auto-adjustment. -->
<family lang="und-Khmr" variant="elegant">
<font weight="100" style="normal" postScriptName="NotoSansKhmer-Regular">
NotoSansKhmer-VF.ttf
@@ -398,8 +459,8 @@
<family lang="und-Ahom">
<font weight="400" style="normal">NotoSansAhom-Regular.otf</font>
</family>
- <family lang="und-Adlm" varFamilyType="1">
- <font postScriptName="NotoSansAdlam-Regular">
+ <family lang="und-Adlm">
+ <font postScriptName="NotoSansAdlam-Regular" supportedAxes="wght">
NotoSansAdlam-VF.ttf
</font>
</family>
@@ -686,8 +747,8 @@
NotoSansTaiViet-Regular.ttf
</font>
</family>
- <family lang="und-Tibt" varFamilyType="1">
- <font postScriptName="NotoSerifTibetan-Regular">
+ <family lang="und-Tibt">
+ <font postScriptName="NotoSerifTibetan-Regular" supportedAxes="wght">
NotoSerifTibetan-VF.ttf
</font>
</family>
@@ -865,27 +926,27 @@
<family lang="und-Dogr">
<font weight="400" style="normal">NotoSerifDogra-Regular.ttf</font>
</family>
- <family lang="und-Medf" varFamilyType="1">
- <font postScriptName="NotoSansMedefaidrin-Regular">
+ <family lang="und-Medf">
+ <font postScriptName="NotoSansMedefaidrin-Regular" supportedAxes="wght">
NotoSansMedefaidrin-VF.ttf
</font>
</family>
- <family lang="und-Soyo" varFamilyType="1">
+ <family lang="und-Soyo" supportedAxes="wght">
<font postScriptName="NotoSansSoyombo-Regular">
NotoSansSoyombo-VF.ttf
</font>
</family>
- <family lang="und-Takr" varFamilyType="1">
+ <family lang="und-Takr" supportedAxes="wght">
<font postScriptName="NotoSansTakri-Regular">
NotoSansTakri-VF.ttf
</font>
</family>
- <family lang="und-Hmnp" varFamilyType="1">
+ <family lang="und-Hmnp" supportedAxes="wght">
<font postScriptName="NotoSerifHmongNyiakeng-Regular">
NotoSerifNyiakengPuachueHmong-VF.ttf
</font>
</family>
- <family lang="und-Yezi" varFamilyType="1">
+ <family lang="und-Yezi" supportedAxes="wght">
<font postScriptName="NotoSerifYezidi-Regular">
NotoSerifYezidi-VF.ttf
</font>
diff --git a/data/fonts/font_fallback_cjkvf.xml b/data/fonts/font_fallback_cjkvf.xml
index 75bc74e..c1ca67e 100644
--- a/data/fonts/font_fallback_cjkvf.xml
+++ b/data/fonts/font_fallback_cjkvf.xml
@@ -11,12 +11,84 @@
effectively add 300 to the weight, this ensures that 900 is the bold
paired with the 500 weight, ensuring adequate contrast.
- TODO(rsheeter) update comment; ordering to match 800 to 900 is no longer required
+
+ The font_fallback.xml defines the list of font used by the system.
+
+ `familyset` node:
+ A `familyset` element must be a root node of the font_fallback.xml. No attributes are allowed
+ to `familyset` node.
+ The `familyset` node can contains `family` and `alias` nodes. Any other nodes will be ignored.
+
+ `family` node:
+ A `family` node defines a single font family definition.
+ A font family is a set of fonts for drawing text in various styles such as weight, slant.
+ There are three types of families, default family, named family and locale fallback family.
+
+ The default family is a special family node appeared the first node of the `familyset` node.
+ The default family is used as first priority fallback.
+ Only `name` attribute can be used for default family node. If the `name` attribute is
+ specified, This family will also works as named family.
+
+ The named family is a family that has name attribute. The named family defines a new fallback.
+ For example, if the name attribute is "serif", it creates serif fallback. Developers can
+ access the fallback by using Typeface#create API.
+ The named family can not have attribute other than `name` attribute. The `name` attribute
+ cannot be empty.
+
+ The locale fallback family is a font family that is used for fallback. The fallback family is
+ used when the named family or default family cannot be used. The locale fallback family can
+ have `lang` attribute and `variant` attribute. The `lang` attribute is an optional comma
+ separated BCP-47i language tag. The `variant` is an optional attribute that can be one one
+ `element`, `compact`. If a `variant` attribute is not specified, it is treated as default.
+
+ `alias` node:
+ An `alias` node defines a alias of named family with changing weight offset. An `alias` node
+ can have mandatory `name` and `to` attribute and optional `weight` attribute. This `alias`
+ defines new fallback that has the name of specified `name` attribute. The fallback list is
+ the same to the fallback that of the name specified with `to` attribute. If `weight` attribute
+ is specified, the base weight offset is shifted to the specified value. For example, if the
+ `weight` is 500, the output text is drawn with 500 of weight.
+
+ `font` node:
+ A `font` node defines a single font definition. There are two types of fonts, static font and
+ variable font.
+
+ A static font can have `weight`, `style`, `index` and `postScriptName` attributes. A `weight`
+ is a mandatory attribute that defines the weight of the font. Any number between 0 to 1000 is
+ valid. A `style` is a mandatory attribute that defines the style of the font. A 'style'
+ attribute can be `normal` or `italic`. An `index` is an optional attribute that defines the
+ index of the font collection. If this is not specified, it is treated as 0. If the font file
+ is not a font collection, this attribute is ignored. A `postScriptName` attribute is an
+ optional attribute. A PostScript name is used for identifying target of system font update.
+ If this is not specified, the system assumes the filename is same to PostScript name of the
+ font file. For example, if the font file is "Roboto-Regular.ttf", the system assume the
+ PostScript name of this font is "Roboto-Regular".
+
+ A variable font can be only defined for the variable font file. A variable font can have
+ `axis` child nodes for specifying axis values. A variable font can have all attribute of
+ static font and can have additional `supportedAxes` attribute. A `supportedAxes` attribute
+ is a comma separated supported axis tags. As of Android V, only `wght` and `ital` axes can
+ be specified.
+
+ If `supportedAxes` attribute is not specified, this `font` node works as static font of the
+ single instance of variable font specified with `axis` children.
+
+ If `supportedAxes` attribute is specified, the system dynamically create font instance for the
+ given weight and style value. If `wght` is specified in `supportedAxes` attribute the `weight`
+ attribute and `axis` child that has `wght` tag become optional and ignored because it is
+ determined by system at runtime. Similarly, if `ital` is specified in `supportedAxes`
+ attribute, the `style` attribute and `axis` child that has `ital` tag become optional and
+ ignored.
+
+ `axis` node:
+ An `axis` node defines a font variation value for a tag. An `axis` node can have two mandatory
+ attributes, `tag` and `value`. If the font is variable font and the same tag `axis` node is
+ specified in `supportedAxes` attribute, the style value works like a default instance.
-->
-<familyset version="23">
+<familyset>
<!-- first font is default -->
- <family name="sans-serif" varFamilyType="2">
- <font>Roboto-Regular.ttf
+ <family name="sans-serif">
+ <font supportedAxes="wght,ital">Roboto-Regular.ttf
<axis tag="wdth" stylevalue="100" />
</font>
</family>
@@ -32,8 +104,8 @@
<alias name="tahoma" to="sans-serif" />
<alias name="verdana" to="sans-serif" />
- <family name="sans-serif-condensed" varFamilyType="2">
- <font>Roboto-Regular.ttf
+ <family name="sans-serif-condensed">
+ <font supportedAxes="wght,ital">Roboto-Regular.ttf
<axis tag="wdth" stylevalue="75" />
</font>
</family>
@@ -72,8 +144,8 @@
<font weight="400" style="normal" postScriptName="ComingSoon-Regular">ComingSoon.ttf</font>
</family>
- <family name="cursive" varFamilyType="1">
- <font>DancingScript-Regular.ttf</font>
+ <family name="cursive">
+ <font supportedAxes="wght">DancingScript-Regular.ttf</font>
</family>
<family name="sans-serif-smallcaps">
@@ -90,8 +162,8 @@
</family>
<alias name="source-sans-pro-semi-bold" to="source-sans-pro" weight="600"/>
- <family name="roboto-flex" varFamilyType="2">
- <font>RobotoFlex-Regular.ttf
+ <family name="roboto-flex">
+ <font supportedAxes="wght">RobotoFlex-Regular.ttf
<axis tag="wdth" stylevalue="100" />
</font>
</family>
@@ -109,11 +181,11 @@
</font>
<font weight="700" style="normal">NotoNaskhArabicUI-Bold.ttf</font>
</family>
- <family lang="und-Ethi" varFamilyType="1">
- <font postScriptName="NotoSansEthiopic-Regular">
+ <family lang="und-Ethi">
+ <font postScriptName="NotoSansEthiopic-Regular" supportedAxes="wght">
NotoSansEthiopic-VF.ttf
</font>
- <font fallbackFor="serif" postScriptName="NotoSerifEthiopic-Regular">
+ <font fallbackFor="serif" postScriptName="NotoSerifEthiopic-Regular" supportedAxes="wght">
NotoSerifEthiopic-VF.ttf
</font>
</family>
@@ -140,32 +212,32 @@
</font>
<font weight="700" style="normal">NotoSansThaiUI-Bold.ttf</font>
</family>
- <family lang="und-Armn" varFamilyType="1">
- <font postScriptName="NotoSansArmenian-Regular">
+ <family lang="und-Armn">
+ <font postScriptName="NotoSansArmenian-Regular" supportedAxes="wght">
NotoSansArmenian-VF.ttf
</font>
- <font fallbackFor="serif" postScriptName="NotoSerifArmenian-Regular">
+ <font fallbackFor="serif" postScriptName="NotoSerifArmenian-Regular" supportedAxes="wght">
NotoSerifArmenian-VF.ttf
</font>
</family>
- <family lang="und-Geor,und-Geok" varFamilyType="1">
- <font postScriptName="NotoSansGeorgian-Regular">
+ <family lang="und-Geor,und-Geok">
+ <font postScriptName="NotoSansGeorgian-Regular" supportedAxes="wght">
NotoSansGeorgian-VF.ttf
</font>
- <font fallbackFor="serif" postScriptName="NotoSerifGeorgian-Regular">
+ <font fallbackFor="serif" postScriptName="NotoSerifGeorgian-Regular" supportedAxes="wght">
NotoSerifGeorgian-VF.ttf
</font>
</family>
- <family lang="und-Deva" variant="elegant" varFamilyType="1">
- <font postScriptName="NotoSansDevanagari-Regular">
+ <family lang="und-Deva" variant="elegant">
+ <font postScriptName="NotoSansDevanagari-Regular" supportedAxes="wght">
NotoSansDevanagari-VF.ttf
</font>
- <font fallbackFor="serif" postScriptName="NotoSerifDevanagari-Regular">
+ <font fallbackFor="serif" postScriptName="NotoSerifDevanagari-Regular" supportedAxes="wght">
NotoSerifDevanagari-VF.ttf
</font>
</family>
- <family lang="und-Deva" variant="compact" varFamilyType="1">
- <font postScriptName="NotoSansDevanagariUI-Regular">
+ <family lang="und-Deva" variant="compact">
+ <font postScriptName="NotoSansDevanagariUI-Regular" supportedAxes="wght">
NotoSansDevanagariUI-VF.ttf
</font>
</family>
@@ -178,21 +250,9 @@
NotoSansGujarati-Regular.ttf
</font>
<font weight="700" style="normal">NotoSansGujarati-Bold.ttf</font>
- <font weight="400" style="normal" fallbackFor="serif"
- postScriptName="NotoSerifGujarati-Regular">NotoSerifGujarati-VF.ttf
- <axis tag="wght" stylevalue="400"/>
- </font>
- <font weight="500" style="normal" fallbackFor="serif"
- postScriptName="NotoSerifGujarati-Regular">NotoSerifGujarati-VF.ttf
- <axis tag="wght" stylevalue="500"/>
- </font>
- <font weight="600" style="normal" fallbackFor="serif"
- postScriptName="NotoSerifGujarati-Regular">NotoSerifGujarati-VF.ttf
- <axis tag="wght" stylevalue="600"/>
- </font>
- <font weight="700" style="normal" fallbackFor="serif"
- postScriptName="NotoSerifGujarati-Regular">NotoSerifGujarati-VF.ttf
- <axis tag="wght" stylevalue="700"/>
+ <font style="normal" fallbackFor="serif" postScriptName="NotoSerifGujarati-Regular"
+ supportedAxes="wght">
+ NotoSerifGujarati-VF.ttf
</font>
</family>
<family lang="und-Gujr" variant="compact">
@@ -201,81 +261,81 @@
</font>
<font weight="700" style="normal">NotoSansGujaratiUI-Bold.ttf</font>
</family>
- <family lang="und-Guru" variant="elegant" varFamilyType="1">
- <font postScriptName="NotoSansGurmukhi-Regular">
+ <family lang="und-Guru" variant="elegant">
+ <font postScriptName="NotoSansGurmukhi-Regular" supportedAxes="wght">
NotoSansGurmukhi-VF.ttf
</font>
- <font fallbackFor="serif" postScriptName="NotoSerifGurmukhi-Regular">
+ <font fallbackFor="serif" postScriptName="NotoSerifGurmukhi-Regular" supportedAxes="wght">
NotoSerifGurmukhi-VF.ttf
</font>
</family>
- <family lang="und-Guru" variant="compact" varFamilyType="1">
- <font postScriptName="NotoSansGurmukhiUI-Regular">
+ <family lang="und-Guru" variant="compact">
+ <font postScriptName="NotoSansGurmukhiUI-Regular" supportedAxes="wght">
NotoSansGurmukhiUI-VF.ttf
</font>
</family>
- <family lang="und-Taml" variant="elegant" varFamilyType="1">
- <font postScriptName="NotoSansTamil-Regular">
+ <family lang="und-Taml" variant="elegant">
+ <font postScriptName="NotoSansTamil-Regular" supportedAxes="wght">
NotoSansTamil-VF.ttf
</font>
- <font fallbackFor="serif" postScriptName="NotoSerifTamil-Regular">
+ <font fallbackFor="serif" postScriptName="NotoSerifTamil-Regular" supportedAxes="wght">
NotoSerifTamil-VF.ttf
</font>
</family>
- <family lang="und-Taml" variant="compact" varFamilyType="1">
- <font postScriptName="NotoSansTamilUI-Regular">
+ <family lang="und-Taml" variant="compact">
+ <font postScriptName="NotoSansTamilUI-Regular" supportedAxes="wght">
NotoSansTamilUI-VF.ttf
</font>
</family>
- <family lang="und-Mlym" variant="elegant" varFamilyType="1">
- <font postScriptName="NotoSansMalayalam-Regular">
+ <family lang="und-Mlym" variant="elegant">
+ <font postScriptName="NotoSansMalayalam-Regular" supportedAxes="wght">
NotoSansMalayalam-VF.ttf
</font>
- <font fallbackFor="serif" postScriptName="NotoSerifMalayalam-Regular">
+ <font fallbackFor="serif" postScriptName="NotoSerifMalayalam-Regular" supportedAxes="wght">
NotoSerifMalayalam-VF.ttf
</font>
</family>
- <family lang="und-Mlym" variant="compact" varFamilyType="1">
- <font postScriptName="NotoSansMalayalamUI-Regular">
+ <family lang="und-Mlym" variant="compact">
+ <font postScriptName="NotoSansMalayalamUI-Regular" supportedAxes="wght">
NotoSansMalayalamUI-VF.ttf
</font>
</family>
- <family lang="und-Beng" variant="elegant" varFamilyType="1">
- <font postScriptName="NotoSansBengali-Regular">
+ <family lang="und-Beng" variant="elegant">
+ <font postScriptName="NotoSansBengali-Regular" supportedAxes="wght">
NotoSansBengali-VF.ttf
</font>
<font fallbackFor="serif" postScriptName="NotoSerifBengali-Regular">
NotoSerifBengali-VF.ttf
</font>
</family>
- <family lang="und-Beng" variant="compact" varFamilyType="1">
- <font postScriptName="NotoSansBengaliUI-Regular">
+ <family lang="und-Beng" variant="compact">
+ <font postScriptName="NotoSansBengaliUI-Regular" supportedAxes="wght">
NotoSansBengaliUI-VF.ttf
</font>
</family>
- <family lang="und-Telu" variant="elegant" varFamilyType="1">
- <font postScriptName="NotoSansTelugu-Regular">
+ <family lang="und-Telu" variant="elegant">
+ <font postScriptName="NotoSansTelugu-Regular" supportedAxes="wght">
NotoSansTelugu-VF.ttf
</font>
- <font fallbackFor="serif" postScriptName="NotoSerifTelugu-Regular">
+ <font fallbackFor="serif" postScriptName="NotoSerifTelugu-Regular" supportedAxes="wght">
NotoSerifTelugu-VF.ttf
</font>
</family>
- <family lang="und-Telu" variant="compact" varFamilyType="1">
- <font postScriptName="NotoSansTeluguUI-Regular">
+ <family lang="und-Telu" variant="compact">
+ <font postScriptName="NotoSansTeluguUI-Regular" supportedAxes="wght">
NotoSansTeluguUI-VF.ttf
</font>
</family>
- <family lang="und-Knda" variant="elegant" varFamilyType="1">
- <font postScriptName="NotoSansKannada-Regular">
+ <family lang="und-Knda" variant="elegant">
+ <font postScriptName="NotoSansKannada-Regular" supportedAxes="wght">
NotoSansKannada-VF.ttf
</font>
- <font fallbackFor="serif" postScriptName="NotoSerifKannada-Regular">
+ <font fallbackFor="serif" postScriptName="NotoSerifKannada-Regular" supportedAxes="wght">
NotoSerifKannada-VF.ttf
</font>
</family>
- <family lang="und-Knda" variant="compact" varFamilyType="1">
- <font postScriptName="NotoSansKannadaUI-Regular">
+ <family lang="und-Knda" variant="compact">
+ <font postScriptName="NotoSansKannadaUI-Regular" supportedAxes="wght">
NotoSansKannadaUI-VF.ttf
</font>
</family>
@@ -290,19 +350,20 @@
</font>
<font weight="700" style="normal">NotoSansOriyaUI-Bold.ttf</font>
</family>
- <family lang="und-Sinh" variant="elegant" varFamilyType="1">
- <font postScriptName="NotoSansSinhala-Regular">
+ <family lang="und-Sinh" variant="elegant">
+ <font postScriptName="NotoSansSinhala-Regular" supportedAxes="wght">
NotoSansSinhala-VF.ttf
</font>
<font fallbackFor="serif" postScriptName="NotoSerifSinhala-Regular">
NotoSerifSinhala-VF.ttf
</font>
</family>
- <family lang="und-Sinh" variant="compact" varFamilyType="1">
- <font postScriptName="NotoSansSinhalaUI-Regular">
+ <family lang="und-Sinh" variant="compact">
+ <font postScriptName="NotoSansSinhalaUI-Regular" supportedAxes="wght">
NotoSansSinhalaUI-VF.ttf
</font>
</family>
+ <!-- TODO: NotoSansKhmer uses non-standard wght value, so cannot use auto-adjustment. -->
<family lang="und-Khmr" variant="elegant">
<font weight="100" style="normal" postScriptName="NotoSansKhmer-Regular">
NotoSansKhmer-VF.ttf
@@ -398,8 +459,8 @@
<family lang="und-Ahom">
<font weight="400" style="normal">NotoSansAhom-Regular.otf</font>
</family>
- <family lang="und-Adlm" varFamilyType="1">
- <font postScriptName="NotoSansAdlam-Regular">
+ <family lang="und-Adlm">
+ <font postScriptName="NotoSansAdlam-Regular" supportedAxes="wght">
NotoSansAdlam-VF.ttf
</font>
</family>
@@ -686,8 +747,8 @@
NotoSansTaiViet-Regular.ttf
</font>
</family>
- <family lang="und-Tibt" varFamilyType="1">
- <font postScriptName="NotoSerifTibetan-Regular">
+ <family lang="und-Tibt">
+ <font postScriptName="NotoSerifTibetan-Regular" supportedAxes="wght">
NotoSerifTibetan-VF.ttf
</font>
</family>
@@ -707,123 +768,36 @@
<font weight="400" style="normal">NotoSansSymbols-Regular-Subsetted.ttf</font>
</family>
<family lang="zh-Hans">
- <font weight="100" style="normal" index="2" postScriptName="NotoSansCJKjp-Thin">
+ <font weight="400" style="normal" index="2" postScriptName="NotoSansCJKjp-Thin"
+ supportedAxes="wght">
NotoSansCJK-Regular.ttc
- <axis tag="wght" stylevalue="100"/>
- </font>
- <font weight="200" style="normal" index="2" postScriptName="NotoSansCJKjp-Thin">
- NotoSansCJK-Regular.ttc
- <axis tag="wght" stylevalue="200"/>
- </font>
- <font weight="300" style="normal" index="2" postScriptName="NotoSansCJKjp-Thin">
- NotoSansCJK-Regular.ttc
- <axis tag="wght" stylevalue="300"/>
- </font>
- <font weight="400" style="normal" index="2" postScriptName="NotoSansCJKjp-Thin">
- NotoSansCJK-Regular.ttc
- <axis tag="wght" stylevalue="400"/>
- </font>
- <font weight="500" style="normal" index="2" postScriptName="NotoSansCJKjp-Thin">
- NotoSansCJK-Regular.ttc
- <axis tag="wght" stylevalue="500"/>
- </font>
- <font weight="600" style="normal" index="2" postScriptName="NotoSansCJKjp-Thin">
- NotoSansCJK-Regular.ttc
- <axis tag="wght" stylevalue="600"/>
- </font>
- <font weight="700" style="normal" index="2" postScriptName="NotoSansCJKjp-Thin">
- NotoSansCJK-Regular.ttc
- <axis tag="wght" stylevalue="700"/>
- </font>
- <font weight="800" style="normal" index="2" postScriptName="NotoSansCJKjp-Thin">
- NotoSansCJK-Regular.ttc
- <axis tag="wght" stylevalue="800"/>
- </font>
- <font weight="900" style="normal" index="2" postScriptName="NotoSansCJKjp-Thin">
- NotoSansCJK-Regular.ttc
- <axis tag="wght" stylevalue="900"/>
+ <!-- The default instance of NotoSansCJK-Regular.ttc is wght=100, so specify wght=400
+ for making regular style as default. -->
+ <axis tag="wght" stylevalue="400" />
</font>
<font weight="400" style="normal" index="2" fallbackFor="serif"
postScriptName="NotoSerifCJKjp-Regular">NotoSerifCJK-Regular.ttc
</font>
</family>
<family lang="zh-Hant,zh-Bopo">
- <font weight="100" style="normal" index="3" postScriptName="NotoSansCJKjp-Thin">
+ <font weight="400" style="normal" index="3" postScriptName="NotoSansCJKjp-Thin"
+ supportedAxes="wght">
NotoSansCJK-Regular.ttc
- <axis tag="wght" stylevalue="100"/>
- </font>
- <font weight="200" style="normal" index="3" postScriptName="NotoSansCJKjp-Thin">
- NotoSansCJK-Regular.ttc
- <axis tag="wght" stylevalue="200"/>
- </font>
- <font weight="300" style="normal" index="3" postScriptName="NotoSansCJKjp-Thin">
- NotoSansCJK-Regular.ttc
- <axis tag="wght" stylevalue="300"/>
- </font>
- <font weight="400" style="normal" index="3" postScriptName="NotoSansCJKjp-Thin">
- NotoSansCJK-Regular.ttc
- <axis tag="wght" stylevalue="400"/>
- </font>
- <font weight="500" style="normal" index="3" postScriptName="NotoSansCJKjp-Thin">
- NotoSansCJK-Regular.ttc
- <axis tag="wght" stylevalue="500"/>
- </font>
- <font weight="600" style="normal" index="3" postScriptName="NotoSansCJKjp-Thin">
- NotoSansCJK-Regular.ttc
- <axis tag="wght" stylevalue="600"/>
- </font>
- <font weight="700" style="normal" index="3" postScriptName="NotoSansCJKjp-Thin">
- NotoSansCJK-Regular.ttc
- <axis tag="wght" stylevalue="700"/>
- </font>
- <font weight="800" style="normal" index="3" postScriptName="NotoSansCJKjp-Thin">
- NotoSansCJK-Regular.ttc
- <axis tag="wght" stylevalue="800"/>
- </font>
- <font weight="900" style="normal" index="3" postScriptName="NotoSansCJKjp-Thin">
- NotoSansCJK-Regular.ttc
- <axis tag="wght" stylevalue="900"/>
+ <!-- The default instance of NotoSansCJK-Regular.ttc is wght=100, so specify wght=400
+ for making regular style as default. -->
+ <axis tag="wght" stylevalue="400" />
</font>
<font weight="400" style="normal" index="3" fallbackFor="serif"
postScriptName="NotoSerifCJKjp-Regular">NotoSerifCJK-Regular.ttc
</font>
</family>
<family lang="ja">
- <font weight="100" style="normal" index="0" postScriptName="NotoSansCJKjp-Thin">
+ <font weight="400" style="normal" index="0" postScriptName="NotoSansCJKjp-Thin"
+ supportedAxes="wght">
NotoSansCJK-Regular.ttc
- <axis tag="wght" stylevalue="100"/>
- </font>
- <font weight="200" style="normal" index="0" postScriptName="NotoSansCJKjp-Thin">
- NotoSansCJK-Regular.ttc
- <axis tag="wght" stylevalue="200"/>
- </font>
- <font weight="300" style="normal" index="0" postScriptName="NotoSansCJKjp-Thin">
- NotoSansCJK-Regular.ttc
- <axis tag="wght" stylevalue="300"/>
- </font>
- <font weight="400" style="normal" index="0" postScriptName="NotoSansCJKjp-Thin">
- NotoSansCJK-Regular.ttc
- <axis tag="wght" stylevalue="400"/>
- </font>
- <font weight="500" style="normal" index="0" postScriptName="NotoSansCJKjp-Thin">
- NotoSansCJK-Regular.ttc
- <axis tag="wght" stylevalue="500"/>
- </font>
- <font weight="600" style="normal" index="0" postScriptName="NotoSansCJKjp-Thin">
- NotoSansCJK-Regular.ttc
- <axis tag="wght" stylevalue="600"/>
- </font>
- <font weight="700" style="normal" index="0" postScriptName="NotoSansCJKjp-Thin">
- NotoSansCJK-Regular.ttc
- <axis tag="wght" stylevalue="700"/>
- </font>
- <font weight="800" style="normal" index="0" postScriptName="NotoSansCJKjp-Thin">
- NotoSansCJK-Regular.ttc
- <axis tag="wght" stylevalue="800"/>
- </font>
- <font weight="900" style="normal" index="0" postScriptName="NotoSansCJKjp-Thin">
- NotoSansCJK-Regular.ttc
- <axis tag="wght" stylevalue="900"/>
+ <!-- The default instance of NotoSansCJK-Regular.ttc is wght=100, so specify wght=400
+ for making regular style as default. -->
+ <axis tag="wght" stylevalue="400" />
</font>
<font weight="400" style="normal" index="0" fallbackFor="serif"
postScriptName="NotoSerifCJKjp-Regular">NotoSerifCJK-Regular.ttc
@@ -840,41 +814,12 @@
</font>
</family>
<family lang="ko">
- <font weight="100" style="normal" index="1" postScriptName="NotoSansCJKjp-Thin">
+ <font weight="400" style="normal" index="1" postScriptName="NotoSansCJKjp-Thin"
+ supportedAxes="wght">
NotoSansCJK-Regular.ttc
- <axis tag="wght" stylevalue="100"/>
- </font>
- <font weight="200" style="normal" index="1" postScriptName="NotoSansCJKjp-Thin">
- NotoSansCJK-Regular.ttc
- <axis tag="wght" stylevalue="200"/>
- </font>
- <font weight="300" style="normal" index="1" postScriptName="NotoSansCJKjp-Thin">
- NotoSansCJK-Regular.ttc
- <axis tag="wght" stylevalue="300"/>
- </font>
- <font weight="400" style="normal" index="1" postScriptName="NotoSansCJKjp-Thin">
- NotoSansCJK-Regular.ttc
- <axis tag="wght" stylevalue="400"/>
- </font>
- <font weight="500" style="normal" index="1" postScriptName="NotoSansCJKjp-Thin">
- NotoSansCJK-Regular.ttc
- <axis tag="wght" stylevalue="500"/>
- </font>
- <font weight="600" style="normal" index="1" postScriptName="NotoSansCJKjp-Thin">
- NotoSansCJK-Regular.ttc
- <axis tag="wght" stylevalue="600"/>
- </font>
- <font weight="700" style="normal" index="1" postScriptName="NotoSansCJKjp-Thin">
- NotoSansCJK-Regular.ttc
- <axis tag="wght" stylevalue="700"/>
- </font>
- <font weight="800" style="normal" index="1" postScriptName="NotoSansCJKjp-Thin">
- NotoSansCJK-Regular.ttc
- <axis tag="wght" stylevalue="800"/>
- </font>
- <font weight="900" style="normal" index="1" postScriptName="NotoSansCJKjp-Thin">
- NotoSansCJK-Regular.ttc
- <axis tag="wght" stylevalue="900"/>
+ <!-- The default instance of NotoSansCJK-Regular.ttc is wght=100, so specify wght=400
+ for making regular style as default. -->
+ <axis tag="wght" stylevalue="400" />
</font>
<font weight="400" style="normal" index="1" fallbackFor="serif"
postScriptName="NotoSerifCJKjp-Regular">NotoSerifCJK-Regular.ttc
@@ -997,27 +942,27 @@
<family lang="und-Dogr">
<font weight="400" style="normal">NotoSerifDogra-Regular.ttf</font>
</family>
- <family lang="und-Medf" varFamilyType="1">
- <font postScriptName="NotoSansMedefaidrin-Regular">
+ <family lang="und-Medf">
+ <font postScriptName="NotoSansMedefaidrin-Regular" supportedAxes="wght">
NotoSansMedefaidrin-VF.ttf
</font>
</family>
- <family lang="und-Soyo" varFamilyType="1">
+ <family lang="und-Soyo" supportedAxes="wght">
<font postScriptName="NotoSansSoyombo-Regular">
NotoSansSoyombo-VF.ttf
</font>
</family>
- <family lang="und-Takr" varFamilyType="1">
+ <family lang="und-Takr" supportedAxes="wght">
<font postScriptName="NotoSansTakri-Regular">
NotoSansTakri-VF.ttf
</font>
</family>
- <family lang="und-Hmnp" varFamilyType="1">
+ <family lang="und-Hmnp" supportedAxes="wght">
<font postScriptName="NotoSerifHmongNyiakeng-Regular">
NotoSerifNyiakengPuachueHmong-VF.ttf
</font>
</family>
- <family lang="und-Yezi" varFamilyType="1">
+ <family lang="und-Yezi" supportedAxes="wght">
<font postScriptName="NotoSerifYezidi-Regular">
NotoSerifYezidi-VF.ttf
</font>
diff --git a/graphics/java/android/graphics/FontListParser.java b/graphics/java/android/graphics/FontListParser.java
index 52b0b95..17c2dd9 100644
--- a/graphics/java/android/graphics/FontListParser.java
+++ b/graphics/java/android/graphics/FontListParser.java
@@ -16,10 +16,6 @@
package android.graphics;
-import static android.graphics.fonts.FontFamily.Builder.VARIABLE_FONT_FAMILY_TYPE_NONE;
-import static android.graphics.fonts.FontFamily.Builder.VARIABLE_FONT_FAMILY_TYPE_SINGLE_FONT_WGHT_ITAL;
-import static android.graphics.fonts.FontFamily.Builder.VARIABLE_FONT_FAMILY_TYPE_SINGLE_FONT_WGHT_ONLY;
-import static android.graphics.fonts.FontFamily.Builder.VARIABLE_FONT_FAMILY_TYPE_TWO_FONTS_WGHT;
import static android.text.FontConfig.NamedFamilyList;
import android.annotation.NonNull;
@@ -32,7 +28,6 @@
import android.os.LocaleList;
import android.text.FontConfig;
import android.util.ArraySet;
-import android.util.Log;
import android.util.Xml;
import org.xmlpull.v1.XmlPullParser;
@@ -65,6 +60,7 @@
private static final String VARIANT_ELEGANT = "elegant";
// XML constants for Font.
+ public static final String ATTR_SUPPORTED_AXES = "supportedAxes";
public static final String ATTR_INDEX = "index";
public static final String ATTR_WEIGHT = "weight";
public static final String ATTR_POSTSCRIPT_NAME = "postScriptName";
@@ -78,6 +74,10 @@
public static final String ATTR_TAG = "tag";
public static final String ATTR_STYLEVALUE = "stylevalue";
+ // The tag string for variable font type resolution.
+ private static final String TAG_WGHT = "wght";
+ private static final String TAG_ITAL = "ital";
+
/* Parse fallback list (no names) */
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
public static FontConfig parse(InputStream in) throws XmlPullParserException, IOException {
@@ -263,7 +263,6 @@
final String lang = parser.getAttributeValue("", "lang");
final String variant = parser.getAttributeValue(null, "variant");
final String ignore = parser.getAttributeValue(null, "ignore");
- final String varFamilyTypeStr = parser.getAttributeValue(null, "varFamilyType");
final List<FontConfig.Font> fonts = new ArrayList<>();
while (keepReading(parser)) {
if (parser.getEventType() != XmlPullParser.START_TAG) continue;
@@ -286,45 +285,12 @@
intVariant = FontConfig.FontFamily.VARIANT_ELEGANT;
}
}
- int varFamilyType = VARIABLE_FONT_FAMILY_TYPE_NONE;
- if (varFamilyTypeStr != null) {
- varFamilyType = Integer.parseInt(varFamilyTypeStr);
- if (varFamilyType <= -1 || varFamilyType > 3) {
- Log.e(TAG, "Error: unexpected varFamilyType value: " + varFamilyTypeStr);
- varFamilyType = VARIABLE_FONT_FAMILY_TYPE_NONE;
- }
-
- // validation but don't read font content for performance reasons.
- switch (varFamilyType) {
- case VARIABLE_FONT_FAMILY_TYPE_SINGLE_FONT_WGHT_ONLY:
- if (fonts.size() != 1) {
- Log.e(TAG, "Error: Single font support wght axis, but two or more fonts are"
- + " included in the font family.");
- varFamilyType = VARIABLE_FONT_FAMILY_TYPE_NONE;
- }
- break;
- case VARIABLE_FONT_FAMILY_TYPE_SINGLE_FONT_WGHT_ITAL:
- if (fonts.size() != 1) {
- Log.e(TAG, "Error: Single font support both ital and wght axes, but two or"
- + " more fonts are included in the font family.");
- varFamilyType = VARIABLE_FONT_FAMILY_TYPE_NONE;
- }
- break;
- case VARIABLE_FONT_FAMILY_TYPE_TWO_FONTS_WGHT:
- if (fonts.size() != 2) {
- Log.e(TAG, "Error: two fonts that support wght axis, but one or three or"
- + " more fonts are included in the font family.");
- varFamilyType = VARIABLE_FONT_FAMILY_TYPE_NONE;
- }
- }
- }
boolean skip = (ignore != null && (ignore.equals("true") || ignore.equals("1")));
if (skip || fonts.isEmpty()) {
return null;
}
- return new FontConfig.FontFamily(fonts, LocaleList.forLanguageTags(lang), intVariant,
- varFamilyType);
+ return new FontConfig.FontFamily(fonts, LocaleList.forLanguageTags(lang), intVariant);
}
private static void throwIfAttributeExists(String attrName, XmlPullParser parser) {
@@ -407,6 +373,7 @@
boolean isItalic = STYLE_ITALIC.equals(parser.getAttributeValue(null, ATTR_STYLE));
String fallbackFor = parser.getAttributeValue(null, ATTR_FALLBACK_FOR);
String postScriptName = parser.getAttributeValue(null, ATTR_POSTSCRIPT_NAME);
+ final String supportedAxes = parser.getAttributeValue(null, ATTR_SUPPORTED_AXES);
StringBuilder filename = new StringBuilder();
while (keepReading(parser)) {
if (parser.getEventType() == XmlPullParser.TEXT) {
@@ -422,6 +389,18 @@
}
String sanitizedName = FILENAME_WHITESPACE_PATTERN.matcher(filename).replaceAll("");
+ int varTypeAxes = 0;
+ if (supportedAxes != null) {
+ for (String tag : supportedAxes.split(",")) {
+ String strippedTag = tag.strip();
+ if (strippedTag.equals(TAG_WGHT)) {
+ varTypeAxes |= FontConfig.Font.VAR_TYPE_AXES_WGHT;
+ } else if (strippedTag.equals(TAG_ITAL)) {
+ varTypeAxes |= FontConfig.Font.VAR_TYPE_AXES_ITAL;
+ }
+ }
+ }
+
if (postScriptName == null) {
// If post script name was not provided, assume the file name is same to PostScript
// name.
@@ -462,7 +441,8 @@
),
index,
varSettings,
- fallbackFor);
+ fallbackFor,
+ varTypeAxes);
}
private static String findUpdatedFontFile(String psName,
diff --git a/graphics/java/android/graphics/fonts/SystemFonts.java b/graphics/java/android/graphics/fonts/SystemFonts.java
index 3ef714ed..a90961e 100644
--- a/graphics/java/android/graphics/fonts/SystemFonts.java
+++ b/graphics/java/android/graphics/fonts/SystemFonts.java
@@ -100,6 +100,71 @@
}
}
+ /** @hide */
+ @VisibleForTesting
+ public static @FontFamily.Builder.VariableFontFamilyType int resolveVarFamilyType(
+ @NonNull FontConfig.FontFamily xmlFamily,
+ @Nullable String familyName) {
+ int wghtCount = 0;
+ int italCount = 0;
+ int targetFonts = 0;
+ boolean hasItalicFont = false;
+
+ List<FontConfig.Font> fonts = xmlFamily.getFontList();
+ for (int i = 0; i < fonts.size(); ++i) {
+ FontConfig.Font font = fonts.get(i);
+
+ if (familyName == null) { // for default family
+ if (font.getFontFamilyName() != null) {
+ continue; // this font is not for the default family.
+ }
+ } else { // for the specific family
+ if (!familyName.equals(font.getFontFamilyName())) {
+ continue; // this font is not for given family.
+ }
+ }
+
+ final int varTypeAxes = font.getVarTypeAxes();
+ if (varTypeAxes == 0) {
+ // If we see static font, we can immediately return as VAR_TYPE_NONE.
+ return FontFamily.Builder.VARIABLE_FONT_FAMILY_TYPE_NONE;
+ }
+
+ if ((varTypeAxes & FontConfig.Font.VAR_TYPE_AXES_WGHT) != 0) {
+ wghtCount++;
+ }
+
+ if ((varTypeAxes & FontConfig.Font.VAR_TYPE_AXES_ITAL) != 0) {
+ italCount++;
+ }
+
+ if (font.getStyle().getSlant() == FontStyle.FONT_SLANT_ITALIC) {
+ hasItalicFont = true;
+ }
+ targetFonts++;
+ }
+
+ if (italCount == 0) { // No ital font.
+ if (targetFonts == 1 && wghtCount == 1) {
+ // If there is only single font that has wght, use it for regular style and
+ // use synthetic bolding for italic.
+ return FontFamily.Builder.VARIABLE_FONT_FAMILY_TYPE_SINGLE_FONT_WGHT_ONLY;
+ } else if (targetFonts == 2 && wghtCount == 2 && hasItalicFont) {
+ // If there are two fonts and italic font is available, use them for regular and
+ // italic separately. (It is impossible to have two italic fonts. It will end up
+ // with Typeface creation failure.)
+ return FontFamily.Builder.VARIABLE_FONT_FAMILY_TYPE_TWO_FONTS_WGHT;
+ }
+ } else if (italCount == 1) {
+ // If ital font is included, a single font should support both wght and ital.
+ if (wghtCount == 1 && targetFonts == 1) {
+ return FontFamily.Builder.VARIABLE_FONT_FAMILY_TYPE_SINGLE_FONT_WGHT_ITAL;
+ }
+ }
+ // Otherwise, unsupported.
+ return FontFamily.Builder.VARIABLE_FONT_FAMILY_TYPE_NONE;
+ }
+
private static void pushFamilyToFallback(@NonNull FontConfig.FontFamily xmlFamily,
@NonNull ArrayMap<String, NativeFamilyListSet> fallbackMap,
@NonNull Map<String, ByteBuffer> cache) {
@@ -126,7 +191,7 @@
}
final FontFamily defaultFamily = defaultFonts.isEmpty() ? null : createFontFamily(
- defaultFonts, languageTags, variant, xmlFamily.getVariableFontFamilyType(), false,
+ defaultFonts, languageTags, variant, resolveVarFamilyType(xmlFamily, null), false,
cache);
// Insert family into fallback map.
for (int i = 0; i < fallbackMap.size(); i++) {
@@ -145,7 +210,7 @@
}
} else {
final FontFamily family = createFontFamily(fallback, languageTags, variant,
- xmlFamily.getVariableFontFamilyType(), false, cache);
+ resolveVarFamilyType(xmlFamily, name), false, cache);
if (family != null) {
familyListSet.familyList.add(family);
} else if (defaultFamily != null) {
@@ -217,7 +282,8 @@
final FontFamily family = createFontFamily(
xmlFamily.getFontList(),
xmlFamily.getLocaleList().toLanguageTags(), xmlFamily.getVariant(),
- xmlFamily.getVariableFontFamilyType(),
+ resolveVarFamilyType(xmlFamily,
+ null /* all fonts under named family should be treated as default */),
true, // named family is always default
bufferCache);
if (family == null) {
diff --git a/graphics/java/android/graphics/text/LineBreakConfig.java b/graphics/java/android/graphics/text/LineBreakConfig.java
index b21bf11..7d55928 100644
--- a/graphics/java/android/graphics/text/LineBreakConfig.java
+++ b/graphics/java/android/graphics/text/LineBreakConfig.java
@@ -176,6 +176,9 @@
* - If at least one locale in the locale list contains Japanese script, this option is
* equivalent to {@link #LINE_BREAK_STYLE_STRICT}.
* - Otherwise, this option is equivalent to {@link #LINE_BREAK_STYLE_NONE}.
+ *
+ * <p>
+ * Note: future versions may have special line breaking style rules for other locales.
*/
@FlaggedApi(FLAG_WORD_STYLE_AUTO)
public static final int LINE_BREAK_STYLE_AUTO = 5;
@@ -249,6 +252,9 @@
* option is equivalent to {@link #LINE_BREAK_WORD_STYLE_PHRASE} if the result of its line
* count is less than 5 lines.
* - Otherwise, this option is equivalent to {@link #LINE_BREAK_WORD_STYLE_NONE}.
+ *
+ * <p>
+ * Note: future versions may have special line breaking word style rules for other locales.
*/
@FlaggedApi(FLAG_WORD_STYLE_AUTO)
public static final int LINE_BREAK_WORD_STYLE_AUTO = 2;
diff --git a/keystore/java/android/security/AndroidProtectedConfirmation.java b/keystore/java/android/security/AndroidProtectedConfirmation.java
index dfe485a..268e0a5 100644
--- a/keystore/java/android/security/AndroidProtectedConfirmation.java
+++ b/keystore/java/android/security/AndroidProtectedConfirmation.java
@@ -59,6 +59,10 @@
/**
* Requests keystore call into the confirmationui HAL to display a prompt.
+ * @deprecated Android Protected Confirmation had a low adoption rate among Android device
+ * makers and developers alike. Given the lack of devices supporting the
+ * feature, it is deprecated. Developers can use auth-bound Keystore keys
+ * as a partial replacement.
*
* @param listener the binder to use for callbacks.
* @param promptText the prompt to display.
@@ -68,6 +72,7 @@
* @return one of the {@code CONFIRMATIONUI_*} constants, for
* example {@code KeyStore.CONFIRMATIONUI_OK}.
*/
+ @Deprecated
public int presentConfirmationPrompt(IConfirmationCallback listener, String promptText,
byte[] extraData, String locale, int uiOptionsAsFlags) {
try {
@@ -84,11 +89,16 @@
/**
* Requests keystore call into the confirmationui HAL to cancel displaying a prompt.
+ * @deprecated Android Protected Confirmation had a low adoption rate among Android device
+ * makers and developers alike. Given the lack of devices supporting the
+ * feature, it is deprecated. Developers can use auth-bound Keystore keys
+ * as a partial replacement.
*
* @param listener the binder passed to the {@link #presentConfirmationPrompt} method.
* @return one of the {@code CONFIRMATIONUI_*} constants, for
* example {@code KeyStore.CONFIRMATIONUI_OK}.
*/
+ @Deprecated
public int cancelConfirmationPrompt(IConfirmationCallback listener) {
try {
getService().cancelPrompt(listener);
@@ -103,9 +113,14 @@
/**
* Requests keystore to check if the confirmationui HAL is available.
+ * @deprecated Android Protected Confirmation had a low adoption rate among Android device
+ * makers and developers alike. Given the lack of devices supporting the
+ * feature, it is deprecated. Developers can use auth-bound Keystore keys
+ * as a partial replacement.
*
* @return whether the confirmationUI HAL is available.
*/
+ @Deprecated
public boolean isConfirmationPromptSupported() {
try {
return getService().isSupported();
diff --git a/keystore/java/android/security/keystore2/AndroidKeyStoreKeyPairGeneratorSpi.java b/keystore/java/android/security/keystore2/AndroidKeyStoreKeyPairGeneratorSpi.java
index 9c05a3a..83ddfc5 100644
--- a/keystore/java/android/security/keystore2/AndroidKeyStoreKeyPairGeneratorSpi.java
+++ b/keystore/java/android/security/keystore2/AndroidKeyStoreKeyPairGeneratorSpi.java
@@ -109,13 +109,29 @@
}
}
+ // For curve 25519, KeyMint uses the KM_ALGORITHM_EC constant, but in the Java layer we need
+ // to distinguish between Curve 25519 and other EC algorithms, so we use a different constant
+ // with a value that is outside the range of the enum used for KeyMint algorithms.
+ private static final int ALGORITHM_XDH = KeymasterDefs.KM_ALGORITHM_EC + 1200;
+ private static final int ALGORITHM_ED25519 = ALGORITHM_XDH + 1;
+
/**
- * XDH represents Curve 25519 providers.
+ * XDH represents Curve 25519 agreement key provider.
*/
public static class XDH extends AndroidKeyStoreKeyPairGeneratorSpi {
// XDH is treated as EC.
public XDH() {
- super(KeymasterDefs.KM_ALGORITHM_EC);
+ super(ALGORITHM_XDH);
+ }
+ }
+
+ /**
+ * ED25519 represents Curve 25519 signing key provider.
+ */
+ public static class ED25519 extends AndroidKeyStoreKeyPairGeneratorSpi {
+ // ED25519 is treated as EC.
+ public ED25519() {
+ super(ALGORITHM_ED25519);
}
}
@@ -241,7 +257,9 @@
KeyGenParameterSpec spec;
boolean encryptionAtRestRequired = false;
- int keymasterAlgorithm = mOriginalKeymasterAlgorithm;
+ int keymasterAlgorithm = (mOriginalKeymasterAlgorithm == ALGORITHM_XDH
+ || mOriginalKeymasterAlgorithm == ALGORITHM_ED25519)
+ ? KeymasterDefs.KM_ALGORITHM_EC : mOriginalKeymasterAlgorithm;
if (params instanceof KeyGenParameterSpec) {
spec = (KeyGenParameterSpec) params;
} else if (params instanceof KeyPairGeneratorSpec) {
@@ -610,6 +628,15 @@
if (algSpecificSpec instanceof ECGenParameterSpec) {
ECGenParameterSpec ecSpec = (ECGenParameterSpec) algSpecificSpec;
mEcCurveName = ecSpec.getName();
+ if (mOriginalKeymasterAlgorithm == ALGORITHM_XDH
+ && !mEcCurveName.equalsIgnoreCase("x25519")) {
+ throw new InvalidAlgorithmParameterException("XDH algorithm only supports"
+ + " x25519 curve.");
+ } else if (mOriginalKeymasterAlgorithm == ALGORITHM_ED25519
+ && !mEcCurveName.equalsIgnoreCase("ed25519")) {
+ throw new InvalidAlgorithmParameterException("Ed25519 algorithm only"
+ + " supports ed25519 curve.");
+ }
final Integer ecSpecKeySizeBits = SUPPORTED_EC_CURVE_NAME_TO_SIZE.get(
mEcCurveName.toLowerCase(Locale.US));
if (ecSpecKeySizeBits == null) {
diff --git a/keystore/java/android/security/keystore2/AndroidKeyStoreProvider.java b/keystore/java/android/security/keystore2/AndroidKeyStoreProvider.java
index 11278e8..d204f13 100644
--- a/keystore/java/android/security/keystore2/AndroidKeyStoreProvider.java
+++ b/keystore/java/android/security/keystore2/AndroidKeyStoreProvider.java
@@ -86,11 +86,14 @@
put("KeyPairGenerator.EC", PACKAGE_NAME + ".AndroidKeyStoreKeyPairGeneratorSpi$EC");
put("KeyPairGenerator.RSA", PACKAGE_NAME + ".AndroidKeyStoreKeyPairGeneratorSpi$RSA");
put("KeyPairGenerator.XDH", PACKAGE_NAME + ".AndroidKeyStoreKeyPairGeneratorSpi$XDH");
+ put("KeyPairGenerator.ED25519", PACKAGE_NAME
+ + ".AndroidKeyStoreKeyPairGeneratorSpi$ED25519");
// java.security.KeyFactory
putKeyFactoryImpl("EC");
putKeyFactoryImpl("RSA");
putKeyFactoryImpl("XDH");
+ putKeyFactoryImpl("ED25519");
// javax.crypto.KeyGenerator
put("KeyGenerator.AES", PACKAGE_NAME + ".AndroidKeyStoreKeyGeneratorSpi$AES");
diff --git a/libs/WindowManager/Shell/Android.bp b/libs/WindowManager/Shell/Android.bp
index 4cdc06a..a12fa5f 100644
--- a/libs/WindowManager/Shell/Android.bp
+++ b/libs/WindowManager/Shell/Android.bp
@@ -39,6 +39,7 @@
}
// Sources that have no dependencies that can be used directly downstream of this library
+// TODO(b/322791067): move these sources to WindowManager-Shell-shared
filegroup {
name: "wm_shell_util-sources",
srcs: [
@@ -137,6 +138,12 @@
},
}
+java_library {
+ name: "WindowManager-Shell-shared",
+
+ srcs: ["shared/**/*.java"],
+}
+
android_library {
name: "WindowManager-Shell",
srcs: [
@@ -162,6 +169,7 @@
"com_android_wm_shell_flags_lib",
"com.android.window.flags.window-aconfig-java",
"WindowManager-Shell-proto",
+ "WindowManager-Shell-shared",
"perfetto_trace_java_protos",
"dagger2",
"jsr330",
diff --git a/libs/WindowManager/Shell/aconfig/multitasking.aconfig b/libs/WindowManager/Shell/aconfig/multitasking.aconfig
index 0e04658..2b95f30 100644
--- a/libs/WindowManager/Shell/aconfig/multitasking.aconfig
+++ b/libs/WindowManager/Shell/aconfig/multitasking.aconfig
@@ -30,13 +30,6 @@
}
flag {
- name: "enable_pip_ui_state_on_entering"
- namespace: "multitasking"
- description: "Enables PiP UI state callback on entering"
- bug: "303718131"
-}
-
-flag {
name: "enable_pip2_implementation"
namespace: "multitasking"
description: "Enables the new implementation of PiP (PiP2)"
@@ -57,3 +50,10 @@
description: "Enables new animations for expand and collapse for bubbles"
bug: "311450609"
}
+
+flag {
+ name: "enable_pip_umo_experience"
+ namespace: "multitasking"
+ description: "Enables new UMO experience for PiP menu"
+ bug: "307998712"
+}
diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleTaskViewTest.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleTaskViewTest.kt
index 4c76168..398fd55 100644
--- a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleTaskViewTest.kt
+++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleTaskViewTest.kt
@@ -22,7 +22,6 @@
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.wm.shell.taskview.TaskView
-import com.android.wm.shell.taskview.TaskViewTaskController
import com.google.common.truth.Truth.assertThat
import com.google.common.util.concurrent.MoreExecutors.directExecutor
@@ -30,6 +29,8 @@
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.mock
+import org.mockito.kotlin.never
+import org.mockito.kotlin.verify
@SmallTest
@RunWith(AndroidJUnit4::class)
@@ -37,10 +38,11 @@
private lateinit var bubbleTaskView: BubbleTaskView
private val context = ApplicationProvider.getApplicationContext<Context>()
+ private lateinit var taskView: TaskView
@Before
fun setUp() {
- val taskView = TaskView(context, mock<TaskViewTaskController>())
+ taskView = mock()
bubbleTaskView = BubbleTaskView(taskView, directExecutor())
}
@@ -72,4 +74,19 @@
assertThat(actualTaskId).isEqualTo(123)
assertThat(actualComponentName).isEqualTo(componentName)
}
+
+ @Test
+ fun cleanup_invalidTaskId_doesNotRemoveTask() {
+ bubbleTaskView.cleanup()
+ verify(taskView, never()).removeTask()
+ }
+
+ @Test
+ fun cleanup_validTaskId_removesTask() {
+ val componentName = ComponentName(context, "TestClass")
+ bubbleTaskView.listener.onTaskCreated(123, componentName)
+
+ bubbleTaskView.cleanup()
+ verify(taskView).removeTask()
+ }
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/util/CounterRotator.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/CounterRotator.java
similarity index 98%
rename from libs/WindowManager/Shell/src/com/android/wm/shell/util/CounterRotator.java
rename to libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/CounterRotator.java
index 7e95814..fd3a749 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/util/CounterRotator.java
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/CounterRotator.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.wm.shell.util;
+package com.android.wm.shell.shared;
import android.graphics.Point;
import android.util.RotationUtils;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/util/TransitionUtil.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/TransitionUtil.java
similarity index 98%
rename from libs/WindowManager/Shell/src/com/android/wm/shell/util/TransitionUtil.java
rename to libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/TransitionUtil.java
index 936faa3..dcd4062 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/util/TransitionUtil.java
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/TransitionUtil.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.wm.shell.util;
+package com.android.wm.shell.shared;
import static android.app.ActivityTaskManager.INVALID_TASK_ID;
import static android.view.RemoteAnimationTarget.MODE_CHANGING;
@@ -28,14 +28,13 @@
import static android.view.WindowManager.TRANSIT_OPEN;
import static android.view.WindowManager.TRANSIT_TO_BACK;
import static android.view.WindowManager.TRANSIT_TO_FRONT;
+import static android.window.TransitionInfo.FLAG_FIRST_CUSTOM;
import static android.window.TransitionInfo.FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY;
import static android.window.TransitionInfo.FLAG_IS_DISPLAY;
import static android.window.TransitionInfo.FLAG_IS_WALLPAPER;
import static android.window.TransitionInfo.FLAG_MOVED_TO_TOP;
import static android.window.TransitionInfo.FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT;
-import static com.android.wm.shell.common.split.SplitScreenConstants.FLAG_IS_DIVIDER_BAR;
-
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SuppressLint;
@@ -53,6 +52,8 @@
/** Various utility functions for transitions. */
public class TransitionUtil {
+ /** Flag applied to a transition change to identify it as a divider bar for animation. */
+ public static final int FLAG_IS_DIVIDER_BAR = FLAG_FIRST_CUSTOM;
/** @return true if the transition was triggered by opening something vs closing something */
public static boolean isOpeningType(@WindowManager.TransitionType int type) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationAdapter.java b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationAdapter.java
index 8241e1a..8d30db6 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationAdapter.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationAdapter.java
@@ -29,7 +29,7 @@
import androidx.annotation.NonNull;
-import com.android.wm.shell.util.TransitionUtil;
+import com.android.wm.shell.shared.TransitionUtil;
/**
* Wrapper to handle the ActivityEmbedding animation update in one
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunner.java
index 44ee561..539832e 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunner.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunner.java
@@ -46,7 +46,7 @@
import com.android.internal.annotations.VisibleForTesting;
import com.android.wm.shell.activityembedding.ActivityEmbeddingAnimationAdapter.SnapshotAdapter;
import com.android.wm.shell.common.ScreenshotUtils;
-import com.android.wm.shell.util.TransitionUtil;
+import com.android.wm.shell.shared.TransitionUtil;
import java.util.ArrayList;
import java.util.List;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationSpec.java b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationSpec.java
index efa5a1a..0272f1c 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationSpec.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationSpec.java
@@ -38,7 +38,7 @@
import android.window.TransitionInfo;
import com.android.internal.policy.TransitionAnimation;
-import com.android.wm.shell.util.TransitionUtil;
+import com.android.wm.shell.shared.TransitionUtil;
/** Animation spec for ActivityEmbedding transition. */
// TODO(b/206557124): provide an easier way to customize animation
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingController.java
index b4e852c..1f9358e 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingController.java
@@ -38,9 +38,9 @@
import androidx.annotation.Nullable;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.wm.shell.shared.TransitionUtil;
import com.android.wm.shell.sysui.ShellInit;
import com.android.wm.shell.transition.Transitions;
-import com.android.wm.shell.util.TransitionUtil;
import java.util.List;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java
index 87c8f52..f32f030 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java
@@ -51,6 +51,8 @@
import com.android.wm.shell.bubbles.bar.BubbleBarExpandedView;
import com.android.wm.shell.bubbles.bar.BubbleBarLayerView;
import com.android.wm.shell.common.bubbles.BubbleInfo;
+import com.android.wm.shell.taskview.TaskView;
+import com.android.wm.shell.taskview.TaskViewTaskController;
import java.io.PrintWriter;
import java.util.List;
@@ -105,6 +107,8 @@
private BubbleExpandedView mExpandedView;
@Nullable
private BubbleBarExpandedView mBubbleBarExpandedView;
+ @Nullable
+ private BubbleTaskView mBubbleTaskView;
private BubbleViewInfoTask mInflationTask;
private boolean mInflateSynchronously;
@@ -394,6 +398,21 @@
}
/**
+ * Returns the existing {@link #mBubbleTaskView} if it's not {@code null}. Otherwise a new
+ * instance of {@link BubbleTaskView} is created.
+ */
+ public BubbleTaskView getOrCreateBubbleTaskView(Context context, BubbleController controller) {
+ if (mBubbleTaskView == null) {
+ TaskViewTaskController taskViewTaskController = new TaskViewTaskController(context,
+ controller.getTaskOrganizer(),
+ controller.getTaskViewTransitions(), controller.getSyncTransactionQueue());
+ TaskView taskView = new TaskView(context, taskViewTaskController);
+ mBubbleTaskView = new BubbleTaskView(taskView, controller.getMainExecutor());
+ }
+ return mBubbleTaskView;
+ }
+
+ /**
* @return the ShortcutInfo id if it exists, or the metadata shortcut id otherwise.
*/
String getShortcutId() {
@@ -415,6 +434,10 @@
* the bubble.
*/
void cleanupExpandedView() {
+ cleanupExpandedView(true);
+ }
+
+ private void cleanupExpandedView(boolean cleanupTaskView) {
if (mExpandedView != null) {
mExpandedView.cleanUpExpandedState();
mExpandedView = null;
@@ -423,17 +446,37 @@
mBubbleBarExpandedView.cleanUpExpandedState();
mBubbleBarExpandedView = null;
}
+ if (cleanupTaskView) {
+ cleanupTaskView();
+ }
if (mIntent != null) {
mIntent.unregisterCancelListener(mIntentCancelListener);
}
mIntentActive = false;
}
+ private void cleanupTaskView() {
+ if (mBubbleTaskView != null) {
+ mBubbleTaskView.cleanup();
+ mBubbleTaskView = null;
+ }
+ }
+
/**
* Call when all the views should be removed/cleaned up.
*/
void cleanupViews() {
- cleanupExpandedView();
+ cleanupViews(true);
+ }
+
+ /**
+ * Call when all the views should be removed/cleaned up.
+ *
+ * <p>If we're switching between bar and floating modes, pass {@code false} on
+ * {@code cleanupTaskView} to avoid recreating it in the new mode.
+ */
+ void cleanupViews(boolean cleanupTaskView) {
+ cleanupExpandedView(cleanupTaskView);
mIconView = null;
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
index a5f7880..1a6bf28 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
@@ -173,7 +173,7 @@
private final Context mContext;
private final BubblesImpl mImpl = new BubblesImpl();
private Bubbles.BubbleExpandListener mExpandListener;
- @Nullable private BubbleStackView.SurfaceSynchronizer mSurfaceSynchronizer;
+ @Nullable private final BubbleStackView.SurfaceSynchronizer mSurfaceSynchronizer;
private final FloatingContentCoordinator mFloatingContentCoordinator;
private final BubbleDataRepository mDataRepository;
private final WindowManagerShellWrapper mWindowManagerShellWrapper;
@@ -197,12 +197,12 @@
private final Handler mMainHandler;
private final ShellExecutor mBackgroundExecutor;
- private BubbleLogger mLogger;
- private BubbleData mBubbleData;
+ private final BubbleLogger mLogger;
+ private final BubbleData mBubbleData;
@Nullable private BubbleStackView mStackView;
@Nullable private BubbleBarLayerView mLayerView;
private BubbleIconFactory mBubbleIconFactory;
- private BubblePositioner mBubblePositioner;
+ private final BubblePositioner mBubblePositioner;
private Bubbles.SysuiProxy mSysuiProxy;
// Tracks the id of the current (foreground) user.
@@ -232,13 +232,17 @@
/** Whether or not the BubbleStackView has been added to the WindowManager. */
private boolean mAddedToWindowManager = false;
- /** Saved screen density, used to detect display size changes in {@link #onConfigChanged}. */
+ /**
+ * Saved screen density, used to detect display size changes in {@link #onConfigurationChanged}.
+ */
private int mDensityDpi = Configuration.DENSITY_DPI_UNDEFINED;
- /** Saved screen bounds, used to detect screen size changes in {@link #onConfigChanged}. **/
- private Rect mScreenBounds = new Rect();
+ /**
+ * Saved screen bounds, used to detect screen size changes in {@link #onConfigurationChanged}.
+ */
+ private final Rect mScreenBounds = new Rect();
- /** Saved font scale, used to detect font size changes in {@link #onConfigChanged}. */
+ /** Saved font scale, used to detect font size changes in {@link #onConfigurationChanged}. */
private float mFontScale = 0;
/** Saved direction, used to detect layout direction changes @link #onConfigChanged}. */
@@ -253,9 +257,9 @@
private boolean mIsStatusBarShade = true;
/** One handed mode controller to register transition listener. */
- private Optional<OneHandedController> mOneHandedOptional;
+ private final Optional<OneHandedController> mOneHandedOptional;
/** Drag and drop controller to register listener for onDragStarted. */
- private Optional<DragAndDropController> mDragAndDropController;
+ private final Optional<DragAndDropController> mDragAndDropController;
/** Used to send bubble events to launcher. */
private Bubbles.BubbleStateListener mBubbleStateListener;
@@ -731,9 +735,11 @@
}
} else {
if (mStackView == null) {
+ BubbleStackViewManager bubbleStackViewManager =
+ BubbleStackViewManager.fromBubbleController(this);
mStackView = new BubbleStackView(
- mContext, this, mBubbleData, mSurfaceSynchronizer,
- mFloatingContentCoordinator, this, mMainExecutor);
+ mContext, bubbleStackViewManager, mBubblePositioner, mBubbleData,
+ mSurfaceSynchronizer, mFloatingContentCoordinator, this, mMainExecutor);
mStackView.onOrientationChanged();
if (mExpandListener != null) {
mStackView.setExpandListener(mExpandListener);
@@ -893,7 +899,6 @@
* Called by the BubbleStackView and whenever all bubbles have animated out, and none have been
* added in the meantime.
*/
- @VisibleForTesting
public void onAllBubblesAnimatedOut() {
if (mStackView != null) {
mStackView.setVisibility(INVISIBLE);
@@ -1047,7 +1052,6 @@
return mBubbleData.hasBubbles() || mBubbleData.isShowingOverflow();
}
- @VisibleForTesting
public boolean isStackExpanded() {
return mBubbleData.isExpanded();
}
@@ -1105,9 +1109,8 @@
* <p>This is used by external callers (launcher).
*/
@VisibleForTesting
- public void expandStackAndSelectBubbleFromLauncher(String key, int bubbleBarOffsetX,
- int bubbleBarOffsetY) {
- mBubblePositioner.setBubbleBarPosition(bubbleBarOffsetX, bubbleBarOffsetY);
+ public void expandStackAndSelectBubbleFromLauncher(String key, Rect bubbleBarBounds) {
+ mBubblePositioner.setBubbleBarPosition(bubbleBarBounds);
if (BubbleOverflow.KEY.equals(key)) {
mBubbleData.setSelectedBubbleFromLauncher(mBubbleData.getOverflow());
@@ -1366,8 +1369,9 @@
mStackView.resetOverflowView();
mStackView.removeAllViews();
}
- // cleanup existing bubble views so they can be recreated later if needed.
- mBubbleData.getBubbles().forEach(Bubble::cleanupViews);
+ // cleanup existing bubble views so they can be recreated later if needed, but retain
+ // TaskView.
+ mBubbleData.getBubbles().forEach(b -> b.cleanupViews(/* cleanupTaskView= */ false));
// remove the current bubble container from window manager, null it out, and create a new
// container based on the current mode.
@@ -1478,7 +1482,6 @@
* <p>
* Must be called from the main thread.
*/
- @VisibleForTesting
@MainThread
public void removeBubble(String key, int reason) {
if (mBubbleData.hasAnyBubbleWithKey(key)) {
@@ -1486,36 +1489,6 @@
}
}
- // TODO(b/316358859): remove this method after task views are shared across modes
- /**
- * Removes the bubble with the given key after task removal, unless the task was removed as
- * a result of mode switching, in which case, the bubble isn't removed because it will be
- * re-inflated for the new mode.
- */
- @MainThread
- public void removeFloatingBubbleAfterTaskRemoval(String key, int reason) {
- // if we're floating remove the bubble. otherwise, we're here because the task was removed
- // after switching modes. See b/316358859
- if (!isShowingAsBubbleBar()) {
- removeBubble(key, reason);
- }
- }
-
- // TODO(b/316358859): remove this method after task views are shared across modes
- /**
- * Removes the bubble with the given key after task removal, unless the task was removed as
- * a result of mode switching, in which case, the bubble isn't removed because it will be
- * re-inflated for the new mode.
- */
- @MainThread
- public void removeBarBubbleAfterTaskRemoval(String key, int reason) {
- // if we're showing as bubble bar remove the bubble. otherwise, we're here because the task
- // was removed after switching modes. See b/316358859
- if (isShowingAsBubbleBar()) {
- removeBubble(key, reason);
- }
- }
-
/**
* Removes all the bubbles.
* <p>
@@ -2198,10 +2171,10 @@
}
@Override
- public void showBubble(String key, int bubbleBarOffsetX, int bubbleBarOffsetY) {
+ public void showBubble(String key, Rect bubbleBarBounds) {
mMainExecutor.execute(
() -> mController.expandStackAndSelectBubbleFromLauncher(
- key, bubbleBarOffsetX, bubbleBarOffsetY));
+ key, bubbleBarBounds));
}
@Override
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java
index 9f7d0ac..efc4d8b 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java
@@ -27,12 +27,10 @@
import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_BUBBLES;
import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME;
import static com.android.wm.shell.bubbles.BubblePositioner.MAX_HEIGHT;
-import static com.android.wm.shell.transition.Transitions.ENABLE_SHELL_TRANSITIONS;
import android.annotation.NonNull;
import android.annotation.SuppressLint;
import android.app.ActivityOptions;
-import android.app.ActivityTaskManager;
import android.app.PendingIntent;
import android.content.ComponentName;
import android.content.Context;
@@ -49,7 +47,6 @@
import android.graphics.PorterDuff;
import android.graphics.Rect;
import android.graphics.drawable.ShapeDrawable;
-import android.os.RemoteException;
import android.util.AttributeSet;
import android.util.FloatProperty;
import android.util.IntProperty;
@@ -311,8 +308,7 @@
+ " bubble=" + getBubbleKey());
}
if (mBubble != null) {
- mController.removeFloatingBubbleAfterTaskRemoval(
- mBubble.getKey(), Bubbles.DISMISS_TASK_FINISHED);
+ mController.removeBubble(mBubble.getKey(), Bubbles.DISMISS_TASK_FINISHED);
}
if (mTaskView != null) {
// Release the surface
@@ -1105,32 +1101,11 @@
return ((LinearLayout.LayoutParams) mManageButton.getLayoutParams()).getMarginStart();
}
- /**
- * Cleans up anything related to the task. The TaskView itself is released after the task
- * has been removed.
- *
- * If this view should be reused after this method is called, then
- * {@link #initialize(BubbleController, BubbleStackView, boolean, BubbleTaskView)}
- * must be invoked first.
- */
+ /** Hide the task view. */
public void cleanUpExpandedState() {
if (DEBUG_BUBBLE_EXPANDED_VIEW) {
Log.d(TAG, "cleanUpExpandedState: bubble=" + getBubbleKey() + " task=" + mTaskId);
}
- if (getTaskId() != INVALID_TASK_ID) {
- // Ensure the task is removed from WM
- if (ENABLE_SHELL_TRANSITIONS) {
- if (mTaskView != null) {
- mTaskView.removeTask();
- }
- } else {
- try {
- ActivityTaskManager.getService().removeTask(getTaskId());
- } catch (RemoteException e) {
- Log.w(TAG, e.getMessage());
- }
- }
- }
if (mTaskView != null) {
mTaskView.setVisibility(GONE);
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java
index a76bd26..d62c86c 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java
@@ -19,7 +19,6 @@
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Insets;
-import android.graphics.Point;
import android.graphics.PointF;
import android.graphics.Rect;
import android.graphics.RectF;
@@ -97,7 +96,7 @@
private PointF mRestingStackPosition;
private boolean mShowingInBubbleBar;
- private final Point mBubbleBarPosition = new Point();
+ private final Rect mBubbleBarBounds = new Rect();
public BubblePositioner(Context context, WindowManager windowManager) {
mContext = context;
@@ -791,15 +790,10 @@
}
/**
- * Sets the position of the bubble bar in screen coordinates.
- *
- * @param offsetX the offset of the bubble bar from the edge of the screen on the X axis
- * @param offsetY the offset of the bubble bar from the edge of the screen on the Y axis
+ * Sets the position of the bubble bar in display coordinates.
*/
- public void setBubbleBarPosition(int offsetX, int offsetY) {
- mBubbleBarPosition.set(
- getAvailableRect().width() - offsetX,
- getAvailableRect().height() + mInsets.top + mInsets.bottom - offsetY);
+ public void setBubbleBarPosition(Rect bubbleBarBounds) {
+ mBubbleBarBounds.set(bubbleBarBounds);
}
/**
@@ -820,7 +814,7 @@
/** The bottom position of the expanded view when showing above the bubble bar. */
public int getExpandedViewBottomForBubbleBar() {
- return mBubbleBarPosition.y - mExpandedViewPadding;
+ return mBubbleBarBounds.top - mExpandedViewPadding;
}
/**
@@ -831,9 +825,9 @@
}
/**
- * Returns the on screen co-ordinates of the bubble bar.
+ * Returns the display coordinates of the bubble bar.
*/
- public Point getBubbleBarPosition() {
- return mBubbleBarPosition;
+ public Rect getBubbleBarBounds() {
+ return mBubbleBarBounds;
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java
index a619401..9facef3 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java
@@ -204,7 +204,7 @@
Choreographer.getInstance().postFrameCallback(frameCallback);
}
};
- private final BubbleController mBubbleController;
+ private final BubbleStackViewManager mManager;
private final BubbleData mBubbleData;
private final Bubbles.SysuiProxy.Provider mSysuiProxyProvider;
private StackViewState mStackViewState = new StackViewState();
@@ -858,6 +858,7 @@
private BubbleOverflow mBubbleOverflow;
private StackEducationView mStackEduView;
+ private StackEducationView.Manager mStackEducationViewManager;
private ManageEducationView mManageEduView;
private DismissView mDismissView;
@@ -873,15 +874,16 @@
private BubblePositioner mPositioner;
@SuppressLint("ClickableViewAccessibility")
- public BubbleStackView(Context context, BubbleController bubbleController,
- BubbleData data, @Nullable SurfaceSynchronizer synchronizer,
+ public BubbleStackView(Context context, BubbleStackViewManager bubbleStackViewManager,
+ BubblePositioner bubblePositioner, BubbleData data,
+ @Nullable SurfaceSynchronizer synchronizer,
FloatingContentCoordinator floatingContentCoordinator,
Bubbles.SysuiProxy.Provider sysuiProxyProvider,
ShellExecutor mainExecutor) {
super(context);
mMainExecutor = mainExecutor;
- mBubbleController = bubbleController;
+ mManager = bubbleStackViewManager;
mBubbleData = data;
mSysuiProxyProvider = sysuiProxyProvider;
@@ -893,7 +895,7 @@
mExpandedViewPadding = res.getDimensionPixelSize(R.dimen.bubble_expanded_view_padding);
int elevation = res.getDimensionPixelSize(R.dimen.bubble_elevation);
- mPositioner = mBubbleController.getPositioner();
+ mPositioner = bubblePositioner;
final TypedArray ta = mContext.obtainStyledAttributes(
new int[]{android.R.attr.dialogCornerRadius});
@@ -903,7 +905,7 @@
final Runnable onBubbleAnimatedOut = () -> {
if (getBubbleCount() == 0) {
mExpandedViewTemporarilyHidden = false;
- mBubbleController.onAllBubblesAnimatedOut();
+ mManager.onAllBubblesAnimatedOut();
}
};
mStackAnimationController = new StackAnimationController(
@@ -1383,7 +1385,9 @@
return false;
}
if (mStackEduView == null) {
- mStackEduView = new StackEducationView(mContext, mPositioner, mBubbleController);
+ mStackEducationViewManager = mManager::updateWindowFlagsForBackpress;
+ mStackEduView =
+ new StackEducationView(mContext, mPositioner, mStackEducationViewManager);
addView(mStackEduView);
}
return showStackEdu();
@@ -1412,7 +1416,9 @@
private void updateUserEdu() {
if (isStackEduVisible() && !mStackEduView.isHiding()) {
removeView(mStackEduView);
- mStackEduView = new StackEducationView(mContext, mPositioner, mBubbleController);
+ mStackEducationViewManager = mManager::updateWindowFlagsForBackpress;
+ mStackEduView =
+ new StackEducationView(mContext, mPositioner, mStackEducationViewManager);
addView(mStackEduView);
showStackEdu();
}
@@ -2106,7 +2112,7 @@
logBubbleEvent(mExpandedBubble, FrameworkStatsLog.BUBBLE_UICHANGED__ACTION__EXPANDED);
logBubbleEvent(mExpandedBubble,
FrameworkStatsLog.BUBBLE_UICHANGED__ACTION__STACK_EXPANDED);
- mBubbleController.isNotificationPanelExpanded(notifPanelExpanded -> {
+ mManager.checkNotificationPanelExpandedState(notifPanelExpanded -> {
if (!notifPanelExpanded && mIsExpanded) {
startMonitoringSwipeUpGesture();
}
@@ -2227,7 +2233,7 @@
*/
void hideCurrentInputMethod() {
mPositioner.setImeVisible(false, 0);
- mBubbleController.hideCurrentInputMethod();
+ mManager.hideCurrentInputMethod();
}
/** Set the stack position to whatever the positioner says. */
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackViewManager.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackViewManager.kt
new file mode 100644
index 0000000..fb597a0
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackViewManager.kt
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.bubbles
+
+import java.util.function.Consumer
+
+/** Defines callbacks from [BubbleStackView] to its manager. */
+interface BubbleStackViewManager {
+
+ /** Notifies that all bubbles animated out. */
+ fun onAllBubblesAnimatedOut()
+
+ /** Notifies whether backpress should be intercepted. */
+ fun updateWindowFlagsForBackpress(interceptBack: Boolean)
+
+ /**
+ * Checks the current expansion state of the notification panel, and invokes [callback] with the
+ * result.
+ */
+ fun checkNotificationPanelExpandedState(callback: Consumer<Boolean>)
+
+ /** Requests to hide the current input method. */
+ fun hideCurrentInputMethod()
+
+ companion object {
+
+ @JvmStatic
+ fun fromBubbleController(controller: BubbleController) = object : BubbleStackViewManager {
+ override fun onAllBubblesAnimatedOut() {
+ controller.onAllBubblesAnimatedOut()
+ }
+
+ override fun updateWindowFlagsForBackpress(interceptBack: Boolean) {
+ controller.updateWindowFlagsForBackpress(interceptBack)
+ }
+
+ override fun checkNotificationPanelExpandedState(callback: Consumer<Boolean>) {
+ controller.isNotificationPanelExpanded(callback)
+ }
+
+ override fun hideCurrentInputMethod() {
+ controller.hideCurrentInputMethod()
+ }
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskView.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskView.kt
index 2fcd133..65f8e48 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskView.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskView.kt
@@ -16,10 +16,14 @@
package com.android.wm.shell.bubbles
+import android.app.ActivityTaskManager
import android.app.ActivityTaskManager.INVALID_TASK_ID
import android.content.ComponentName
+import android.os.RemoteException
+import android.util.Log
import androidx.annotation.VisibleForTesting
import com.android.wm.shell.taskview.TaskView
+import com.android.wm.shell.transition.Transitions.ENABLE_SHELL_TRANSITIONS
import java.util.concurrent.Executor
/**
@@ -78,4 +82,28 @@
init {
taskView.setListener(executor, listener)
}
+
+ /**
+ * Removes the [TaskView] from window manager.
+ *
+ * This should be called after all other cleanup animations have finished.
+ */
+ fun cleanup() {
+ if (taskId != INVALID_TASK_ID) {
+ // Ensure the task is removed from WM
+ if (ENABLE_SHELL_TRANSITIONS) {
+ taskView.removeTask()
+ } else {
+ try {
+ ActivityTaskManager.getService().removeTask(taskId)
+ } catch (e: RemoteException) {
+ Log.w(TAG, e.message ?: "")
+ }
+ }
+ }
+ }
+
+ private companion object {
+ const val TAG = "BubbleTaskView"
+ }
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java
index 5855a81..5fc67d7 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java
@@ -29,7 +29,6 @@
import android.content.Context;
import android.content.Intent;
import android.graphics.Rect;
-import android.os.RemoteException;
import android.util.Log;
import android.view.View;
@@ -183,8 +182,11 @@
+ " bubble=" + getBubbleKey());
}
if (mBubble != null) {
- mController.removeBarBubbleAfterTaskRemoval(
- mBubble.getKey(), Bubbles.DISMISS_TASK_FINISHED);
+ mController.removeBubble(mBubble.getKey(), Bubbles.DISMISS_TASK_FINISHED);
+ }
+ if (mTaskView != null) {
+ mTaskView.release();
+ mTaskView = null;
}
}
@@ -228,24 +230,6 @@
return false;
}
- /** Cleans up anything related to the task and {@code TaskView}. */
- public void cleanUpTaskView() {
- if (DEBUG_BUBBLE_EXPANDED_VIEW) {
- Log.d(TAG, "cleanUpExpandedState: bubble=" + getBubbleKey() + " task=" + mTaskId);
- }
- if (mTaskId != INVALID_TASK_ID) {
- try {
- ActivityTaskManager.getService().removeTask(mTaskId);
- } catch (RemoteException e) {
- Log.w(TAG, e.getMessage());
- }
- }
- if (mTaskView != null) {
- mTaskView.release();
- mTaskView = null;
- }
- }
-
/** Returns the bubble key associated with this view. */
@Nullable
public String getBubbleKey() {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleViewInfoTask.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleViewInfoTask.java
index c3d899e..5fc10a9 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleViewInfoTask.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleViewInfoTask.java
@@ -46,8 +46,6 @@
import com.android.wm.shell.R;
import com.android.wm.shell.bubbles.bar.BubbleBarExpandedView;
import com.android.wm.shell.bubbles.bar.BubbleBarLayerView;
-import com.android.wm.shell.taskview.TaskView;
-import com.android.wm.shell.taskview.TaskViewTaskController;
import java.lang.ref.WeakReference;
import java.util.Objects;
@@ -175,7 +173,7 @@
BubbleViewInfo info = new BubbleViewInfo();
if (!skipInflation && !b.isInflated()) {
- BubbleTaskView bubbleTaskView = createBubbleTaskView(c, controller);
+ BubbleTaskView bubbleTaskView = b.getOrCreateBubbleTaskView(c, controller);
LayoutInflater inflater = LayoutInflater.from(c);
info.bubbleBarExpandedView = (BubbleBarExpandedView) inflater.inflate(
R.layout.bubble_bar_expanded_view, layerView, false /* attachToRoot */);
@@ -205,7 +203,7 @@
R.layout.bubble_view, stackView, false /* attachToRoot */);
info.imageView.initialize(controller.getPositioner());
- BubbleTaskView bubbleTaskView = createBubbleTaskView(c, controller);
+ BubbleTaskView bubbleTaskView = b.getOrCreateBubbleTaskView(c, controller);
info.expandedView = (BubbleExpandedView) inflater.inflate(
R.layout.bubble_expanded_view, stackView, false /* attachToRoot */);
info.expandedView.initialize(
@@ -225,15 +223,6 @@
}
return info;
}
-
- private static BubbleTaskView createBubbleTaskView(
- Context context, BubbleController controller) {
- TaskViewTaskController taskViewTaskController = new TaskViewTaskController(context,
- controller.getTaskOrganizer(),
- controller.getTaskViewTransitions(), controller.getSyncTransactionQueue());
- TaskView taskView = new TaskView(context, taskViewTaskController);
- return new BubbleTaskView(taskView, controller.getMainExecutor());
- }
}
/**
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblesTransitionObserver.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblesTransitionObserver.java
index 9e8a385..c1f704a 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblesTransitionObserver.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblesTransitionObserver.java
@@ -24,8 +24,8 @@
import androidx.annotation.NonNull;
+import com.android.wm.shell.shared.TransitionUtil;
import com.android.wm.shell.transition.Transitions;
-import com.android.wm.shell.util.TransitionUtil;
/**
* Observer used to identify tasks that are opening or moving to front. If a bubble activity is
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubbles.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubbles.aidl
index 5776ad1..7a5afec 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubbles.aidl
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubbles.aidl
@@ -17,6 +17,7 @@
package com.android.wm.shell.bubbles;
import android.content.Intent;
+import android.graphics.Rect;
import com.android.wm.shell.bubbles.IBubblesListener;
/**
@@ -29,7 +30,7 @@
oneway void unregisterBubbleListener(in IBubblesListener listener) = 2;
- oneway void showBubble(in String key, in int bubbleBarOffsetX, in int bubbleBarOffsetY) = 3;
+ oneway void showBubble(in String key, in Rect bubbleBarBounds) = 3;
oneway void removeBubble(in String key) = 4;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/StackEducationView.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/StackEducationView.kt
index 95f1017..c4108c4 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/StackEducationView.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/StackEducationView.kt
@@ -35,7 +35,7 @@
class StackEducationView(
context: Context,
private val positioner: BubblePositioner,
- private val controller: BubbleController
+ private val manager: Manager
) : LinearLayout(context) {
companion object {
@@ -44,6 +44,12 @@
private const val ANIMATE_DURATION_SHORT: Long = 40
}
+ /** Callbacks to notify managers of [StackEducationView] about events. */
+ interface Manager {
+ /** Notifies whether backpress should be intercepted. */
+ fun updateWindowFlagsForBackpress(interceptBack: Boolean)
+ }
+
private val view by lazy { requireViewById<View>(R.id.stack_education_layout) }
private val titleTextView by lazy { requireViewById<TextView>(R.id.stack_education_title) }
private val descTextView by lazy { requireViewById<TextView>(R.id.stack_education_description) }
@@ -93,7 +99,7 @@
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
setOnKeyListener(null)
- controller.updateWindowFlagsForBackpress(false /* interceptBack */)
+ manager.updateWindowFlagsForBackpress(false /* interceptBack */)
}
private fun setTextColor() {
@@ -124,7 +130,7 @@
isHiding = false
if (visibility == VISIBLE) return false
- controller.updateWindowFlagsForBackpress(true /* interceptBack */)
+ manager.updateWindowFlagsForBackpress(true /* interceptBack */)
layoutParams.width =
if (positioner.isLargeScreen || positioner.isLandscape)
context.resources.getDimensionPixelSize(R.dimen.bubbles_user_education_width)
@@ -185,7 +191,7 @@
if (visibility != VISIBLE || isHiding) return
isHiding = true
- controller.updateWindowFlagsForBackpress(false /* interceptBack */)
+ manager.updateWindowFlagsForBackpress(false /* interceptBack */)
animate()
.alpha(0f)
.setDuration(if (isExpanding) ANIMATE_DURATION_SHORT else ANIMATE_DURATION)
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarAnimationHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarAnimationHelper.java
index 893a87f..84a616f 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarAnimationHelper.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarAnimationHelper.java
@@ -22,6 +22,7 @@
import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Point;
+import android.graphics.Rect;
import android.util.Log;
import android.util.Size;
import android.view.View;
@@ -149,12 +150,12 @@
bbev.setVisibility(VISIBLE);
// Set the pivot point for the scale, so the view animates out from the bubble bar.
- Point bubbleBarPosition = mPositioner.getBubbleBarPosition();
+ Rect bubbleBarBounds = mPositioner.getBubbleBarBounds();
mExpandedViewContainerMatrix.setScale(
1f - EXPANDED_VIEW_ANIMATE_SCALE_AMOUNT,
1f - EXPANDED_VIEW_ANIMATE_SCALE_AMOUNT,
- bubbleBarPosition.x,
- bubbleBarPosition.y);
+ bubbleBarBounds.centerX(),
+ bubbleBarBounds.top);
bbev.setAnimationMatrix(mExpandedViewContainerMatrix);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java
index 3cf23ac..00d683e 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java
@@ -272,7 +272,6 @@
if (mTaskView != null) {
removeView(mTaskView);
}
- mBubbleTaskViewHelper.cleanUpTaskView();
}
mMenuViewController.hideMenu(false /* animated */);
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipMenuController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipMenuController.java
similarity index 88%
rename from libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipMenuController.java
rename to libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipMenuController.java
index 0775f52..2f1189a 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipMenuController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipMenuController.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2020 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,7 +14,7 @@
* limitations under the License.
*/
-package com.android.wm.shell.pip;
+package com.android.wm.shell.common.pip;
import static android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
import static android.view.WindowManager.LayoutParams.FLAG_SLIPPERY;
@@ -33,12 +33,13 @@
import android.view.WindowManager;
import com.android.wm.shell.R;
+import com.android.wm.shell.ShellTaskOrganizer;
import java.util.List;
/**
- * Interface to allow {@link com.android.wm.shell.pip.PipTaskOrganizer} to call into
- * PiP menu when certain events happen (task appear/vanish, PiP move, etc.)
+ * Interface to interact with PiP menu when certain events happen
+ * (task appear/vanish, PiP move, etc.).
*/
public interface PipMenuController {
@@ -52,15 +53,15 @@
float ALPHA_NO_CHANGE = -1f;
/**
- * Called when
- * {@link PipTaskOrganizer#onTaskAppeared(RunningTaskInfo, SurfaceControl)}
+ * Called when out implementation of
+ * {@link ShellTaskOrganizer.TaskListener#onTaskAppeared(RunningTaskInfo, SurfaceControl)}
* is called.
*/
void attach(SurfaceControl leash);
/**
- * Called when
- * {@link PipTaskOrganizer#onTaskVanished(RunningTaskInfo)} is called.
+ * Called when our implementation of
+ * {@link ShellTaskOrganizer.TaskListener#onTaskVanished(RunningTaskInfo)} is called.
*/
void detach();
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitScreenConstants.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitScreenConstants.java
index 49db8d9..e8c809e 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitScreenConstants.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitScreenConstants.java
@@ -20,10 +20,11 @@
import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
-import static android.window.TransitionInfo.FLAG_FIRST_CUSTOM;
import android.annotation.IntDef;
+import com.android.wm.shell.shared.TransitionUtil;
+
/** Helper utility class of methods and constants that are available to be imported in Launcher. */
public class SplitScreenConstants {
/** Duration used for every split fade-in or fade-out. */
@@ -126,7 +127,7 @@
WINDOWING_MODE_FREEFORM};
/** Flag applied to a transition change to identify it as a divider bar for animation. */
- public static final int FLAG_IS_DIVIDER_BAR = FLAG_FIRST_CUSTOM;
+ public static final int FLAG_IS_DIVIDER_BAR = TransitionUtil.FLAG_IS_DIVIDER_BAR;
public static final String splitPositionToString(@SplitPosition int pos) {
switch (pos) {
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 7b98fa6..8eecf1c 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
@@ -18,18 +18,23 @@
import android.annotation.NonNull;
import android.content.Context;
+import android.os.Handler;
import com.android.wm.shell.ShellTaskOrganizer;
import com.android.wm.shell.common.DisplayController;
import com.android.wm.shell.common.DisplayInsetsController;
import com.android.wm.shell.common.ShellExecutor;
+import com.android.wm.shell.common.SystemWindows;
import com.android.wm.shell.common.annotations.ShellMainThread;
import com.android.wm.shell.common.pip.PipBoundsAlgorithm;
import com.android.wm.shell.common.pip.PipBoundsState;
import com.android.wm.shell.common.pip.PipDisplayLayoutState;
+import com.android.wm.shell.common.pip.PipMediaController;
+import com.android.wm.shell.common.pip.PipUiEventLogger;
import com.android.wm.shell.common.pip.PipUtils;
import com.android.wm.shell.dagger.WMShellBaseModule;
import com.android.wm.shell.dagger.WMSingleton;
+import com.android.wm.shell.pip2.phone.PhonePipMenuController;
import com.android.wm.shell.pip2.phone.PipController;
import com.android.wm.shell.pip2.phone.PipScheduler;
import com.android.wm.shell.pip2.phone.PipTransition;
@@ -86,4 +91,16 @@
@ShellMainThread ShellExecutor mainExecutor) {
return new PipScheduler(context, pipBoundsState, mainExecutor);
}
+
+ @WMSingleton
+ @Provides
+ static PhonePipMenuController providePipPhoneMenuController(Context context,
+ PipBoundsState pipBoundsState, PipMediaController pipMediaController,
+ SystemWindows systemWindows,
+ PipUiEventLogger pipUiEventLogger,
+ @ShellMainThread ShellExecutor mainExecutor,
+ @ShellMainThread Handler mainHandler) {
+ return new PhonePipMenuController(context, pipBoundsState, pipMediaController,
+ systemWindows, pipUiEventLogger, mainExecutor, mainHandler);
+ }
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt
index c0fc02fa..f82212d 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt
@@ -86,10 +86,10 @@
) {
visibleTasksListeners[visibleTasksListener] = executor
displayData.keyIterator().forEach { displayId ->
- val visibleTasks = getVisibleTaskCount(displayId)
+ val visibleTasksCount = getVisibleTaskCount(displayId)
val stashed = isStashed(displayId)
executor.execute {
- visibleTasksListener.onVisibilityChanged(displayId, visibleTasks > 0)
+ visibleTasksListener.onTasksVisibilityChanged(displayId, visibleTasksCount)
visibleTasksListener.onStashedChanged(displayId, stashed)
}
}
@@ -222,10 +222,8 @@
val otherDisplays = displayData.keyIterator().asSequence().filter { it != displayId }
for (otherDisplayId in otherDisplays) {
if (displayData[otherDisplayId].visibleTasks.remove(taskId)) {
- // Task removed from other display, check if we should notify listeners
- if (displayData[otherDisplayId].visibleTasks.isEmpty()) {
- notifyVisibleTaskListeners(otherDisplayId, hasVisibleFreeformTasks = false)
- }
+ notifyVisibleTaskListeners(otherDisplayId,
+ displayData[otherDisplayId].visibleTasks.size)
}
}
}
@@ -248,15 +246,15 @@
)
}
- // Check if count changed and if there was no tasks or this is the first task
- if (prevCount != newCount && (prevCount == 0 || newCount == 0)) {
- notifyVisibleTaskListeners(displayId, newCount > 0)
+ // Check if count changed
+ if (prevCount != newCount) {
+ notifyVisibleTaskListeners(displayId, newCount)
}
}
- private fun notifyVisibleTaskListeners(displayId: Int, hasVisibleFreeformTasks: Boolean) {
+ private fun notifyVisibleTaskListeners(displayId: Int, visibleTasksCount: Int) {
visibleTasksListeners.forEach { (listener, executor) ->
- executor.execute { listener.onVisibilityChanged(displayId, hasVisibleFreeformTasks) }
+ executor.execute { listener.onTasksVisibilityChanged(displayId, visibleTasksCount) }
}
}
@@ -379,9 +377,9 @@
*/
interface VisibleTasksListener {
/**
- * Called when the desktop starts or stops showing freeform tasks.
+ * Called when the desktop changes the number of visible freeform tasks.
*/
- fun onVisibilityChanged(displayId: Int, hasVisibleFreeformTasks: Boolean) {}
+ fun onTasksVisibilityChanged(displayId: Int, visibleTasksCount: Int) {}
/**
* Called when the desktop stashed status changes.
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java
index da1ca8d..6250fc5 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java
@@ -17,6 +17,8 @@
package com.android.wm.shell.desktopmode;
import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
+import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
+import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION;
import static com.android.wm.shell.desktopmode.EnterDesktopTaskTransitionHandler.FINAL_FREEFORM_SCALE;
@@ -86,7 +88,6 @@
mTaskSurface = taskSurface;
mRootTdaOrganizer = taskDisplayAreaOrganizer;
mCurrentType = IndicatorType.NO_INDICATOR;
- createView();
}
/**
@@ -127,34 +128,15 @@
mView = new View(mContext);
final SurfaceControl.Builder builder = new SurfaceControl.Builder();
mRootTdaOrganizer.attachToDisplayArea(mTaskInfo.displayId, builder);
- String description;
- switch (mCurrentType) {
- case TO_DESKTOP_INDICATOR:
- description = "Desktop indicator";
- break;
- case TO_FULLSCREEN_INDICATOR:
- description = "Fullscreen indicator";
- break;
- case TO_SPLIT_LEFT_INDICATOR:
- description = "Split Left indicator";
- break;
- case TO_SPLIT_RIGHT_INDICATOR:
- description = "Split Right indicator";
- break;
- default:
- description = "Invalid indicator";
- break;
- }
mLeash = builder
- .setName(description)
+ .setName("Desktop Mode Visual Indicator")
.setContainerLayer()
.build();
t.show(mLeash);
final WindowManager.LayoutParams lp =
- new WindowManager.LayoutParams(screenWidth, screenHeight,
- WindowManager.LayoutParams.TYPE_APPLICATION,
- WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, PixelFormat.TRANSPARENT);
- lp.setTitle(description + " for Task=" + mTaskInfo.taskId);
+ new WindowManager.LayoutParams(screenWidth, screenHeight, TYPE_APPLICATION,
+ FLAG_NOT_FOCUSABLE, PixelFormat.TRANSPARENT);
+ lp.setTitle("Desktop Mode Visual Indicator");
lp.setTrustedOverlay();
final WindowlessWindowManager windowManager = new WindowlessWindowManager(
mTaskInfo.configuration, mLeash,
@@ -201,6 +183,9 @@
*/
private void transitionIndicator(IndicatorType newType) {
if (mCurrentType == newType) return;
+ if (mView == null) {
+ createView();
+ }
if (mCurrentType == IndicatorType.NO_INDICATOR) {
fadeInIndicator(newType);
} else if (newType == IndicatorType.NO_INDICATOR) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
index 4f5c39a..a089e81 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
@@ -106,8 +106,8 @@
visualIndicator = null
}
private val taskVisibilityListener = object : VisibleTasksListener {
- override fun onVisibilityChanged(displayId: Int, hasVisibleFreeformTasks: Boolean) {
- launchAdjacentController.launchAdjacentEnabled = !hasVisibleFreeformTasks
+ override fun onTasksVisibilityChanged(displayId: Int, visibleTasksCount: Int) {
+ launchAdjacentController.launchAdjacentEnabled = visibleTasksCount == 0
}
}
private val dragToDesktopStateListener = object : DragToDesktopStateListener {
@@ -919,22 +919,27 @@
}
if (taskBounds.top <= transitionAreaHeight) {
moveToFullscreenWithAnimation(taskInfo, position)
+ return
}
if (inputCoordinate.x <= transitionAreaWidth) {
releaseVisualIndicator()
- var wct = WindowContainerTransaction()
+ val wct = WindowContainerTransaction()
addMoveToSplitChanges(wct, taskInfo)
splitScreenController.requestEnterSplitSelect(taskInfo, wct,
SPLIT_POSITION_TOP_OR_LEFT, taskBounds)
+ return
}
if (inputCoordinate.x >= (displayController.getDisplayLayout(taskInfo.displayId)?.width()
?.minus(transitionAreaWidth) ?: return)) {
releaseVisualIndicator()
- var wct = WindowContainerTransaction()
+ val wct = WindowContainerTransaction()
addMoveToSplitChanges(wct, taskInfo)
splitScreenController.requestEnterSplitSelect(taskInfo, wct,
SPLIT_POSITION_BOTTOM_OR_RIGHT, taskBounds)
+ return
}
+ // A freeform drag-move ended, remove the indicator immediately.
+ releaseVisualIndicator()
}
/**
@@ -1028,14 +1033,16 @@
SingleInstanceRemoteListener<DesktopTasksController, IDesktopTaskListener>
private val listener: VisibleTasksListener = object : VisibleTasksListener {
- override fun onVisibilityChanged(displayId: Int, visible: Boolean) {
+ override fun onTasksVisibilityChanged(displayId: Int, visibleTasksCount: Int) {
KtProtoLog.v(
WM_SHELL_DESKTOP_MODE,
- "IDesktopModeImpl: onVisibilityChanged display=%d visible=%b",
+ "IDesktopModeImpl: onVisibilityChanged display=%d visible=%d",
displayId,
- visible
+ visibleTasksCount
)
- remoteListener.call { l -> l.onVisibilityChanged(displayId, visible) }
+ remoteListener.call {
+ l -> l.onTasksVisibilityChanged(displayId, visibleTasksCount)
+ }
}
override fun onStashedChanged(displayId: Int, stashed: Boolean) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt
index c3a82ce..39610e3 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt
@@ -26,6 +26,7 @@
import android.window.WindowContainerTransaction
import com.android.wm.shell.RootTaskDisplayAreaOrganizer
import com.android.wm.shell.protolog.ShellProtoLogGroup
+import com.android.wm.shell.shared.TransitionUtil
import com.android.wm.shell.splitscreen.SplitScreenController
import com.android.wm.shell.transition.Transitions
import com.android.wm.shell.transition.Transitions.TRANSIT_DESKTOP_MODE_CANCEL_DRAG_TO_DESKTOP
@@ -33,7 +34,6 @@
import com.android.wm.shell.transition.Transitions.TRANSIT_DESKTOP_MODE_START_DRAG_TO_DESKTOP
import com.android.wm.shell.transition.Transitions.TransitionHandler
import com.android.wm.shell.util.KtProtoLog
-import com.android.wm.shell.util.TransitionUtil
import com.android.wm.shell.windowdecor.DesktopModeWindowDecoration
import com.android.wm.shell.windowdecor.MoveToDesktopAnimator
import com.android.wm.shell.windowdecor.MoveToDesktopAnimator.Companion.DRAG_FREEFORM_SCALE
@@ -239,7 +239,7 @@
show(change.leash)
}
} else if (TransitionInfo.isIndependent(change, info)) {
- // Root.
+ // Root(s).
when (state) {
is TransitionState.FromSplit -> {
state.splitRootChange = change
@@ -256,6 +256,9 @@
}
}
is TransitionState.FromFullscreen -> {
+ // Most of the time we expect one change/task here, which should be the
+ // same that initiated the drag and that should be layered on top of
+ // everything.
if (change.taskInfo?.taskId == state.draggedTaskId) {
state.draggedTaskChange = change
val bounds = change.endAbsBounds
@@ -265,7 +268,18 @@
show(change.leash)
}
} else {
- throw IllegalStateException("Expected root to be dragged task")
+ // It's possible to see an additional change that isn't the dragged
+ // task when the dragged task is translucent and so the task behind it
+ // is included in the transition since it was visible and is now being
+ // occluded by the Home task. Just layer it at the bottom and save it
+ // in case we need to restore order if the drag is cancelled.
+ state.otherRootChanges.add(change)
+ val bounds = change.endAbsBounds
+ startTransaction.apply {
+ setLayer(change.leash, appLayers - i)
+ setWindowCrop(change.leash, bounds.width(), bounds.height())
+ show(change.leash)
+ }
}
}
}
@@ -515,8 +529,18 @@
val wct = WindowContainerTransaction()
when (state) {
is TransitionState.FromFullscreen -> {
+ // There may have been tasks sent behind home that are not the dragged task (like
+ // when the dragged task is translucent and that makes the task behind it visible).
+ // Restore the order of those first.
+ state.otherRootChanges.mapNotNull { it.container }.forEach { wc ->
+ // TODO(b/322852244): investigate why even though these "other" tasks are
+ // reordered in front of home and behind the translucent dragged task, its
+ // surface is not visible on screen.
+ wct.reorder(wc, true /* toTop */)
+ }
val wc = state.draggedTaskChange?.container
?: error("Dragged task should be non-null before cancelling")
+ // Then the dragged task a the very top.
wct.reorder(wc, true /* toTop */)
}
is TransitionState.FromSplit -> {
@@ -574,6 +598,7 @@
override var draggedTaskChange: Change? = null,
override var cancelled: Boolean = false,
override var startAborted: Boolean = false,
+ var otherRootChanges: MutableList<Change> = mutableListOf()
) : TransitionState()
data class FromSplit(
override val draggedTaskId: Int,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopTaskListener.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopTaskListener.aidl
index 39128a8..8ed87f2 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopTaskListener.aidl
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopTaskListener.aidl
@@ -22,8 +22,8 @@
*/
interface IDesktopTaskListener {
- /** Desktop task visibility has change. Visible if at least 1 task is visible. */
- oneway void onVisibilityChanged(int displayId, boolean visible);
+ /** Desktop tasks visibility has changed. Visible if at least 1 task is visible. */
+ oneway void onTasksVisibilityChanged(int displayId, int visibleTasksCount);
/** Desktop task stashed status has changed. */
oneway void onStashedChanged(int displayId, boolean stashed);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/keyguard/KeyguardTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/keyguard/KeyguardTransitionHandler.java
index e63bbc0..73de231 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/keyguard/KeyguardTransitionHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/keyguard/KeyguardTransitionHandler.java
@@ -27,7 +27,7 @@
import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_UNOCCLUDING;
import static android.view.WindowManager.TRANSIT_SLEEP;
-import static com.android.wm.shell.util.TransitionUtil.isOpeningType;
+import static com.android.wm.shell.shared.TransitionUtil.isOpeningType;
import android.annotation.NonNull;
import android.annotation.Nullable;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java
index 07b8f11..52a06e0 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java
@@ -86,6 +86,7 @@
import com.android.wm.shell.common.pip.PipBoundsAlgorithm;
import com.android.wm.shell.common.pip.PipBoundsState;
import com.android.wm.shell.common.pip.PipDisplayLayoutState;
+import com.android.wm.shell.common.pip.PipMenuController;
import com.android.wm.shell.common.pip.PipUiEventLogger;
import com.android.wm.shell.common.pip.PipUtils;
import com.android.wm.shell.pip.phone.PipMotionHelper;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java
index 8e375a9..896ca96 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java
@@ -68,13 +68,14 @@
import com.android.wm.shell.common.pip.PipBoundsAlgorithm;
import com.android.wm.shell.common.pip.PipBoundsState;
import com.android.wm.shell.common.pip.PipDisplayLayoutState;
+import com.android.wm.shell.common.pip.PipMenuController;
import com.android.wm.shell.common.pip.PipUtils;
import com.android.wm.shell.protolog.ShellProtoLogGroup;
+import com.android.wm.shell.shared.TransitionUtil;
import com.android.wm.shell.splitscreen.SplitScreenController;
import com.android.wm.shell.sysui.ShellInit;
import com.android.wm.shell.transition.CounterRotatorHelper;
import com.android.wm.shell.transition.Transitions;
-import com.android.wm.shell.util.TransitionUtil;
import java.io.PrintWriter;
import java.lang.annotation.Retention;
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 0e70736..d1fd207 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
@@ -40,6 +40,7 @@
import com.android.wm.shell.ShellTaskOrganizer;
import com.android.wm.shell.common.pip.PipBoundsAlgorithm;
import com.android.wm.shell.common.pip.PipBoundsState;
+import com.android.wm.shell.common.pip.PipMenuController;
import com.android.wm.shell.common.split.SplitScreenUtils;
import com.android.wm.shell.sysui.ShellInit;
import com.android.wm.shell.transition.Transitions;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipMenuController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipMenuController.java
index 7606526..d8e8b58 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipMenuController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipMenuController.java
@@ -41,8 +41,8 @@
import com.android.wm.shell.common.pip.PipBoundsState;
import com.android.wm.shell.common.pip.PipMediaController;
import com.android.wm.shell.common.pip.PipMediaController.ActionListener;
+import com.android.wm.shell.common.pip.PipMenuController;
import com.android.wm.shell.common.pip.PipUiEventLogger;
-import com.android.wm.shell.pip.PipMenuController;
import com.android.wm.shell.pip.PipSurfaceTransactionHelper;
import com.android.wm.shell.protolog.ShellProtoLogGroup;
import com.android.wm.shell.splitscreen.SplitScreenController;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java
index c6803f7..843c84a 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java
@@ -40,7 +40,7 @@
import com.android.internal.protolog.common.ProtoLog;
import com.android.wm.shell.R;
import com.android.wm.shell.common.SystemWindows;
-import com.android.wm.shell.pip.PipMenuController;
+import com.android.wm.shell.common.pip.PipMenuController;
import com.android.wm.shell.protolog.ShellProtoLogGroup;
import java.util.List;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipTaskOrganizer.java
index 21223c9a..cac63eb 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipTaskOrganizer.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipTaskOrganizer.java
@@ -28,10 +28,10 @@
import com.android.wm.shell.common.pip.PipBoundsAlgorithm;
import com.android.wm.shell.common.pip.PipBoundsState;
import com.android.wm.shell.common.pip.PipDisplayLayoutState;
+import com.android.wm.shell.common.pip.PipMenuController;
import com.android.wm.shell.common.pip.PipUiEventLogger;
import com.android.wm.shell.common.pip.PipUtils;
import com.android.wm.shell.pip.PipAnimationController;
-import com.android.wm.shell.pip.PipMenuController;
import com.android.wm.shell.pip.PipParamsChangedForwarder;
import com.android.wm.shell.pip.PipSurfaceTransactionHelper;
import com.android.wm.shell.pip.PipTaskOrganizer;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipTransition.java
index 571c839..c2f4d72a 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipTransition.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipTransition.java
@@ -25,10 +25,10 @@
import static android.view.WindowManager.TRANSIT_TO_BACK;
import static android.view.WindowManager.transitTypeToString;
+import static com.android.wm.shell.common.pip.PipMenuController.ALPHA_NO_CHANGE;
import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_LEAVE_PIP;
import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_REMOVE_STACK;
import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_TO_PIP;
-import static com.android.wm.shell.pip.PipMenuController.ALPHA_NO_CHANGE;
import static com.android.wm.shell.pip.PipTransitionState.ENTERED_PIP;
import static com.android.wm.shell.pip.PipTransitionState.ENTERING_PIP;
import static com.android.wm.shell.pip.PipTransitionState.EXITING_PIP;
@@ -71,9 +71,9 @@
import com.android.wm.shell.pip.PipTransitionController;
import com.android.wm.shell.pip.PipTransitionState;
import com.android.wm.shell.protolog.ShellProtoLogGroup;
+import com.android.wm.shell.shared.TransitionUtil;
import com.android.wm.shell.sysui.ShellInit;
import com.android.wm.shell.transition.Transitions;
-import com.android.wm.shell.util.TransitionUtil;
import java.util.ArrayList;
import java.util.List;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/PipSurfaceTransactionHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/PipSurfaceTransactionHelper.java
new file mode 100644
index 0000000..24077a3
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/PipSurfaceTransactionHelper.java
@@ -0,0 +1,277 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.pip2;
+
+import android.content.Context;
+import android.graphics.Matrix;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.view.Choreographer;
+import android.view.SurfaceControl;
+
+import com.android.wm.shell.R;
+import com.android.wm.shell.transition.Transitions;
+
+/**
+ * Abstracts the common operations on {@link SurfaceControl.Transaction} for PiP transition.
+ */
+public class PipSurfaceTransactionHelper {
+ /** for {@link #scale(SurfaceControl.Transaction, SurfaceControl, Rect, Rect)} operation */
+ private final Matrix mTmpTransform = new Matrix();
+ private final float[] mTmpFloat9 = new float[9];
+ private final RectF mTmpSourceRectF = new RectF();
+ private final RectF mTmpDestinationRectF = new RectF();
+ private final Rect mTmpDestinationRect = new Rect();
+
+ private int mCornerRadius;
+ private int mShadowRadius;
+
+ public PipSurfaceTransactionHelper(Context context) {
+ onDensityOrFontScaleChanged(context);
+ }
+
+ /**
+ * Called when display size or font size of settings changed
+ *
+ * @param context the current context
+ */
+ public void onDensityOrFontScaleChanged(Context context) {
+ mCornerRadius = context.getResources().getDimensionPixelSize(R.dimen.pip_corner_radius);
+ mShadowRadius = context.getResources().getDimensionPixelSize(R.dimen.pip_shadow_radius);
+ }
+
+ /**
+ * Operates the alpha on a given transaction and leash
+ * @return same {@link PipSurfaceTransactionHelper} instance for method chaining
+ */
+ public PipSurfaceTransactionHelper alpha(SurfaceControl.Transaction tx, SurfaceControl leash,
+ float alpha) {
+ tx.setAlpha(leash, alpha);
+ return this;
+ }
+
+ /**
+ * Operates the crop (and position) on a given transaction and leash
+ * @return same {@link PipSurfaceTransactionHelper} instance for method chaining
+ */
+ public PipSurfaceTransactionHelper crop(SurfaceControl.Transaction tx, SurfaceControl leash,
+ Rect destinationBounds) {
+ tx.setWindowCrop(leash, destinationBounds.width(), destinationBounds.height())
+ .setPosition(leash, destinationBounds.left, destinationBounds.top);
+ return this;
+ }
+
+ /**
+ * Operates the scale (setMatrix) on a given transaction and leash
+ * @return same {@link PipSurfaceTransactionHelper} instance for method chaining
+ */
+ public PipSurfaceTransactionHelper scale(SurfaceControl.Transaction tx, SurfaceControl leash,
+ Rect sourceBounds, Rect destinationBounds) {
+ mTmpDestinationRectF.set(destinationBounds);
+ return scale(tx, leash, sourceBounds, mTmpDestinationRectF, 0 /* degrees */);
+ }
+
+ /**
+ * Operates the scale (setMatrix) on a given transaction and leash
+ * @return same {@link PipSurfaceTransactionHelper} instance for method chaining
+ */
+ public PipSurfaceTransactionHelper scale(SurfaceControl.Transaction tx, SurfaceControl leash,
+ Rect sourceBounds, RectF destinationBounds) {
+ return scale(tx, leash, sourceBounds, destinationBounds, 0 /* degrees */);
+ }
+
+ /**
+ * Operates the scale (setMatrix) on a given transaction and leash
+ * @return same {@link PipSurfaceTransactionHelper} instance for method chaining
+ */
+ public PipSurfaceTransactionHelper scale(SurfaceControl.Transaction tx, SurfaceControl leash,
+ Rect sourceBounds, Rect destinationBounds, float degrees) {
+ mTmpDestinationRectF.set(destinationBounds);
+ return scale(tx, leash, sourceBounds, mTmpDestinationRectF, degrees);
+ }
+
+ /**
+ * Operates the scale (setMatrix) on a given transaction and leash, along with a rotation.
+ * @return same {@link PipSurfaceTransactionHelper} instance for method chaining
+ */
+ public PipSurfaceTransactionHelper scale(SurfaceControl.Transaction tx, SurfaceControl leash,
+ Rect sourceBounds, RectF destinationBounds, float degrees) {
+ mTmpSourceRectF.set(sourceBounds);
+ // We want the matrix to position the surface relative to the screen coordinates so offset
+ // the source to 0,0
+ mTmpSourceRectF.offsetTo(0, 0);
+ mTmpDestinationRectF.set(destinationBounds);
+ mTmpTransform.setRectToRect(mTmpSourceRectF, mTmpDestinationRectF, Matrix.ScaleToFit.FILL);
+ mTmpTransform.postRotate(degrees,
+ mTmpDestinationRectF.centerX(), mTmpDestinationRectF.centerY());
+ tx.setMatrix(leash, mTmpTransform, mTmpFloat9);
+ return this;
+ }
+
+ /**
+ * Operates the scale (setMatrix) on a given transaction and leash
+ * @return same {@link PipSurfaceTransactionHelper} instance for method chaining
+ */
+ public PipSurfaceTransactionHelper scaleAndCrop(SurfaceControl.Transaction tx,
+ SurfaceControl leash, Rect sourceRectHint,
+ Rect sourceBounds, Rect destinationBounds, Rect insets,
+ boolean isInPipDirection, float fraction) {
+ mTmpDestinationRect.set(sourceBounds);
+ // Similar to {@link #scale}, we want to position the surface relative to the screen
+ // coordinates so offset the bounds to 0,0
+ mTmpDestinationRect.offsetTo(0, 0);
+ mTmpDestinationRect.inset(insets);
+ // Scale to the bounds no smaller than the destination and offset such that the top/left
+ // of the scaled inset source rect aligns with the top/left of the destination bounds
+ final float scale;
+ if (isInPipDirection
+ && sourceRectHint != null && sourceRectHint.width() < sourceBounds.width()) {
+ // scale by sourceRectHint if it's not edge-to-edge, for entering PiP transition only.
+ final float endScale = sourceBounds.width() <= sourceBounds.height()
+ ? (float) destinationBounds.width() / sourceRectHint.width()
+ : (float) destinationBounds.height() / sourceRectHint.height();
+ final float startScale = sourceBounds.width() <= sourceBounds.height()
+ ? (float) destinationBounds.width() / sourceBounds.width()
+ : (float) destinationBounds.height() / sourceBounds.height();
+ scale = (1 - fraction) * startScale + fraction * endScale;
+ } else {
+ scale = Math.max((float) destinationBounds.width() / sourceBounds.width(),
+ (float) destinationBounds.height() / sourceBounds.height());
+ }
+ final float left = destinationBounds.left - insets.left * scale;
+ final float top = destinationBounds.top - insets.top * scale;
+ mTmpTransform.setScale(scale, scale);
+ tx.setMatrix(leash, mTmpTransform, mTmpFloat9)
+ .setCrop(leash, mTmpDestinationRect)
+ .setPosition(leash, left, top);
+ return this;
+ }
+
+ /**
+ * Operates the rotation according to the given degrees and scale (setMatrix) according to the
+ * source bounds and rotated destination bounds. The crop will be the unscaled source bounds.
+ * @return same {@link PipSurfaceTransactionHelper} instance for method chaining
+ */
+ public PipSurfaceTransactionHelper rotateAndScaleWithCrop(SurfaceControl.Transaction tx,
+ SurfaceControl leash, Rect sourceBounds, Rect destinationBounds, Rect insets,
+ float degrees, float positionX, float positionY, boolean isExpanding,
+ boolean clockwise) {
+ mTmpDestinationRect.set(sourceBounds);
+ mTmpDestinationRect.inset(insets);
+ final int srcW = mTmpDestinationRect.width();
+ final int srcH = mTmpDestinationRect.height();
+ final int destW = destinationBounds.width();
+ final int destH = destinationBounds.height();
+ // Scale by the short side so there won't be empty area if the aspect ratio of source and
+ // destination are different.
+ final float scale = srcW <= srcH ? (float) destW / srcW : (float) destH / srcH;
+ final Rect crop = mTmpDestinationRect;
+ crop.set(0, 0, Transitions.SHELL_TRANSITIONS_ROTATION ? destH
+ : destW, Transitions.SHELL_TRANSITIONS_ROTATION ? destW : destH);
+ // Inverse scale for crop to fit in screen coordinates.
+ crop.scale(1 / scale);
+ crop.offset(insets.left, insets.top);
+ if (isExpanding) {
+ // Expand bounds (shrink insets) in source orientation.
+ positionX -= insets.left * scale;
+ positionY -= insets.top * scale;
+ } else {
+ // Shrink bounds (expand insets) in destination orientation.
+ if (clockwise) {
+ positionX -= insets.top * scale;
+ positionY += insets.left * scale;
+ } else {
+ positionX += insets.top * scale;
+ positionY -= insets.left * scale;
+ }
+ }
+ mTmpTransform.setScale(scale, scale);
+ mTmpTransform.postRotate(degrees);
+ mTmpTransform.postTranslate(positionX, positionY);
+ tx.setMatrix(leash, mTmpTransform, mTmpFloat9).setCrop(leash, crop);
+ return this;
+ }
+
+ /**
+ * Resets the scale (setMatrix) on a given transaction and leash if there's any
+ *
+ * @return same {@link PipSurfaceTransactionHelper} instance for method chaining
+ */
+ public PipSurfaceTransactionHelper resetScale(SurfaceControl.Transaction tx,
+ SurfaceControl leash,
+ Rect destinationBounds) {
+ tx.setMatrix(leash, Matrix.IDENTITY_MATRIX, mTmpFloat9)
+ .setPosition(leash, destinationBounds.left, destinationBounds.top);
+ return this;
+ }
+
+ /**
+ * Operates the round corner radius on a given transaction and leash
+ * @return same {@link PipSurfaceTransactionHelper} instance for method chaining
+ */
+ public PipSurfaceTransactionHelper round(SurfaceControl.Transaction tx, SurfaceControl leash,
+ boolean applyCornerRadius) {
+ tx.setCornerRadius(leash, applyCornerRadius ? mCornerRadius : 0);
+ return this;
+ }
+
+ /**
+ * Operates the round corner radius on a given transaction and leash, scaled by bounds
+ * @return same {@link PipSurfaceTransactionHelper} instance for method chaining
+ */
+ public PipSurfaceTransactionHelper round(SurfaceControl.Transaction tx, SurfaceControl leash,
+ Rect fromBounds, Rect toBounds) {
+ final float scale = (float) (Math.hypot(fromBounds.width(), fromBounds.height())
+ / Math.hypot(toBounds.width(), toBounds.height()));
+ tx.setCornerRadius(leash, mCornerRadius * scale);
+ return this;
+ }
+
+ /**
+ * Operates the shadow radius on a given transaction and leash
+ * @return same {@link PipSurfaceTransactionHelper} instance for method chaining
+ */
+ public PipSurfaceTransactionHelper shadow(SurfaceControl.Transaction tx, SurfaceControl leash,
+ boolean applyShadowRadius) {
+ tx.setShadowRadius(leash, applyShadowRadius ? mShadowRadius : 0);
+ return this;
+ }
+
+ /**
+ * Interface to standardize {@link SurfaceControl.Transaction} generation across PiP.
+ */
+ public interface SurfaceControlTransactionFactory {
+ /**
+ * @return a new transaction to operate on.
+ */
+ SurfaceControl.Transaction getTransaction();
+ }
+
+ /**
+ * Implementation of {@link SurfaceControlTransactionFactory} that returns
+ * {@link SurfaceControl.Transaction} with VsyncId being set.
+ */
+ public static class VsyncSurfaceControlTransactionFactory
+ implements SurfaceControlTransactionFactory {
+ @Override
+ public SurfaceControl.Transaction getTransaction() {
+ final SurfaceControl.Transaction tx = new SurfaceControl.Transaction();
+ tx.setFrameTimelineVsync(Choreographer.getInstance().getVsyncId());
+ return tx;
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PhonePipMenuController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PhonePipMenuController.java
new file mode 100644
index 0000000..2478252
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PhonePipMenuController.java
@@ -0,0 +1,608 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.pip2.phone;
+
+import static android.view.WindowManager.SHELL_ROOT_LAYER_PIP;
+
+import android.annotation.Nullable;
+import android.app.ActivityManager;
+import android.app.RemoteAction;
+import android.content.Context;
+import android.graphics.Matrix;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.os.Debug;
+import android.os.Handler;
+import android.os.RemoteException;
+import android.util.Size;
+import android.view.MotionEvent;
+import android.view.SurfaceControl;
+import android.view.View;
+import android.view.ViewRootImpl;
+import android.view.WindowManagerGlobal;
+
+import com.android.internal.protolog.common.ProtoLog;
+import com.android.wm.shell.common.ShellExecutor;
+import com.android.wm.shell.common.SystemWindows;
+import com.android.wm.shell.common.pip.PipBoundsState;
+import com.android.wm.shell.common.pip.PipMediaController;
+import com.android.wm.shell.common.pip.PipMediaController.ActionListener;
+import com.android.wm.shell.common.pip.PipMenuController;
+import com.android.wm.shell.common.pip.PipUiEventLogger;
+import com.android.wm.shell.pip2.PipSurfaceTransactionHelper;
+import com.android.wm.shell.protolog.ShellProtoLogGroup;
+
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Manages the PiP menu view which can show menu options or a scrim.
+ *
+ * The current media session provides actions whenever there are no valid actions provided by the
+ * current PiP activity. Otherwise, those actions always take precedence.
+ */
+public class PhonePipMenuController implements PipMenuController {
+
+ private static final String TAG = "PhonePipMenuController";
+ private static final boolean DEBUG = false;
+
+ public static final int MENU_STATE_NONE = 0;
+ public static final int MENU_STATE_FULL = 1;
+
+ /**
+ * A listener interface to receive notification on changes in PIP.
+ */
+ public interface Listener {
+ /**
+ * Called when the PIP menu visibility change has started.
+ *
+ * @param menuState the new, about-to-change state of the menu
+ * @param resize whether or not to resize the PiP with the state change
+ */
+ void onPipMenuStateChangeStart(int menuState, boolean resize, Runnable callback);
+
+ /**
+ * Called when the PIP menu state has finished changing/animating.
+ *
+ * @param menuState the new state of the menu.
+ */
+ void onPipMenuStateChangeFinish(int menuState);
+
+ /**
+ * Called when the PIP requested to be expanded.
+ */
+ void onPipExpand();
+
+ /**
+ * Called when the PIP requested to be dismissed.
+ */
+ void onPipDismiss();
+
+ /**
+ * Called when the PIP requested to show the menu.
+ */
+ void onPipShowMenu();
+
+ /**
+ * Called when the PIP requested to enter Split.
+ */
+ void onEnterSplit();
+ }
+
+ private final Matrix mMoveTransform = new Matrix();
+ private final Rect mTmpSourceBounds = new Rect();
+ private final RectF mTmpSourceRectF = new RectF();
+ private final RectF mTmpDestinationRectF = new RectF();
+ private final Context mContext;
+ private final PipBoundsState mPipBoundsState;
+ private final PipMediaController mMediaController;
+ private final ShellExecutor mMainExecutor;
+ private final Handler mMainHandler;
+
+ private final PipSurfaceTransactionHelper.SurfaceControlTransactionFactory
+ mSurfaceControlTransactionFactory;
+ private final float[] mTmpTransform = new float[9];
+
+ private final ArrayList<Listener> mListeners = new ArrayList<>();
+ private final SystemWindows mSystemWindows;
+ private final PipUiEventLogger mPipUiEventLogger;
+
+ private List<RemoteAction> mAppActions;
+ private RemoteAction mCloseAction;
+ private List<RemoteAction> mMediaActions;
+
+ private int mMenuState;
+
+ private PipMenuView mPipMenuView;
+
+ private SurfaceControl mLeash;
+
+ private ActionListener mMediaActionListener = new ActionListener() {
+ @Override
+ public void onMediaActionsChanged(List<RemoteAction> mediaActions) {
+ mMediaActions = new ArrayList<>(mediaActions);
+ updateMenuActions();
+ }
+ };
+
+ public PhonePipMenuController(Context context, PipBoundsState pipBoundsState,
+ PipMediaController mediaController, SystemWindows systemWindows,
+ PipUiEventLogger pipUiEventLogger,
+ ShellExecutor mainExecutor, Handler mainHandler) {
+ mContext = context;
+ mPipBoundsState = pipBoundsState;
+ mMediaController = mediaController;
+ mSystemWindows = systemWindows;
+ mMainExecutor = mainExecutor;
+ mMainHandler = mainHandler;
+ mPipUiEventLogger = pipUiEventLogger;
+
+ mSurfaceControlTransactionFactory =
+ new PipSurfaceTransactionHelper.VsyncSurfaceControlTransactionFactory();
+ }
+
+ public boolean isMenuVisible() {
+ return mPipMenuView != null && mMenuState != MENU_STATE_NONE;
+ }
+
+ /**
+ * Attach the menu when the PiP task first appears.
+ */
+ @Override
+ public void attach(SurfaceControl leash) {
+ mLeash = leash;
+ attachPipMenuView();
+ }
+
+ /**
+ * Detach the menu when the PiP task is gone.
+ */
+ @Override
+ public void detach() {
+ hideMenu();
+ detachPipMenuView();
+ mLeash = null;
+ }
+
+ void attachPipMenuView() {
+ // In case detach was not called (e.g. PIP unexpectedly closed)
+ if (mPipMenuView != null) {
+ detachPipMenuView();
+ }
+ mPipMenuView = new PipMenuView(mContext, this, mMainExecutor, mMainHandler,
+ mPipUiEventLogger);
+ mPipMenuView.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() {
+ @Override
+ public void onViewAttachedToWindow(View v) {
+ v.getViewRootImpl().addSurfaceChangedCallback(
+ new ViewRootImpl.SurfaceChangedCallback() {
+ @Override
+ public void surfaceCreated(SurfaceControl.Transaction t) {
+ final SurfaceControl sc = getSurfaceControl();
+ if (sc != null) {
+ t.reparent(sc, mLeash);
+ // make menu on top of the surface
+ t.setLayer(sc, Integer.MAX_VALUE);
+ }
+ }
+
+ @Override
+ public void surfaceReplaced(SurfaceControl.Transaction t) {
+ }
+
+ @Override
+ public void surfaceDestroyed() {
+ }
+ });
+ }
+
+ @Override
+ public void onViewDetachedFromWindow(View v) {
+ }
+ });
+
+ mSystemWindows.addView(mPipMenuView,
+ getPipMenuLayoutParams(mContext, MENU_WINDOW_TITLE, 0 /* width */, 0 /* height */),
+ 0, SHELL_ROOT_LAYER_PIP);
+ setShellRootAccessibilityWindow();
+
+ // Make sure the initial actions are set
+ updateMenuActions();
+ }
+
+ private void detachPipMenuView() {
+ if (mPipMenuView == null) {
+ return;
+ }
+
+ mSystemWindows.removeView(mPipMenuView);
+ mPipMenuView = null;
+ }
+
+ /**
+ * Updates the layout parameters of the menu.
+ * @param destinationBounds New Menu bounds.
+ */
+ @Override
+ public void updateMenuBounds(Rect destinationBounds) {
+ mSystemWindows.updateViewLayout(mPipMenuView,
+ getPipMenuLayoutParams(mContext, MENU_WINDOW_TITLE, destinationBounds.width(),
+ destinationBounds.height()));
+ updateMenuLayout(destinationBounds);
+ }
+
+ @Override
+ public void onFocusTaskChanged(ActivityManager.RunningTaskInfo taskInfo) {
+ if (mPipMenuView != null) {
+ mPipMenuView.onFocusTaskChanged(taskInfo);
+ }
+ }
+
+ /**
+ * Tries to grab a surface control from {@link PipMenuView}. If this isn't available for some
+ * reason (ie. the window isn't ready yet, thus {@link ViewRootImpl} is
+ * {@code null}), it will get the leash that the WindowlessWM has assigned to it.
+ */
+ public SurfaceControl getSurfaceControl() {
+ return mSystemWindows.getViewSurface(mPipMenuView);
+ }
+
+ /**
+ * Adds a new menu activity listener.
+ */
+ public void addListener(Listener listener) {
+ if (!mListeners.contains(listener)) {
+ mListeners.add(listener);
+ }
+ }
+
+ @Nullable
+ Size getEstimatedMinMenuSize() {
+ return mPipMenuView == null ? null : mPipMenuView.getEstimatedMinMenuSize();
+ }
+
+ /**
+ * When other components requests the menu controller directly to show the menu, we must
+ * first fire off the request to the other listeners who will then propagate the call
+ * back to the controller with the right parameters.
+ */
+ @Override
+ public void showMenu() {
+ mListeners.forEach(Listener::onPipShowMenu);
+ }
+
+ /**
+ * Similar to {@link #showMenu(int, Rect, boolean, boolean, boolean)} but only show the menu
+ * upon PiP window transition is finished.
+ */
+ public void showMenuWithPossibleDelay(int menuState, Rect stackBounds, boolean allowMenuTimeout,
+ boolean willResizeMenu, boolean showResizeHandle) {
+ if (willResizeMenu) {
+ // hide all visible controls including close button and etc. first, this is to ensure
+ // menu is totally invisible during the transition to eliminate unpleasant artifacts
+ fadeOutMenu();
+ }
+ showMenuInternal(menuState, stackBounds, allowMenuTimeout, willResizeMenu,
+ willResizeMenu /* withDelay=willResizeMenu here */, showResizeHandle);
+ }
+
+ /**
+ * Shows the menu activity immediately.
+ */
+ public void showMenu(int menuState, Rect stackBounds, boolean allowMenuTimeout,
+ boolean willResizeMenu, boolean showResizeHandle) {
+ showMenuInternal(menuState, stackBounds, allowMenuTimeout, willResizeMenu,
+ false /* withDelay */, showResizeHandle);
+ }
+
+ private void showMenuInternal(int menuState, Rect stackBounds, boolean allowMenuTimeout,
+ boolean willResizeMenu, boolean withDelay, boolean showResizeHandle) {
+ if (DEBUG) {
+ ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "%s: showMenu() state=%s"
+ + " isMenuVisible=%s"
+ + " allowMenuTimeout=%s"
+ + " willResizeMenu=%s"
+ + " withDelay=%s"
+ + " showResizeHandle=%s"
+ + " callers=\n%s", TAG, menuState, isMenuVisible(), allowMenuTimeout,
+ willResizeMenu, withDelay, showResizeHandle, Debug.getCallers(5, " "));
+ }
+
+ if (!checkPipMenuState()) {
+ return;
+ }
+
+ // Sync the menu bounds before showing it in case it is out of sync.
+ movePipMenu(null /* pipLeash */, null /* transaction */, stackBounds,
+ PipMenuController.ALPHA_NO_CHANGE);
+ updateMenuBounds(stackBounds);
+
+ mPipMenuView.showMenu(menuState, stackBounds, allowMenuTimeout, willResizeMenu, withDelay,
+ showResizeHandle);
+ }
+
+ /**
+ * Move the PiP menu, which does a translation and possibly a scale transformation.
+ */
+ @Override
+ public void movePipMenu(@Nullable SurfaceControl pipLeash,
+ @Nullable SurfaceControl.Transaction t,
+ Rect destinationBounds, float alpha) {
+ if (destinationBounds.isEmpty()) {
+ return;
+ }
+
+ if (!checkPipMenuState()) {
+ return;
+ }
+
+ // TODO(b/286307861) transaction should be applied outside of PiP menu controller
+ if (pipLeash != null && t != null) {
+ t.apply();
+ }
+ }
+
+ /**
+ * Does an immediate window crop of the PiP menu.
+ */
+ @Override
+ public void resizePipMenu(@Nullable SurfaceControl pipLeash,
+ @Nullable SurfaceControl.Transaction t,
+ Rect destinationBounds) {
+ if (destinationBounds.isEmpty()) {
+ return;
+ }
+
+ if (!checkPipMenuState()) {
+ return;
+ }
+
+ // TODO(b/286307861) transaction should be applied outside of PiP menu controller
+ if (pipLeash != null && t != null) {
+ t.apply();
+ }
+ }
+
+ private boolean checkPipMenuState() {
+ if (mPipMenuView == null || mPipMenuView.getViewRootImpl() == null) {
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "%s: Not going to move PiP, either menu or its parent is not created.", TAG);
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Pokes the menu, indicating that the user is interacting with it.
+ */
+ public void pokeMenu() {
+ final boolean isMenuVisible = isMenuVisible();
+ if (DEBUG) {
+ ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "%s: pokeMenu() isMenuVisible=%b", TAG, isMenuVisible);
+ }
+ if (isMenuVisible) {
+ mPipMenuView.pokeMenu();
+ }
+ }
+
+ private void fadeOutMenu() {
+ final boolean isMenuVisible = isMenuVisible();
+ if (DEBUG) {
+ ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "%s: fadeOutMenu() isMenuVisible=%b", TAG, isMenuVisible);
+ }
+ if (isMenuVisible) {
+ mPipMenuView.fadeOutMenu();
+ }
+ }
+
+ /**
+ * Hides the menu view.
+ */
+ public void hideMenu() {
+ final boolean isMenuVisible = isMenuVisible();
+ if (isMenuVisible) {
+ mPipMenuView.hideMenu();
+ }
+ }
+
+ /**
+ * Hides the menu view.
+ *
+ * @param animationType the animation type to use upon hiding the menu
+ * @param resize whether or not to resize the PiP with the state change
+ */
+ public void hideMenu(@PipMenuView.AnimationType int animationType, boolean resize) {
+ final boolean isMenuVisible = isMenuVisible();
+ if (DEBUG) {
+ ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "%s: hideMenu() state=%s"
+ + " isMenuVisible=%s"
+ + " animationType=%s"
+ + " resize=%s"
+ + " callers=\n%s", TAG, mMenuState, isMenuVisible,
+ animationType, resize,
+ Debug.getCallers(5, " "));
+ }
+ if (isMenuVisible) {
+ mPipMenuView.hideMenu(resize, animationType);
+ }
+ }
+
+ /**
+ * Hides the menu activity.
+ */
+ public void hideMenu(Runnable onStartCallback, Runnable onEndCallback) {
+ if (isMenuVisible()) {
+ // If the menu is visible in either the closed or full state, then hide the menu and
+ // trigger the animation trigger afterwards
+ if (onStartCallback != null) {
+ onStartCallback.run();
+ }
+ mPipMenuView.hideMenu(onEndCallback);
+ }
+ }
+
+ /**
+ * Sets the menu actions to the actions provided by the current PiP menu.
+ */
+ @Override
+ public void setAppActions(List<RemoteAction> appActions,
+ RemoteAction closeAction) {
+ mAppActions = appActions;
+ mCloseAction = closeAction;
+ updateMenuActions();
+ }
+
+ void onPipExpand() {
+ mListeners.forEach(Listener::onPipExpand);
+ }
+
+ void onPipDismiss() {
+ mListeners.forEach(Listener::onPipDismiss);
+ }
+
+ void onEnterSplit() {
+ mListeners.forEach(Listener::onEnterSplit);
+ }
+
+ /**
+ * @return the best set of actions to show in the PiP menu.
+ */
+ private List<RemoteAction> resolveMenuActions() {
+ if (isValidActions(mAppActions)) {
+ return mAppActions;
+ }
+ return mMediaActions;
+ }
+
+ /**
+ * Updates the PiP menu with the best set of actions provided.
+ */
+ private void updateMenuActions() {
+ if (mPipMenuView != null) {
+ mPipMenuView.setActions(mPipBoundsState.getBounds(),
+ resolveMenuActions(), mCloseAction);
+ }
+ }
+
+ /**
+ * Returns whether the set of actions are valid.
+ */
+ private static boolean isValidActions(List<?> actions) {
+ return actions != null && actions.size() > 0;
+ }
+
+ /**
+ * Handles changes in menu visibility.
+ */
+ void onMenuStateChangeStart(int menuState, boolean resize, Runnable callback) {
+ if (DEBUG) {
+ ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "%s: onMenuStateChangeStart() mMenuState=%s"
+ + " menuState=%s resize=%s"
+ + " callers=\n%s", TAG, mMenuState, menuState, resize,
+ Debug.getCallers(5, " "));
+ }
+
+ if (menuState != mMenuState) {
+ mListeners.forEach(l -> l.onPipMenuStateChangeStart(menuState, resize, callback));
+ if (menuState == MENU_STATE_FULL) {
+ // Once visible, start listening for media action changes. This call will trigger
+ // the menu actions to be updated again.
+ mMediaController.addActionListener(mMediaActionListener);
+ } else {
+ // Once hidden, stop listening for media action changes. This call will trigger
+ // the menu actions to be updated again.
+ mMediaController.removeActionListener(mMediaActionListener);
+ }
+
+ try {
+ WindowManagerGlobal.getWindowSession().grantEmbeddedWindowFocus(null /* window */,
+ mSystemWindows.getFocusGrantToken(mPipMenuView),
+ menuState != MENU_STATE_NONE /* grantFocus */);
+ } catch (RemoteException e) {
+ ProtoLog.e(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "%s: Unable to update focus as menu appears/disappears, %s", TAG, e);
+ }
+ }
+ }
+
+ void onMenuStateChangeFinish(int menuState) {
+ if (menuState != mMenuState) {
+ mListeners.forEach(l -> l.onPipMenuStateChangeFinish(menuState));
+ }
+ mMenuState = menuState;
+ setShellRootAccessibilityWindow();
+ }
+
+ private void setShellRootAccessibilityWindow() {
+ switch (mMenuState) {
+ case MENU_STATE_NONE:
+ mSystemWindows.setShellRootAccessibilityWindow(0, SHELL_ROOT_LAYER_PIP, null);
+ break;
+ default:
+ mSystemWindows.setShellRootAccessibilityWindow(0, SHELL_ROOT_LAYER_PIP,
+ mPipMenuView);
+ break;
+ }
+ }
+
+ /**
+ * Handles a pointer event sent from pip input consumer.
+ */
+ void handlePointerEvent(MotionEvent ev) {
+ if (mPipMenuView == null) {
+ return;
+ }
+
+ if (ev.isTouchEvent()) {
+ mPipMenuView.dispatchTouchEvent(ev);
+ } else {
+ mPipMenuView.dispatchGenericMotionEvent(ev);
+ }
+ }
+
+ /**
+ * Tell the PIP Menu to recalculate its layout given its current position on the display.
+ */
+ public void updateMenuLayout(Rect bounds) {
+ final boolean isMenuVisible = isMenuVisible();
+ if (DEBUG) {
+ ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "%s: updateMenuLayout() state=%s"
+ + " isMenuVisible=%s"
+ + " callers=\n%s", TAG, mMenuState, isMenuVisible,
+ Debug.getCallers(5, " "));
+ }
+ if (isMenuVisible) {
+ mPipMenuView.updateMenuLayout(bounds);
+ }
+ }
+
+ void dump(PrintWriter pw, String prefix) {
+ final String innerPrefix = prefix + " ";
+ pw.println(prefix + TAG);
+ pw.println(innerPrefix + "mMenuState=" + mMenuState);
+ pw.println(innerPrefix + "mPipMenuView=" + mPipMenuView);
+ pw.println(innerPrefix + "mListeners=" + mListeners.size());
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipMenuActionView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipMenuActionView.java
new file mode 100644
index 0000000..7252675
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipMenuActionView.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.pip2.phone;
+
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+
+import com.android.wm.shell.R;
+
+/**
+ * Container layout wraps single action image view drawn in PiP menu and can restrict the size of
+ * action image view (see pip_menu_action.xml).
+ */
+public class PipMenuActionView extends FrameLayout {
+ private ImageView mImageView;
+ private View mCustomCloseBackground;
+
+ public PipMenuActionView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+ mImageView = findViewById(R.id.image);
+ mCustomCloseBackground = findViewById(R.id.custom_close_bg);
+ }
+
+ /** pass through to internal {@link #mImageView} */
+ public void setImageDrawable(Drawable drawable) {
+ mImageView.setImageDrawable(drawable);
+ }
+
+ /** pass through to internal {@link #mCustomCloseBackground} */
+ public void setCustomCloseBackgroundVisibility(@Visibility int visibility) {
+ mCustomCloseBackground.setVisibility(visibility);
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipMenuIconsAlgorithm.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipMenuIconsAlgorithm.java
new file mode 100644
index 0000000..b5e575b
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipMenuIconsAlgorithm.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.pip2.phone;
+
+import android.content.Context;
+import android.graphics.Rect;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+
+/**
+ * Helper class to calculate and place the menu icons on the PIP Menu.
+ */
+public class PipMenuIconsAlgorithm {
+
+ private static final String TAG = "PipMenuIconsAlgorithm";
+
+ protected ViewGroup mViewRoot;
+ protected ViewGroup mTopEndContainer;
+ protected View mDragHandle;
+ protected View mEnterSplitButton;
+ protected View mSettingsButton;
+ protected View mDismissButton;
+
+ protected PipMenuIconsAlgorithm(Context context) {
+ }
+
+ /**
+ * Bind the necessary views.
+ */
+ public void bindViews(ViewGroup viewRoot, ViewGroup topEndContainer, View dragHandle,
+ View enterSplitButton, View settingsButton, View dismissButton) {
+ mViewRoot = viewRoot;
+ mTopEndContainer = topEndContainer;
+ mDragHandle = dragHandle;
+ mEnterSplitButton = enterSplitButton;
+ mSettingsButton = settingsButton;
+ mDismissButton = dismissButton;
+ }
+
+ /**
+ * Updates the position of the drag handle based on where the PIP window is on the screen.
+ */
+ public void onBoundsChanged(Rect bounds) {
+ // On phones, the menu icons are always static and will never move based on the PIP window
+ // position. No need to do anything here.
+ }
+
+ /**
+ * Set the gravity on the given view.
+ */
+ protected static void setLayoutGravity(View v, int gravity) {
+ if (v.getLayoutParams() instanceof FrameLayout.LayoutParams) {
+ FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) v.getLayoutParams();
+ params.gravity = gravity;
+ v.setLayoutParams(params);
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipMenuView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipMenuView.java
new file mode 100644
index 0000000..a5b76c7
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipMenuView.java
@@ -0,0 +1,630 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.pip2.phone;
+
+import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TASK;
+import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
+import static android.provider.Settings.ACTION_PICTURE_IN_PICTURE_SETTINGS;
+import static android.view.accessibility.AccessibilityManager.FLAG_CONTENT_CONTROLS;
+import static android.view.accessibility.AccessibilityManager.FLAG_CONTENT_ICONS;
+import static android.view.accessibility.AccessibilityNodeInfo.ACTION_CLICK;
+
+import static com.android.wm.shell.pip.phone.PhonePipMenuController.MENU_STATE_FULL;
+import static com.android.wm.shell.pip.phone.PhonePipMenuController.MENU_STATE_NONE;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+import android.animation.ValueAnimator;
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.ActivityManager;
+import android.app.PendingIntent;
+import android.app.RemoteAction;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Color;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.Icon;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.UserHandle;
+import android.util.Pair;
+import android.util.Size;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.accessibility.AccessibilityManager;
+import android.view.accessibility.AccessibilityNodeInfo;
+import android.widget.FrameLayout;
+import android.widget.LinearLayout;
+
+import com.android.internal.protolog.common.ProtoLog;
+import com.android.wm.shell.R;
+import com.android.wm.shell.animation.Interpolators;
+import com.android.wm.shell.common.ShellExecutor;
+import com.android.wm.shell.common.pip.PipUiEventLogger;
+import com.android.wm.shell.common.pip.PipUtils;
+import com.android.wm.shell.protolog.ShellProtoLogGroup;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Translucent window that gets started on top of a task in PIP to allow the user to control it.
+ */
+public class PipMenuView extends FrameLayout {
+
+ private static final String TAG = "PipMenuView";
+
+ private static final int ANIMATION_NONE_DURATION_MS = 0;
+ private static final int ANIMATION_HIDE_DURATION_MS = 125;
+
+ /** No animation performed during menu hide. */
+ public static final int ANIM_TYPE_NONE = 0;
+ /** Fade out the menu until it's invisible. Used when the PIP window remains visible. */
+ public static final int ANIM_TYPE_HIDE = 1;
+ /** Fade out the menu in sync with the PIP window. */
+ public static final int ANIM_TYPE_DISMISS = 2;
+
+ @IntDef(prefix = { "ANIM_TYPE_" }, value = {
+ ANIM_TYPE_NONE,
+ ANIM_TYPE_HIDE,
+ ANIM_TYPE_DISMISS
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface AnimationType {}
+
+ private static final int INITIAL_DISMISS_DELAY = 3500;
+ private static final int POST_INTERACTION_DISMISS_DELAY = 2000;
+ private static final long MENU_SHOW_ON_EXPAND_START_DELAY = 30;
+
+ private static final float MENU_BACKGROUND_ALPHA = 0.54f;
+ private static final float DISABLED_ACTION_ALPHA = 0.54f;
+
+ private int mMenuState;
+ private boolean mAllowMenuTimeout = true;
+ private boolean mAllowTouches = true;
+ private int mDismissFadeOutDurationMs;
+ private final List<RemoteAction> mActions = new ArrayList<>();
+ private RemoteAction mCloseAction;
+
+ private AccessibilityManager mAccessibilityManager;
+ private Drawable mBackgroundDrawable;
+ private View mMenuContainer;
+ private LinearLayout mActionsGroup;
+ private int mBetweenActionPaddingLand;
+
+ private AnimatorSet mMenuContainerAnimator;
+ private final PhonePipMenuController mController;
+ private final PipUiEventLogger mPipUiEventLogger;
+
+ private ValueAnimator.AnimatorUpdateListener mMenuBgUpdateListener =
+ new ValueAnimator.AnimatorUpdateListener() {
+ @Override
+ public void onAnimationUpdate(ValueAnimator animation) {
+ final float alpha = (float) animation.getAnimatedValue();
+ mBackgroundDrawable.setAlpha((int) (MENU_BACKGROUND_ALPHA * alpha * 255));
+ }
+ };
+
+ private ShellExecutor mMainExecutor;
+ private Handler mMainHandler;
+
+ /**
+ * Whether the most recent showing of the menu caused a PIP resize, such as when PIP is too
+ * small and it is resized on menu show to fit the actions.
+ */
+ private boolean mDidLastShowMenuResize;
+ private final Runnable mHideMenuRunnable = this::hideMenu;
+
+ protected View mViewRoot;
+ protected View mSettingsButton;
+ protected View mDismissButton;
+ protected View mEnterSplitButton;
+ protected View mTopEndContainer;
+ protected PipMenuIconsAlgorithm mPipMenuIconsAlgorithm;
+
+ // How long the shell will wait for the app to close the PiP if a custom action is set.
+ private final int mPipForceCloseDelay;
+
+ public PipMenuView(Context context, PhonePipMenuController controller,
+ ShellExecutor mainExecutor, Handler mainHandler, PipUiEventLogger pipUiEventLogger) {
+ super(context, null, 0);
+ mContext = context;
+ mController = controller;
+ mMainExecutor = mainExecutor;
+ mMainHandler = mainHandler;
+ mPipUiEventLogger = pipUiEventLogger;
+
+ mAccessibilityManager = context.getSystemService(AccessibilityManager.class);
+ inflate(context, R.layout.pip_menu, this);
+
+ mPipForceCloseDelay = context.getResources().getInteger(
+ R.integer.config_pipForceCloseDelay);
+
+ mBackgroundDrawable = mContext.getDrawable(R.drawable.pip_menu_background);
+ mBackgroundDrawable.setAlpha(0);
+ mViewRoot = findViewById(R.id.background);
+ mViewRoot.setBackground(mBackgroundDrawable);
+ mMenuContainer = findViewById(R.id.menu_container);
+ mMenuContainer.setAlpha(0);
+ mTopEndContainer = findViewById(R.id.top_end_container);
+ mSettingsButton = findViewById(R.id.settings);
+ mSettingsButton.setAlpha(0);
+ mSettingsButton.setOnClickListener((v) -> {
+ if (v.getAlpha() != 0) {
+ showSettings();
+ }
+ });
+ mDismissButton = findViewById(R.id.dismiss);
+ mDismissButton.setAlpha(0);
+ mDismissButton.setOnClickListener(v -> dismissPip());
+ findViewById(R.id.expand_button).setOnClickListener(v -> {
+ if (mMenuContainer.getAlpha() != 0) {
+ expandPip();
+ }
+ });
+
+ mEnterSplitButton = findViewById(R.id.enter_split);
+ mEnterSplitButton.setAlpha(0);
+ mEnterSplitButton.setOnClickListener(v -> {
+ if (mEnterSplitButton.getAlpha() != 0) {
+ enterSplit();
+ }
+ });
+
+ // this disables the ripples
+ mEnterSplitButton.setEnabled(false);
+
+ findViewById(R.id.resize_handle).setAlpha(0);
+
+ mActionsGroup = findViewById(R.id.actions_group);
+ mBetweenActionPaddingLand = getResources().getDimensionPixelSize(
+ R.dimen.pip_between_action_padding_land);
+ mPipMenuIconsAlgorithm = new PipMenuIconsAlgorithm(mContext);
+ mPipMenuIconsAlgorithm.bindViews((ViewGroup) mViewRoot, (ViewGroup) mTopEndContainer,
+ findViewById(R.id.resize_handle), mEnterSplitButton, mSettingsButton,
+ mDismissButton);
+ mDismissFadeOutDurationMs = context.getResources()
+ .getInteger(R.integer.config_pipExitAnimationDuration);
+
+ initAccessibility();
+ }
+
+ private void initAccessibility() {
+ this.setAccessibilityDelegate(new AccessibilityDelegate() {
+ @Override
+ public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) {
+ super.onInitializeAccessibilityNodeInfo(host, info);
+ String label = getResources().getString(R.string.pip_menu_title);
+ info.addAction(new AccessibilityNodeInfo.AccessibilityAction(ACTION_CLICK, label));
+ }
+
+ @Override
+ public boolean performAccessibilityAction(View host, int action, Bundle args) {
+ if (action == ACTION_CLICK && mMenuState != MENU_STATE_FULL) {
+ mController.showMenu();
+ }
+ return super.performAccessibilityAction(host, action, args);
+ }
+ });
+ }
+
+ @Override
+ public boolean onKeyUp(int keyCode, KeyEvent event) {
+ if (keyCode == KeyEvent.KEYCODE_ESCAPE) {
+ hideMenu();
+ return true;
+ }
+ return super.onKeyUp(keyCode, event);
+ }
+
+ @Override
+ public boolean shouldDelayChildPressedState() {
+ return true;
+ }
+
+ @Override
+ public boolean dispatchTouchEvent(MotionEvent ev) {
+ if (!mAllowTouches) {
+ return false;
+ }
+
+ if (mAllowMenuTimeout) {
+ repostDelayedHide(POST_INTERACTION_DISMISS_DELAY);
+ }
+
+ return super.dispatchTouchEvent(ev);
+ }
+
+ @Override
+ public boolean dispatchGenericMotionEvent(MotionEvent event) {
+ if (mAllowMenuTimeout) {
+ repostDelayedHide(POST_INTERACTION_DISMISS_DELAY);
+ }
+
+ return super.dispatchGenericMotionEvent(event);
+ }
+
+ void onFocusTaskChanged(ActivityManager.RunningTaskInfo taskInfo) {}
+
+ void showMenu(int menuState, Rect stackBounds, boolean allowMenuTimeout,
+ boolean resizeMenuOnShow, boolean withDelay, boolean showResizeHandle) {
+ mAllowMenuTimeout = allowMenuTimeout;
+ mDidLastShowMenuResize = resizeMenuOnShow;
+ final boolean enableEnterSplit =
+ mContext.getResources().getBoolean(R.bool.config_pipEnableEnterSplitButton);
+ if (mMenuState != menuState) {
+ // Disallow touches if the menu needs to resize while showing, and we are transitioning
+ // to/from a full menu state.
+ boolean disallowTouchesUntilAnimationEnd = resizeMenuOnShow
+ && (mMenuState == MENU_STATE_FULL || menuState == MENU_STATE_FULL);
+ mAllowTouches = !disallowTouchesUntilAnimationEnd;
+ cancelDelayedHide();
+ if (mMenuContainerAnimator != null) {
+ mMenuContainerAnimator.cancel();
+ }
+ mMenuContainerAnimator = new AnimatorSet();
+ ObjectAnimator menuAnim = ObjectAnimator.ofFloat(mMenuContainer, View.ALPHA,
+ mMenuContainer.getAlpha(), 1f);
+ menuAnim.addUpdateListener(mMenuBgUpdateListener);
+ ObjectAnimator settingsAnim = ObjectAnimator.ofFloat(mSettingsButton, View.ALPHA,
+ mSettingsButton.getAlpha(), 1f);
+ ObjectAnimator dismissAnim = ObjectAnimator.ofFloat(mDismissButton, View.ALPHA,
+ mDismissButton.getAlpha(), 1f);
+ if (menuState == MENU_STATE_FULL) {
+ mMenuContainerAnimator.playTogether(menuAnim, settingsAnim, dismissAnim);
+ }
+ mMenuContainerAnimator.setInterpolator(Interpolators.ALPHA_IN);
+ mMenuContainerAnimator.setDuration(ANIMATION_HIDE_DURATION_MS);
+ mMenuContainerAnimator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ mAllowTouches = true;
+ notifyMenuStateChangeFinish(menuState);
+ if (allowMenuTimeout) {
+ repostDelayedHide(INITIAL_DISMISS_DELAY);
+ }
+ }
+
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ mAllowTouches = true;
+ }
+ });
+ if (withDelay) {
+ // starts the menu container animation after window expansion is completed
+ notifyMenuStateChangeStart(menuState, resizeMenuOnShow, () -> {
+ if (mMenuContainerAnimator == null) {
+ return;
+ }
+ mMenuContainerAnimator.setStartDelay(MENU_SHOW_ON_EXPAND_START_DELAY);
+ setVisibility(VISIBLE);
+ mMenuContainerAnimator.start();
+ });
+ } else {
+ notifyMenuStateChangeStart(menuState, resizeMenuOnShow, null);
+ setVisibility(VISIBLE);
+ mMenuContainerAnimator.start();
+ }
+ updateActionViews(menuState, stackBounds);
+ } else {
+ // If we are already visible, then just start the delayed dismiss and unregister any
+ // existing input consumers from the previous drag
+ if (allowMenuTimeout) {
+ repostDelayedHide(POST_INTERACTION_DISMISS_DELAY);
+ }
+ }
+ }
+
+ /**
+ * Different from {@link #hideMenu()}, this function does not try to finish this menu activity
+ * and instead, it fades out the controls by setting the alpha to 0 directly without menu
+ * visibility callbacks invoked.
+ */
+ void fadeOutMenu() {
+ mMenuContainer.setAlpha(0f);
+ mSettingsButton.setAlpha(0f);
+ mDismissButton.setAlpha(0f);
+ mEnterSplitButton.setAlpha(0f);
+ }
+
+ void pokeMenu() {
+ cancelDelayedHide();
+ }
+
+ void updateMenuLayout(Rect bounds) {
+ mPipMenuIconsAlgorithm.onBoundsChanged(bounds);
+ }
+
+ void hideMenu() {
+ hideMenu(null);
+ }
+
+ void hideMenu(Runnable animationEndCallback) {
+ hideMenu(animationEndCallback, true /* notifyMenuVisibility */, mDidLastShowMenuResize,
+ ANIM_TYPE_HIDE);
+ }
+
+ void hideMenu(boolean resize, @AnimationType int animationType) {
+ hideMenu(null /* animationFinishedRunnable */, true /* notifyMenuVisibility */, resize,
+ animationType);
+ }
+
+ void hideMenu(final Runnable animationFinishedRunnable, boolean notifyMenuVisibility,
+ boolean resize, @AnimationType int animationType) {
+ if (mMenuState != MENU_STATE_NONE) {
+ cancelDelayedHide();
+ if (notifyMenuVisibility) {
+ notifyMenuStateChangeStart(MENU_STATE_NONE, resize, null);
+ }
+ mMenuContainerAnimator = new AnimatorSet();
+ ObjectAnimator menuAnim = ObjectAnimator.ofFloat(mMenuContainer, View.ALPHA,
+ mMenuContainer.getAlpha(), 0f);
+ menuAnim.addUpdateListener(mMenuBgUpdateListener);
+ ObjectAnimator settingsAnim = ObjectAnimator.ofFloat(mSettingsButton, View.ALPHA,
+ mSettingsButton.getAlpha(), 0f);
+ ObjectAnimator dismissAnim = ObjectAnimator.ofFloat(mDismissButton, View.ALPHA,
+ mDismissButton.getAlpha(), 0f);
+ ObjectAnimator enterSplitAnim = ObjectAnimator.ofFloat(mEnterSplitButton, View.ALPHA,
+ mEnterSplitButton.getAlpha(), 0f);
+ mMenuContainerAnimator.playTogether(menuAnim, settingsAnim, dismissAnim,
+ enterSplitAnim);
+ mMenuContainerAnimator.setInterpolator(Interpolators.ALPHA_OUT);
+ mMenuContainerAnimator.setDuration(getFadeOutDuration(animationType));
+ mMenuContainerAnimator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ setVisibility(GONE);
+ if (notifyMenuVisibility) {
+ notifyMenuStateChangeFinish(MENU_STATE_NONE);
+ }
+ if (animationFinishedRunnable != null) {
+ animationFinishedRunnable.run();
+ }
+ }
+ });
+ mMenuContainerAnimator.start();
+ }
+ }
+
+ /**
+ * @return Estimated minimum {@link Size} to hold the actions.
+ * See also {@link #updateActionViews(Rect)}
+ */
+ Size getEstimatedMinMenuSize() {
+ final int pipActionSize = getResources().getDimensionPixelSize(R.dimen.pip_action_size);
+ // the minimum width would be (2 * pipActionSize) since we have settings and dismiss button
+ // on the top action container.
+ final int width = Math.max(2, mActions.size()) * pipActionSize;
+ final int height = getResources().getDimensionPixelSize(R.dimen.pip_expand_action_size)
+ + getResources().getDimensionPixelSize(R.dimen.pip_action_padding)
+ + getResources().getDimensionPixelSize(R.dimen.pip_expand_container_edge_margin);
+ return new Size(width, height);
+ }
+
+ void setActions(Rect stackBounds, @Nullable List<RemoteAction> actions,
+ @Nullable RemoteAction closeAction) {
+ mActions.clear();
+ if (actions != null && !actions.isEmpty()) {
+ mActions.addAll(actions);
+ }
+ mCloseAction = closeAction;
+ if (mMenuState == MENU_STATE_FULL) {
+ updateActionViews(mMenuState, stackBounds);
+ }
+ }
+
+ private void updateActionViews(int menuState, Rect stackBounds) {
+ ViewGroup expandContainer = findViewById(R.id.expand_container);
+ ViewGroup actionsContainer = findViewById(R.id.actions_container);
+ actionsContainer.setOnTouchListener((v, ev) -> {
+ // Do nothing, prevent click through to parent
+ return true;
+ });
+
+ // Update the expand button only if it should show with the menu
+ expandContainer.setVisibility(menuState == MENU_STATE_FULL
+ ? View.VISIBLE
+ : View.INVISIBLE);
+
+ LayoutParams expandedLp =
+ (LayoutParams) expandContainer.getLayoutParams();
+ if (mActions.isEmpty() || menuState == MENU_STATE_NONE) {
+ actionsContainer.setVisibility(View.INVISIBLE);
+
+ // Update the expand container margin to adjust the center of the expand button to
+ // account for the existence of the action container
+ expandedLp.topMargin = 0;
+ expandedLp.bottomMargin = 0;
+ } else {
+ actionsContainer.setVisibility(View.VISIBLE);
+ if (mActionsGroup != null) {
+ // Ensure we have as many buttons as actions
+ final LayoutInflater inflater = LayoutInflater.from(mContext);
+ while (mActionsGroup.getChildCount() < mActions.size()) {
+ final PipMenuActionView actionView = (PipMenuActionView) inflater.inflate(
+ R.layout.pip_menu_action, mActionsGroup, false);
+ mActionsGroup.addView(actionView);
+ }
+
+ // Update the visibility of all views
+ for (int i = 0; i < mActionsGroup.getChildCount(); i++) {
+ mActionsGroup.getChildAt(i).setVisibility(i < mActions.size()
+ ? View.VISIBLE
+ : View.GONE);
+ }
+
+ // Recreate the layout
+ final boolean isLandscapePip = stackBounds != null
+ && (stackBounds.width() > stackBounds.height());
+ for (int i = 0; i < mActions.size(); i++) {
+ final RemoteAction action = mActions.get(i);
+ final PipMenuActionView actionView =
+ (PipMenuActionView) mActionsGroup.getChildAt(i);
+ final boolean isCloseAction = mCloseAction != null && Objects.equals(
+ mCloseAction.getActionIntent(), action.getActionIntent());
+
+ final int iconType = action.getIcon().getType();
+ if (iconType == Icon.TYPE_URI || iconType == Icon.TYPE_URI_ADAPTIVE_BITMAP) {
+ // Disallow loading icon from content URI
+ actionView.setImageDrawable(null);
+ } else {
+ // TODO: Check if the action drawable has changed before we reload it
+ action.getIcon().loadDrawableAsync(mContext, d -> {
+ if (d != null) {
+ d.setTint(Color.WHITE);
+ actionView.setImageDrawable(d);
+ }
+ }, mMainHandler);
+ }
+ actionView.setCustomCloseBackgroundVisibility(
+ isCloseAction ? View.VISIBLE : View.GONE);
+ actionView.setContentDescription(action.getContentDescription());
+ if (action.isEnabled()) {
+ actionView.setOnClickListener(
+ v -> onActionViewClicked(action.getActionIntent(), isCloseAction));
+ }
+ actionView.setEnabled(action.isEnabled());
+ actionView.setAlpha(action.isEnabled() ? 1f : DISABLED_ACTION_ALPHA);
+
+ // Update the margin between actions
+ LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams)
+ actionView.getLayoutParams();
+ lp.leftMargin = (isLandscapePip && i > 0) ? mBetweenActionPaddingLand : 0;
+ }
+ }
+
+ // Update the expand container margin to adjust the center of the expand button to
+ // account for the existence of the action container
+ expandedLp.topMargin = getResources().getDimensionPixelSize(
+ R.dimen.pip_action_padding);
+ expandedLp.bottomMargin = getResources().getDimensionPixelSize(
+ R.dimen.pip_expand_container_edge_margin);
+ }
+ expandContainer.requestLayout();
+ }
+
+ private void notifyMenuStateChangeStart(int menuState, boolean resize, Runnable callback) {
+ mController.onMenuStateChangeStart(menuState, resize, callback);
+ }
+
+ private void notifyMenuStateChangeFinish(int menuState) {
+ mMenuState = menuState;
+ mController.onMenuStateChangeFinish(menuState);
+ }
+
+ private void expandPip() {
+ // Do not notify menu visibility when hiding the menu, the controller will do this when it
+ // handles the message
+ hideMenu(mController::onPipExpand, false /* notifyMenuVisibility */, true /* resize */,
+ ANIM_TYPE_HIDE);
+ mPipUiEventLogger.log(
+ PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_EXPAND_TO_FULLSCREEN);
+ }
+
+ private void dismissPip() {
+ if (mMenuState != MENU_STATE_NONE) {
+ // Do not call hideMenu() directly. Instead, let the menu controller handle it just as
+ // any other dismissal that will update the touch state and fade out the PIP task
+ // and the menu view at the same time.
+ mController.onPipDismiss();
+ mPipUiEventLogger.log(PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_TAP_TO_REMOVE);
+ }
+ }
+
+ /**
+ * Execute the {@link PendingIntent} attached to the {@link PipMenuActionView}.
+ * If the given {@link PendingIntent} matches {@link #mCloseAction}, we need to make sure
+ * the PiP is removed after a certain timeout in case the app does not respond in a
+ * timely manner.
+ */
+ private void onActionViewClicked(@NonNull PendingIntent intent, boolean isCloseAction) {
+ try {
+ intent.send();
+ } catch (PendingIntent.CanceledException e) {
+ ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "%s: Failed to send action, %s", TAG, e);
+ }
+ if (isCloseAction) {
+ mPipUiEventLogger.log(PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_CUSTOM_CLOSE);
+ mAllowTouches = false;
+ mMainExecutor.executeDelayed(() -> {
+ hideMenu();
+ // TODO: it's unsafe to call onPipDismiss with a delay here since
+ // we may have a different PiP by the time this runnable is executed.
+ mController.onPipDismiss();
+ mAllowTouches = true;
+ }, mPipForceCloseDelay);
+ }
+ }
+
+ private void enterSplit() {
+ // Do not notify menu visibility when hiding the menu, the controller will do this when it
+ // handles the message
+ hideMenu(mController::onEnterSplit, false /* notifyMenuVisibility */, true /* resize */,
+ ANIM_TYPE_HIDE);
+ }
+
+
+ private void showSettings() {
+ final Pair<ComponentName, Integer> topPipActivityInfo =
+ PipUtils.getTopPipActivity(mContext);
+ if (topPipActivityInfo.first != null) {
+ final Intent settingsIntent = new Intent(ACTION_PICTURE_IN_PICTURE_SETTINGS,
+ Uri.fromParts("package", topPipActivityInfo.first.getPackageName(), null));
+ settingsIntent.setFlags(FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_CLEAR_TASK);
+ mContext.startActivityAsUser(settingsIntent, UserHandle.of(topPipActivityInfo.second));
+ mPipUiEventLogger.log(PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_SHOW_SETTINGS);
+ }
+ }
+
+ private void cancelDelayedHide() {
+ mMainExecutor.removeCallbacks(mHideMenuRunnable);
+ }
+
+ private void repostDelayedHide(int delay) {
+ int recommendedTimeout = mAccessibilityManager.getRecommendedTimeoutMillis(delay,
+ FLAG_CONTENT_ICONS | FLAG_CONTENT_CONTROLS);
+ mMainExecutor.removeCallbacks(mHideMenuRunnable);
+ mMainExecutor.executeDelayed(mHideMenuRunnable, recommendedTimeout);
+ }
+
+ private long getFadeOutDuration(@AnimationType int animationType) {
+ switch (animationType) {
+ case ANIM_TYPE_NONE:
+ return ANIMATION_NONE_DURATION_MS;
+ case ANIM_TYPE_HIDE:
+ return ANIMATION_HIDE_DURATION_MS;
+ case ANIM_TYPE_DISMISS:
+ return mDismissFadeOutDurationMs;
+ default:
+ throw new IllegalStateException("Invalid animation type " + animationType);
+ }
+ }
+}
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 f3d178a..fbf4d13 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
@@ -42,8 +42,8 @@
import com.android.wm.shell.ShellTaskOrganizer;
import com.android.wm.shell.common.pip.PipBoundsAlgorithm;
import com.android.wm.shell.common.pip.PipBoundsState;
+import com.android.wm.shell.common.pip.PipMenuController;
import com.android.wm.shell.common.pip.PipUtils;
-import com.android.wm.shell.pip.PipMenuController;
import com.android.wm.shell.pip.PipTransitionController;
import com.android.wm.shell.sysui.ShellInit;
import com.android.wm.shell.transition.Transitions;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java
index 1232baa..97d3457 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java
@@ -58,10 +58,10 @@
import com.android.internal.protolog.common.ProtoLog;
import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.protolog.ShellProtoLogGroup;
+import com.android.wm.shell.shared.TransitionUtil;
import com.android.wm.shell.sysui.ShellInit;
import com.android.wm.shell.transition.HomeTransitionObserver;
import com.android.wm.shell.transition.Transitions;
-import com.android.wm.shell.util.TransitionUtil;
import java.util.ArrayList;
import java.util.function.Consumer;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java
index 5de8a9b..e8894a83 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java
@@ -48,9 +48,9 @@
import com.android.wm.shell.common.TransactionPool;
import com.android.wm.shell.common.split.SplitDecorManager;
import com.android.wm.shell.protolog.ShellProtoLogGroup;
+import com.android.wm.shell.shared.TransitionUtil;
import com.android.wm.shell.transition.OneShotRemoteHandler;
import com.android.wm.shell.transition.Transitions;
-import com.android.wm.shell.util.TransitionUtil;
import java.util.ArrayList;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
index af05aa2..e5045ae 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
@@ -44,6 +44,8 @@
import static com.android.wm.shell.common.split.SplitScreenConstants.splitPositionToString;
import static com.android.wm.shell.common.split.SplitScreenUtils.reverseSplitPosition;
import static com.android.wm.shell.common.split.SplitScreenUtils.splitFailureMessage;
+import static com.android.wm.shell.shared.TransitionUtil.isClosingType;
+import static com.android.wm.shell.shared.TransitionUtil.isOpeningType;
import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_MAIN;
import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_SIDE;
import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_UNDEFINED;
@@ -64,8 +66,6 @@
import static com.android.wm.shell.transition.Transitions.ENABLE_SHELL_TRANSITIONS;
import static com.android.wm.shell.transition.Transitions.TRANSIT_SPLIT_SCREEN_OPEN_TO_SIDE;
import static com.android.wm.shell.transition.Transitions.TRANSIT_SPLIT_SCREEN_PAIR_OPEN;
-import static com.android.wm.shell.util.TransitionUtil.isClosingType;
-import static com.android.wm.shell.util.TransitionUtil.isOpeningType;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
@@ -135,13 +135,13 @@
import com.android.wm.shell.common.split.SplitWindowManager;
import com.android.wm.shell.protolog.ShellProtoLogGroup;
import com.android.wm.shell.recents.RecentTasksController;
+import com.android.wm.shell.shared.TransitionUtil;
import com.android.wm.shell.splitscreen.SplitScreen.StageType;
import com.android.wm.shell.splitscreen.SplitScreenController.ExitReason;
import com.android.wm.shell.transition.DefaultMixedHandler;
import com.android.wm.shell.transition.LegacyTransitions;
import com.android.wm.shell.transition.Transitions;
import com.android.wm.shell.util.SplitBounds;
-import com.android.wm.shell.util.TransitionUtil;
import com.android.wm.shell.windowdecor.WindowDecorViewModel;
import dalvik.annotation.optimization.NeverCompile;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTransitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTransitions.java
index 84f21f6..198ec82 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTransitions.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTransitions.java
@@ -37,8 +37,8 @@
import androidx.annotation.VisibleForTesting;
+import com.android.wm.shell.shared.TransitionUtil;
import com.android.wm.shell.transition.Transitions;
-import com.android.wm.shell.util.TransitionUtil;
import java.util.ArrayList;
import java.util.Objects;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/CounterRotatorHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/CounterRotatorHelper.java
index 628ce27..b03daaa 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/CounterRotatorHelper.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/CounterRotatorHelper.java
@@ -27,8 +27,8 @@
import androidx.annotation.NonNull;
-import com.android.wm.shell.util.CounterRotator;
-import com.android.wm.shell.util.TransitionUtil;
+import com.android.wm.shell.shared.CounterRotator;
+import com.android.wm.shell.shared.TransitionUtil;
import java.util.List;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java
index 8c2203e..8746b8c 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java
@@ -24,7 +24,7 @@
import static android.view.WindowManager.TRANSIT_PIP;
import static android.window.TransitionInfo.FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY;
-import static com.android.wm.shell.util.TransitionUtil.isOpeningType;
+import static com.android.wm.shell.shared.TransitionUtil.isOpeningType;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -48,11 +48,11 @@
import com.android.wm.shell.pip.PipTransitionController;
import com.android.wm.shell.protolog.ShellProtoLogGroup;
import com.android.wm.shell.recents.RecentsTransitionHandler;
+import com.android.wm.shell.shared.TransitionUtil;
import com.android.wm.shell.splitscreen.SplitScreenController;
import com.android.wm.shell.splitscreen.StageCoordinator;
import com.android.wm.shell.sysui.ShellInit;
import com.android.wm.shell.unfold.UnfoldTransitionHandler;
-import com.android.wm.shell.util.TransitionUtil;
import java.util.ArrayList;
import java.util.Map;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
index 193a4fb..c70a821 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
@@ -109,8 +109,8 @@
import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.common.TransactionPool;
import com.android.wm.shell.protolog.ShellProtoLogGroup;
+import com.android.wm.shell.shared.TransitionUtil;
import com.android.wm.shell.sysui.ShellInit;
-import com.android.wm.shell.util.TransitionUtil;
import java.util.ArrayList;
import java.util.List;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/HomeTransitionObserver.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/HomeTransitionObserver.java
index af31f5f..cb2944c 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/HomeTransitionObserver.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/HomeTransitionObserver.java
@@ -32,7 +32,7 @@
import com.android.wm.shell.common.RemoteCallable;
import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.common.SingleInstanceRemoteListener;
-import com.android.wm.shell.util.TransitionUtil;
+import com.android.wm.shell.shared.TransitionUtil;
/**
* The {@link TransitionObserver} that observes for transitions involving the home
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RemoteTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RemoteTransitionHandler.java
index 293b660..4c4c580 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RemoteTransitionHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RemoteTransitionHandler.java
@@ -39,7 +39,7 @@
import com.android.internal.protolog.common.ProtoLog;
import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.protolog.ShellProtoLogGroup;
-import com.android.wm.shell.util.TransitionUtil;
+import com.android.wm.shell.shared.TransitionUtil;
import java.util.ArrayList;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/TransitionAnimationHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/TransitionAnimationHelper.java
index b012d35..1be85d0 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/TransitionAnimationHelper.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/TransitionAnimationHelper.java
@@ -56,7 +56,7 @@
import com.android.internal.policy.TransitionAnimation;
import com.android.internal.protolog.common.ProtoLog;
import com.android.wm.shell.protolog.ShellProtoLogGroup;
-import com.android.wm.shell.util.TransitionUtil;
+import com.android.wm.shell.shared.TransitionUtil;
/** The helper class that provides methods for adding styles to transition animations. */
public class TransitionAnimationHelper {
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 67fc7e2..5e79681 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
@@ -37,9 +37,9 @@
import static android.window.TransitionInfo.FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT;
import static com.android.wm.shell.common.ExecutorUtils.executeRemoteCallWithTaskPermission;
+import static com.android.wm.shell.shared.TransitionUtil.isClosingType;
+import static com.android.wm.shell.shared.TransitionUtil.isOpeningType;
import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_SHELL_TRANSITIONS;
-import static com.android.wm.shell.util.TransitionUtil.isClosingType;
-import static com.android.wm.shell.util.TransitionUtil.isOpeningType;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -80,13 +80,13 @@
import com.android.wm.shell.common.annotations.ExternalThread;
import com.android.wm.shell.keyguard.KeyguardTransitionHandler;
import com.android.wm.shell.protolog.ShellProtoLogGroup;
+import com.android.wm.shell.shared.TransitionUtil;
import com.android.wm.shell.sysui.ShellCommandHandler;
import com.android.wm.shell.sysui.ShellController;
import com.android.wm.shell.sysui.ShellInit;
import com.android.wm.shell.transition.tracing.LegacyTransitionTracer;
import com.android.wm.shell.transition.tracing.PerfettoTransitionTracer;
import com.android.wm.shell.transition.tracing.TransitionTracer;
-import com.android.wm.shell.util.TransitionUtil;
import java.io.PrintWriter;
import java.util.ArrayList;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/UnfoldTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/UnfoldTransitionHandler.java
index 98d343b..c26604a 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/UnfoldTransitionHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/UnfoldTransitionHandler.java
@@ -35,6 +35,7 @@
import com.android.internal.protolog.common.ProtoLog;
import com.android.wm.shell.common.TransactionPool;
+import com.android.wm.shell.shared.TransitionUtil;
import com.android.wm.shell.sysui.ShellInit;
import com.android.wm.shell.transition.Transitions;
import com.android.wm.shell.transition.Transitions.TransitionFinishCallback;
@@ -43,7 +44,6 @@
import com.android.wm.shell.unfold.animation.FullscreenUnfoldTaskAnimator;
import com.android.wm.shell.unfold.animation.SplitTaskUnfoldAnimator;
import com.android.wm.shell.unfold.animation.UnfoldTaskAnimator;
-import com.android.wm.shell.util.TransitionUtil;
import java.util.ArrayList;
import java.util.List;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java
index e6faa63..96eaa1e 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java
@@ -287,7 +287,7 @@
}
boolean isHandlingDragResize() {
- return mDragResizeListener.isHandlingDragResize();
+ return mDragResizeListener != null && mDragResizeListener.isHandlingDragResize();
}
private void closeDragResizeListener() {
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 4ba05ce..a8b39c41 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
@@ -345,7 +345,7 @@
mTaskOperations.injectBackKey();
} else if (id == R.id.caption_handle || id == R.id.open_menu_button) {
if (!decoration.isHandleMenuActive()) {
- moveTaskToFront(mTaskOrganizer.getRunningTaskInfo(mTaskId));
+ moveTaskToFront(decoration.mTaskInfo);
decoration.createHandleMenu();
} else {
decoration.closeHandleMenu();
@@ -419,10 +419,10 @@
&& id != R.id.maximize_window) {
return false;
}
- moveTaskToFront(mTaskOrganizer.getRunningTaskInfo(mTaskId));
+ final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(mTaskId);
+ moveTaskToFront(decoration.mTaskInfo);
if (!mHasLongClicked && id != R.id.maximize_window) {
- final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(mTaskId);
decoration.closeMaximizeMenuIfNeeded(e);
}
@@ -466,7 +466,8 @@
*/
@Override
public boolean handleMotionEvent(@Nullable View v, MotionEvent e) {
- final RunningTaskInfo taskInfo = mTaskOrganizer.getRunningTaskInfo(mTaskId);
+ final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(mTaskId);
+ final RunningTaskInfo taskInfo = decoration.mTaskInfo;
if (DesktopModeStatus.isEnabled()
&& taskInfo.getWindowingMode() == WINDOWING_MODE_FULLSCREEN) {
return false;
@@ -492,8 +493,6 @@
}
case MotionEvent.ACTION_MOVE: {
mShouldClick = false;
- final DesktopModeWindowDecoration decoration =
- mWindowDecorByTaskId.get(mTaskId);
// If a decor's resize drag zone is active, don't also try to reposition it.
if (decoration.isHandlingDragResize()) break;
decoration.closeMaximizeMenu();
@@ -557,9 +556,10 @@
&& 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 -> {
+ final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(mTaskId);
+ c.toggleDesktopTaskSize(decoration.mTaskInfo, decoration);
+ });
return true;
}
}
@@ -843,7 +843,18 @@
@Nullable
private DesktopModeWindowDecoration getRelevantWindowDecor(MotionEvent ev) {
- if (mSplitScreenController != null && mSplitScreenController.isSplitScreenVisible()) {
+ final DesktopModeWindowDecoration focusedDecor = getFocusedDecor();
+ if (focusedDecor == null) {
+ return null;
+ }
+ final boolean splitScreenVisible = mSplitScreenController != null
+ && mSplitScreenController.isSplitScreenVisible();
+ // It's possible that split tasks are visible but neither is focused, such as when there's
+ // a fullscreen translucent window on top of them. In that case, the relevant decor should
+ // just be that translucent focused window.
+ final boolean focusedTaskInSplit = mSplitScreenController != null
+ && mSplitScreenController.isTaskInSplitScreen(focusedDecor.mTaskInfo.taskId);
+ if (splitScreenVisible && focusedTaskInSplit) {
// We can't look at focused task here as only one task will have focus.
DesktopModeWindowDecoration splitTaskDecor = getSplitScreenDecor(ev);
return splitTaskDecor == null ? getFocusedDecor() : splitTaskDecor;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
index 2023333..3f0a281 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
@@ -36,7 +36,6 @@
import android.graphics.Region;
import android.graphics.drawable.Drawable;
import android.os.Handler;
-import android.util.Log;
import android.view.Choreographer;
import android.view.MotionEvent;
import android.view.SurfaceControl;
@@ -388,27 +387,20 @@
}
boolean isHandlingDragResize() {
- return mDragResizeListener.isHandlingDragResize();
+ return mDragResizeListener != null && mDragResizeListener.isHandlingDragResize();
}
private void loadAppInfo() {
- String packageName = mTaskInfo.realActivity.getPackageName();
PackageManager pm = mContext.getApplicationContext().getPackageManager();
- try {
- final IconProvider provider = new IconProvider(mContext);
- mAppIconDrawable = provider.getIcon(pm.getActivityInfo(mTaskInfo.baseActivity,
- PackageManager.ComponentInfoFlags.of(0)));
- final Resources resources = mContext.getResources();
- final BaseIconFactory factory = new BaseIconFactory(mContext,
- resources.getDisplayMetrics().densityDpi,
- resources.getDimensionPixelSize(R.dimen.desktop_mode_caption_icon_radius));
- mAppIconBitmap = factory.createScaledBitmap(mAppIconDrawable, MODE_DEFAULT);
- final ApplicationInfo applicationInfo = pm.getApplicationInfo(packageName,
- PackageManager.ApplicationInfoFlags.of(0));
- mAppName = pm.getApplicationLabel(applicationInfo);
- } catch (PackageManager.NameNotFoundException e) {
- Log.w(TAG, "Package not found: " + packageName, e);
- }
+ final IconProvider provider = new IconProvider(mContext);
+ mAppIconDrawable = provider.getIcon(mTaskInfo.topActivityInfo);
+ final Resources resources = mContext.getResources();
+ final BaseIconFactory factory = new BaseIconFactory(mContext,
+ resources.getDisplayMetrics().densityDpi,
+ resources.getDimensionPixelSize(R.dimen.desktop_mode_caption_icon_radius));
+ mAppIconBitmap = factory.createScaledBitmap(mAppIconDrawable, MODE_DEFAULT);
+ final ApplicationInfo applicationInfo = mTaskInfo.topActivityInfo.applicationInfo;
+ mAppName = pm.getApplicationLabel(applicationInfo);
}
private void closeDragResizeListener() {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeInputListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeInputListener.java
index 8b38f99..d902621 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeInputListener.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeInputListener.java
@@ -353,6 +353,7 @@
private boolean mShouldHandleEvents;
private int mLastCursorType = PointerIcon.TYPE_DEFAULT;
private Rect mDragStartTaskBounds;
+ private final Rect mTmpRect = new Rect();
private TaskResizeInputEventReceiver(
InputChannel inputChannel, Handler handler, Choreographer choreographer) {
@@ -477,14 +478,15 @@
}
private void updateInputSinkRegionForDrag(Rect taskBounds) {
+ mTmpRect.set(taskBounds);
final DisplayLayout layout = mDisplayController.getDisplayLayout(mDisplayId);
final Region dragTouchRegion = new Region(-taskBounds.left,
-taskBounds.top,
-taskBounds.left + layout.width(),
-taskBounds.top + layout.height());
// Remove the localized task bounds from the touch region.
- taskBounds.offsetTo(0, 0);
- dragTouchRegion.op(taskBounds, Region.Op.DIFFERENCE);
+ mTmpRect.offsetTo(0, 0);
+ dragTouchRegion.op(mTmpRect, Region.Op.DIFFERENCE);
updateSinkInputChannel(dragTouchRegion);
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/ResizeVeil.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/ResizeVeil.java
index 368231e..b0d3b50 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/ResizeVeil.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/ResizeVeil.java
@@ -125,6 +125,7 @@
relayout(taskBounds, t);
if (fadeIn) {
+ cancelAnimation();
mVeilAnimator = new ValueAnimator();
mVeilAnimator.setFloatValues(0f, 1f);
mVeilAnimator.setDuration(RESIZE_ALPHA_DURATION);
@@ -210,15 +211,16 @@
* Animate veil's alpha to 0, fading it out.
*/
public void hideVeil() {
- final ValueAnimator animator = new ValueAnimator();
- animator.setFloatValues(1, 0);
- animator.setDuration(RESIZE_ALPHA_DURATION);
- animator.addUpdateListener(animation -> {
+ cancelAnimation();
+ mVeilAnimator = new ValueAnimator();
+ mVeilAnimator.setFloatValues(1, 0);
+ mVeilAnimator.setDuration(RESIZE_ALPHA_DURATION);
+ mVeilAnimator.addUpdateListener(animation -> {
SurfaceControl.Transaction t = mSurfaceControlTransactionSupplier.get();
- t.setAlpha(mVeilSurface, 1 - animator.getAnimatedFraction());
+ t.setAlpha(mVeilSurface, 1 - mVeilAnimator.getAnimatedFraction());
t.apply();
});
- animator.addListener(new AnimatorListenerAdapter() {
+ mVeilAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
SurfaceControl.Transaction t = mSurfaceControlTransactionSupplier.get();
@@ -226,7 +228,7 @@
t.apply();
}
});
- animator.start();
+ mVeilAnimator.start();
}
@ColorRes
@@ -240,10 +242,20 @@
}
}
+ private void cancelAnimation() {
+ if (mVeilAnimator != null) {
+ mVeilAnimator.removeAllUpdateListeners();
+ mVeilAnimator.cancel();
+ }
+ }
+
/**
* Dispose of veil when it is no longer needed, likely on close of its container decor.
*/
void dispose() {
+ cancelAnimation();
+ mVeilAnimator = null;
+
if (mViewHost != null) {
mViewHost.release();
mViewHost = null;
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleViewInfoTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleViewInfoTest.kt
index 4878df8..75965d6 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleViewInfoTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleViewInfoTest.kt
@@ -56,9 +56,7 @@
import org.mockito.kotlin.mock
import org.mockito.kotlin.whenever
-/**
- * Tests for loading / inflating views & icons for a bubble.
- */
+/** Tests for loading / inflating views & icons for a bubble. */
@SmallTest
@RunWith(AndroidTestingRunner::class)
@RunWithLooper(setAsMainLooper = true)
@@ -76,25 +74,33 @@
@Before
fun setup() {
metadataFlagListener = Bubbles.BubbleMetadataFlagListener {}
- iconFactory = BubbleIconFactory(context,
+ iconFactory =
+ BubbleIconFactory(
+ context,
60,
30,
Color.RED,
- mContext.resources.getDimensionPixelSize(
- R.dimen.importance_ring_stroke_width))
+ mContext.resources.getDimensionPixelSize(R.dimen.importance_ring_stroke_width)
+ )
mainExecutor = TestShellExecutor()
val windowManager = context.getSystemService(WindowManager::class.java)
val shellInit = ShellInit(mainExecutor)
val shellCommandHandler = ShellCommandHandler()
- val shellController = ShellController(context, shellInit, shellCommandHandler,
- mainExecutor)
+ val shellController = ShellController(context, shellInit, shellCommandHandler, mainExecutor)
val bubblePositioner = BubblePositioner(context, windowManager)
- val bubbleData = BubbleData(context, mock<BubbleLogger>(), bubblePositioner,
- BubbleEducationController(context), mainExecutor)
+ val bubbleData =
+ BubbleData(
+ context,
+ mock<BubbleLogger>(),
+ bubblePositioner,
+ BubbleEducationController(context),
+ mainExecutor
+ )
val surfaceSynchronizer = { obj: Runnable -> obj.run() }
- bubbleController = BubbleController(
+ bubbleController =
+ BubbleController(
context,
shellInit,
shellCommandHandler,
@@ -122,18 +128,36 @@
mock<Transitions>(),
mock<SyncTransactionQueue>(),
mock<IWindowManager>(),
- mock<BubbleProperties>())
+ mock<BubbleProperties>()
+ )
- bubbleStackView = BubbleStackView(context, bubbleController, bubbleData,
- surfaceSynchronizer, FloatingContentCoordinator(), bubbleController, mainExecutor)
+ val bubbleStackViewManager = BubbleStackViewManager.fromBubbleController(bubbleController)
+ bubbleStackView =
+ BubbleStackView(
+ context,
+ bubbleStackViewManager,
+ bubblePositioner,
+ bubbleData,
+ surfaceSynchronizer,
+ FloatingContentCoordinator(),
+ bubbleController,
+ mainExecutor
+ )
bubbleBarLayerView = BubbleBarLayerView(context, bubbleController)
}
@Test
fun testPopulate() {
bubble = createBubbleWithShortcut()
- val info = BubbleViewInfoTask.BubbleViewInfo.populate(context,
- bubbleController, bubbleStackView, iconFactory, bubble, false /* skipInflation */)
+ val info =
+ BubbleViewInfoTask.BubbleViewInfo.populate(
+ context,
+ bubbleController,
+ bubbleStackView,
+ iconFactory,
+ bubble,
+ false /* skipInflation */
+ )
assertThat(info!!).isNotNull()
assertThat(info.imageView).isNotNull()
@@ -151,9 +175,15 @@
@Test
fun testPopulateForBubbleBar() {
bubble = createBubbleWithShortcut()
- val info = BubbleViewInfoTask.BubbleViewInfo.populateForBubbleBar(context,
- bubbleController, bubbleBarLayerView, iconFactory, bubble,
- false /* skipInflation */)
+ val info =
+ BubbleViewInfoTask.BubbleViewInfo.populateForBubbleBar(
+ context,
+ bubbleController,
+ bubbleBarLayerView,
+ iconFactory,
+ bubble,
+ false /* skipInflation */
+ )
assertThat(info!!).isNotNull()
assertThat(info.imageView).isNull()
@@ -176,12 +206,18 @@
// exception here if the app has an issue loading the shortcut icon; we default to
// the app icon in that case / none of the icons will be null.
val mockIconFactory = mock<BubbleIconFactory>()
- whenever(mockIconFactory.getBubbleDrawable(eq(context), eq(bubble.shortcutInfo),
- any())).doThrow(RuntimeException())
+ whenever(mockIconFactory.getBubbleDrawable(eq(context), eq(bubble.shortcutInfo), any()))
+ .doThrow(RuntimeException())
- val info = BubbleViewInfoTask.BubbleViewInfo.populateForBubbleBar(context,
- bubbleController, bubbleBarLayerView, iconFactory, bubble,
- true /* skipInflation */)
+ val info =
+ BubbleViewInfoTask.BubbleViewInfo.populateForBubbleBar(
+ context,
+ bubbleController,
+ bubbleBarLayerView,
+ iconFactory,
+ bubble,
+ true /* skipInflation */
+ )
assertThat(info).isNotNull()
assertThat(info?.shortcutInfo).isNotNull()
@@ -194,8 +230,17 @@
private fun createBubbleWithShortcut(): Bubble {
val shortcutInfo = ShortcutInfo.Builder(mContext, "mockShortcutId").build()
- return Bubble("mockKey", shortcutInfo, 1000, Resources.ID_NULL,
- "mockTitle", 0 /* taskId */, "mockLocus", true /* isDismissible */,
- mainExecutor, metadataFlagListener)
+ return Bubble(
+ "mockKey",
+ shortcutInfo,
+ 1000,
+ Resources.ID_NULL,
+ "mockTitle",
+ 0 /* taskId */,
+ "mockLocus",
+ true /* isDismissible */,
+ mainExecutor,
+ metadataFlagListener
+ )
}
-}
\ No newline at end of file
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt
index 3fe78ef..445f74a 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt
@@ -124,7 +124,7 @@
repo.addVisibleTasksListener(listener, executor)
executor.flushAll()
- assertThat(listener.hasVisibleTasksOnDefaultDisplay).isTrue()
+ assertThat(listener.visibleTasksCountOnDefaultDisplay).isEqualTo(1)
assertThat(listener.visibleChangesOnDefaultDisplay).isEqualTo(1)
}
@@ -148,7 +148,7 @@
repo.addVisibleTasksListener(listener, executor)
executor.flushAll()
- assertThat(listener.hasVisibleTasksOnDefaultDisplay).isFalse()
+ assertThat(listener.visibleTasksCountOnDefaultDisplay).isEqualTo(0)
// One call as adding listener notifies it
assertThat(listener.visibleChangesOnDefaultDisplay).isEqualTo(0)
}
@@ -162,8 +162,8 @@
repo.updateVisibleFreeformTasks(DEFAULT_DISPLAY, taskId = 2, visible = true)
executor.flushAll()
- assertThat(listener.hasVisibleTasksOnDefaultDisplay).isTrue()
- assertThat(listener.visibleChangesOnDefaultDisplay).isEqualTo(1)
+ assertThat(listener.visibleTasksCountOnDefaultDisplay).isEqualTo(2)
+ assertThat(listener.visibleChangesOnDefaultDisplay).isEqualTo(2)
}
@Test
@@ -175,16 +175,16 @@
repo.updateVisibleFreeformTasks(DEFAULT_DISPLAY, taskId = 1, visible = true)
executor.flushAll()
- assertThat(listener.hasVisibleTasksOnDefaultDisplay).isTrue()
+ assertThat(listener.visibleTasksCountOnDefaultDisplay).isEqualTo(1)
assertThat(listener.visibleChangesOnDefaultDisplay).isEqualTo(1)
- assertThat(listener.hasVisibleTasksOnSecondaryDisplay).isFalse()
+ assertThat(listener.visibleTasksCountOnSecondaryDisplay).isEqualTo(0)
assertThat(listener.visibleChangesOnSecondaryDisplay).isEqualTo(0)
repo.updateVisibleFreeformTasks(displayId = 1, taskId = 2, visible = true)
executor.flushAll()
// Listener for secondary display is notified
- assertThat(listener.hasVisibleTasksOnSecondaryDisplay).isTrue()
+ assertThat(listener.visibleTasksCountOnSecondaryDisplay).isEqualTo(1)
assertThat(listener.visibleChangesOnSecondaryDisplay).isEqualTo(1)
// No changes to listener for default display
assertThat(listener.visibleChangesOnDefaultDisplay).isEqualTo(1)
@@ -198,7 +198,7 @@
repo.updateVisibleFreeformTasks(DEFAULT_DISPLAY, taskId = 1, visible = true)
executor.flushAll()
- assertThat(listener.hasVisibleTasksOnDefaultDisplay).isTrue()
+ assertThat(listener.visibleTasksCountOnDefaultDisplay).isEqualTo(1)
// Mark task 1 visible on secondary display
repo.updateVisibleFreeformTasks(displayId = 1, taskId = 1, visible = true)
@@ -208,11 +208,11 @@
// 1 - visible task added
// 2 - visible task removed
assertThat(listener.visibleChangesOnDefaultDisplay).isEqualTo(2)
- assertThat(listener.hasVisibleTasksOnDefaultDisplay).isFalse()
+ assertThat(listener.visibleTasksCountOnDefaultDisplay).isEqualTo(0)
// Secondary display should have 1 call for visible task added
assertThat(listener.visibleChangesOnSecondaryDisplay).isEqualTo(1)
- assertThat(listener.hasVisibleTasksOnSecondaryDisplay).isTrue()
+ assertThat(listener.visibleTasksCountOnSecondaryDisplay).isEqualTo(1)
}
@Test
@@ -224,17 +224,17 @@
repo.updateVisibleFreeformTasks(DEFAULT_DISPLAY, taskId = 2, visible = true)
executor.flushAll()
- assertThat(listener.hasVisibleTasksOnDefaultDisplay).isTrue()
+ assertThat(listener.visibleTasksCountOnDefaultDisplay).isEqualTo(2)
repo.updateVisibleFreeformTasks(DEFAULT_DISPLAY, taskId = 1, visible = false)
executor.flushAll()
- assertThat(listener.visibleChangesOnDefaultDisplay).isEqualTo(1)
+ assertThat(listener.visibleChangesOnDefaultDisplay).isEqualTo(3)
repo.updateVisibleFreeformTasks(DEFAULT_DISPLAY, taskId = 2, visible = false)
executor.flushAll()
- assertThat(listener.hasVisibleTasksOnDefaultDisplay).isFalse()
- assertThat(listener.visibleChangesOnDefaultDisplay).isEqualTo(2)
+ assertThat(listener.visibleTasksCountOnDefaultDisplay).isEqualTo(0)
+ assertThat(listener.visibleChangesOnDefaultDisplay).isEqualTo(4)
}
@Test
@@ -397,8 +397,8 @@
}
class TestVisibilityListener : DesktopModeTaskRepository.VisibleTasksListener {
- var hasVisibleTasksOnDefaultDisplay = false
- var hasVisibleTasksOnSecondaryDisplay = false
+ var visibleTasksCountOnDefaultDisplay = 0
+ var visibleTasksCountOnSecondaryDisplay = 0
var visibleChangesOnDefaultDisplay = 0
var visibleChangesOnSecondaryDisplay = 0
@@ -409,14 +409,14 @@
var stashedChangesOnDefaultDisplay = 0
var stashedChangesOnSecondaryDisplay = 0
- override fun onVisibilityChanged(displayId: Int, hasVisibleFreeformTasks: Boolean) {
+ override fun onTasksVisibilityChanged(displayId: Int, visibleTasksCount: Int) {
when (displayId) {
DEFAULT_DISPLAY -> {
- hasVisibleTasksOnDefaultDisplay = hasVisibleFreeformTasks
+ visibleTasksCountOnDefaultDisplay = visibleTasksCount
visibleChangesOnDefaultDisplay++
}
SECOND_DISPLAY -> {
- hasVisibleTasksOnSecondaryDisplay = hasVisibleFreeformTasks
+ visibleTasksCountOnSecondaryDisplay = visibleTasksCount
visibleChangesOnSecondaryDisplay++
}
else -> fail("Visible task listener received unexpected display id: $displayId")
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java
index 193f16d..40e61dd 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java
@@ -27,6 +27,8 @@
import android.app.ActivityManager;
import android.content.ComponentName;
+import android.content.pm.ActivityInfo;
+import android.content.pm.ApplicationInfo;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.content.res.TypedArray;
@@ -202,6 +204,8 @@
.setTaskDescriptionBuilder(taskDescriptionBuilder)
.setVisible(visible)
.build();
+ taskInfo.topActivityInfo = new ActivityInfo();
+ taskInfo.topActivityInfo.applicationInfo = new ApplicationInfo();
taskInfo.realActivity = new ComponentName("com.android.wm.shell.windowdecor",
"DesktopModeWindowDecorationTests");
taskInfo.baseActivity = new ComponentName("com.android.wm.shell.windowdecor",
diff --git a/location/api/current.txt b/location/api/current.txt
index c55676b..c7954fe 100644
--- a/location/api/current.txt
+++ b/location/api/current.txt
@@ -682,6 +682,7 @@
public final class AltitudeConverter {
ctor public AltitudeConverter();
method @WorkerThread public void addMslAltitudeToLocation(@NonNull android.content.Context, @NonNull android.location.Location) throws java.io.IOException;
+ method @FlaggedApi(Flags.FLAG_GEOID_HEIGHTS_VIA_ALTITUDE_HAL) public boolean addMslAltitudeToLocation(@NonNull android.location.Location);
}
}
diff --git a/location/java/android/location/altitude/AltitudeConverter.java b/location/java/android/location/altitude/AltitudeConverter.java
index 6f88912..461dafb 100644
--- a/location/java/android/location/altitude/AltitudeConverter.java
+++ b/location/java/android/location/altitude/AltitudeConverter.java
@@ -16,12 +16,14 @@
package android.location.altitude;
+import android.annotation.FlaggedApi;
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 android.location.flags.Flags;
import com.android.internal.location.altitude.GeoidMap;
import com.android.internal.location.altitude.S2CellIdUtils;
@@ -213,12 +215,12 @@
}
/**
- * Same as {@link #addMslAltitudeToLocation(Context, Location)} except that data will not be
- * loaded from raw assets. Returns true if a Mean Sea Level altitude is added to the
- * {@code location}; otherwise, returns false and leaves the {@code location} unchanged.
- *
- * @hide
+ * Same as {@link #addMslAltitudeToLocation(Context, Location)} except that this method can be
+ * called on the main thread as data will not be loaded from raw assets. Returns true if a Mean
+ * Sea Level altitude is added to the {@code location}; otherwise, returns false and leaves the
+ * {@code location} unchanged.
*/
+ @FlaggedApi(Flags.FLAG_GEOID_HEIGHTS_VIA_ALTITUDE_HAL)
public boolean addMslAltitudeToLocation(@NonNull Location location) {
validate(location);
MapParamsProto geoidHeightParams = GeoidMap.getGeoidHeightParams();
diff --git a/location/java/android/location/flags/gnss.aconfig b/location/java/android/location/flags/location.aconfig
similarity index 77%
rename from location/java/android/location/flags/gnss.aconfig
rename to location/java/android/location/flags/location.aconfig
index 8c7c871..a96fe47 100644
--- a/location/java/android/location/flags/gnss.aconfig
+++ b/location/java/android/location/flags/location.aconfig
@@ -1,6 +1,20 @@
package: "android.location.flags"
flag {
+ name: "fix_service_watcher"
+ namespace: "location"
+ description: "Enable null explicit services in ServiceWatcher"
+ bug: "311210517"
+}
+
+flag {
+ name: "geoid_heights_via_altitude_hal"
+ namespace: "location"
+ description: "Flag for making geoid heights available via the Altitude HAL"
+ bug: "304375846"
+}
+
+flag {
name: "gnss_api_navic_l1"
namespace: "location"
description: "Flag for GNSS API for NavIC L1"
diff --git a/media/java/android/media/MediaCodec.java b/media/java/android/media/MediaCodec.java
index 470a8ac..bfb4b42 100644
--- a/media/java/android/media/MediaCodec.java
+++ b/media/java/android/media/MediaCodec.java
@@ -17,6 +17,7 @@
package android.media;
import android.Manifest;
+import android.annotation.FlaggedApi;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -43,21 +44,25 @@
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.ReadOnlyBufferException;
+import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.BitSet;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
+import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
+import java.util.Optional;
import java.util.Set;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
+import static com.android.media.codec.flags.Flags.FLAG_LARGE_AUDIO_FRAME;
/**
MediaCodec class can be used to access low-level media codecs, i.e. encoder/decoder components.
It is part of the Android low-level multimedia support infrastructure (normally used together
@@ -1824,6 +1829,7 @@
private static final String EOS_AND_DECODE_ONLY_ERROR_MESSAGE = "An input buffer cannot have "
+ "both BUFFER_FLAG_END_OF_STREAM and BUFFER_FLAG_DECODE_ONLY flags";
private static final int CB_CRYPTO_ERROR = 6;
+ private static final int CB_LARGE_FRAME_OUTPUT_AVAILABLE = 7;
private class EventHandler extends Handler {
private MediaCodec mCodec;
@@ -1945,6 +1951,39 @@
break;
}
+ case CB_LARGE_FRAME_OUTPUT_AVAILABLE:
+ {
+ int index = msg.arg2;
+ ArrayDeque<BufferInfo> infos = (ArrayDeque<BufferInfo>)msg.obj;
+ synchronized(mBufferLock) {
+ switch (mBufferMode) {
+ case BUFFER_MODE_LEGACY:
+ validateOutputByteBuffersLocked(mCachedOutputBuffers,
+ index, infos);
+ break;
+ case BUFFER_MODE_BLOCK:
+ while (mOutputFrames.size() <= index) {
+ mOutputFrames.add(null);
+ }
+ OutputFrame frame = mOutputFrames.get(index);
+ if (frame == null) {
+ frame = new OutputFrame(index);
+ mOutputFrames.set(index, frame);
+ }
+ frame.setBufferInfos(infos);
+ frame.setAccessible(true);
+ break;
+ default:
+ throw new IllegalArgumentException(
+ "Unrecognized buffer mode: for large frame output");
+ }
+ }
+ mCallback.onOutputBuffersAvailable(
+ mCodec, index, infos);
+
+ break;
+ }
+
case CB_ERROR:
{
mCallback.onError(mCodec, (MediaCodec.CodecException) msg.obj);
@@ -2836,11 +2875,72 @@
}
}
+ /**
+ * Submit multiple access units to the codec along with multiple
+ * {@link MediaCodec.BufferInfo} describing the contents of the buffer. This method
+ * is supported only in asynchronous mode. While this method can be used for all codecs,
+ * it is meant for buffer batching, which is only supported by codecs that advertise
+ * FEATURE_MultipleFrames. Other codecs will not output large output buffers via
+ * onOutputBuffersAvailable, and instead will output single-access-unit output via
+ * onOutputBufferAvailable.
+ * <p>
+ * Output buffer size can be configured using the following MediaFormat keys.
+ * {@link MediaFormat#KEY_BUFFER_BATCH_MAX_OUTPUT_SIZE} and
+ * {@link MediaFormat#KEY_BUFFER_BATCH_THRESHOLD_OUTPUT_SIZE}.
+ * Details for each access unit present in the buffer should be described using
+ * {@link MediaCodec.BufferInfo}. Access units must be laid out contiguously (without any gaps)
+ * and in order. Multiple access units in the output if present, will be available in
+ * {@link Callback#onOutputBuffersAvailable} or {@link Callback#onOutputBufferAvailable}
+ * in case of single-access-unit output or when output does not contain any buffers,
+ * such as flags.
+ * <p>
+ * All other details for populating {@link MediaCodec.BufferInfo} is the same as described in
+ * {@link #queueInputBuffer}.
+ *
+ * @param index The index of a client-owned input buffer previously returned
+ * in a call to {@link #dequeueInputBuffer}.
+ * @param bufferInfos ArrayDeque of {@link MediaCodec.BufferInfo} that describes the
+ * contents in the buffer. The ArrayDeque and the BufferInfo objects provided
+ * can be recycled by the caller for re-use.
+ * @throws IllegalStateException if not in the Executing state or not in asynchronous mode.
+ * @throws MediaCodec.CodecException upon codec error.
+ * @throws IllegalArgumentException upon if bufferInfos is empty, contains null, or if the
+ * access units are not contiguous.
+ * @throws CryptoException if a crypto object has been specified in
+ * {@link #configure}
+ */
+ @FlaggedApi(FLAG_LARGE_AUDIO_FRAME)
+ public final void queueInputBuffers(
+ int index,
+ @NonNull ArrayDeque<BufferInfo> bufferInfos) {
+ synchronized(mBufferLock) {
+ if (mBufferMode == BUFFER_MODE_BLOCK) {
+ throw new IncompatibleWithBlockModelException("queueInputBuffers() "
+ + "is not compatible with CONFIGURE_FLAG_USE_BLOCK_MODEL. "
+ + "Please use getQueueRequest() to queue buffers");
+ }
+ invalidateByteBufferLocked(mCachedInputBuffers, index, true /* input */);
+ mDequeuedInputBuffers.remove(index);
+ }
+ try {
+ native_queueInputBuffers(
+ index, bufferInfos.toArray());
+ } catch (CryptoException | IllegalStateException | IllegalArgumentException e) {
+ revalidateByteBuffer(mCachedInputBuffers, index, true /* input */);
+ throw e;
+ }
+ }
+
private native final void native_queueInputBuffer(
int index,
int offset, int size, long presentationTimeUs, int flags)
throws CryptoException;
+ private native final void native_queueInputBuffers(
+ int index,
+ @NonNull Object[] infos)
+ throws CryptoException, CodecException;
+
public static final int CRYPTO_MODE_UNENCRYPTED = 0;
public static final int CRYPTO_MODE_AES_CTR = 1;
public static final int CRYPTO_MODE_AES_CBC = 2;
@@ -3464,6 +3564,26 @@
}
/**
+ * Sets MediaCodec.BufferInfo objects describing the access units
+ * contained in this queue request. Access units must be laid out
+ * contiguously without gaps and in order.
+ *
+ * @param infos Represents {@link MediaCodec.BufferInfo} objects to mark
+ * individual access-unit boundaries and the timestamps associated with it.
+ * The buffer is expected to contain the data in a continuous manner.
+ * @return this object
+ */
+ @FlaggedApi(FLAG_LARGE_AUDIO_FRAME)
+ public @NonNull QueueRequest setBufferInfos(@NonNull ArrayDeque<BufferInfo> infos) {
+ if (!isAccessible()) {
+ throw new IllegalStateException("The request is stale");
+ }
+ mBufferInfos.clear();
+ mBufferInfos.addAll(infos);
+ return this;
+ }
+
+ /**
* Add an integer parameter.
* See {@link MediaFormat} for an exhaustive list of supported keys with
* values of type int, that can also be set with {@link MediaFormat#setInteger}.
@@ -3579,10 +3699,18 @@
throw new IllegalStateException("No block is set");
}
setAccessible(false);
+ if (mBufferInfos.isEmpty()) {
+ BufferInfo info = new BufferInfo();
+ info.size = mSize;
+ info.offset = mOffset;
+ info.presentationTimeUs = mPresentationTimeUs;
+ info.flags = mFlags;
+ mBufferInfos.add(info);
+ }
if (mLinearBlock != null) {
mCodec.native_queueLinearBlock(
- mIndex, mLinearBlock, mOffset, mSize, mCryptoInfo,
- mPresentationTimeUs, mFlags,
+ mIndex, mLinearBlock, mCryptoInfo,
+ mBufferInfos.toArray(),
mTuningKeys, mTuningValues);
} else if (mHardwareBuffer != null) {
mCodec.native_queueHardwareBuffer(
@@ -3600,6 +3728,7 @@
mHardwareBuffer = null;
mPresentationTimeUs = 0;
mFlags = 0;
+ mBufferInfos.clear();
mTuningKeys.clear();
mTuningValues.clear();
return this;
@@ -3623,6 +3752,7 @@
private HardwareBuffer mHardwareBuffer = null;
private long mPresentationTimeUs = 0;
private @BufferFlag int mFlags = 0;
+ private final ArrayDeque<BufferInfo> mBufferInfos = new ArrayDeque<>();
private final ArrayList<String> mTuningKeys = new ArrayList<>();
private final ArrayList<Object> mTuningValues = new ArrayList<>();
@@ -3632,11 +3762,8 @@
private native void native_queueLinearBlock(
int index,
@NonNull LinearBlock block,
- int offset,
- int size,
@Nullable CryptoInfo cryptoInfo,
- long presentationTimeUs,
- int flags,
+ @NonNull Object[] bufferInfos,
@NonNull ArrayList<String> keys,
@NonNull ArrayList<Object> values);
@@ -4050,6 +4177,27 @@
}
}
+ private void validateOutputByteBuffersLocked(
+ @Nullable ByteBuffer[] buffers, int index, @NonNull ArrayDeque<BufferInfo> infoDeque) {
+ Optional<BufferInfo> minInfo = infoDeque.stream().min(
+ (info1, info2) -> Integer.compare(info1.offset, info2.offset));
+ Optional<BufferInfo> maxInfo = infoDeque.stream().max(
+ (info1, info2) -> Integer.compare(info1.offset, info2.offset));
+ if (buffers == null) {
+ if (index >= 0) {
+ mValidOutputIndices.set(index);
+ }
+ } else if (index >= 0 && index < buffers.length) {
+ ByteBuffer buffer = buffers[index];
+ if (buffer != null && minInfo.isPresent() && maxInfo.isPresent()) {
+ buffer.setAccessible(true);
+ buffer.limit(maxInfo.get().offset + maxInfo.get().size);
+ buffer.position(minInfo.get().offset);
+ }
+ }
+
+ }
+
private void validateOutputByteBufferLocked(
@Nullable ByteBuffer[] buffers, int index, @NonNull BufferInfo info) {
if (buffers == null) {
@@ -4407,6 +4555,22 @@
return mFlags;
}
+ /*
+ * Returns the BufferInfos associated with this OutputFrame. These BufferInfos
+ * describes the access units present in the OutputFrame. Access units are laid
+ * out contiguously without gaps and in order.
+ */
+ @FlaggedApi(FLAG_LARGE_AUDIO_FRAME)
+ public @NonNull ArrayDeque<BufferInfo> getBufferInfos() {
+ if (mBufferInfos.isEmpty()) {
+ // single BufferInfo could be present.
+ BufferInfo bufferInfo = new BufferInfo();
+ bufferInfo.set(0, 0, mPresentationTimeUs, mFlags);
+ mBufferInfos.add(bufferInfo);
+ }
+ return mBufferInfos;
+ }
+
/**
* Returns a read-only {@link MediaFormat} for this frame. The returned
* object is valid only until the client calls {@link MediaCodec#releaseOutputBuffer}.
@@ -4432,6 +4596,7 @@
mLinearBlock = null;
mHardwareBuffer = null;
mFormat = null;
+ mBufferInfos.clear();
mChangedKeys.clear();
mKeySet.clear();
mLoaded = false;
@@ -4450,6 +4615,11 @@
mFlags = info.flags;
}
+ void setBufferInfos(ArrayDeque<BufferInfo> infos) {
+ mBufferInfos.clear();
+ mBufferInfos.addAll(infos);
+ }
+
boolean isLoaded() {
return mLoaded;
}
@@ -4464,6 +4634,7 @@
private long mPresentationTimeUs = 0;
private @BufferFlag int mFlags = 0;
private MediaFormat mFormat = null;
+ private final ArrayDeque<BufferInfo> mBufferInfos = new ArrayDeque<>();
private final ArrayList<String> mChangedKeys = new ArrayList<>();
private final Set<String> mKeySet = new HashSet<>();
private boolean mAccessible = false;
@@ -5172,6 +5343,32 @@
@NonNull MediaCodec codec, int index, @NonNull BufferInfo info);
/**
+ * Called when multiple access-units are available in the output.
+ *
+ * @param codec The MediaCodec object.
+ * @param index The index of the available output buffer.
+ * @param infos Infos describing the available output buffer {@link MediaCodec.BufferInfo}.
+ * Access units present in the output buffer are laid out contiguously
+ * without gaps and in order.
+ */
+ @FlaggedApi(FLAG_LARGE_AUDIO_FRAME)
+ public void onOutputBuffersAvailable(
+ @NonNull MediaCodec codec, int index, @NonNull ArrayDeque<BufferInfo> infos) {
+ /*
+ * This callback returns multiple BufferInfos when codecs are configured to operate on
+ * large audio frame. Since at this point, we have a single large buffer, returning
+ * each BufferInfo using
+ * {@link Callback#onOutputBufferAvailable onOutputBufferAvailable} may cause the
+ * index to be released to the codec using {@link MediaCodec#releaseOutputBuffer}
+ * before all BuffersInfos can be returned to the client.
+ * Hence this callback is required to be implemented or else an exception is thrown.
+ */
+ throw new IllegalStateException(
+ "Client must override onOutputBuffersAvailable when codec is " +
+ "configured to operate with multiple access units");
+ }
+
+ /**
* Called when the MediaCodec encountered an error
*
* @param codec The MediaCodec object.
diff --git a/media/java/android/media/MediaMuxer.java b/media/java/android/media/MediaMuxer.java
index 30f4562..80b606c 100644
--- a/media/java/android/media/MediaMuxer.java
+++ b/media/java/android/media/MediaMuxer.java
@@ -514,7 +514,7 @@
* The following table summarizes support for specific format keys across android releases.
* Keys marked with '+:' are required.
*
- * <table style="width: 0%">
+ * <table>
* <thead>
* <tr>
* <th rowspan=2>OS Version(s)</th>
@@ -583,7 +583,7 @@
* <p>
* The following table summarizes codec support for containers across android releases:
*
- * <table style="width: 0%">
+ * <table>
* <thead>
* <tr>
* <th rowspan=2>OS Version(s)</th>
diff --git a/media/java/android/media/MediaRecorder.java b/media/java/android/media/MediaRecorder.java
index aa30748..bdfa6301 100644
--- a/media/java/android/media/MediaRecorder.java
+++ b/media/java/android/media/MediaRecorder.java
@@ -65,7 +65,8 @@
*
* <p>A common case of using MediaRecorder to record audio works as follows:
*
- * <pre>MediaRecorder recorder = new MediaRecorder();
+ * <pre>
+ * MediaRecorder recorder = new MediaRecorder(context);
* recorder.setAudioSource(MediaRecorder.AudioSource.MIC);
* recorder.setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP);
* recorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB);
diff --git a/media/java/android/media/metrics/EditingEndedEvent.java b/media/java/android/media/metrics/EditingEndedEvent.java
index 72e6db8..5ed8d40 100644
--- a/media/java/android/media/metrics/EditingEndedEvent.java
+++ b/media/java/android/media/metrics/EditingEndedEvent.java
@@ -86,7 +86,10 @@
*/
public static final int ERROR_CODE_IO_NO_PERMISSION = 8;
- /** */
+ /**
+ * Caused by failing to load data via cleartext HTTP, when the app's network security
+ * configuration does not permit it.
+ */
public static final int ERROR_CODE_IO_CLEARTEXT_NOT_PERMITTED = 9;
/** Caused by reading data out of the data bounds. */
@@ -146,6 +149,9 @@
@Retention(java.lang.annotation.RetentionPolicy.SOURCE)
public @interface ErrorCode {}
+ /** Special value for unknown {@linkplain #getTimeSinceCreatedMillis() time since creation}. */
+ public static final int TIME_SINCE_CREATED_UNKNOWN = -1;
+
private final @ErrorCode int mErrorCode;
@SuppressWarnings("HidingField") // Hiding field from superclass as for playback events.
private final long mTimeSinceCreatedMillis;
@@ -174,16 +180,16 @@
}
/**
- * Gets the elapsed time since creating of the editing session, in milliseconds, or -1 if
- * unknown.
+ * Gets the elapsed time since creating of the editing session, in milliseconds, or {@link
+ * #TIME_SINCE_CREATED_UNKNOWN} if unknown.
*
- * @return The elapsed time since creating the editing session, in milliseconds, or -1 if
- * unknown.
+ * @return The elapsed time since creating the editing session, in milliseconds, or {@link
+ * #TIME_SINCE_CREATED_UNKNOWN} if unknown.
* @see LogSessionId
* @see EditingSession
*/
@Override
- @IntRange(from = -1)
+ @IntRange(from = TIME_SINCE_CREATED_UNKNOWN)
public long getTimeSinceCreatedMillis() {
return mTimeSinceCreatedMillis;
}
@@ -283,7 +289,7 @@
public Builder(@FinalState int finalState) {
mFinalState = finalState;
mErrorCode = ERROR_CODE_NONE;
- mTimeSinceCreatedMillis = -1;
+ mTimeSinceCreatedMillis = TIME_SINCE_CREATED_UNKNOWN;
mMetricsBundle = new Bundle();
}
@@ -291,11 +297,11 @@
* 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.
+ * milliseconds, or {@link #TIME_SINCE_CREATED_UNKNOWN} if unknown.
* @see #getTimeSinceCreatedMillis()
*/
public @NonNull Builder setTimeSinceCreatedMillis(
- @IntRange(from = -1) long timeSinceCreatedMillis) {
+ @IntRange(from = TIME_SINCE_CREATED_UNKNOWN) long timeSinceCreatedMillis) {
mTimeSinceCreatedMillis = timeSinceCreatedMillis;
return this;
}
diff --git a/media/java/android/media/projection/IMediaProjection.aidl b/media/java/android/media/projection/IMediaProjection.aidl
index 388b2c5..2fb0af5 100644
--- a/media/java/android/media/projection/IMediaProjection.aidl
+++ b/media/java/android/media/projection/IMediaProjection.aidl
@@ -18,6 +18,7 @@
import android.media.projection.IMediaProjectionCallback;
import android.os.IBinder;
+import android.app.ActivityOptions.LaunchCookie;
/** {@hide} */
interface IMediaProjection {
@@ -38,22 +39,22 @@
void unregisterCallback(IMediaProjectionCallback callback);
/**
- * Returns the {@link android.os.IBinder} identifying the task to record, or {@code null} if
+ * Returns the {@link LaunchCookie} identifying the task to record, or {@code null} if
* there is none.
*/
@EnforcePermission("MANAGE_MEDIA_PROJECTION")
@JavaPassthrough(annotation = "@android.annotation.RequiresPermission(android.Manifest"
+ ".permission.MANAGE_MEDIA_PROJECTION)")
- IBinder getLaunchCookie();
+ LaunchCookie getLaunchCookie();
/**
- * Updates the {@link android.os.IBinder} identifying the task to record, or {@code null} if
+ * Updates the {@link LaunchCookie} identifying the task to record, or {@code null} if
* there is none.
*/
@EnforcePermission("MANAGE_MEDIA_PROJECTION")
@JavaPassthrough(annotation = "@android.annotation.RequiresPermission(android.Manifest"
+ ".permission.MANAGE_MEDIA_PROJECTION)")
- void setLaunchCookie(in IBinder launchCookie);
+ void setLaunchCookie(in LaunchCookie launchCookie);
/**
* Returns {@code true} if this token is still valid. A token is valid as long as the token
diff --git a/media/java/android/media/projection/MediaProjectionInfo.java b/media/java/android/media/projection/MediaProjectionInfo.java
index c820392..cd0763d 100644
--- a/media/java/android/media/projection/MediaProjectionInfo.java
+++ b/media/java/android/media/projection/MediaProjectionInfo.java
@@ -16,7 +16,7 @@
package android.media.projection;
-import android.os.IBinder;
+import android.app.ActivityOptions.LaunchCookie;
import android.os.Parcel;
import android.os.Parcelable;
import android.os.UserHandle;
@@ -27,9 +27,9 @@
public final class MediaProjectionInfo implements Parcelable {
private final String mPackageName;
private final UserHandle mUserHandle;
- private final IBinder mLaunchCookie;
+ private final LaunchCookie mLaunchCookie;
- public MediaProjectionInfo(String packageName, UserHandle handle, IBinder launchCookie) {
+ public MediaProjectionInfo(String packageName, UserHandle handle, LaunchCookie launchCookie) {
mPackageName = packageName;
mUserHandle = handle;
mLaunchCookie = launchCookie;
@@ -38,7 +38,7 @@
public MediaProjectionInfo(Parcel in) {
mPackageName = in.readString();
mUserHandle = UserHandle.readFromParcel(in);
- mLaunchCookie = in.readStrongBinder();
+ mLaunchCookie = LaunchCookie.readFromParcel(in);
}
public String getPackageName() {
@@ -49,7 +49,7 @@
return mUserHandle;
}
- public IBinder getLaunchCookie() {
+ public LaunchCookie getLaunchCookie() {
return mLaunchCookie;
}
@@ -72,7 +72,7 @@
public String toString() {
return "MediaProjectionInfo{mPackageName="
+ mPackageName + ", mUserHandle="
- + mUserHandle + ", mLaunchCookie"
+ + mUserHandle + ", mLaunchCookie="
+ mLaunchCookie + "}";
}
@@ -85,7 +85,7 @@
public void writeToParcel(Parcel out, int flags) {
out.writeString(mPackageName);
UserHandle.writeToParcel(mUserHandle, out);
- out.writeStrongBinder(mLaunchCookie);
+ LaunchCookie.writeToParcel(mLaunchCookie, out);
}
public static final @android.annotation.NonNull Parcelable.Creator<MediaProjectionInfo> CREATOR =
diff --git a/media/java/android/media/projection/MediaProjectionManager.java b/media/java/android/media/projection/MediaProjectionManager.java
index 9790d02..e3290d6 100644
--- a/media/java/android/media/projection/MediaProjectionManager.java
+++ b/media/java/android/media/projection/MediaProjectionManager.java
@@ -18,8 +18,11 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.annotation.SuppressLint;
import android.annotation.SystemService;
+import android.annotation.TestApi;
import android.app.Activity;
+import android.app.ActivityOptions.LaunchCookie;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
@@ -73,6 +76,9 @@
/** @hide */
public static final String EXTRA_MEDIA_PROJECTION =
"android.media.projection.extra.EXTRA_MEDIA_PROJECTION";
+ /** @hide */
+ public static final String EXTRA_LAUNCH_COOKIE =
+ "android.media.projection.extra.EXTRA_LAUNCH_COOKIE";
/** @hide */
public static final int TYPE_SCREEN_CAPTURE = 0;
@@ -158,17 +164,29 @@
*/
@NonNull
public Intent createScreenCaptureIntent(@NonNull MediaProjectionConfig config) {
- Intent i = new Intent();
- final ComponentName mediaProjectionPermissionDialogComponent =
- ComponentName.unflattenFromString(mContext.getResources()
- .getString(com.android.internal.R.string
- .config_mediaProjectionPermissionDialogComponent));
- i.setComponent(mediaProjectionPermissionDialogComponent);
+ Intent i = createScreenCaptureIntent();
i.putExtra(EXTRA_MEDIA_PROJECTION_CONFIG, config);
return i;
}
/**
+ * Returns an intent similar to {@link #createScreenCaptureIntent()} that will enable screen
+ * recording of the task with the specified launch cookie. This method should only be used for
+ * testing.
+ *
+ * @param launchCookie the launch cookie corresponding to the task to record.
+ * @hide
+ */
+ @SuppressLint("UnflaggedApi")
+ @TestApi
+ @NonNull
+ public Intent createScreenCaptureIntent(@Nullable LaunchCookie launchCookie) {
+ Intent i = createScreenCaptureIntent();
+ i.putExtra(EXTRA_LAUNCH_COOKIE, launchCookie);
+ return i;
+ }
+
+ /**
* Retrieves the {@link MediaProjection} obtained from a successful screen
* capture request. The result code and data from the request are provided by overriding
* {@link Activity#onActivityResult(int, int, Intent) onActivityResult(int, int, Intent)},
diff --git a/media/java/android/media/tv/ITvInputClient.aidl b/media/java/android/media/tv/ITvInputClient.aidl
index 8978277..b644621 100644
--- a/media/java/android/media/tv/ITvInputClient.aidl
+++ b/media/java/android/media/tv/ITvInputClient.aidl
@@ -69,4 +69,6 @@
// For ad response
void onAdResponse(in AdResponse response, int seq);
void onAdBufferConsumed(in AdBuffer buffer, int seq);
+
+ void onTvInputSessionData(in String type, in Bundle data, int seq);
}
diff --git a/media/java/android/media/tv/ITvInputManager.aidl b/media/java/android/media/tv/ITvInputManager.aidl
index 2f6575e..84c197d 100644
--- a/media/java/android/media/tv/ITvInputManager.aidl
+++ b/media/java/android/media/tv/ITvInputManager.aidl
@@ -152,4 +152,6 @@
// For freezing video playback
void setVideoFrozen(in IBinder sessionToken, boolean isFrozen, int userId);
+
+ void notifyTvAdSessionData(in IBinder sessionToken, in String type, in Bundle data, int userId);
}
diff --git a/media/java/android/media/tv/ITvInputSession.aidl b/media/java/android/media/tv/ITvInputSession.aidl
index a93f18d..7b20c39 100644
--- a/media/java/android/media/tv/ITvInputSession.aidl
+++ b/media/java/android/media/tv/ITvInputSession.aidl
@@ -86,4 +86,6 @@
// For freezing video
void setVideoFrozen(boolean isFrozen);
+
+ void notifyTvAdSessionData(in String type, in Bundle data);
}
diff --git a/media/java/android/media/tv/ITvInputSessionCallback.aidl b/media/java/android/media/tv/ITvInputSessionCallback.aidl
index 8e2702a..76e079f 100644
--- a/media/java/android/media/tv/ITvInputSessionCallback.aidl
+++ b/media/java/android/media/tv/ITvInputSessionCallback.aidl
@@ -68,4 +68,6 @@
// For messages sent from the TV input
void onTvMessage(int type, in Bundle data);
+
+ void onTvInputSessionData(in String type, in Bundle data);
}
diff --git a/media/java/android/media/tv/ITvInputSessionWrapper.java b/media/java/android/media/tv/ITvInputSessionWrapper.java
index 921104d..999e2cf 100644
--- a/media/java/android/media/tv/ITvInputSessionWrapper.java
+++ b/media/java/android/media/tv/ITvInputSessionWrapper.java
@@ -82,6 +82,7 @@
private static final int DO_STOP_PLAYBACK = 33;
private static final int DO_START_PLAYBACK = 34;
private static final int DO_SET_VIDEO_FROZEN = 35;
+ private static final int DO_NOTIFY_AD_SESSION_DATA = 36;
private final boolean mIsRecordingSession;
private final HandlerCaller mCaller;
@@ -287,6 +288,7 @@
case DO_NOTIFY_TV_MESSAGE: {
SomeArgs args = (SomeArgs) msg.obj;
mTvInputSessionImpl.onTvMessageReceived((Integer) args.arg1, (Bundle) args.arg2);
+ args.recycle();
break;
}
case DO_STOP_PLAYBACK: {
@@ -301,6 +303,12 @@
mTvInputSessionImpl.setVideoFrozen((Boolean) msg.obj);
break;
}
+ case DO_NOTIFY_AD_SESSION_DATA: {
+ SomeArgs args = (SomeArgs) msg.obj;
+ mTvInputSessionImpl.notifyTvAdSessionData((String) args.arg1, (Bundle) args.arg2);
+ args.recycle();
+ break;
+ }
default: {
Log.w(TAG, "Unhandled message code: " + msg.what);
break;
@@ -488,6 +496,12 @@
}
@Override
+ public void notifyTvAdSessionData(String type, Bundle data) {
+ mCaller.executeOrSendMessage(
+ mCaller.obtainMessageOO(DO_NOTIFY_AD_SESSION_DATA, type, data));
+ }
+
+ @Override
public void setVideoFrozen(boolean isFrozen) {
mCaller.executeOrSendMessage(mCaller.obtainMessageO(DO_SET_VIDEO_FROZEN, isFrozen));
}
diff --git a/media/java/android/media/tv/SignalingDataInfo.aidl b/media/java/android/media/tv/SignalingDataInfo.aidl
new file mode 100644
index 0000000..7108f36
--- /dev/null
+++ b/media/java/android/media/tv/SignalingDataInfo.aidl
@@ -0,0 +1,3 @@
+package android.media.tv;
+
+parcelable SignalingDataInfo;
diff --git a/media/java/android/media/tv/SignalingDataInfo.java b/media/java/android/media/tv/SignalingDataInfo.java
new file mode 100644
index 0000000..b29ea5c
--- /dev/null
+++ b/media/java/android/media/tv/SignalingDataInfo.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.tv;
+
+import android.annotation.NonNull;
+import android.os.Parcelable;
+
+/** @hide */
+public class SignalingDataInfo implements Parcelable {
+ public static final @NonNull Parcelable.Creator<SignalingDataInfo> CREATOR =
+ new Parcelable.Creator<SignalingDataInfo>() {
+ @Override
+ public SignalingDataInfo[] newArray(int size) {
+ return new SignalingDataInfo[size];
+ }
+
+ @Override
+ public SignalingDataInfo createFromParcel(@NonNull android.os.Parcel in) {
+ return new SignalingDataInfo(in);
+ }
+ };
+
+ private int mTableId;
+ private @NonNull String mTable;
+ private int mMetadataType;
+ private int mVersion;
+ private int mGroup;
+ private @NonNull String mEncoding;
+
+ public SignalingDataInfo(
+ int tableId,
+ @NonNull String table,
+ int metadataType,
+ int version,
+ int group,
+ @NonNull String encoding) {
+ this.mTableId = tableId;
+ this.mTable = table;
+ com.android.internal.util.AnnotationValidations.validate(NonNull.class, null, mTable);
+ this.mMetadataType = metadataType;
+ this.mVersion = version;
+ this.mGroup = group;
+ this.mEncoding = encoding;
+ com.android.internal.util.AnnotationValidations.validate(NonNull.class, null, mEncoding);
+ }
+
+ public int getTableId() {
+ return mTableId;
+ }
+
+ public @NonNull String getTable() {
+ return mTable;
+ }
+
+ public int getMetadataType() {
+ return mMetadataType;
+ }
+
+ public int getVersion() {
+ return mVersion;
+ }
+
+ public int getGroup() {
+ return mGroup;
+ }
+
+ public @NonNull String getEncoding() {
+ return mEncoding;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull android.os.Parcel dest, int flags) {
+ dest.writeInt(mTableId);
+ dest.writeString(mTable);
+ dest.writeInt(mMetadataType);
+ dest.writeInt(mVersion);
+ dest.writeInt(mGroup);
+ dest.writeString(mEncoding);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ SignalingDataInfo(@NonNull android.os.Parcel in) {
+ int tableId = in.readInt();
+ String table = in.readString();
+ int metadataType = in.readInt();
+ int version = in.readInt();
+ int group = in.readInt();
+ String encoding = in.readString();
+
+ this.mTableId = tableId;
+ this.mTable = table;
+ com.android.internal.util.AnnotationValidations.validate(NonNull.class, null, mTable);
+ this.mMetadataType = metadataType;
+ this.mVersion = version;
+ this.mGroup = group;
+ this.mEncoding = encoding;
+ com.android.internal.util.AnnotationValidations.validate(NonNull.class, null, mEncoding);
+ }
+}
diff --git a/media/java/android/media/tv/SignalingDataRequest.aidl b/media/java/android/media/tv/SignalingDataRequest.aidl
new file mode 100644
index 0000000..29e89fe
--- /dev/null
+++ b/media/java/android/media/tv/SignalingDataRequest.aidl
@@ -0,0 +1,3 @@
+package android.media.tv;
+
+parcelable SignalingDataRequest;
diff --git a/media/java/android/media/tv/SignalingDataRequest.java b/media/java/android/media/tv/SignalingDataRequest.java
new file mode 100644
index 0000000..dcf1d48
--- /dev/null
+++ b/media/java/android/media/tv/SignalingDataRequest.java
@@ -0,0 +1,170 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.tv;
+
+import android.annotation.NonNull;
+import android.os.Parcelable;
+
+/**
+ * Request to retrieve the Low-level Signalling Tables (LLS) and Service-layer Signalling (SLS)
+ * metadata.
+ *
+ * <p>For more details on each type of metadata that can be requested, refer to the ATSC standard
+ * A/344:2023-5 9.2.10 - Query Signaling Data API.
+ *
+ * @hide
+ */
+public class SignalingDataRequest extends BroadcastInfoRequest implements Parcelable {
+ private static final @TvInputManager.BroadcastInfoType int REQUEST_TYPE =
+ TvInputManager.BROADCAST_INFO_TYPE_SIGNALING_DATA;
+
+ public static final @NonNull Parcelable.Creator<SignalingDataRequest> CREATOR =
+ new Parcelable.Creator<SignalingDataRequest>() {
+ @Override
+ public SignalingDataRequest[] newArray(int size) {
+ return new SignalingDataRequest[size];
+ }
+
+ @Override
+ public SignalingDataRequest createFromParcel(@NonNull android.os.Parcel in) {
+ return new SignalingDataRequest(in);
+ }
+ };
+
+ /** SLS Metadata: All metadata objects for the requested service(s) */
+ public static final int SLS_METADATA_ALL = 0x7FFFFFF;
+
+ /** SLS Metadata: APD for the requested service(s) */
+ public static final int SLS_METADATA_APD = 1;
+
+ /** SLS Metadata: USBD for the requested service(s) */
+ public static final int SLS_METADATA_USBD = 1 << 1;
+
+ /** SLS Metadata: S-TSID for the requested service(s) */
+ public static final int SLS_METADATA_STSID = 1 << 2;
+
+ /** SLS Metadata: DASH MPD for the requested service(s) */
+ public static final int SLS_METADATA_MPD = 1 << 3;
+
+ /** SLS Metadata: User Service Description for MMTP */
+ public static final int SLS_METADATA_USD = 1 << 4;
+
+ /** SLS Metadata: MMT Package Access Table for the requested service(s) */
+ public static final int SLS_METADATA_PAT = 1 << 5;
+
+ /** SLS Metadata: MMT Package Table for the requested service(s) */
+ public static final int SLS_METADATA_MPT = 1 << 6;
+
+ /** SLS Metadata: MMT Media Presentation Information Table for the requested service(s) */
+ public static final int SLS_METADATA_MPIT = 1 << 7;
+
+ /** SLS Metadata: MMT Clock Relation Information for the requested service(s) */
+ public static final int SLS_METADATA_CRIT = 1 << 8;
+
+ /** SLS Metadata: MMT Device Capabilities Information Table for the requested service(s) */
+ public static final int SLS_METADATA_DCIT = 1 << 9;
+
+ /** SLS Metadata: HTML Entry Pages Location Description for the requested service(s) */
+ public static final int SLS_METADATA_HELD = 1 << 10;
+
+ /** SLS Metadata: Distribution Window Desciription for the requested service(s) */
+ public static final int SLS_METADATA_DWD = 1 << 11;
+
+ /** SLS Metadata: MMT Application Event Information for the requested service(s) */
+ public static final int SLS_METADATA_AEI = 1 << 12;
+
+ /** SLS Metadata: Video Stream Properties Descriptor */
+ public static final int SLS_METADATA_VSPD = 1 << 13;
+
+ /** SLS Metadata: ATSC Staggercast Descriptor */
+ public static final int SLS_METADATA_ASD = 1 << 14;
+
+ /** SLS Metadata: Inband Event Descriptor */
+ public static final int SLS_METADATA_IED = 1 << 15;
+
+ /** SLS Metadata: Caption Asset Descriptor */
+ public static final int SLS_METADATA_CAD = 1 << 16;
+
+ /** SLS Metadata: Audio Stream Properties Descriptor */
+ public static final int SLS_METADATA_ASPD = 1 << 17;
+
+ /** SLS Metadata: Security Properties Descriptor */
+ public static final int SLS_METADATA_SSD = 1 << 18;
+
+ /** SLS Metadata: ROUTE/DASH Application Dynamic Event for the requested service(s) */
+ public static final int SLS_METADATA_EMSG = 1 << 19;
+
+ /** SLS Metadata: MMT Application Dynamic Event for the requested service(s) */
+ public static final int SLS_METADATA_EVTI = 1 << 20;
+
+ /** Regional Service Availability Table for the requested service(s) */
+ public static final int SLS_METADATA_RSAT = 1 << 21;
+
+ private final int mGroup;
+ private @NonNull final int[] mLlsTableIds;
+ private final int mSlsMetadataTypes;
+
+ SignalingDataRequest(
+ int requestId,
+ int option,
+ int group,
+ @NonNull int[] llsTableIds,
+ int slsMetadataTypes) {
+ super(REQUEST_TYPE, requestId, option);
+ mGroup = group;
+ mLlsTableIds = llsTableIds;
+ mSlsMetadataTypes = slsMetadataTypes;
+ }
+
+ SignalingDataRequest(@NonNull android.os.Parcel in) {
+ super(REQUEST_TYPE, in);
+
+ int group = in.readInt();
+ int[] llsTableIds = in.createIntArray();
+ int slsMetadataTypes = in.readInt();
+
+ this.mGroup = group;
+ this.mLlsTableIds = llsTableIds;
+ com.android.internal.util.AnnotationValidations.validate(NonNull.class, null, mLlsTableIds);
+ this.mSlsMetadataTypes = slsMetadataTypes;
+ }
+
+ public int getGroup() {
+ return mGroup;
+ }
+
+ public @NonNull int[] getLlsTableIds() {
+ return mLlsTableIds;
+ }
+
+ public int getSlsMetadataTypes() {
+ return mSlsMetadataTypes;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull android.os.Parcel dest, int flags) {
+ super.writeToParcel(dest, flags);
+ dest.writeInt(mGroup);
+ dest.writeIntArray(mLlsTableIds);
+ dest.writeInt(mSlsMetadataTypes);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+}
diff --git a/media/java/android/media/tv/SignalingDataResponse.aidl b/media/java/android/media/tv/SignalingDataResponse.aidl
new file mode 100644
index 0000000..a548e4e
--- /dev/null
+++ b/media/java/android/media/tv/SignalingDataResponse.aidl
@@ -0,0 +1,3 @@
+package android.media.tv;
+
+parcelable SignalingDataResponse;
diff --git a/media/java/android/media/tv/SignalingDataResponse.java b/media/java/android/media/tv/SignalingDataResponse.java
new file mode 100644
index 0000000..3e4c790
--- /dev/null
+++ b/media/java/android/media/tv/SignalingDataResponse.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.media.tv;
+
+import android.annotation.NonNull;
+import android.os.Parcelable;
+
+import java.util.List;
+
+/** @hide */
+public class SignalingDataResponse extends BroadcastInfoResponse implements Parcelable {
+ public static final @NonNull Parcelable.Creator<SignalingDataResponse> CREATOR =
+ new Parcelable.Creator<SignalingDataResponse>() {
+ @Override
+ public SignalingDataResponse[] newArray(int size) {
+ return new SignalingDataResponse[size];
+ }
+
+ @Override
+ public SignalingDataResponse createFromParcel(@NonNull android.os.Parcel in) {
+ return new SignalingDataResponse(in);
+ }
+ };
+ private static final @TvInputManager.BroadcastInfoType int RESPONSE_TYPE =
+ TvInputManager.BROADCAST_INFO_TYPE_SIGNALING_DATA;
+ private final @NonNull int[] mTableIds;
+ private final int mMetadataTypes;
+ private final @NonNull List<SignalingDataInfo> mSignalingDataInfoList;
+
+ public SignalingDataResponse(
+ int requestId,
+ int sequence,
+ @ResponseResult int responseResult,
+ @NonNull int[] tableIds,
+ int metadataTypes,
+ @NonNull List<SignalingDataInfo> signalingDataInfoList) {
+ super(RESPONSE_TYPE, requestId, sequence, responseResult);
+ this.mTableIds = tableIds;
+ com.android.internal.util.AnnotationValidations.validate(NonNull.class, null, mTableIds);
+ this.mMetadataTypes = metadataTypes;
+ this.mSignalingDataInfoList = signalingDataInfoList;
+ com.android.internal.util.AnnotationValidations.validate(
+ NonNull.class, null, mSignalingDataInfoList);
+ }
+
+ public @NonNull int[] getTableIds() {
+ return mTableIds;
+ }
+
+ public int getMetadataTypes() {
+ return mMetadataTypes;
+ }
+
+ public @NonNull List<SignalingDataInfo> getSignalingDataInfoList() {
+ return mSignalingDataInfoList;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull android.os.Parcel dest, int flags) {
+ super.writeToParcel(dest, flags);
+ dest.writeIntArray(mTableIds);
+ dest.writeInt(mMetadataTypes);
+ dest.writeParcelableList(mSignalingDataInfoList, flags);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ SignalingDataResponse(@NonNull android.os.Parcel in) {
+ super(RESPONSE_TYPE, in);
+
+ int[] tableIds = in.createIntArray();
+ int metadataTypes = in.readInt();
+ List<SignalingDataInfo> signalingDataInfoList = new java.util.ArrayList<>();
+ in.readParcelableList(signalingDataInfoList, SignalingDataInfo.class.getClassLoader());
+
+ this.mTableIds = tableIds;
+ com.android.internal.util.AnnotationValidations.validate(NonNull.class, null, mTableIds);
+ this.mMetadataTypes = metadataTypes;
+ this.mSignalingDataInfoList = signalingDataInfoList;
+ com.android.internal.util.AnnotationValidations.validate(
+ NonNull.class, null, mSignalingDataInfoList);
+ }
+}
diff --git a/media/java/android/media/tv/TvInputManager.java b/media/java/android/media/tv/TvInputManager.java
index 2b31bfe..8720bfe 100644
--- a/media/java/android/media/tv/TvInputManager.java
+++ b/media/java/android/media/tv/TvInputManager.java
@@ -22,6 +22,7 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.RequiresPermission;
+import android.annotation.StringDef;
import android.annotation.SystemApi;
import android.annotation.SystemService;
import android.annotation.TestApi;
@@ -489,10 +490,19 @@
/** @hide */
@Retention(RetentionPolicy.SOURCE)
- @IntDef(prefix = "BROADCAST_INFO_TYPE_", value =
- {BROADCAST_INFO_TYPE_TS, BROADCAST_INFO_TYPE_TABLE, BROADCAST_INFO_TYPE_SECTION,
- BROADCAST_INFO_TYPE_PES, BROADCAST_INFO_STREAM_EVENT, BROADCAST_INFO_TYPE_DSMCC,
- BROADCAST_INFO_TYPE_COMMAND, BROADCAST_INFO_TYPE_TIMELINE})
+ @IntDef(
+ prefix = "BROADCAST_INFO_TYPE_",
+ value = {
+ BROADCAST_INFO_TYPE_TS,
+ BROADCAST_INFO_TYPE_TABLE,
+ BROADCAST_INFO_TYPE_SECTION,
+ BROADCAST_INFO_TYPE_PES,
+ BROADCAST_INFO_STREAM_EVENT,
+ BROADCAST_INFO_TYPE_DSMCC,
+ BROADCAST_INFO_TYPE_COMMAND,
+ BROADCAST_INFO_TYPE_TIMELINE,
+ BROADCAST_INFO_TYPE_SIGNALING_DATA
+ })
public @interface BroadcastInfoType {}
public static final int BROADCAST_INFO_TYPE_TS = 1;
@@ -505,6 +515,9 @@
public static final int BROADCAST_INFO_TYPE_TIMELINE = 8;
/** @hide */
+ public static final int BROADCAST_INFO_TYPE_SIGNALING_DATA = 9;
+
+ /** @hide */
@Retention(RetentionPolicy.SOURCE)
@IntDef(prefix = "SIGNAL_STRENGTH_",
value = {SIGNAL_STRENGTH_LOST, SIGNAL_STRENGTH_WEAK, SIGNAL_STRENGTH_STRONG})
@@ -523,6 +536,220 @@
*/
public static final int SIGNAL_STRENGTH_STRONG = 3;
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @StringDef(prefix = "SESSION_DATA_TYPE_", value = {
+ SESSION_DATA_TYPE_TUNED,
+ SESSION_DATA_TYPE_TRACK_SELECTED,
+ SESSION_DATA_TYPE_TRACKS_CHANGED,
+ SESSION_DATA_TYPE_VIDEO_AVAILABLE,
+ SESSION_DATA_TYPE_VIDEO_UNAVAILABLE,
+ SESSION_DATA_TYPE_BROADCAST_INFO_RESPONSE,
+ SESSION_DATA_TYPE_AD_RESPONSE,
+ SESSION_DATA_TYPE_AD_BUFFER_CONSUMED,
+ SESSION_DATA_TYPE_TV_MESSAGE})
+ public @interface SessionDataType {}
+
+ /**
+ * Informs the application that the session has been tuned to the given channel.
+ *
+ * @see TvInputService.Session#notifyTvInputSessionData(String, Bundle)
+ * @see SESSION_DATA_KEY_CHANNEL_URI
+ * @hide
+ */
+ public static final String SESSION_DATA_TYPE_TUNED = "tuned";
+
+ /**
+ * Sends the type and ID of a selected track. This is used to inform the application that a
+ * specific track is selected.
+ *
+ * @see TvInputService.Session#notifyTvInputSessionData(String, Bundle)
+ * @see SESSION_DATA_KEY_TRACK_TYPE
+ * @see SESSION_DATA_KEY_TRACK_ID
+ * @hide
+ */
+ public static final String SESSION_DATA_TYPE_TRACK_SELECTED = "track_selected";
+
+ /**
+ * Sends the list of all audio/video/subtitle tracks.
+ *
+ * @see TvInputService.Session#notifyTvInputSessionData(String, Bundle)
+ * @see SESSION_DATA_KEY_TRACKS
+ * @hide
+ */
+ public static final String SESSION_DATA_TYPE_TRACKS_CHANGED = "tracks_changed";
+
+ /**
+ * Informs the application that the video is now available for watching.
+ *
+ * @see TvInputService.Session#notifyTvInputSessionData(String, Bundle)
+ * @hide
+ */
+ public static final String SESSION_DATA_TYPE_VIDEO_AVAILABLE = "video_available";
+
+ /**
+ * Informs the application that the video became unavailable for some reason.
+ *
+ * @see TvInputService.Session#notifyTvInputSessionData(String, Bundle)
+ * @see SESSION_DATA_KEY_VIDEO_UNAVAILABLE_REASON
+ * @hide
+ */
+ public static final String SESSION_DATA_TYPE_VIDEO_UNAVAILABLE = "video_unavailable";
+
+ /**
+ * Notifies response for broadcast info.
+ *
+ * @see TvInputService.Session#notifyTvInputSessionData(String, Bundle)
+ * @see SESSION_DATA_KEY_BROADCAST_INFO_RESPONSE
+ * @hide
+ */
+ public static final String SESSION_DATA_TYPE_BROADCAST_INFO_RESPONSE =
+ "broadcast_info_response";
+
+ /**
+ * Notifies response for advertisement.
+ *
+ * @see TvInputService.Session#notifyTvInputSessionData(String, Bundle)
+ * @see SESSION_DATA_KEY_AD_RESPONSE
+ * @hide
+ */
+ public static final String SESSION_DATA_TYPE_AD_RESPONSE = "ad_response";
+
+ /**
+ * Notifies the advertisement buffer is consumed.
+ *
+ * @see TvInputService.Session#notifyTvInputSessionData(String, Bundle)
+ * @see SESSION_DATA_KEY_AD_BUFFER
+ * @hide
+ */
+ public static final String SESSION_DATA_TYPE_AD_BUFFER_CONSUMED = "ad_buffer_consumed";
+
+ /**
+ * Sends the TV message.
+ *
+ * @see TvInputService.Session#notifyTvInputSessionData(String, Bundle)
+ * @see TvInputService.Session#notifyTvMessage(int, Bundle)
+ * @see SESSION_DATA_KEY_TV_MESSAGE_TYPE
+ * @hide
+ */
+ public static final String SESSION_DATA_TYPE_TV_MESSAGE = "tv_message";
+
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @StringDef(prefix = "SESSION_DATA_KEY_", value = {
+ SESSION_DATA_KEY_CHANNEL_URI,
+ SESSION_DATA_KEY_TRACK_TYPE,
+ SESSION_DATA_KEY_TRACK_ID,
+ SESSION_DATA_KEY_TRACKS,
+ SESSION_DATA_KEY_VIDEO_UNAVAILABLE_REASON,
+ SESSION_DATA_KEY_BROADCAST_INFO_RESPONSE,
+ SESSION_DATA_KEY_AD_RESPONSE,
+ SESSION_DATA_KEY_AD_BUFFER,
+ SESSION_DATA_KEY_TV_MESSAGE_TYPE})
+ public @interface SessionDataKey {}
+
+ /**
+ * The URI of a channel.
+ *
+ * <p> Type: android.net.Uri
+ *
+ * @see TvInputService.Session#notifyTvInputSessionData(String, Bundle)
+ * @hide
+ */
+ public static final String SESSION_DATA_KEY_CHANNEL_URI = "channel_uri";
+
+ /**
+ * The type of the track.
+ *
+ * <p>One of {@link TvTrackInfo#TYPE_AUDIO}, {@link TvTrackInfo#TYPE_VIDEO},
+ * {@link TvTrackInfo#TYPE_SUBTITLE}.
+ *
+ * <p> Type: Integer
+ *
+ * @see TvTrackInfo#getType()
+ * @see TvInputService.Session#notifyTvInputSessionData(String, Bundle)
+ * @hide
+ */
+ public static final String SESSION_DATA_KEY_TRACK_TYPE = "track_type";
+
+ /**
+ * The ID of the track.
+ *
+ * <p> Type: String
+ *
+ * @see TvTrackInfo#getId()
+ * @see TvInputService.Session#notifyTvInputSessionData(String, Bundle)
+ * @hide
+ */
+ public static final String SESSION_DATA_KEY_TRACK_ID = "track_id";
+
+ /**
+ * A list which includes track information.
+ *
+ * <p> Type: {@code java.util.List<android.media.tv.TvTrackInfo> }
+ *
+ * @see TvInputService.Session#notifyTvInputSessionData(String, Bundle)
+ * @hide
+ */
+ public static final String SESSION_DATA_KEY_TRACKS = "tracks";
+
+ /**
+ * The reason why the video became unavailable.
+ * <p>The value can be {@link VIDEO_UNAVAILABLE_REASON_BUFFERING},
+ * {@link VIDEO_UNAVAILABLE_REASON_AUDIO_ONLY}, etc.
+ *
+ * <p> Type: Integer
+ *
+ * @see TvInputService.Session#notifyTvInputSessionData(String, Bundle)
+ * @hide
+ */
+ public static final String SESSION_DATA_KEY_VIDEO_UNAVAILABLE_REASON =
+ "video_unavailable_reason";
+
+ /**
+ * An object of {@link BroadcastInfoResponse}.
+ *
+ * <p> Type: android.media.tv.BroadcastInfoResponse
+ *
+ * @see TvInputService.Session#notifyTvInputSessionData(String, Bundle)
+ * @hide
+ */
+ public static final String SESSION_DATA_KEY_BROADCAST_INFO_RESPONSE = "broadcast_info_response";
+
+ /**
+ * An object of {@link AdResponse}.
+ *
+ * <p> Type: android.media.tv.AdResponse
+ *
+ * @see TvInputService.Session#notifyTvInputSessionData(String, Bundle)
+ * @hide
+ */
+ public static final String SESSION_DATA_KEY_AD_RESPONSE = "ad_response";
+
+ /**
+ * An object of {@link AdBuffer}.
+ *
+ * <p> Type: android.media.tv.AdBuffer
+ *
+ * @see TvInputService.Session#notifyTvInputSessionData(String, Bundle)
+ * @hide
+ */
+ public static final String SESSION_DATA_KEY_AD_BUFFER = "ad_buffer";
+
+ /**
+ * The type of TV message.
+ * <p>It can be one of {@link TV_MESSAGE_TYPE_WATERMARK},
+ * {@link TV_MESSAGE_TYPE_CLOSED_CAPTION}, {@link TV_MESSAGE_TYPE_OTHER}
+ *
+ * <p> Type: Integer
+ *
+ * @see TvInputService.Session#notifyTvInputSessionData(String, Bundle)
+ * @hide
+ */
+ public static final String SESSION_DATA_KEY_TV_MESSAGE_TYPE = "tv_message_type";
+
+
/**
* An unknown state of the client pid gets from the TvInputManager. Client gets this value when
* query through {@link getClientPid(String sessionId)} fails.
@@ -1259,6 +1486,17 @@
});
}
}
+
+ void postTvInputSessionData(String type, Bundle data) {
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ if (mSession.getAdSession() != null) {
+ mSession.getAdSession().notifyTvInputSessionData(type, data);
+ }
+ }
+ });
+ }
}
/**
@@ -1808,6 +2046,18 @@
record.postAdBufferConsumed(buffer);
}
}
+
+ @Override
+ public void onTvInputSessionData(String type, Bundle data, int seq) {
+ synchronized (mSessionCallbackRecordMap) {
+ SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq);
+ if (record == null) {
+ Log.e(TAG, "Callback not found for seq " + seq);
+ return;
+ }
+ record.postTvInputSessionData(type, data);
+ }
+ }
};
ITvInputManagerCallback managerCallback = new ITvInputManagerCallback.Stub() {
@Override
@@ -3832,6 +4082,21 @@
}
}
+ /**
+ * Notifies data from session of linked TvAdService.
+ */
+ public void notifyTvAdSessionData(String type, Bundle data) {
+ if (mToken == null) {
+ Log.w(TAG, "The session has been already released");
+ return;
+ }
+ try {
+ mService.notifyTvAdSessionData(mToken, type, data, mUserId);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
private final class InputEventHandler extends Handler {
public static final int MSG_SEND_INPUT_EVENT = 1;
public static final int MSG_TIMEOUT_INPUT_EVENT = 2;
diff --git a/media/java/android/media/tv/TvInputService.java b/media/java/android/media/tv/TvInputService.java
index 6301a27..a022b1c 100644
--- a/media/java/android/media/tv/TvInputService.java
+++ b/media/java/android/media/tv/TvInputService.java
@@ -34,6 +34,7 @@
import android.hardware.hdmi.HdmiDeviceInfo;
import android.media.AudioPresentation;
import android.media.PlaybackParams;
+import android.media.tv.ad.TvAdManager;
import android.media.tv.interactive.TvInteractiveAppService;
import android.net.Uri;
import android.os.AsyncTask;
@@ -1259,6 +1260,37 @@
}
/**
+ * Notifies data related to this session to corresponding linked
+ * {@link android.media.tv.ad.TvAdService} object via TvAdView.
+ *
+ * <p>Methods like {@link #notifyBroadcastInfoResponse(BroadcastInfoResponse)} sends the
+ * related data to linked {@link android.media.tv.interactive.TvInteractiveAppService}, but
+ * don't work for {@link android.media.tv.ad.TvAdService}. The method is used specifically
+ * for {@link android.media.tv.ad.TvAdService} use cases.
+ *
+ * @param type data type
+ * @param data the related data values
+ * @hide
+ */
+ public void notifyTvInputSessionData(
+ @NonNull @TvInputManager.SessionDataType String type, @NonNull Bundle data) {
+ executeOrPostRunnableOnMainThread(new Runnable() {
+ @MainThread
+ @Override
+ public void run() {
+ try {
+ if (DEBUG) Log.d(TAG, "notifyTvInputSessionData");
+ if (mSessionCallback != null) {
+ mSessionCallback.onTvInputSessionData(type, data);
+ }
+ } catch (RemoteException e) {
+ Log.w(TAG, "error in notifyTvInputSessionData", e);
+ }
+ }
+ });
+ }
+
+ /**
* Assigns a size and position to the surface passed in {@link #onSetSurface}. The position
* is relative to the overlay view that sits on top of this surface.
*
@@ -1401,6 +1433,20 @@
public void onAdBufferReady(@NonNull AdBuffer buffer) {
}
+
+ /**
+ * Called when data from the linked {@link android.media.tv.ad.TvAdService} is received.
+ *
+ * @param type the type of the data
+ * @param data a bundle contains the data received
+ * @see android.media.tv.ad.TvAdService.Session#notifyTvAdSessionData(String, Bundle)
+ * @see android.media.tv.ad.TvAdView#setTvView(TvView)
+ * @hide
+ */
+ public void onTvAdSessionData(
+ @NonNull @TvAdManager.SessionDataType String type, @NonNull Bundle data) {
+ }
+
/**
* Tunes to a given channel.
*
@@ -2166,6 +2212,10 @@
onAdBufferReady(buffer);
}
+ void notifyTvAdSessionData(String type, Bundle data) {
+ onTvAdSessionData(type, data);
+ }
+
void onTvMessageReceived(int type, Bundle data) {
onTvMessage(type, data);
}
diff --git a/media/java/android/media/tv/ad/ITvAdClient.aidl b/media/java/android/media/tv/ad/ITvAdClient.aidl
index 34d96b3..49046d0 100644
--- a/media/java/android/media/tv/ad/ITvAdClient.aidl
+++ b/media/java/android/media/tv/ad/ITvAdClient.aidl
@@ -16,6 +16,7 @@
package android.media.tv.ad;
+import android.os.Bundle;
import android.view.InputChannel;
/**
@@ -27,4 +28,11 @@
void onSessionCreated(in String serviceId, IBinder token, in InputChannel channel, int seq);
void onSessionReleased(int seq);
void onLayoutSurface(int left, int top, int right, int bottom, int seq);
+ void onRequestCurrentVideoBounds(int seq);
+ void onRequestCurrentChannelUri(int seq);
+ void onRequestTrackInfoList(int seq);
+ void onRequestCurrentTvInputId(int seq);
+ void onRequestSigning(
+ in String id, in String algorithm, in String alias, in byte[] data, int seq);
+ void onTvAdSessionData(in String type, in Bundle data, int seq);
}
\ No newline at end of file
diff --git a/media/java/android/media/tv/ad/ITvAdManager.aidl b/media/java/android/media/tv/ad/ITvAdManager.aidl
index 9620065..5c5f095 100644
--- a/media/java/android/media/tv/ad/ITvAdManager.aidl
+++ b/media/java/android/media/tv/ad/ITvAdManager.aidl
@@ -31,6 +31,7 @@
*/
interface ITvAdManager {
List<TvAdServiceInfo> getTvAdServiceList(int userId);
+ void sendAppLinkCommand(String serviceId, in Bundle command, int userId);
void createSession(
in ITvAdClient client, in String serviceId, in String type, int seq, int userId);
void releaseSession(in IBinder sessionToken, int userId);
@@ -58,4 +59,7 @@
int userId);
void relayoutMediaView(in IBinder sessionToken, in Rect frame, int userId);
void removeMediaView(in IBinder sessionToken, int userId);
+
+ void notifyTvInputSessionData(
+ in IBinder sessionToken, in String type, in Bundle data, int userId);
}
diff --git a/media/java/android/media/tv/ad/ITvAdSession.aidl b/media/java/android/media/tv/ad/ITvAdSession.aidl
index 69afb17..eba0bc8 100644
--- a/media/java/android/media/tv/ad/ITvAdSession.aidl
+++ b/media/java/android/media/tv/ad/ITvAdSession.aidl
@@ -46,4 +46,6 @@
void createMediaView(in IBinder windowToken, in Rect frame);
void relayoutMediaView(in Rect frame);
void removeMediaView();
+
+ void notifyTvInputSessionData(in String type, in Bundle data);
}
diff --git a/media/java/android/media/tv/ad/ITvAdSessionCallback.aidl b/media/java/android/media/tv/ad/ITvAdSessionCallback.aidl
index f21ef19..e4e5cbb 100644
--- a/media/java/android/media/tv/ad/ITvAdSessionCallback.aidl
+++ b/media/java/android/media/tv/ad/ITvAdSessionCallback.aidl
@@ -17,6 +17,7 @@
package android.media.tv.ad;
import android.media.tv.ad.ITvAdSession;
+import android.os.Bundle;
/**
* Helper interface for ITvAdSession to allow TvAdService to notify the system service when there is
@@ -26,4 +27,10 @@
oneway interface ITvAdSessionCallback {
void onSessionCreated(in ITvAdSession session);
void onLayoutSurface(int left, int top, int right, int bottom);
+ void onRequestCurrentVideoBounds();
+ void onRequestCurrentChannelUri();
+ void onRequestTrackInfoList();
+ void onRequestCurrentTvInputId();
+ void onRequestSigning(in String id, in String algorithm, in String alias, in byte[] data);
+ void onTvAdSessionData(in String type, in Bundle data);
}
\ No newline at end of file
diff --git a/media/java/android/media/tv/ad/ITvAdSessionWrapper.java b/media/java/android/media/tv/ad/ITvAdSessionWrapper.java
index 251351d..e10a20e 100644
--- a/media/java/android/media/tv/ad/ITvAdSessionWrapper.java
+++ b/media/java/android/media/tv/ad/ITvAdSessionWrapper.java
@@ -65,6 +65,7 @@
private static final int DO_SEND_SIGNING_RESULT = 14;
private static final int DO_NOTIFY_ERROR = 15;
private static final int DO_NOTIFY_TV_MESSAGE = 16;
+ private static final int DO_NOTIFY_INPUT_SESSION_DATA = 17;
private final HandlerCaller mCaller;
private TvAdService.Session mSessionImpl;
@@ -180,6 +181,12 @@
args.recycle();
break;
}
+ case DO_NOTIFY_INPUT_SESSION_DATA: {
+ SomeArgs args = (SomeArgs) msg.obj;
+ mSessionImpl.notifyTvInputSessionData((String) args.arg1, (Bundle) args.arg2);
+ args.recycle();
+ break;
+ }
default: {
Log.w(TAG, "Unhandled message code: " + msg.what);
break;
@@ -280,6 +287,12 @@
mCaller.executeOrSendMessage(mCaller.obtainMessage(DO_REMOVE_MEDIA_VIEW));
}
+ @Override
+ public void notifyTvInputSessionData(String type, Bundle data) {
+ mCaller.executeOrSendMessage(
+ mCaller.obtainMessageOO(DO_NOTIFY_INPUT_SESSION_DATA, type, data));
+ }
+
private final class TvAdEventReceiver extends InputEventReceiver {
TvAdEventReceiver(InputChannel inputChannel, Looper looper) {
super(inputChannel, looper);
diff --git a/media/java/android/media/tv/ad/TvAdManager.java b/media/java/android/media/tv/ad/TvAdManager.java
index 4dce72f..02c0d75 100644
--- a/media/java/android/media/tv/ad/TvAdManager.java
+++ b/media/java/android/media/tv/ad/TvAdManager.java
@@ -16,12 +16,15 @@
package android.media.tv.ad;
+import android.annotation.CallbackExecutor;
import android.annotation.FlaggedApi;
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.annotation.StringDef;
import android.annotation.SystemService;
import android.content.Context;
import android.graphics.Rect;
+import android.media.tv.AdBuffer;
import android.media.tv.TvInputManager;
import android.media.tv.TvTrackInfo;
import android.media.tv.flags.Flags;
@@ -43,7 +46,10 @@
import com.android.internal.util.Preconditions;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
+import java.util.Iterator;
import java.util.List;
import java.util.concurrent.Executor;
@@ -57,6 +63,198 @@
// TODO: implement more methods and unhide APIs.
private static final String TAG = "TvAdManager";
+ /**
+ * Key for package name in app link.
+ * <p>Type: String
+ *
+ * @see #sendAppLinkCommand(String, Bundle)
+ * @hide
+ */
+ public static final String APP_LINK_KEY_PACKAGE_NAME = "package_name";
+
+ /**
+ * Key for class name in app link.
+ * <p>Type: String
+ *
+ * @see #sendAppLinkCommand(String, Bundle)
+ * @hide
+ */
+ public static final String APP_LINK_KEY_CLASS_NAME = "class_name";
+
+ /**
+ * Key for command type in app link command.
+ * <p>Type: String
+ *
+ * @see #sendAppLinkCommand(String, Bundle)
+ * @hide
+ */
+ public static final String APP_LINK_KEY_COMMAND_TYPE = "command_type";
+
+ /**
+ * Key for service ID in app link command.
+ * <p>Type: String
+ *
+ * @see #sendAppLinkCommand(String, Bundle)
+ * @hide
+ */
+ public static final String APP_LINK_KEY_SERVICE_ID = "service_id";
+
+ /**
+ * Key for back URI in app link command.
+ * <p>Type: String
+ *
+ * @see #sendAppLinkCommand(String, Bundle)
+ * @hide
+ */
+ public static final String APP_LINK_KEY_BACK_URI = "back_uri";
+
+ /**
+ * Broadcast intent action to send app command to TV app.
+ *
+ * @see #sendAppLinkCommand(String, Bundle)
+ * @hide
+ */
+ public static final String ACTION_APP_LINK_COMMAND =
+ "android.media.tv.ad.action.APP_LINK_COMMAND";
+
+ /**
+ * Intent key for TV input ID. It's used to send app command to TV app.
+ * <p>Type: String
+ *
+ * @see #sendAppLinkCommand(String, Bundle)
+ * @see #ACTION_APP_LINK_COMMAND
+ * @hide
+ */
+ public static final String INTENT_KEY_TV_INPUT_ID = "tv_input_id";
+
+ /**
+ * Intent key for TV AD service ID. It's used to send app command to TV app.
+ * <p>Type: String
+ *
+ * @see #sendAppLinkCommand(String, Bundle)
+ * @see #ACTION_APP_LINK_COMMAND
+ * @see TvAdServiceInfo#getId()
+ * @hide
+ */
+ public static final String INTENT_KEY_AD_SERVICE_ID = "ad_service_id";
+
+ /**
+ * Intent key for TV channel URI. It's used to send app command to TV app.
+ * <p>Type: android.net.Uri
+ *
+ * @see #sendAppLinkCommand(String, Bundle)
+ * @see #ACTION_APP_LINK_COMMAND
+ * @hide
+ */
+ public static final String INTENT_KEY_CHANNEL_URI = "channel_uri";
+
+ /**
+ * Intent key for command type. It's used to send app command to TV app. The value of this key
+ * could vary according to TV apps.
+ * <p>Type: String
+ *
+ * @see #sendAppLinkCommand(String, Bundle)
+ * @see #ACTION_APP_LINK_COMMAND
+ * @hide
+ */
+ public static final String INTENT_KEY_COMMAND_TYPE = "command_type";
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @StringDef(prefix = "SESSION_DATA_TYPE_", value = {
+ SESSION_DATA_TYPE_AD_REQUEST,
+ SESSION_DATA_TYPE_AD_BUFFER_READY,
+ SESSION_DATA_TYPE_BROADCAST_INFO_REQUEST,
+ SESSION_DATA_TYPE_REMOVE_BROADCAST_INFO_REQUEST})
+ public @interface SessionDataType {}
+
+ /**
+ * Sends an advertisement request to be processed by the related TV input.
+ *
+ * @see TvAdService.Session#notifyTvAdSessionData(String, Bundle)
+ * @see SESSION_DATA_KEY_AD_REQUEST
+ * @hide
+ */
+ public static final String SESSION_DATA_TYPE_AD_REQUEST = "ad_request";
+
+ /**
+ * Notifies the advertisement buffer is ready.
+ *
+ * @see TvAdService.Session#notifyTvAdSessionData(String, Bundle)
+ * @see SESSION_DATA_KEY_AD_BUFFER
+ * @hide
+ */
+ public static final String SESSION_DATA_TYPE_AD_BUFFER_READY = "ad_buffer_ready";
+
+ /**
+ * Sends request for broadcast info.
+ *
+ * @see TvAdService.Session#notifyTvAdSessionData(String, Bundle)
+ * @see SESSION_DATA_KEY_BROADCAST_INFO_RESQUEST
+ * @hide
+ */
+ public static final String SESSION_DATA_TYPE_BROADCAST_INFO_REQUEST = "broadcast_info_request";
+
+ /**
+ * Removes request for broadcast info.
+ *
+ * @see TvAdService.Session#notifyTvAdSessionData(String, Bundle)
+ * @see SESSION_DATA_KEY_BROADCAST_INFO_REQUEST_ID
+ * @hide
+ */
+ public static final String SESSION_DATA_TYPE_REMOVE_BROADCAST_INFO_REQUEST =
+ "remove_broadcast_info_request";
+
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @StringDef(prefix = "SESSION_DATA_KEY_", value = {
+ SESSION_DATA_KEY_AD_REQUEST,
+ SESSION_DATA_KEY_AD_BUFFER,
+ SESSION_DATA_KEY_BROADCAST_INFO_REQUEST,
+ SESSION_DATA_KEY_REQUEST_ID})
+ public @interface SessionDataKey {}
+
+ /**
+ * An object of {@link android.media.tv.AdRequest}.
+ *
+ * <p> Type: android.media.tv.AdRequest
+ *
+ * @see TvAdService.Session#notifyTvAdSessionData(String, Bundle)
+ * @hide
+ */
+ public static final String SESSION_DATA_KEY_AD_REQUEST = "ad_request";
+
+ /**
+ * An object of {@link AdBuffer}.
+ *
+ * <p> Type: android.media.tv.AdBuffer
+ *
+ * @see TvAdService.Session#notifyTvAdSessionData(String, Bundle)
+ * @hide
+ */
+ public static final String SESSION_DATA_KEY_AD_BUFFER = "ad_buffer";
+
+ /**
+ * An object of {@link android.media.tv.BroadcastInfoRequest}.
+ *
+ * <p> Type: android.media.tv.BroadcastInfoRequest
+ *
+ * @see TvAdService.Session#notifyTvAdSessionData(String, Bundle)
+ * @hide
+ */
+ public static final String SESSION_DATA_KEY_BROADCAST_INFO_REQUEST = "broadcast_info_request";
+
+ /**
+ * The ID of {@link android.media.tv.BroadcastInfoRequest}.
+ *
+ * <p> Type: Integer
+ *
+ * @see TvAdService.Session#notifyTvAdSessionData(String, Bundle)
+ * @hide
+ */
+ public static final String SESSION_DATA_KEY_REQUEST_ID = "request_id";
+
private final ITvAdManager mService;
private final int mUserId;
@@ -125,6 +323,79 @@
}
}
+ @Override
+ public void onRequestCurrentVideoBounds(int seq) {
+ synchronized (mSessionCallbackRecordMap) {
+ SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq);
+ if (record == null) {
+ Log.e(TAG, "Callback not found for seq " + seq);
+ return;
+ }
+ record.postRequestCurrentVideoBounds();
+ }
+ }
+
+ @Override
+ public void onRequestCurrentChannelUri(int seq) {
+ synchronized (mSessionCallbackRecordMap) {
+ SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq);
+ if (record == null) {
+ Log.e(TAG, "Callback not found for seq " + seq);
+ return;
+ }
+ record.postRequestCurrentChannelUri();
+ }
+ }
+
+ @Override
+ public void onRequestTrackInfoList(int seq) {
+ synchronized (mSessionCallbackRecordMap) {
+ SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq);
+ if (record == null) {
+ Log.e(TAG, "Callback not found for seq " + seq);
+ return;
+ }
+ record.postRequestTrackInfoList();
+ }
+ }
+
+ @Override
+ public void onRequestCurrentTvInputId(int seq) {
+ synchronized (mSessionCallbackRecordMap) {
+ SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq);
+ if (record == null) {
+ Log.e(TAG, "Callback not found for seq " + seq);
+ return;
+ }
+ record.postRequestCurrentTvInputId();
+ }
+ }
+
+ @Override
+ public void onRequestSigning(
+ String id, String algorithm, String alias, byte[] data, int seq) {
+ synchronized (mSessionCallbackRecordMap) {
+ SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq);
+ if (record == null) {
+ Log.e(TAG, "Callback not found for seq " + seq);
+ return;
+ }
+ record.postRequestSigning(id, algorithm, alias, data);
+ }
+ }
+
+ @Override
+ public void onTvAdSessionData(String type, Bundle data, int seq) {
+ synchronized (mSessionCallbackRecordMap) {
+ SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq);
+ if (record == null) {
+ Log.e(TAG, "Callback not found for seq " + seq);
+ return;
+ }
+ record.postTvAdSessionData(type, data);
+ }
+ }
+
};
ITvAdManagerCallback managerCallback =
@@ -220,6 +491,59 @@
}
/**
+ * Sends app link command.
+ *
+ * @param serviceId The ID of TV AD service which the command to be sent to. The ID can be found
+ * in {@link TvAdServiceInfo#getId()}.
+ * @param command The command to be sent.
+ * @hide
+ */
+ public void sendAppLinkCommand(@NonNull String serviceId, @NonNull Bundle command) {
+ try {
+ mService.sendAppLinkCommand(serviceId, command, mUserId);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Registers a {@link TvAdServiceCallback}.
+ *
+ * @param callback A callback used to monitor status of the TV AD services.
+ * @param executor A {@link Executor} that the status change will be delivered to.
+ * @hide
+ */
+ public void registerCallback(
+ @CallbackExecutor @NonNull Executor executor,
+ @NonNull TvAdServiceCallback callback) {
+ Preconditions.checkNotNull(callback);
+ Preconditions.checkNotNull(executor);
+ synchronized (mLock) {
+ mCallbackRecords.add(new TvAdServiceCallbackRecord(callback, executor));
+ }
+ }
+
+ /**
+ * Unregisters the existing {@link TvAdServiceCallback}.
+ *
+ * @param callback The existing callback to remove.
+ * @hide
+ */
+ public void unregisterCallback(@NonNull final TvAdServiceCallback callback) {
+ Preconditions.checkNotNull(callback);
+ synchronized (mLock) {
+ for (Iterator<TvAdServiceCallbackRecord> it = mCallbackRecords.iterator();
+ it.hasNext(); ) {
+ TvAdServiceCallbackRecord record = it.next();
+ if (record.getCallback() == callback) {
+ it.remove();
+ break;
+ }
+ }
+ }
+ }
+
+ /**
* The Session provides the per-session functionality of AD service.
* @hide
*/
@@ -533,6 +857,88 @@
}
}
+ /**
+ * Notifies data from session of linked TvInputService.
+ */
+ public void notifyTvInputSessionData(String type, Bundle data) {
+ if (mToken == null) {
+ Log.w(TAG, "The session has been already released");
+ return;
+ }
+ try {
+ mService.notifyTvInputSessionData(mToken, type, data, mUserId);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Dispatches an input event to this session.
+ *
+ * @param event An {@link InputEvent} to dispatch. Cannot be {@code null}.
+ * @param token A token used to identify the input event later in the callback.
+ * @param callback A callback used to receive the dispatch result. Cannot be {@code null}.
+ * @param handler A {@link Handler} that the dispatch result will be delivered to. Cannot be
+ * {@code null}.
+ * @return Returns {@link #DISPATCH_HANDLED} if the event was handled. Returns
+ * {@link #DISPATCH_NOT_HANDLED} if the event was not handled. Returns
+ * {@link #DISPATCH_IN_PROGRESS} if the event is in progress and the callback will
+ * be invoked later.
+ * @hide
+ */
+ public int dispatchInputEvent(@NonNull InputEvent event, Object token,
+ @NonNull FinishedInputEventCallback callback, @NonNull Handler handler) {
+ Preconditions.checkNotNull(event);
+ Preconditions.checkNotNull(callback);
+ Preconditions.checkNotNull(handler);
+ synchronized (mHandler) {
+ if (mInputChannel == null) {
+ return DISPATCH_NOT_HANDLED;
+ }
+ PendingEvent p = obtainPendingEventLocked(event, token, callback, handler);
+ if (Looper.myLooper() == Looper.getMainLooper()) {
+ // Already running on the main thread so we can send the event immediately.
+ return sendInputEventOnMainLooperLocked(p);
+ }
+
+ // Post the event to the main thread.
+ Message msg = mHandler.obtainMessage(InputEventHandler.MSG_SEND_INPUT_EVENT, p);
+ msg.setAsynchronous(true);
+ mHandler.sendMessage(msg);
+ return DISPATCH_IN_PROGRESS;
+ }
+ }
+
+ private PendingEvent obtainPendingEventLocked(InputEvent event, Object token,
+ FinishedInputEventCallback callback, Handler handler) {
+ PendingEvent p = mPendingEventPool.acquire();
+ if (p == null) {
+ p = new PendingEvent();
+ }
+ p.mEvent = event;
+ p.mEventToken = token;
+ p.mCallback = callback;
+ p.mEventHandler = handler;
+ return p;
+ }
+
+ /**
+ * Callback that is invoked when an input event that was dispatched to this session has been
+ * finished.
+ *
+ * @hide
+ */
+ public interface FinishedInputEventCallback {
+ /**
+ * Called when the dispatched input event is finished.
+ *
+ * @param token A token passed to {@link #dispatchInputEvent}.
+ * @param handled {@code true} if the dispatched input event was handled properly.
+ * {@code false} otherwise.
+ */
+ void onFinishedInputEvent(Object token, boolean handled);
+ }
+
private final class InputEventHandler extends Handler {
public static final int MSG_SEND_INPUT_EVENT = 1;
public static final int MSG_TIMEOUT_INPUT_EVENT = 2;
@@ -639,23 +1045,6 @@
mPendingEventPool.release(p);
}
- /**
- * Callback that is invoked when an input event that was dispatched to this session has been
- * finished.
- *
- * @hide
- */
- public interface FinishedInputEventCallback {
- /**
- * Called when the dispatched input event is finished.
- *
- * @param token A token passed to {@link #dispatchInputEvent}.
- * @param handled {@code true} if the dispatched input event was handled properly.
- * {@code false} otherwise.
- */
- void onFinishedInputEvent(Object token, boolean handled);
- }
-
private final class TvInputEventSender extends InputEventSender {
TvInputEventSender(InputChannel inputChannel, Looper looper) {
super(inputChannel, looper);
@@ -730,6 +1119,58 @@
public void onLayoutSurface(Session session, int left, int top, int right, int bottom) {
}
+ /**
+ * This is called when {@link TvAdService.Session#requestCurrentVideoBounds} is
+ * called.
+ *
+ * @param session A {@link TvAdService.Session} associated with this callback.
+ */
+ public void onRequestCurrentVideoBounds(Session session) {
+ }
+
+ /**
+ * This is called when {@link TvAdService.Session#requestCurrentChannelUri} is
+ * called.
+ *
+ * @param session A {@link TvAdService.Session} associated with this callback.
+ */
+ public void onRequestCurrentChannelUri(Session session) {
+ }
+
+ /**
+ * This is called when {@link TvAdService.Session#requestTrackInfoList} is
+ * called.
+ *
+ * @param session A {@link TvAdService.Session} associated with this callback.
+ */
+ public void onRequestTrackInfoList(Session session) {
+ }
+
+ /**
+ * This is called when {@link TvAdService.Session#requestCurrentTvInputId} is
+ * called.
+ *
+ * @param session A {@link TvAdService.Session} associated with this callback.
+ */
+ public void onRequestCurrentTvInputId(Session session) {
+ }
+
+ /**
+ * This is called when
+ * {@link TvAdService.Session#requestSigning(String, String, String, byte[])} is
+ * called.
+ *
+ * @param session A {@link TvAdService.Session} associated with this callback.
+ * @param signingId the ID to identify the request.
+ * @param algorithm the standard name of the signature algorithm requested, such as
+ * MD5withRSA, SHA256withDSA, etc.
+ * @param alias the alias of the corresponding {@link java.security.KeyStore}.
+ * @param data the original bytes to be signed.
+ */
+ public void onRequestSigning(
+ Session session, String signingId, String algorithm, String alias, byte[] data) {
+ }
+
}
/**
@@ -810,6 +1251,62 @@
}
});
}
+
+ void postRequestCurrentVideoBounds() {
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ mSessionCallback.onRequestCurrentVideoBounds(mSession);
+ }
+ });
+ }
+
+ void postRequestCurrentChannelUri() {
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ mSessionCallback.onRequestCurrentChannelUri(mSession);
+ }
+ });
+ }
+
+ void postRequestTrackInfoList() {
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ mSessionCallback.onRequestTrackInfoList(mSession);
+ }
+ });
+ }
+
+ void postRequestCurrentTvInputId() {
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ mSessionCallback.onRequestCurrentTvInputId(mSession);
+ }
+ });
+ }
+
+ void postRequestSigning(String id, String algorithm, String alias, byte[] data) {
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ mSessionCallback.onRequestSigning(mSession, id, algorithm, alias, data);
+ }
+ });
+ }
+
+ void postTvAdSessionData(String type, Bundle data) {
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ if (mSession.getInputSession() != null) {
+ mSession.getInputSession().notifyTvAdSessionData(type, data);
+ }
+ }
+ });
+ }
}
private static final class TvAdServiceCallbackRecord {
diff --git a/media/java/android/media/tv/ad/TvAdService.java b/media/java/android/media/tv/ad/TvAdService.java
index 5d81837..4d8f5c8b 100644
--- a/media/java/android/media/tv/ad/TvAdService.java
+++ b/media/java/android/media/tv/ad/TvAdService.java
@@ -31,6 +31,7 @@
import android.graphics.Rect;
import android.media.tv.TvInputManager;
import android.media.tv.TvTrackInfo;
+import android.media.tv.TvView;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
@@ -132,6 +133,9 @@
/**
* Called when app link command is received.
+ *
+ * @see TvAdManager#sendAppLinkCommand(String, Bundle)
+ * @hide
*/
public void onAppLinkCommand(@NonNull Bundle command) {
}
@@ -279,6 +283,143 @@
onResetAdService();
}
+ /**
+ * Requests the bounds of the current video.
+ * @hide
+ */
+ @CallSuper
+ public void requestCurrentVideoBounds() {
+ executeOrPostRunnableOnMainThread(new Runnable() {
+ @MainThread
+ @Override
+ public void run() {
+ try {
+ if (DEBUG) {
+ Log.d(TAG, "requestCurrentVideoBounds");
+ }
+ if (mSessionCallback != null) {
+ mSessionCallback.onRequestCurrentVideoBounds();
+ }
+ } catch (RemoteException e) {
+ Log.w(TAG, "error in requestCurrentVideoBounds", e);
+ }
+ }
+ });
+ }
+
+ /**
+ * Requests the URI of the current channel.
+ * @hide
+ */
+ @CallSuper
+ public void requestCurrentChannelUri() {
+ executeOrPostRunnableOnMainThread(new Runnable() {
+ @MainThread
+ @Override
+ public void run() {
+ try {
+ if (DEBUG) {
+ Log.d(TAG, "requestCurrentChannelUri");
+ }
+ if (mSessionCallback != null) {
+ mSessionCallback.onRequestCurrentChannelUri();
+ }
+ } catch (RemoteException e) {
+ Log.w(TAG, "error in requestCurrentChannelUri", e);
+ }
+ }
+ });
+ }
+
+ /**
+ * Requests the list of {@link TvTrackInfo}.
+ * @hide
+ */
+ @CallSuper
+ public void requestTrackInfoList() {
+ executeOrPostRunnableOnMainThread(new Runnable() {
+ @MainThread
+ @Override
+ public void run() {
+ try {
+ if (DEBUG) {
+ Log.d(TAG, "requestTrackInfoList");
+ }
+ if (mSessionCallback != null) {
+ mSessionCallback.onRequestTrackInfoList();
+ }
+ } catch (RemoteException e) {
+ Log.w(TAG, "error in requestTrackInfoList", e);
+ }
+ }
+ });
+ }
+
+ /**
+ * Requests current TV input ID.
+ *
+ * @see android.media.tv.TvInputInfo
+ * @hide
+ */
+ @CallSuper
+ public void requestCurrentTvInputId() {
+ executeOrPostRunnableOnMainThread(new Runnable() {
+ @MainThread
+ @Override
+ public void run() {
+ try {
+ if (DEBUG) {
+ Log.d(TAG, "requestCurrentTvInputId");
+ }
+ if (mSessionCallback != null) {
+ mSessionCallback.onRequestCurrentTvInputId();
+ }
+ } catch (RemoteException e) {
+ Log.w(TAG, "error in requestCurrentTvInputId", e);
+ }
+ }
+ });
+ }
+
+ /**
+ * Requests signing of the given data.
+ *
+ * <p>This is used when the corresponding server of the AD service app requires signing
+ * during handshaking, and the service doesn't have the built-in private key. The private
+ * key is provided by the content providers and pre-built in the related app, such as TV
+ * app.
+ *
+ * @param signingId the ID to identify the request. When a result is received, this ID can
+ * be used to correlate the result with the request.
+ * @param algorithm the standard name of the signature algorithm requested, such as
+ * MD5withRSA, SHA256withDSA, etc. The name is from standards like
+ * FIPS PUB 186-4 and PKCS #1.
+ * @param alias the alias of the corresponding {@link java.security.KeyStore}.
+ * @param data the original bytes to be signed.
+ *
+ * @see #onSigningResult(String, byte[])
+ */
+ @CallSuper
+ public void requestSigning(@NonNull String signingId, @NonNull String algorithm,
+ @NonNull String alias, @NonNull byte[] data) {
+ executeOrPostRunnableOnMainThread(new Runnable() {
+ @MainThread
+ @Override
+ public void run() {
+ try {
+ if (DEBUG) {
+ Log.d(TAG, "requestSigning");
+ }
+ if (mSessionCallback != null) {
+ mSessionCallback.onRequestSigning(signingId, algorithm, alias, data);
+ }
+ } catch (RemoteException e) {
+ Log.w(TAG, "error in requestSigning", e);
+ }
+ }
+ });
+ }
+
@Override
public boolean onKeyDown(int keyCode, @NonNull KeyEvent event) {
return false;
@@ -470,6 +611,19 @@
}
/**
+ * Called when data from the linked {@link android.media.tv.TvInputService} is received.
+ *
+ * @param type the type of the data
+ * @param data a bundle contains the data received
+ * @see android.media.tv.TvInputService.Session#notifyTvAdSessionData(String, Bundle)
+ * @see android.media.tv.ad.TvAdView#setTvView(TvView)
+ * @hide
+ */
+ public void onTvInputSessionData(
+ @NonNull @TvInputManager.SessionDataType String type, @NonNull Bundle data) {
+ }
+
+ /**
* Called when the size of the media view is changed by the application.
*
* <p>This is always called at least once when the session is created regardless of whether
@@ -497,6 +651,33 @@
}
/**
+ * Notifies data related to this session to corresponding linked
+ * {@link android.media.tv.TvInputService} object via TvView.
+ *
+ * @param type data type
+ * @param data the related data values
+ * @see TvAdView#setTvView(TvView)
+ * @hide
+ */
+ public void notifyTvAdSessionData(
+ @NonNull @TvAdManager.SessionDataType String type, @NonNull Bundle data) {
+ executeOrPostRunnableOnMainThread(new Runnable() {
+ @MainThread
+ @Override
+ public void run() {
+ try {
+ if (DEBUG) Log.d(TAG, "notifyTvAdSessionData");
+ if (mSessionCallback != null) {
+ mSessionCallback.onTvAdSessionData(type, data);
+ }
+ } catch (RemoteException e) {
+ Log.w(TAG, "error in notifyTvAdSessionData", e);
+ }
+ }
+ });
+ }
+
+ /**
* Takes care of dispatching incoming input events and tells whether the event was handled.
*/
int dispatchInputEvent(InputEvent event, InputEventReceiver receiver) {
@@ -594,6 +775,10 @@
onTvMessage(type, data);
}
+ void notifyTvInputSessionData(String type, Bundle data) {
+ onTvInputSessionData(type, data);
+ }
+
private void executeOrPostRunnableOnMainThread(Runnable action) {
synchronized (mLock) {
if (mSessionCallback == null) {
diff --git a/media/java/android/media/tv/ad/TvAdView.java b/media/java/android/media/tv/ad/TvAdView.java
index ec23b7c..604dbd5 100644
--- a/media/java/android/media/tv/ad/TvAdView.java
+++ b/media/java/android/media/tv/ad/TvAdView.java
@@ -28,17 +28,21 @@
import android.media.tv.TvInputManager;
import android.media.tv.TvTrackInfo;
import android.media.tv.TvView;
+import android.media.tv.ad.TvAdManager.Session.FinishedInputEventCallback;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.util.AttributeSet;
import android.util.Log;
import android.util.Xml;
+import android.view.InputEvent;
+import android.view.KeyEvent;
import android.view.Surface;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.View;
import android.view.ViewGroup;
+import android.view.ViewRootImpl;
import java.util.List;
import java.util.concurrent.Executor;
@@ -88,6 +92,7 @@
private boolean mMediaViewCreated;
private Rect mMediaViewFrame;
+ private OnUnhandledInputEventListener mOnUnhandledInputEventListener;
@@ -327,6 +332,109 @@
mSession.dispatchSurfaceChanged(format, width, height);
}
+ private final FinishedInputEventCallback mFinishedInputEventCallback =
+ new FinishedInputEventCallback() {
+ @Override
+ public void onFinishedInputEvent(Object token, boolean handled) {
+ if (DEBUG) {
+ Log.d(TAG, "onFinishedInputEvent(token=" + token + ", handled="
+ + handled + ")");
+ }
+ if (handled) {
+ return;
+ }
+ // TODO: Re-order unhandled events.
+ InputEvent event = (InputEvent) token;
+ if (dispatchUnhandledInputEvent(event)) {
+ return;
+ }
+ ViewRootImpl viewRootImpl = getViewRootImpl();
+ if (viewRootImpl != null) {
+ viewRootImpl.dispatchUnhandledInputEvent(event);
+ }
+ }
+ };
+
+ /**
+ * Dispatches an unhandled input event to the next receiver.
+ *
+ * It gives the host application a chance to dispatch the unhandled input events.
+ *
+ * @param event The input event.
+ * @return {@code true} if the event was handled by the view, {@code false} otherwise.
+ * @hide
+ */
+ public boolean dispatchUnhandledInputEvent(@NonNull InputEvent event) {
+ if (mOnUnhandledInputEventListener != null) {
+ if (mOnUnhandledInputEventListener.onUnhandledInputEvent(event)) {
+ return true;
+ }
+ }
+ return onUnhandledInputEvent(event);
+ }
+
+ /**
+ * Called when an unhandled input event also has not been handled by the user provided
+ * callback. This is the last chance to handle the unhandled input event in the
+ * TvAdView.
+ *
+ * @param event The input event.
+ * @return If you handled the event, return {@code true}. If you want to allow the event to be
+ * handled by the next receiver, return {@code false}.
+ */
+ public boolean onUnhandledInputEvent(@NonNull InputEvent event) {
+ return false;
+ }
+
+ /**
+ * Sets a listener to be invoked when an input event is not handled
+ * by the TV AD service.
+ *
+ * @param listener The callback to be invoked when the unhandled input event is received.
+ * @hide
+ */
+ public void setOnUnhandledInputEventListener(
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull OnUnhandledInputEventListener listener) {
+ mOnUnhandledInputEventListener = listener;
+ // TODO: handle CallbackExecutor
+ }
+
+ /**
+ * Gets the {@link OnUnhandledInputEventListener}.
+ * <p>Returns {@code null} if the listener is not set or is cleared.
+ *
+ * @see #setOnUnhandledInputEventListener(Executor, OnUnhandledInputEventListener)
+ * @see #clearOnUnhandledInputEventListener()
+ * @hide
+ */
+ @Nullable
+ public OnUnhandledInputEventListener getOnUnhandledInputEventListener() {
+ return mOnUnhandledInputEventListener;
+ }
+
+ /**
+ * Clears the {@link OnUnhandledInputEventListener}.
+ * @hide
+ */
+ public void clearOnUnhandledInputEventListener() {
+ mOnUnhandledInputEventListener = null;
+ }
+
+ @Override
+ public boolean dispatchKeyEvent(@NonNull KeyEvent event) {
+ if (super.dispatchKeyEvent(event)) {
+ return true;
+ }
+ if (mSession == null) {
+ return false;
+ }
+ InputEvent copiedEvent = event.copy();
+ int ret = mSession.dispatchInputEvent(copiedEvent, copiedEvent, mFinishedInputEventCallback,
+ mHandler);
+ return ret != TvAdManager.Session.DISPATCH_NOT_HANDLED;
+ }
+
/**
* Prepares the AD service of corresponding {@link TvAdService}.
*
@@ -504,6 +612,24 @@
}
/**
+ * Interface definition for a callback to be invoked when the unhandled input event is received.
+ * @hide
+ */
+ public interface OnUnhandledInputEventListener {
+ /**
+ * Called when an input event was not handled by the TV AD service.
+ *
+ * <p>This is called asynchronously from where the event is dispatched. It gives the host
+ * application a chance to handle the unhandled input events.
+ *
+ * @param event The input event.
+ * @return If you handled the event, return {@code true}. If you want to allow the event to
+ * be handled by the next receiver, return {@code false}.
+ */
+ boolean onUnhandledInputEvent(@NonNull InputEvent event);
+ }
+
+ /**
* Sets the callback to be invoked when an event is dispatched to this TvAdView.
*
* @param callback the callback to receive events. MUST NOT be {@code null}.
@@ -606,6 +732,117 @@
mUseRequestedSurfaceLayout = true;
requestLayout();
}
+
+ @Override
+ public void onRequestCurrentVideoBounds(TvAdManager.Session session) {
+ if (DEBUG) {
+ Log.d(TAG, "onRequestCurrentVideoBounds");
+ }
+ if (this != mSessionCallback) {
+ Log.w(TAG, "onRequestCurrentVideoBounds - session not created");
+ return;
+ }
+ synchronized (mCallbackLock) {
+ if (mCallbackExecutor != null) {
+ mCallbackExecutor.execute(() -> {
+ synchronized (mCallbackLock) {
+ if (mCallback != null) {
+ mCallback.onRequestCurrentVideoBounds(mServiceId);
+ }
+ }
+ });
+ }
+ }
+ }
+
+ @Override
+ public void onRequestCurrentChannelUri(TvAdManager.Session session) {
+ if (DEBUG) {
+ Log.d(TAG, "onRequestCurrentChannelUri");
+ }
+ if (this != mSessionCallback) {
+ Log.w(TAG, "onRequestCurrentChannelUri - session not created");
+ return;
+ }
+ synchronized (mCallbackLock) {
+ if (mCallbackExecutor != null) {
+ mCallbackExecutor.execute(() -> {
+ synchronized (mCallbackLock) {
+ if (mCallback != null) {
+ mCallback.onRequestCurrentChannelUri(mServiceId);
+ }
+ }
+ });
+ }
+ }
+ }
+
+ @Override
+ public void onRequestTrackInfoList(TvAdManager.Session session) {
+ if (DEBUG) {
+ Log.d(TAG, "onRequestTrackInfoList");
+ }
+ if (this != mSessionCallback) {
+ Log.w(TAG, "onRequestTrackInfoList - session not created");
+ return;
+ }
+ synchronized (mCallbackLock) {
+ if (mCallbackExecutor != null) {
+ mCallbackExecutor.execute(() -> {
+ synchronized (mCallbackLock) {
+ if (mCallback != null) {
+ mCallback.onRequestTrackInfoList(mServiceId);
+ }
+ }
+ });
+ }
+ }
+ }
+
+ @Override
+ public void onRequestCurrentTvInputId(TvAdManager.Session session) {
+ if (DEBUG) {
+ Log.d(TAG, "onRequestCurrentTvInputId");
+ }
+ if (this != mSessionCallback) {
+ Log.w(TAG, "onRequestCurrentTvInputId - session not created");
+ return;
+ }
+ synchronized (mCallbackLock) {
+ if (mCallbackExecutor != null) {
+ mCallbackExecutor.execute(() -> {
+ synchronized (mCallbackLock) {
+ if (mCallback != null) {
+ mCallback.onRequestCurrentTvInputId(mServiceId);
+ }
+ }
+ });
+ }
+ }
+ }
+
+ @Override
+ public void onRequestSigning(TvAdManager.Session session, String id, String algorithm,
+ String alias, byte[] data) {
+ if (DEBUG) {
+ Log.d(TAG, "onRequestSigning");
+ }
+ if (this != mSessionCallback) {
+ Log.w(TAG, "onRequestSigning - session not created");
+ return;
+ }
+ synchronized (mCallbackLock) {
+ if (mCallbackExecutor != null) {
+ mCallbackExecutor.execute(() -> {
+ synchronized (mCallbackLock) {
+ if (mCallback != null) {
+ mCallback.onRequestSigning(mServiceId, id, algorithm, alias, data);
+ }
+ }
+ });
+ }
+ }
+ }
}
/**
@@ -613,5 +850,55 @@
* @hide
*/
public abstract static class TvAdCallback {
+
+ /**
+ * This is called when {@link TvAdService.Session#requestCurrentVideoBounds()}
+ * is called.
+ *
+ * @param serviceId The ID of the TV AD service bound to this view.
+ */
+ public void onRequestCurrentVideoBounds(@NonNull String serviceId) {
+ }
+
+ /**
+ * This is called when {@link TvAdService.Session#requestCurrentChannelUri()} is
+ * called.
+ *
+ * @param serviceId The ID of the AD service bound to this view.
+ */
+ public void onRequestCurrentChannelUri(@NonNull String serviceId) {
+ }
+
+ /**
+ * This is called when {@link TvAdService.Session#requestTrackInfoList()} is called.
+ *
+ * @param serviceId The ID of the AD service bound to this view.
+ */
+ public void onRequestTrackInfoList(@NonNull String serviceId) {
+ }
+
+ /**
+ * This is called when {@link TvAdService.Session#requestCurrentTvInputId()} is called.
+ *
+ * @param serviceId The ID of the AD service bound to this view.
+ */
+ public void onRequestCurrentTvInputId(@NonNull String serviceId) {
+ }
+
+ /**
+ * This is called when
+ * {@link TvAdService.Session#requestSigning(String, String, String, byte[])} is called.
+ *
+ * @param serviceId The ID of the AD service bound to this view.
+ * @param signingId the ID to identify the request.
+ * @param algorithm the standard name of the signature algorithm requested, such as
+ * MD5withRSA, SHA256withDSA, etc.
+ * @param alias the alias of the corresponding {@link java.security.KeyStore}.
+ * @param data the original bytes to be signed.
+ *
+ */
+ public void onRequestSigning(@NonNull String serviceId, @NonNull String signingId,
+ @NonNull String algorithm, @NonNull String alias, @NonNull byte[] data) {
+ }
}
}
diff --git a/media/java/android/media/tv/interactive/ITvInteractiveAppClient.aidl b/media/java/android/media/tv/interactive/ITvInteractiveAppClient.aidl
index 7b58531..1404d7c 100644
--- a/media/java/android/media/tv/interactive/ITvInteractiveAppClient.aidl
+++ b/media/java/android/media/tv/interactive/ITvInteractiveAppClient.aidl
@@ -63,6 +63,8 @@
void onRequestTvRecordingInfoList(in int type, int seq);
void onRequestSigning(in String id, in String algorithm, in String alias, in byte[] data,
int seq);
+ void onRequestSigning2(in String id, in String algorithm, in String host,
+ int port, 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/ITvInteractiveAppSessionCallback.aidl b/media/java/android/media/tv/interactive/ITvInteractiveAppSessionCallback.aidl
index cb89181..3c91a3e 100644
--- a/media/java/android/media/tv/interactive/ITvInteractiveAppSessionCallback.aidl
+++ b/media/java/android/media/tv/interactive/ITvInteractiveAppSessionCallback.aidl
@@ -61,6 +61,7 @@
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 onRequestSigning2(in String id, in String algorithm, in String host, int port, in byte[] data);
void onRequestCertificate(in String host, int port);
void onAdRequest(in AdRequest request);
}
diff --git a/media/java/android/media/tv/interactive/TvInteractiveAppManager.java b/media/java/android/media/tv/interactive/TvInteractiveAppManager.java
index 011744f..498eec6 100755
--- a/media/java/android/media/tv/interactive/TvInteractiveAppManager.java
+++ b/media/java/android/media/tv/interactive/TvInteractiveAppManager.java
@@ -657,6 +657,19 @@
}
@Override
+ public void onRequestSigning2(
+ String id, String algorithm, String host, int port, byte[] data, int seq) {
+ synchronized (mSessionCallbackRecordMap) {
+ SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq);
+ if (record == null) {
+ Log.e(TAG, "Callback not found for seq " + seq);
+ return;
+ }
+ record.postRequestSigning(id, algorithm, host, port, data);
+ }
+ }
+
+ @Override
public void onRequestCertificate(String host, int port, int seq) {
synchronized (mSessionCallbackRecordMap) {
SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq);
@@ -2258,6 +2271,17 @@
});
}
+ void postRequestSigning(String id, String algorithm, String host, int port,
+ byte[] data) {
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ mSessionCallback.onRequestSigning(mSession, id, algorithm, host,
+ port, data);
+ }
+ });
+ }
+
void postRequestCertificate(String host, int port) {
mHandler.post(new Runnable() {
@Override
@@ -2609,6 +2633,25 @@
}
/**
+ * This is called when
+ * {@link TvInteractiveAppService.Session#requestSigning(String, String, String, int, byte[])} is
+ * called.
+ *
+ * @param session A {@link TvInteractiveAppService.Session} associated with this callback.
+ * @param signingId the ID to identify the request.
+ * @param algorithm the standard name of the signature algorithm requested, such as
+ * MD5withRSA, SHA256withDSA, etc.
+ * @param host The host of the SSL CLient Authentication Server
+ * @param port The port of the SSL Client Authentication Server
+ * @param data the original bytes to be signed.
+ * @hide
+ */
+ public void onRequestSigning(
+ Session session, String signingId, String algorithm, String host,
+ int port, byte[] data) {
+ }
+
+ /**
* This is called when the service requests a SSL certificate for client validation.
*
* @param session A {@link TvInteractiveAppService.Session} associated with this callback.
diff --git a/media/java/android/media/tv/interactive/TvInteractiveAppService.java b/media/java/android/media/tv/interactive/TvInteractiveAppService.java
index 054b272..7b6dc38 100755
--- a/media/java/android/media/tv/interactive/TvInteractiveAppService.java
+++ b/media/java/android/media/tv/interactive/TvInteractiveAppService.java
@@ -1645,6 +1645,50 @@
}
/**
+ * Requests signing of the given data.
+ *
+ * <p>This is used when the corresponding server of the broadcast-independent interactive
+ * app requires signing during handshaking, and the interactive app service doesn't have
+ * the built-in private key. The private key is provided by the content providers and
+ * pre-built in the related app, such as TV app.
+ *
+ * @param signingId the ID to identify the request. When a result is received, this ID can
+ * be used to correlate the result with the request.
+ * @param algorithm the standard name of the signature algorithm requested, such as
+ * MD5withRSA, SHA256withDSA, etc. The name is from standards like
+ * FIPS PUB 186-4 and PKCS #1.
+ * @param host the host of the SSL client authentication server.
+ * @param port the port of the SSL client authentication server.
+ * @param data the original bytes to be signed.
+ *
+ * @see #onSigningResult(String, byte[])
+ * @see TvInteractiveAppView#createBiInteractiveApp(Uri, Bundle)
+ * @see TvInteractiveAppView#BI_INTERACTIVE_APP_KEY_ALIAS
+ * @hide
+ */
+ @CallSuper
+ public void requestSigning(@NonNull String signingId, @NonNull String algorithm,
+ @NonNull String host, int port, @NonNull byte[] data) {
+ executeOrPostRunnableOnMainThread(new Runnable() {
+ @MainThread
+ @Override
+ public void run() {
+ try {
+ if (DEBUG) {
+ Log.d(TAG, "requestSigning");
+ }
+ if (mSessionCallback != null) {
+ mSessionCallback.onRequestSigning2(signingId, algorithm,
+ host, port, data);
+ }
+ } catch (RemoteException e) {
+ Log.w(TAG, "error in requestSigning", e);
+ }
+ }
+ });
+ }
+
+ /**
* Requests a SSL certificate for client validation.
*
* @param host the host name of the SSL authentication server.
diff --git a/media/jni/android_media_MediaCodec.cpp b/media/jni/android_media_MediaCodec.cpp
index ef90bf9..8cdd59e 100644
--- a/media/jni/android_media_MediaCodec.cpp
+++ b/media/jni/android_media_MediaCodec.cpp
@@ -163,6 +163,13 @@
static struct {
jclass clazz;
jmethodID ctorId;
+ jmethodID sizeId;
+ jmethodID addId;
+} gArrayDequeInfo;
+
+static struct {
+ jclass clazz;
+ jmethodID ctorId;
jmethodID setInternalStateId;
jfieldID contextId;
jfieldID validId;
@@ -200,8 +207,14 @@
jfieldID queueRequestIndexID;
jfieldID outputFrameLinearBlockID;
jfieldID outputFrameHardwareBufferID;
+ jfieldID outputFramebufferInfosID;
jfieldID outputFrameChangedKeysID;
jfieldID outputFrameFormatID;
+ jfieldID bufferInfoFlags;
+ jfieldID bufferInfoOffset;
+ jfieldID bufferInfoSize;
+ jfieldID bufferInfoPresentationTimeUs;
+
};
static fields_t gFields;
@@ -412,6 +425,22 @@
index, offset, size, timeUs, flags, errorDetailMsg);
}
+status_t JMediaCodec::queueInputBuffers(
+ size_t index,
+ size_t offset,
+ size_t size,
+ const sp<RefBase> &infos,
+ AString *errorDetailMsg) {
+
+ sp<BufferInfosWrapper> auInfo((BufferInfosWrapper *)infos.get());
+ return mCodec->queueInputBuffers(
+ index,
+ offset,
+ size,
+ auInfo,
+ errorDetailMsg);
+}
+
status_t JMediaCodec::queueSecureInputBuffer(
size_t index,
size_t offset,
@@ -430,10 +459,11 @@
}
status_t JMediaCodec::queueBuffer(
- size_t index, const std::shared_ptr<C2Buffer> &buffer, int64_t timeUs,
- uint32_t flags, const sp<AMessage> &tunings, AString *errorDetailMsg) {
+ size_t index, const std::shared_ptr<C2Buffer> &buffer,
+ const sp<RefBase> &infos, const sp<AMessage> &tunings, AString *errorDetailMsg) {
+ sp<BufferInfosWrapper> auInfo((BufferInfosWrapper *)infos.get());
return mCodec->queueBuffer(
- index, buffer, timeUs, flags, tunings, errorDetailMsg);
+ index, buffer, auInfo, tunings, errorDetailMsg);
}
status_t JMediaCodec::queueEncryptedLinearBlock(
@@ -446,13 +476,13 @@
const uint8_t iv[16],
CryptoPlugin::Mode mode,
const CryptoPlugin::Pattern &pattern,
- int64_t presentationTimeUs,
- uint32_t flags,
+ const sp<RefBase> &infos,
const sp<AMessage> &tunings,
AString *errorDetailMsg) {
+ sp<BufferInfosWrapper> auInfo((BufferInfosWrapper *)infos.get());
return mCodec->queueEncryptedBuffer(
index, buffer, offset, subSamples, numSubSamples, key, iv, mode, pattern,
- presentationTimeUs, flags, tunings, errorDetailMsg);
+ auInfo, tunings, errorDetailMsg);
}
status_t JMediaCodec::dequeueInputBuffer(size_t *index, int64_t timeoutUs) {
@@ -722,6 +752,42 @@
return OK;
}
+void maybeSetBufferInfos(JNIEnv *env, jobject &frame, const sp<BufferInfosWrapper> &bufInfos) {
+ if (!bufInfos) {
+ return;
+ }
+ std::vector<AccessUnitInfo> &infos = bufInfos.get()->value;
+ if (infos.empty()) {
+ return;
+ }
+ ScopedLocalRef<jobject> dequeObj{env, env->NewObject(
+ gArrayDequeInfo.clazz, gArrayDequeInfo.ctorId)};
+ jint offset = 0;
+ std::vector<jobject> jObjectInfos;
+ for (int i = 0 ; i < infos.size(); i++) {
+ jobject bufferInfo = env->NewObject(
+ gBufferInfo.clazz, gBufferInfo.ctorId);
+ if (bufferInfo != NULL) {
+ env->CallVoidMethod(bufferInfo, gBufferInfo.setId,
+ offset,
+ (jint)(infos)[i].mSize,
+ (infos)[i].mTimestamp,
+ (infos)[i].mFlags);
+ (void)env->CallBooleanMethod(
+ dequeObj.get(), gArrayDequeInfo.addId, bufferInfo);
+ offset += (infos)[i].mSize;
+ jObjectInfos.push_back(bufferInfo);
+ }
+ }
+ env->SetObjectField(
+ frame,
+ gFields.outputFramebufferInfosID,
+ dequeObj.get());
+ for (int i = 0; i < jObjectInfos.size(); i++) {
+ env->DeleteLocalRef(jObjectInfos[i]);
+ }
+}
+
status_t JMediaCodec::getOutputFrame(
JNIEnv *env, jobject frame, size_t index) const {
sp<MediaCodecBuffer> buffer;
@@ -732,6 +798,11 @@
}
if (buffer->size() > 0) {
+ sp<RefBase> obj;
+ sp<BufferInfosWrapper> bufInfos;
+ if (buffer->meta()->findObject("accessUnitInfo", &obj)) {
+ bufInfos = std::move(((decltype(bufInfos.get()))obj.get()));
+ }
std::shared_ptr<C2Buffer> c2Buffer = buffer->asC2Buffer();
if (c2Buffer) {
switch (c2Buffer->data().type()) {
@@ -747,6 +818,7 @@
(jlong)context.release(),
true);
env->SetObjectField(frame, gFields.outputFrameLinearBlockID, linearBlock.get());
+ maybeSetBufferInfos(env, frame, bufInfos);
break;
}
case C2BufferData::GRAPHIC: {
@@ -787,6 +859,7 @@
(jlong)context.release(),
true);
env->SetObjectField(frame, gFields.outputFrameLinearBlockID, linearBlock.get());
+ maybeSetBufferInfos(env, frame, bufInfos);
} else {
// No-op.
}
@@ -1250,6 +1323,7 @@
void JMediaCodec::handleCallback(const sp<AMessage> &msg) {
int32_t arg1, arg2 = 0;
jobject obj = NULL;
+ std::vector<jobject> jObjectInfos;
CHECK(msg->findInt32("callbackID", &arg1));
JNIEnv *env = AndroidRuntime::getJNIEnv();
@@ -1287,6 +1361,35 @@
break;
}
+ case MediaCodec::CB_LARGE_FRAME_OUTPUT_AVAILABLE:
+ {
+ sp<RefBase> spobj = nullptr;
+ CHECK(msg->findInt32("index", &arg2));
+ CHECK(msg->findObject("accessUnitInfo", &spobj));
+ if (spobj != nullptr) {
+ sp<BufferInfosWrapper> bufferInfoParamsWrapper {
+ (BufferInfosWrapper *)spobj.get()};
+ std::vector<AccessUnitInfo> &bufferInfoParams =
+ bufferInfoParamsWrapper.get()->value;
+ obj = env->NewObject(gArrayDequeInfo.clazz, gArrayDequeInfo.ctorId);
+ jint offset = 0;
+ for (int i = 0 ; i < bufferInfoParams.size(); i++) {
+ jobject bufferInfo = env->NewObject(gBufferInfo.clazz, gBufferInfo.ctorId);
+ if (bufferInfo != NULL) {
+ env->CallVoidMethod(bufferInfo, gBufferInfo.setId,
+ offset,
+ (jint)(bufferInfoParams)[i].mSize,
+ (bufferInfoParams)[i].mTimestamp,
+ (bufferInfoParams)[i].mFlags);
+ (void)env->CallBooleanMethod(obj, gArrayDequeInfo.addId, bufferInfo);
+ offset += (bufferInfoParams)[i].mSize;
+ jObjectInfos.push_back(bufferInfo);
+ }
+ }
+ }
+ break;
+ }
+
case MediaCodec::CB_CRYPTO_ERROR:
{
int32_t err, actionCode;
@@ -1346,6 +1449,9 @@
arg2,
obj);
+ for (int i = 0; i < jObjectInfos.size(); i++) {
+ env->DeleteLocalRef(jObjectInfos[i]);
+ }
env->DeleteLocalRef(obj);
}
@@ -1913,6 +2019,103 @@
codec->getExceptionMessage(errorDetailMsg.c_str()).c_str());
}
+static status_t extractInfosFromObject(
+ JNIEnv * const env,
+ jint * const initialOffset,
+ jint * const totalSize,
+ std::vector<AccessUnitInfo> * const infos,
+ const jobjectArray &objArray,
+ AString * const errorDetailMsg) {
+ if (totalSize == nullptr
+ || initialOffset == nullptr
+ || infos == nullptr) {
+ if (errorDetailMsg) {
+ *errorDetailMsg = "Error: Null arguments provided for extracting Access unit info";
+ }
+ return BAD_VALUE;
+ }
+ const jsize numEntries = env->GetArrayLength(objArray);
+ if (numEntries <= 0) {
+ if (errorDetailMsg) {
+ *errorDetailMsg = "Error: No BufferInfo found while queuing for large frame input";
+ }
+ return BAD_VALUE;
+ }
+ *initialOffset = 0;
+ *totalSize = 0;
+ for (jsize i = 0; i < numEntries; i++) {
+ jobject param = env->GetObjectArrayElement(objArray, i);
+ if (param == NULL) {
+ if (errorDetailMsg) {
+ *errorDetailMsg = "Error: Queuing a null BufferInfo";
+ }
+ return BAD_VALUE;
+ }
+ size_t offset = static_cast<size_t>(env->GetIntField(param, gFields.bufferInfoOffset));
+ size_t size = static_cast<size_t>(env->GetIntField(param, gFields.bufferInfoSize));
+ uint32_t flags = static_cast<uint32_t>(env->GetIntField(param, gFields.bufferInfoFlags));
+ if (flags == 0 && size == 0) {
+ if (errorDetailMsg) {
+ *errorDetailMsg = "Error: Queuing an empty BufferInfo";
+ }
+ return BAD_VALUE;
+ }
+ if (i == 0) {
+ *initialOffset = offset;
+ }
+ if (CC_UNLIKELY((offset > UINT32_MAX)
+ || ((long)(offset + size) > UINT32_MAX)
+ || ((offset - *initialOffset) != *totalSize))) {
+ if (errorDetailMsg) {
+ *errorDetailMsg = "Error: offset/size in BufferInfo";
+ }
+ return BAD_VALUE;
+ }
+ infos->emplace_back(
+ flags,
+ size,
+ env->GetLongField(param, gFields.bufferInfoPresentationTimeUs));
+ *totalSize += size;
+ }
+ return OK;
+}
+
+static void android_media_MediaCodec_queueInputBuffers(
+ JNIEnv *env,
+ jobject thiz,
+ jint index,
+ jobjectArray objArray) {
+ ALOGV("android_media_MediaCodec_queueInputBuffers");
+ sp<JMediaCodec> codec = getMediaCodec(env, thiz);
+ if (codec == NULL || codec->initCheck() != OK || objArray == NULL) {
+ throwExceptionAsNecessary(env, INVALID_OPERATION, codec);
+ return;
+ }
+ sp<BufferInfosWrapper> infoObj =
+ new BufferInfosWrapper{decltype(infoObj->value)()};
+ AString errorDetailMsg;
+ jint initialOffset = 0;
+ jint totalSize = 0;
+ status_t err = extractInfosFromObject(
+ env,
+ &initialOffset,
+ &totalSize,
+ &infoObj->value,
+ objArray,
+ &errorDetailMsg);
+ if (err == OK) {
+ err = codec->queueInputBuffers(
+ index,
+ initialOffset,
+ totalSize,
+ infoObj,
+ &errorDetailMsg);
+ }
+ throwExceptionAsNecessary(
+ env, err, ACTION_CODE_FATAL,
+ codec->getExceptionMessage(errorDetailMsg.c_str()).c_str());
+}
+
struct NativeCryptoInfo {
NativeCryptoInfo(JNIEnv *env, jobject cryptoInfoObj)
: mEnv{env},
@@ -2559,8 +2762,7 @@
static void android_media_MediaCodec_native_queueLinearBlock(
JNIEnv *env, jobject thiz, jint index, jobject bufferObj,
- jint offset, jint size, jobject cryptoInfoObj,
- jlong presentationTimeUs, jint flags, jobject keys, jobject values) {
+ jobject cryptoInfoObj, jobjectArray objArray, jobject keys, jobject values) {
ALOGV("android_media_MediaCodec_native_queueLinearBlock");
sp<JMediaCodec> codec = getMediaCodec(env, thiz);
@@ -2578,7 +2780,24 @@
"error occurred while converting tunings from Java to native");
return;
}
-
+ jint totalSize;
+ jint initialOffset;
+ std::vector<AccessUnitInfo> infoVec;
+ AString errorDetailMsg;
+ err = extractInfosFromObject(env,
+ &initialOffset,
+ &totalSize,
+ &infoVec,
+ objArray,
+ &errorDetailMsg);
+ if (err != OK) {
+ throwExceptionAsNecessary(
+ env, INVALID_OPERATION, ACTION_CODE_FATAL,
+ codec->getExceptionMessage(errorDetailMsg.c_str()).c_str());
+ return;
+ }
+ sp<BufferInfosWrapper> infos =
+ new BufferInfosWrapper{std::move(infoVec)};
std::shared_ptr<C2Buffer> buffer;
sp<hardware::HidlMemory> memory;
ScopedLocalRef<jobject> lock{env, env->GetObjectField(bufferObj, gLinearBlockInfo.lockId)};
@@ -2587,10 +2806,10 @@
JMediaCodecLinearBlock *context =
(JMediaCodecLinearBlock *)env->GetLongField(bufferObj, gLinearBlockInfo.contextId);
if (codec->hasCryptoOrDescrambler()) {
- extractMemoryFromContext(context, offset, size, &memory);
- offset += context->mHidlMemoryOffset;
+ extractMemoryFromContext(context, initialOffset, totalSize, &memory);
+ initialOffset += context->mHidlMemoryOffset;
} else {
- extractBufferFromContext(context, offset, size, &buffer);
+ extractBufferFromContext(context, initialOffset, totalSize, &buffer);
}
}
env->MonitorExit(lock.get());
@@ -2601,7 +2820,6 @@
return;
}
- AString errorDetailMsg;
if (codec->hasCryptoOrDescrambler()) {
if (!memory) {
// It means there was an unexpected failure in extractMemoryFromContext above
@@ -2615,7 +2833,7 @@
return;
}
auto cryptoInfo =
- cryptoInfoObj ? NativeCryptoInfo{env, cryptoInfoObj} : NativeCryptoInfo{size};
+ cryptoInfoObj ? NativeCryptoInfo{env, cryptoInfoObj} : NativeCryptoInfo{totalSize};
if (env->ExceptionCheck()) {
// Creation of cryptoInfo failed. Let the exception bubble up.
return;
@@ -2623,13 +2841,12 @@
err = codec->queueEncryptedLinearBlock(
index,
memory,
- offset,
+ initialOffset,
cryptoInfo.mSubSamples, cryptoInfo.mNumSubSamples,
(const uint8_t *)cryptoInfo.mKey, (const uint8_t *)cryptoInfo.mIv,
cryptoInfo.mMode,
cryptoInfo.mPattern,
- presentationTimeUs,
- flags,
+ infos,
tunings,
&errorDetailMsg);
ALOGI_IF(err != OK, "queueEncryptedLinearBlock returned err = %d", err);
@@ -2646,7 +2863,7 @@
return;
}
err = codec->queueBuffer(
- index, buffer, presentationTimeUs, flags, tunings, &errorDetailMsg);
+ index, buffer, infos, tunings, &errorDetailMsg);
}
throwExceptionAsNecessary(
env, err, ACTION_CODE_FATAL,
@@ -2704,8 +2921,11 @@
std::shared_ptr<C2Buffer> buffer = C2Buffer::CreateGraphicBuffer(block->share(
block->crop(), C2Fence{}));
AString errorDetailMsg;
+ sp<BufferInfosWrapper> infos =
+ new BufferInfosWrapper{decltype(infos->value)()};
+ infos->value.emplace_back(flags, 0 /*not used*/, presentationTimeUs);
err = codec->queueBuffer(
- index, buffer, presentationTimeUs, flags, tunings, &errorDetailMsg);
+ index, buffer, infos, tunings, &errorDetailMsg);
throwExceptionAsNecessary(
env, err, ACTION_CODE_FATAL,
codec->getExceptionMessage(errorDetailMsg.c_str()).c_str());
@@ -3214,6 +3434,10 @@
env->GetFieldID(clazz.get(), "mLinearBlock", "Landroid/media/MediaCodec$LinearBlock;");
CHECK(gFields.outputFrameLinearBlockID != NULL);
+ gFields.outputFramebufferInfosID =
+ env->GetFieldID(clazz.get(), "mBufferInfos", "Ljava/util/ArrayDeque;");
+ CHECK(gFields.outputFramebufferInfosID != NULL);
+
gFields.outputFrameHardwareBufferID =
env->GetFieldID(clazz.get(), "mHardwareBuffer", "Landroid/hardware/HardwareBuffer;");
CHECK(gFields.outputFrameHardwareBufferID != NULL);
@@ -3401,6 +3625,19 @@
gArrayListInfo.addId = env->GetMethodID(clazz.get(), "add", "(Ljava/lang/Object;)Z");
CHECK(gArrayListInfo.addId != NULL);
+ clazz.reset(env->FindClass("java/util/ArrayDeque"));
+ CHECK(clazz.get() != NULL);
+ gArrayDequeInfo.clazz = (jclass)env->NewGlobalRef(clazz.get());
+
+ gArrayDequeInfo.ctorId = env->GetMethodID(clazz.get(), "<init>", "()V");
+ CHECK(gArrayDequeInfo.ctorId != NULL);
+
+ gArrayDequeInfo.sizeId = env->GetMethodID(clazz.get(), "size", "()I");
+ CHECK(gArrayDequeInfo.sizeId != NULL);
+
+ gArrayDequeInfo.addId = env->GetMethodID(clazz.get(), "add", "(Ljava/lang/Object;)Z");
+ CHECK(gArrayDequeInfo.addId != NULL);
+
clazz.reset(env->FindClass("android/media/MediaCodec$LinearBlock"));
CHECK(clazz.get() != NULL);
@@ -3444,6 +3681,12 @@
gBufferInfo.setId = env->GetMethodID(clazz.get(), "set", "(IIJI)V");
CHECK(gBufferInfo.setId != NULL);
+
+ gFields.bufferInfoSize = env->GetFieldID(clazz.get(), "size", "I");
+ gFields.bufferInfoFlags = env->GetFieldID(clazz.get(), "flags", "I");
+ gFields.bufferInfoOffset = env->GetFieldID(clazz.get(), "offset", "I");
+ gFields.bufferInfoPresentationTimeUs =
+ env->GetFieldID(clazz.get(), "presentationTimeUs", "J");
}
static void android_media_MediaCodec_native_setup(
@@ -3701,6 +3944,9 @@
{ "native_queueInputBuffer", "(IIIJI)V",
(void *)android_media_MediaCodec_queueInputBuffer },
+ { "native_queueInputBuffers", "(I[Ljava/lang/Object;)V",
+ (void *)android_media_MediaCodec_queueInputBuffers },
+
{ "native_queueSecureInputBuffer", "(IILandroid/media/MediaCodec$CryptoInfo;JI)V",
(void *)android_media_MediaCodec_queueSecureInputBuffer },
@@ -3711,8 +3957,8 @@
{ "native_closeMediaImage", "(J)V", (void *)android_media_MediaCodec_closeMediaImage },
{ "native_queueLinearBlock",
- "(ILandroid/media/MediaCodec$LinearBlock;IILandroid/media/MediaCodec$CryptoInfo;JI"
- "Ljava/util/ArrayList;Ljava/util/ArrayList;)V",
+ "(ILandroid/media/MediaCodec$LinearBlock;Landroid/media/MediaCodec$CryptoInfo;"
+ "[Ljava/lang/Object;Ljava/util/ArrayList;Ljava/util/ArrayList;)V",
(void *)android_media_MediaCodec_native_queueLinearBlock },
{ "native_queueHardwareBuffer",
diff --git a/media/jni/android_media_MediaCodec.h b/media/jni/android_media_MediaCodec.h
index fbaf64f..02708ef 100644
--- a/media/jni/android_media_MediaCodec.h
+++ b/media/jni/android_media_MediaCodec.h
@@ -35,6 +35,7 @@
namespace android {
struct ABuffer;
+struct AccessUnitInfo;
struct ALooper;
struct AMessage;
struct AString;
@@ -93,6 +94,13 @@
size_t offset, size_t size, int64_t timeUs, uint32_t flags,
AString *errorDetailMsg);
+ status_t queueInputBuffers(
+ size_t index,
+ size_t offset,
+ size_t size,
+ const sp<RefBase> &auInfo,
+ AString *errorDetailMsg = NULL);
+
status_t queueSecureInputBuffer(
size_t index,
size_t offset,
@@ -108,7 +116,7 @@
status_t queueBuffer(
size_t index, const std::shared_ptr<C2Buffer> &buffer,
- int64_t timeUs, uint32_t flags, const sp<AMessage> &tunings,
+ const sp<RefBase> &infos, const sp<AMessage> &tunings,
AString *errorDetailMsg);
status_t queueEncryptedLinearBlock(
@@ -121,8 +129,7 @@
const uint8_t iv[16],
CryptoPlugin::Mode mode,
const CryptoPlugin::Pattern &pattern,
- int64_t presentationTimeUs,
- uint32_t flags,
+ const sp<RefBase> &infos,
const sp<AMessage> &tunings,
AString *errorDetailMsg);
diff --git a/media/jni/soundpool/SoundDecoder.cpp b/media/jni/soundpool/SoundDecoder.cpp
index 5ed10b0..ae57634 100644
--- a/media/jni/soundpool/SoundDecoder.cpp
+++ b/media/jni/soundpool/SoundDecoder.cpp
@@ -29,14 +29,15 @@
// before the SoundDecoder thread closes.
static constexpr int32_t kWaitTimeBeforeCloseMs = 1000;
-SoundDecoder::SoundDecoder(SoundManager* soundManager, size_t threads)
+SoundDecoder::SoundDecoder(SoundManager* soundManager, size_t threads, int32_t threadPriority)
: mSoundManager(soundManager)
{
ALOGV("%s(%p, %zu)", __func__, soundManager, threads);
// ThreadPool is created, but we don't launch any threads.
mThreadPool = std::make_unique<ThreadPool>(
std::min(threads, (size_t)std::thread::hardware_concurrency()),
- "SoundDecoder_");
+ "SoundDecoder_",
+ threadPriority);
}
SoundDecoder::~SoundDecoder()
diff --git a/media/jni/soundpool/SoundDecoder.h b/media/jni/soundpool/SoundDecoder.h
index 7b62114..3f44a0d 100644
--- a/media/jni/soundpool/SoundDecoder.h
+++ b/media/jni/soundpool/SoundDecoder.h
@@ -28,7 +28,7 @@
*/
class SoundDecoder {
public:
- SoundDecoder(SoundManager* soundManager, size_t threads);
+ SoundDecoder(SoundManager* soundManager, size_t threads, int32_t threadPriority);
~SoundDecoder();
void loadSound(int32_t soundID) NO_THREAD_SAFETY_ANALYSIS; // uses unique_lock
void quit();
diff --git a/media/jni/soundpool/SoundManager.cpp b/media/jni/soundpool/SoundManager.cpp
index 5b16174..fa35813 100644
--- a/media/jni/soundpool/SoundManager.cpp
+++ b/media/jni/soundpool/SoundManager.cpp
@@ -29,7 +29,7 @@
static const size_t kDecoderThreads = std::thread::hardware_concurrency() >= 4 ? 2 : 1;
SoundManager::SoundManager()
- : mDecoder{std::make_unique<SoundDecoder>(this, kDecoderThreads)}
+ : mDecoder{std::make_unique<SoundDecoder>(this, kDecoderThreads, ANDROID_PRIORITY_NORMAL)}
{
ALOGV("%s()", __func__);
}
diff --git a/media/jni/soundpool/StreamManager.cpp b/media/jni/soundpool/StreamManager.cpp
index 66fec1c..e11ccbc 100644
--- a/media/jni/soundpool/StreamManager.cpp
+++ b/media/jni/soundpool/StreamManager.cpp
@@ -126,7 +126,8 @@
mThreadPool = std::make_unique<ThreadPool>(
std::min((size_t)streams, // do not make more threads than streams to play
std::min(threads, (size_t)std::thread::hardware_concurrency())),
- "SoundPool_");
+ "SoundPool_",
+ ANDROID_PRIORITY_AUDIO);
}
#pragma clang diagnostic pop
diff --git a/media/jni/soundpool/StreamManager.h b/media/jni/soundpool/StreamManager.h
index 340b49b..a4cb286 100644
--- a/media/jni/soundpool/StreamManager.h
+++ b/media/jni/soundpool/StreamManager.h
@@ -46,9 +46,9 @@
*/
class JavaThread {
public:
- JavaThread(std::function<void()> f, const char *name)
+ JavaThread(std::function<void()> f, const char *name, int32_t threadPriority)
: mF{std::move(f)} {
- createThreadEtc(staticFunction, this, name, ANDROID_PRIORITY_AUDIO);
+ createThreadEtc(staticFunction, this, name, threadPriority);
}
JavaThread(JavaThread &&) = delete; // uses "this" ptr, not moveable.
@@ -109,9 +109,11 @@
*/
class ThreadPool {
public:
- ThreadPool(size_t maxThreadCount, std::string name)
+ ThreadPool(size_t maxThreadCount, std::string name,
+ int32_t threadPriority = ANDROID_PRIORITY_NORMAL)
: mMaxThreadCount(maxThreadCount)
- , mName{std::move(name)} { }
+ , mName{std::move(name)}
+ , mThreadPriority(threadPriority) {}
~ThreadPool() { quit(); }
@@ -159,7 +161,8 @@
const int32_t id = mNextThreadId;
mThreads.emplace_back(std::make_unique<JavaThread>(
[this, id, mf = std::move(f)] { mf(id); --mActiveThreadCount; },
- (mName + std::to_string(id)).c_str()));
+ (mName + std::to_string(id)).c_str(),
+ mThreadPriority));
++mActiveThreadCount;
return id;
}
@@ -180,6 +183,7 @@
private:
const size_t mMaxThreadCount;
const std::string mName;
+ const int32_t mThreadPriority;
std::atomic_size_t mActiveThreadCount = 0;
diff --git a/media/tests/projection/src/android/media/projection/FakeIMediaProjection.java b/media/tests/projection/src/android/media/projection/FakeIMediaProjection.java
index 774de5f..0df36af 100644
--- a/media/tests/projection/src/android/media/projection/FakeIMediaProjection.java
+++ b/media/tests/projection/src/android/media/projection/FakeIMediaProjection.java
@@ -19,7 +19,7 @@
import static android.Manifest.permission.MANAGE_MEDIA_PROJECTION;
import android.annotation.EnforcePermission;
-import android.os.IBinder;
+import android.app.ActivityOptions.LaunchCookie;
import android.os.PermissionEnforcer;
import android.os.RemoteException;
@@ -29,7 +29,7 @@
*/
public final class FakeIMediaProjection extends IMediaProjection.Stub {
boolean mIsStarted = false;
- IBinder mLaunchCookie = null;
+ LaunchCookie mLaunchCookie = null;
IMediaProjectionCallback mIMediaProjectionCallback = null;
FakeIMediaProjection(PermissionEnforcer enforcer) {
@@ -80,14 +80,14 @@
@Override
@EnforcePermission(MANAGE_MEDIA_PROJECTION)
- public IBinder getLaunchCookie() throws RemoteException {
+ public LaunchCookie getLaunchCookie() throws RemoteException {
getLaunchCookie_enforcePermission();
return mLaunchCookie;
}
@Override
@EnforcePermission(MANAGE_MEDIA_PROJECTION)
- public void setLaunchCookie(IBinder launchCookie) throws RemoteException {
+ public void setLaunchCookie(LaunchCookie launchCookie) throws RemoteException {
setLaunchCookie_enforcePermission();
mLaunchCookie = launchCookie;
}
diff --git a/media/tests/projection/src/android/media/projection/MediaProjectionTest.java b/media/tests/projection/src/android/media/projection/MediaProjectionTest.java
index 2e0396f..1323e89 100644
--- a/media/tests/projection/src/android/media/projection/MediaProjectionTest.java
+++ b/media/tests/projection/src/android/media/projection/MediaProjectionTest.java
@@ -31,15 +31,14 @@
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.nullable;
import static org.mockito.Mockito.doReturn;
-import static org.mockito.Mockito.mock;
import android.annotation.Nullable;
+import android.app.ActivityOptions.LaunchCookie;
import android.compat.testing.PlatformCompatChangeRule;
import android.hardware.display.DisplayManager;
import android.hardware.display.VirtualDisplay;
import android.hardware.display.VirtualDisplayConfig;
import android.os.Handler;
-import android.os.IBinder;
import android.os.Looper;
import android.os.RemoteException;
import android.os.test.FakePermissionEnforcer;
@@ -117,7 +116,7 @@
permissionEnforcer.grant(MANAGE_MEDIA_PROJECTION);
// Support the MediaProjection instance.
mFakeIMediaProjection = new FakeIMediaProjection(permissionEnforcer);
- mFakeIMediaProjection.setLaunchCookie(mock(IBinder.class));
+ mFakeIMediaProjection.setLaunchCookie(new LaunchCookie());
mMediaProjection = new MediaProjection(mTestableContext, mFakeIMediaProjection,
mDisplayManager);
diff --git a/packages/CredentialManager/res/layout/credman_dropdown_bottom_sheet.xml b/packages/CredentialManager/res/layout/credman_dropdown_bottom_sheet.xml
index 929756c..b97e992 100644
--- a/packages/CredentialManager/res/layout/credman_dropdown_bottom_sheet.xml
+++ b/packages/CredentialManager/res/layout/credman_dropdown_bottom_sheet.xml
@@ -17,6 +17,7 @@
android:id="@android:id/content"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
+ android:minWidth="@dimen/dropdown_touch_target_min_width"
android:orientation="horizontal"
android:layout_marginEnd="@dimen/dropdown_layout_horizontal_margin"
android:elevation="3dp">
diff --git a/packages/CredentialManager/res/layout/credman_dropdown_presentation_layout.xml b/packages/CredentialManager/res/layout/credman_dropdown_presentation_layout.xml
index 1fe5e0e..261154f 100644
--- a/packages/CredentialManager/res/layout/credman_dropdown_presentation_layout.xml
+++ b/packages/CredentialManager/res/layout/credman_dropdown_presentation_layout.xml
@@ -17,6 +17,7 @@
android:id="@android:id/content"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
+ android:minWidth="@dimen/dropdown_touch_target_min_width"
android:layout_marginEnd="@dimen/dropdown_layout_horizontal_margin"
android:elevation="3dp">
@@ -24,7 +25,7 @@
android:id="@android:id/icon1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:contentDescription="@string/provider_icon_content_description"
+ android:contentDescription="@string/dropdown_presentation_more_sign_in_options_text"
android:layout_centerVertical="true"
android:layout_alignParentStart="true"
android:background="@null"/>
diff --git a/packages/CredentialManager/res/values/dimens.xml b/packages/CredentialManager/res/values/dimens.xml
index 3a8c78f..53852cb 100644
--- a/packages/CredentialManager/res/values/dimens.xml
+++ b/packages/CredentialManager/res/values/dimens.xml
@@ -27,4 +27,5 @@
<dimen name="autofill_dropdown_textview_max_width">230dp</dimen>
<dimen name="dropdown_layout_horizontal_margin">24dp</dimen>
<integer name="autofill_max_visible_datasets">3</integer>
+ <dimen name="dropdown_touch_target_min_width">48dp</dimen>
</resources>
\ No newline at end of file
diff --git a/packages/CtsShim/OWNERS b/packages/CtsShim/OWNERS
index 9419771..f5741a0 100644
--- a/packages/CtsShim/OWNERS
+++ b/packages/CtsShim/OWNERS
@@ -1,3 +1,2 @@
-ioffe@google.com
-toddke@google.com
-patb@google.com
\ No newline at end of file
+include /PACKAGE_MANAGER_OWNERS
+ioffe@google.com
\ No newline at end of file
diff --git a/packages/PackageInstaller/AndroidManifest.xml b/packages/PackageInstaller/AndroidManifest.xml
index 5a21d59..09e0d61 100644
--- a/packages/PackageInstaller/AndroidManifest.xml
+++ b/packages/PackageInstaller/AndroidManifest.xml
@@ -75,19 +75,15 @@
</intent-filter>
</activity>
- <!-- NOTE: the workaround to fix the screen flash problem. Remember to check the problem
- is resolved for new implementation -->
<activity android:name=".InstallStaging"
- android:theme="@style/Theme.AlertDialogActivity.NoDim"
- android:exported="false" />
+ android:exported="false" />
<activity android:name=".DeleteStagedFileOnResult"
android:theme="@style/Theme.AlertDialogActivity.NoActionBar"
android:exported="false" />
<activity android:name=".PackageInstallerActivity"
- android:theme="@style/Theme.AlertDialogActivity.NoAnimation"
- android:exported="false" />
+ android:exported="false" />
<activity android:name=".InstallInstalling"
android:theme="@style/Theme.AlertDialogActivity.NoAnimation"
diff --git a/packages/PackageInstaller/OWNERS b/packages/PackageInstaller/OWNERS
index 2736870..acbdff8 100644
--- a/packages/PackageInstaller/OWNERS
+++ b/packages/PackageInstaller/OWNERS
@@ -1,5 +1,4 @@
-svetoslavganov@google.com
+# Bug component: 1002434
include /PACKAGE_MANAGER_OWNERS
-# For automotive related changes
-rogerxue@google.com
+sumedhsen@google.com
\ No newline at end of file
diff --git a/packages/PackageInstaller/res/values/themes.xml b/packages/PackageInstaller/res/values/themes.xml
index 811fa73..9a06229 100644
--- a/packages/PackageInstaller/res/values/themes.xml
+++ b/packages/PackageInstaller/res/values/themes.xml
@@ -32,9 +32,4 @@
<item name="android:windowNoTitle">true</item>
</style>
- <style name="Theme.AlertDialogActivity.NoDim"
- parent="@style/Theme.AlertDialogActivity.NoActionBar">
- <item name="android:backgroundDimAmount">0</item>
- </style>
-
</resources>
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/PackageInstallerActivity.java b/packages/PackageInstaller/src/com/android/packageinstaller/PackageInstallerActivity.java
index ceb580d..7dc157f 100644
--- a/packages/PackageInstaller/src/com/android/packageinstaller/PackageInstallerActivity.java
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/PackageInstallerActivity.java
@@ -308,7 +308,6 @@
}
private void initiateInstall() {
- bindUi();
String pkgName = mPkgInfo.packageName;
// Check if there is already a package on the device with this name
// but it has been renamed to something else.
@@ -448,6 +447,7 @@
if (mAppSnippet != null) {
// load placeholder layout with OK button disabled until we override this layout in
// startInstallConfirm
+ bindUi();
checkIfAllowedAndInitiateInstall();
}
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/card/CardModel.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/card/CardModel.kt
index b2a8b87..960ebcc 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/card/CardModel.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/card/CardModel.kt
@@ -21,6 +21,7 @@
data class CardButton(
val text: String,
+ val contentDescription: String? = null,
val onClick: () -> Unit,
)
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/card/SettingsCard.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/card/SettingsCard.kt
index c7845fa..700fa48 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/card/SettingsCard.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/card/SettingsCard.kt
@@ -45,6 +45,8 @@
import androidx.compose.ui.graphics.takeOrElse
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.semantics.contentDescription
+import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.dp
import com.android.settingslib.spa.debug.UiModePreviews
import com.android.settingslib.spa.framework.theme.SettingsDimension
@@ -182,7 +184,11 @@
@Composable
private fun Button(button: CardButton, color: Color) {
- TextButton(onClick = button.onClick) {
+ TextButton(
+ onClick = button.onClick,
+ modifier =
+ Modifier.semantics { button.contentDescription?.let { this.contentDescription = it } }
+ ) {
Text(text = button.text, color = color)
}
}
diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/card/SettingsCardTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/card/SettingsCardTest.kt
index beb9433..b5b2525 100644
--- a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/card/SettingsCardTest.kt
+++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/card/SettingsCardTest.kt
@@ -36,8 +36,7 @@
@RunWith(AndroidJUnit4::class)
class SettingsCardTest {
- @get:Rule
- val composeTestRule = createComposeRule()
+ @get:Rule val composeTestRule = createComposeRule()
private val context: Context = ApplicationProvider.getApplicationContext()
@@ -76,9 +75,7 @@
CardModel(
title = "",
text = "",
- buttons = listOf(
- CardButton(text = TEXT) {}
- ),
+ buttons = listOf(CardButton(text = TEXT) {}),
)
)
}
@@ -94,9 +91,7 @@
CardModel(
title = "",
text = "",
- buttons = listOf(
- CardButton(text = TEXT) { buttonClicked = true }
- ),
+ buttons = listOf(CardButton(text = TEXT) { buttonClicked = true }),
)
)
}
@@ -107,6 +102,25 @@
}
@Test
+ fun settingsCard_buttonHaveContentDescription() {
+ composeTestRule.setContent {
+ SettingsCard(
+ CardModel(
+ title = "",
+ text = "",
+ buttons = listOf(CardButton(
+ text = TEXT,
+ contentDescription = CONTENT_DESCRIPTION,
+ ) {}
+ ),
+ )
+ )
+ }
+
+ composeTestRule.onNodeWithContentDescription(CONTENT_DESCRIPTION).assertIsDisplayed()
+ }
+
+ @Test
fun settingsCard_dismiss() {
composeTestRule.setContent {
var isVisible by remember { mutableStateOf(true) }
@@ -130,5 +144,6 @@
private companion object {
const val TITLE = "Title"
const val TEXT = "Text"
+ const val CONTENT_DESCRIPTION = "content-description"
}
}
diff --git a/packages/SettingsProvider/src/android/provider/settings/backup/SystemSettings.java b/packages/SettingsProvider/src/android/provider/settings/backup/SystemSettings.java
index 38ec931..4c255a5 100644
--- a/packages/SettingsProvider/src/android/provider/settings/backup/SystemSettings.java
+++ b/packages/SettingsProvider/src/android/provider/settings/backup/SystemSettings.java
@@ -107,6 +107,7 @@
Settings.System.TOUCHPAD_POINTER_SPEED,
Settings.System.TOUCHPAD_NATURAL_SCROLLING,
Settings.System.TOUCHPAD_TAP_TO_CLICK,
+ Settings.System.TOUCHPAD_TAP_DRAGGING,
Settings.System.TOUCHPAD_RIGHT_CLICK_ZONE,
Settings.System.CAMERA_FLASH_NOTIFICATION,
Settings.System.SCREEN_FLASH_NOTIFICATION,
diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/SystemSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/SystemSettingsValidators.java
index 98941c7..011b42f 100644
--- a/packages/SettingsProvider/src/android/provider/settings/validators/SystemSettingsValidators.java
+++ b/packages/SettingsProvider/src/android/provider/settings/validators/SystemSettingsValidators.java
@@ -209,6 +209,7 @@
VALIDATORS.put(System.TOUCHPAD_POINTER_SPEED, new InclusiveIntegerRangeValidator(-7, 7));
VALIDATORS.put(System.TOUCHPAD_NATURAL_SCROLLING, BOOLEAN_VALIDATOR);
VALIDATORS.put(System.TOUCHPAD_TAP_TO_CLICK, BOOLEAN_VALIDATOR);
+ VALIDATORS.put(System.TOUCHPAD_TAP_DRAGGING, BOOLEAN_VALIDATOR);
VALIDATORS.put(System.TOUCHPAD_RIGHT_CLICK_ZONE, BOOLEAN_VALIDATOR);
VALIDATORS.put(System.LOCK_TO_APP_ENABLED, BOOLEAN_VALIDATOR);
VALIDATORS.put(
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java
index 1670a70..1e146a5 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java
@@ -3002,6 +3002,9 @@
dumpSetting(s, p,
Settings.System.TOUCHPAD_TAP_TO_CLICK,
SystemSettingsProto.Touchpad.TAP_TO_CLICK);
+ dumpSetting(s, p,
+ Settings.System.TOUCHPAD_TAP_DRAGGING,
+ SystemSettingsProto.Touchpad.TAP_DRAGGING);
p.end(touchpadToken);
dumpSetting(s, p,
diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp
index 90593d6..900a2f8 100644
--- a/packages/SystemUI/Android.bp
+++ b/packages/SystemUI/Android.bp
@@ -214,6 +214,8 @@
javacflags: [
"-Adagger.fastInit=enabled",
+ "-Adagger.explicitBindingConflictsWithInject=ERROR",
+ "-Adagger.strictMultibindingValidation=enabled",
"-Aroom.schemaLocation=frameworks/base/packages/SystemUI/schemas",
],
kotlincflags: ["-Xjvm-default=all"],
diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml
index 54ab5d1..0050676 100644
--- a/packages/SystemUI/AndroidManifest.xml
+++ b/packages/SystemUI/AndroidManifest.xml
@@ -1083,5 +1083,25 @@
<!-- Allow SystemUI to listen for the capabilities defined in the linked xml -->
<property android:name="android.net.PROPERTY_SELF_CERTIFIED_CAPABILITIES"
android:value="@xml/self_certified_network_capabilities_both" />
+
+
+ <service
+ android:name="com.android.systemui.dreams.homecontrols.HomeControlsDreamService"
+ android:exported="false"
+ android:enabled="false"
+ android:label="@string/home_controls_dream_label"
+ android:description="@string/home_controls_dream_description"
+ android:permission="android.permission.BIND_DREAM_SERVICE"
+ android:icon="@drawable/controls_icon"
+ >
+
+ <intent-filter>
+ <action android:name="android.service.dreams.DreamService" />
+ <category android:name="android.intent.category.DEFAULT" />
+ </intent-filter>
+ <meta-data
+ android:name="android.service.dream"
+ android:resource="@xml/home_controls_dream_metadata" />
+ </service>
</application>
</manifest>
diff --git a/packages/SystemUI/aconfig/cross_device_control.aconfig b/packages/SystemUI/aconfig/cross_device_control.aconfig
new file mode 100644
index 0000000..d3f14c1
--- /dev/null
+++ b/packages/SystemUI/aconfig/cross_device_control.aconfig
@@ -0,0 +1,15 @@
+package: "com.android.systemui"
+
+flag {
+ name: "legacy_le_audio_sharing"
+ namespace: "pixel_cross_device_control"
+ description: "Gates the legacy le audio sharing UI."
+ bug: "322295262"
+}
+
+flag {
+ name: "enable_personal_le_audio_sharing"
+ namespace: "pixel_cross_device_control"
+ description: "Gates the personal le audio sharing UI in UMO."
+ bug: "322295480"
+}
diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig
index a2530d5..0ea2b1f 100644
--- a/packages/SystemUI/aconfig/systemui.aconfig
+++ b/packages/SystemUI/aconfig/systemui.aconfig
@@ -59,14 +59,6 @@
}
flag {
- name: "notification_lifetime_extension_refactor"
- namespace: "systemui"
- description: "Enables moving notification lifetime extension management from SystemUI to "
- "Notification Manager Service"
- bug: "299448097"
-}
-
-flag {
name: "notifications_live_data_store_refactor"
namespace: "systemui"
description: "Replaces NotifLiveDataStore with ActiveNotificationListRepository, and updates consumers. "
@@ -371,3 +363,10 @@
description: "Enables refactored logic for SysUI+WM unlock/occlusion code paths"
bug: "278086361"
}
+
+flag {
+ name: "enable_keyguard_compose"
+ namespace: "systemui"
+ description: "Enables the compose version of keyguard."
+ bug: "301968149"
+}
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityLaunchAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityLaunchAnimator.kt
index 9c46ebdc..8194055 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityLaunchAnimator.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityLaunchAnimator.kt
@@ -644,7 +644,7 @@
var candidate: RemoteAnimationTarget? = null
for (it in apps) {
if (it.mode == RemoteAnimationTarget.MODE_OPENING) {
- if (it.taskInfo != null && !it.hasAnimatingParent) {
+ if (!it.hasAnimatingParent) {
return it
}
if (candidate == null) {
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/PlatformButtons.kt b/packages/SystemUI/compose/core/src/com/android/compose/PlatformButtons.kt
index bb2fbf7..a18b460 100644
--- a/packages/SystemUI/compose/core/src/com/android/compose/PlatformButtons.kt
+++ b/packages/SystemUI/compose/core/src/com/android/compose/PlatformButtons.kt
@@ -17,6 +17,7 @@
package com.android.compose
+import androidx.annotation.DrawableRes
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.RowScope
@@ -24,8 +25,13 @@
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ButtonColors
import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.IconButtonColors
+import androidx.compose.material3.IconButtonDefaults
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.android.compose.theme.LocalAndroidColorScheme
@@ -89,6 +95,29 @@
)
}
+@Composable
+fun PlatformIconButton(
+ onClick: () -> Unit,
+ modifier: Modifier = Modifier,
+ enabled: Boolean = true,
+ colors: IconButtonColors = iconButtonColors(),
+ @DrawableRes iconResource: Int,
+ contentDescription: String?,
+) {
+ IconButton(
+ modifier = modifier,
+ onClick = onClick,
+ enabled = enabled,
+ colors = colors,
+ ) {
+ Icon(
+ painter = painterResource(id = iconResource),
+ contentDescription = contentDescription,
+ tint = colors.contentColor,
+ )
+ }
+}
+
private val DefaultPlatformButtonVerticalPadding = 6.dp
private val ButtonPaddings = PaddingValues(horizontal = 16.dp, vertical = 8.dp)
@@ -109,6 +138,13 @@
}
@Composable
+private fun iconButtonColors(): IconButtonColors {
+ return IconButtonDefaults.filledIconButtonColors(
+ contentColor = LocalAndroidColorScheme.current.onSurface,
+ )
+}
+
+@Composable
private fun outlineButtonBorder(): BorderStroke {
return BorderStroke(
width = 1.dp,
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PasswordBouncer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PasswordBouncer.kt
index f9b91cf..bd539a7 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PasswordBouncer.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PasswordBouncer.kt
@@ -20,6 +20,7 @@
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.material3.IconButtonDefaults
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.TextField
@@ -36,16 +37,21 @@
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.key.Key
import androidx.compose.ui.input.key.key
import androidx.compose.ui.input.key.onInterceptKeyBeforeSoftKeyboard
+import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
+import com.android.compose.PlatformIconButton
import com.android.systemui.bouncer.ui.viewmodel.PasswordBouncerViewModel
+import com.android.systemui.res.R
/** UI for the input part of a password-requiring version of the bouncer. */
@Composable
@@ -64,6 +70,7 @@
val password: String by viewModel.password.collectAsState()
val isInputEnabled: Boolean by viewModel.isInputEnabled.collectAsState()
val animateFailure: Boolean by viewModel.animateFailure.collectAsState()
+ val isImeSwitcherButtonVisible by viewModel.isImeSwitcherButtonVisible.collectAsState()
DisposableEffect(Unit) {
viewModel.onShown()
@@ -116,5 +123,28 @@
false
}
},
+ trailingIcon =
+ if (isImeSwitcherButtonVisible) {
+ { ImeSwitcherButton(viewModel, color) }
+ } else null
+ )
+}
+
+/** Button for changing the password input method (IME). */
+@Composable
+private fun ImeSwitcherButton(
+ viewModel: PasswordBouncerViewModel,
+ color: Color,
+) {
+ val context = LocalContext.current
+ PlatformIconButton(
+ onClick = { viewModel.onImeSwitcherButtonClicked(context.displayId) },
+ iconResource = R.drawable.ic_lockscreen_ime,
+ contentDescription = stringResource(R.string.accessibility_ime_switch_button),
+ colors =
+ IconButtonDefaults.filledIconButtonColors(
+ contentColor = color,
+ containerColor = Color.Transparent,
+ )
)
}
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 ff5a698..92bc1f1 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
@@ -124,7 +124,7 @@
// TODO(b/315490861): Remove these conversions once Compose can be used throughout SysUI.
fun CommunalSceneKey.toTransitionSceneKey(): SceneKey {
- return SceneKey(name = toString(), identity = this)
+ return SceneKey(debugName = toString(), identity = this)
}
/**
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt
index 42fcd13..5e27d82 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt
@@ -21,19 +21,27 @@
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.android.compose.animation.scene.SceneScope
+import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.keyguard.shared.KeyguardShadeMigrationNssl
import com.android.systemui.notifications.ui.composable.NotificationStack
import com.android.systemui.scene.shared.flag.SceneContainerFlags
import com.android.systemui.statusbar.notification.stack.AmbientState
import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout
import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController
+import com.android.systemui.statusbar.notification.stack.NotificationStackSizeCalculator
import com.android.systemui.statusbar.notification.stack.shared.flexiNotifsEnabled
import com.android.systemui.statusbar.notification.stack.ui.view.SharedNotificationContainer
import com.android.systemui.statusbar.notification.stack.ui.viewbinder.NotificationStackAppearanceViewBinder
+import com.android.systemui.statusbar.notification.stack.ui.viewbinder.SharedNotificationContainerBinder
import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationStackAppearanceViewModel
import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationsPlaceholderViewModel
+import com.android.systemui.statusbar.notification.stack.ui.viewmodel.SharedNotificationContainerViewModel
import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+@SysUISingleton
class NotificationSection
@Inject
constructor(
@@ -42,22 +50,41 @@
controller: NotificationStackScrollLayoutController,
sceneContainerFlags: SceneContainerFlags,
sharedNotificationContainer: SharedNotificationContainer,
+ sharedNotificationContainerViewModel: SharedNotificationContainerViewModel,
stackScrollLayout: NotificationStackScrollLayout,
notificationStackAppearanceViewModel: NotificationStackAppearanceViewModel,
ambientState: AmbientState,
+ notificationStackSizeCalculator: NotificationStackSizeCalculator,
+ @Main mainDispatcher: CoroutineDispatcher,
) {
init {
- if (sceneContainerFlags.flexiNotifsEnabled()) {
+ if (!KeyguardShadeMigrationNssl.isUnexpectedlyInLegacyMode()) {
+ // This scene container section moves the NSSL to the SharedNotificationContainer. This
+ // also requires that SharedNotificationContainer gets moved to the SceneWindowRootView
+ // by the SceneWindowRootViewBinder.
+ // Prior to Scene Container, but when the KeyguardShadeMigrationNssl flag is enabled,
+ // NSSL is moved into this container by the NotificationStackScrollLayoutSection.
(stackScrollLayout.parent as? ViewGroup)?.removeView(stackScrollLayout)
sharedNotificationContainer.addNotificationStackScrollLayout(stackScrollLayout)
- NotificationStackAppearanceViewBinder.bind(
- context,
+ SharedNotificationContainerBinder.bind(
sharedNotificationContainer,
- notificationStackAppearanceViewModel,
- ambientState,
+ sharedNotificationContainerViewModel,
+ sceneContainerFlags,
controller,
+ notificationStackSizeCalculator,
+ mainDispatcher,
)
+
+ if (sceneContainerFlags.flexiNotifsEnabled()) {
+ NotificationStackAppearanceViewBinder.bind(
+ context,
+ sharedNotificationContainer,
+ notificationStackAppearanceViewModel,
+ ambientState,
+ controller,
+ )
+ }
}
}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationScrimNestedScrollConnection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationScrimNestedScrollConnection.kt
new file mode 100644
index 0000000..2ba78cf
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationScrimNestedScrollConnection.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.notifications.ui.composable
+
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
+import com.android.compose.nestedscroll.PriorityNestedScrollConnection
+
+/**
+ * A [NestedScrollConnection] that listens for all vertical scroll events and responds in the
+ * following way:
+ * - If you **scroll up**, it **first brings the [scrimOffset]** back to the [minScrimOffset] and
+ * then allows scrolling of the children (usually the content).
+ * - If you **scroll down**, it **first allows scrolling of the children** (usually the content) and
+ * then resets the [scrimOffset] to [maxScrimOffset].
+ */
+fun NotificationScrimNestedScrollConnection(
+ scrimOffset: () -> Float,
+ onScrimOffsetChanged: (Float) -> Unit,
+ minScrimOffset: () -> Float,
+ maxScrimOffset: Float,
+ contentHeight: () -> Float,
+ minVisibleScrimHeight: () -> Float,
+): PriorityNestedScrollConnection {
+ return PriorityNestedScrollConnection(
+ orientation = Orientation.Vertical,
+ // scrolling up and inner content is taller than the scrim, so scrim needs to
+ // expand; content can scroll once scrim is at the minScrimOffset.
+ canStartPreScroll = { offsetAvailable, offsetBeforeStart ->
+ offsetAvailable < 0 &&
+ offsetBeforeStart == 0f &&
+ contentHeight() > minVisibleScrimHeight() &&
+ scrimOffset() > minScrimOffset()
+ },
+ // scrolling down and content is done scrolling to top. After that, the scrim
+ // needs to collapse; collapse the scrim until it is at the maxScrimOffset.
+ canStartPostScroll = { offsetAvailable, _ ->
+ offsetAvailable > 0 && scrimOffset() < maxScrimOffset
+ },
+ canStartPostFling = { false },
+ canContinueScroll = {
+ val currentHeight = scrimOffset()
+ minScrimOffset() < currentHeight && currentHeight < maxScrimOffset
+ },
+ canScrollOnFling = true,
+ onStart = { /* do nothing */},
+ onScroll = { offsetAvailable ->
+ val currentHeight = scrimOffset()
+ val amountConsumed =
+ if (offsetAvailable > 0) {
+ val amountLeft = maxScrimOffset - currentHeight
+ offsetAvailable.coerceAtMost(amountLeft)
+ } else {
+ val amountLeft = minScrimOffset() - currentHeight
+ offsetAvailable.coerceAtLeast(amountLeft)
+ }
+ onScrimOffsetChanged(currentHeight + amountConsumed)
+ amountConsumed
+ },
+ // Don't consume the velocity on pre/post fling
+ onStop = { 0f },
+ )
+}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt
index e835d3e..0e08a19 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt
@@ -20,32 +20,53 @@
import android.util.Log
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.WindowInsets
+import androidx.compose.foundation.layout.asPaddingValues
+import androidx.compose.foundation.layout.displayCutout
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.systemBars
+import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
+import androidx.compose.ui.draw.drawBehind
+import androidx.compose.ui.graphics.BlendMode
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
+import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.layout.LayoutCoordinates
import androidx.compose.ui.layout.boundsInWindow
import androidx.compose.ui.layout.onPlaced
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.layout.positionInWindow
+import androidx.compose.ui.platform.LocalConfiguration
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp
import com.android.compose.animation.scene.ElementKey
+import com.android.compose.animation.scene.NestedScrollBehavior
import com.android.compose.animation.scene.SceneScope
import com.android.compose.modifiers.height
import com.android.systemui.notifications.ui.composable.Notifications.Form
+import com.android.systemui.scene.ui.composable.Gone
+import com.android.systemui.scene.ui.composable.Shade
+import com.android.systemui.shade.ui.composable.ShadeHeader
import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationsPlaceholderViewModel
import kotlin.math.roundToInt
@@ -100,33 +121,109 @@
@Composable
fun SceneScope.NotificationScrollingStack(
viewModel: NotificationsPlaceholderViewModel,
+ maxScrimTop: () -> Float,
modifier: Modifier = Modifier,
) {
+ val density = LocalDensity.current
val cornerRadius by viewModel.cornerRadiusDp.collectAsState()
-
- val contentHeight by viewModel.intrinsicContentHeight.collectAsState()
-
val expansionFraction by viewModel.expandFraction.collectAsState(0f)
- Box(
- modifier =
- modifier
- .verticalNestedScrollToScene()
- .fillMaxWidth()
- .element(Notifications.Elements.NotificationScrim)
- .graphicsLayer {
- shape = RoundedCornerShape(cornerRadius.dp)
- clip = true
- alpha = expansionFraction
- }
- .background(MaterialTheme.colorScheme.surface)
- .debugBackground(viewModel, Color(0.5f, 0.5f, 0f, 0.2f))
- ) {
- NotificationPlaceholder(
- viewModel = viewModel,
- form = Form.Stack,
- modifier = Modifier.fillMaxWidth().height { contentHeight.roundToInt() }
+ val navBarHeight =
+ with(density) { WindowInsets.systemBars.asPaddingValues().calculateBottomPadding().toPx() }
+ val statusBarHeight =
+ with(density) { WindowInsets.systemBars.asPaddingValues().calculateTopPadding().toPx() }
+ val displayCutoutHeight =
+ with(density) { WindowInsets.displayCutout.asPaddingValues().calculateTopPadding().toPx() }
+ val screenHeight =
+ with(density) { LocalConfiguration.current.screenHeightDp.dp.toPx() } +
+ navBarHeight +
+ maxOf(statusBarHeight, displayCutoutHeight)
+
+ val contentHeight = viewModel.intrinsicContentHeight.collectAsState()
+
+ // the offset for the notifications scrim. Its upper bound is 0, and its lower bound is
+ // calculated in minScrimOffset. The scrim is the same height as the screen minus the
+ // height of the Shade Header, and at rest (scrimOffset = 0) its top bound is at maxScrimStartY.
+ // When fully expanded (scrimOffset = minScrimOffset), its top bound is at minScrimStartY,
+ // which is equal to the height of the Shade Header. Thus, when the scrim is fully expanded, the
+ // entire height of the scrim is visible on screen.
+ val scrimOffset = remember { mutableStateOf(0f) }
+
+ val minScrimTop = with(density) { ShadeHeader.Dimensions.CollapsedHeight.toPx() }
+
+ // The minimum offset for the scrim. The scrim is considered fully expanded when it
+ // is at this offset.
+ val minScrimOffset: () -> Float = { minScrimTop - maxScrimTop() }
+
+ // The height of the scrim visible on screen when it is in its resting (collapsed) state.
+ val minVisibleScrimHeight: () -> Float = { screenHeight - maxScrimTop() }
+
+ // we are not scrolled to the top unless the scrim is at its maximum offset.
+ LaunchedEffect(viewModel, scrimOffset) {
+ snapshotFlow { scrimOffset.value >= 0f }
+ .collect { isScrolledToTop -> viewModel.setScrolledToTop(isScrolledToTop) }
+ }
+
+ // if contentHeight drops below minimum visible scrim height while scrim is
+ // expanded, reset scrim offset.
+ LaunchedEffect(contentHeight, screenHeight, maxScrimTop, scrimOffset) {
+ snapshotFlow { contentHeight.value < minVisibleScrimHeight() && scrimOffset.value < 0f }
+ .collect { shouldCollapse -> if (shouldCollapse) scrimOffset.value = 0f }
+ }
+
+ Box(modifier = modifier.element(Notifications.Elements.NotificationScrim)) {
+ Spacer(
+ modifier =
+ Modifier.fillMaxSize()
+ .graphicsLayer {
+ shape = RoundedCornerShape(cornerRadius.dp)
+ clip = true
+ }
+ .drawBehind { drawRect(Color.Black, blendMode = BlendMode.DstOut) }
)
+ Box(
+ modifier =
+ Modifier.fillMaxSize()
+ .offset { IntOffset(0, scrimOffset.value.roundToInt()) }
+ .graphicsLayer {
+ shape = RoundedCornerShape(cornerRadius.dp)
+ clip = true
+ alpha =
+ if (layoutState.isTransitioningBetween(Gone, Shade)) {
+ (expansionFraction / 0.3f).coerceAtMost(1f)
+ } else 1f
+ }
+ .background(MaterialTheme.colorScheme.surface)
+ .debugBackground(viewModel, Color(0.5f, 0.5f, 0f, 0.2f))
+ ) {
+ NotificationPlaceholder(
+ viewModel = viewModel,
+ form = Form.Stack,
+ modifier =
+ Modifier.verticalNestedScrollToScene(
+ topBehavior = NestedScrollBehavior.EdgeWithPreview,
+ )
+ .nestedScroll(
+ remember(
+ scrimOffset,
+ maxScrimTop,
+ minScrimTop,
+ ) {
+ NotificationScrimNestedScrollConnection(
+ scrimOffset = { scrimOffset.value },
+ onScrimOffsetChanged = { scrimOffset.value = it },
+ minScrimOffset = minScrimOffset,
+ maxScrimOffset = 0f,
+ contentHeight = { contentHeight.value },
+ minVisibleScrimHeight = minVisibleScrimHeight,
+ )
+ }
+ )
+ .verticalScroll(rememberScrollState())
+ .fillMaxWidth()
+ .height { (contentHeight.value + navBarHeight).roundToInt() },
+ )
+ }
}
}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt
index bbfe0fd..5531f9c 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt
@@ -36,6 +36,7 @@
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
@@ -46,6 +47,11 @@
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.graphicsLayer
+import androidx.compose.ui.layout.LayoutCoordinates
+import androidx.compose.ui.layout.boundsInWindow
+import androidx.compose.ui.layout.onPlaced
+import androidx.compose.ui.layout.positionInWindow
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.unit.dp
@@ -56,7 +62,7 @@
import com.android.systemui.compose.modifiers.sysuiResTag
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
-import com.android.systemui.notifications.ui.composable.HeadsUpNotificationSpace
+import com.android.systemui.notifications.ui.composable.Notifications
import com.android.systemui.qs.footer.ui.compose.FooterActions
import com.android.systemui.qs.ui.viewmodel.QuickSettingsSceneViewModel
import com.android.systemui.scene.shared.model.SceneKey
@@ -116,6 +122,8 @@
statusBarIconController: StatusBarIconController,
modifier: Modifier = Modifier,
) {
+ val cornerRadius by viewModel.notifications.cornerRadiusDp.collectAsState()
+
// TODO(b/280887232): implement the real UI.
Box(modifier = modifier.fillMaxSize()) {
val isCustomizing by viewModel.qsSceneAdapter.isCustomizing.collectAsState()
@@ -234,10 +242,32 @@
}
}
}
- HeadsUpNotificationSpace(
- viewModel = viewModel.notifications,
- isPeekFromBottom = true,
- modifier = Modifier.padding(16.dp).fillMaxSize(),
+ // Scrim with height 0 aligned to bottom of the screen to facilitate shared element
+ // transition from Shade scene.
+ Box(
+ modifier =
+ Modifier.element(Notifications.Elements.NotificationScrim)
+ .fillMaxWidth()
+ .height(0.dp)
+ .graphicsLayer {
+ shape = RoundedCornerShape(cornerRadius.dp)
+ clip = true
+ alpha = 1f
+ }
+ .background(MaterialTheme.colorScheme.surface)
+ .align(Alignment.BottomCenter)
+ .onPlaced { coordinates: LayoutCoordinates ->
+ viewModel.notifications.onContentTopChanged(
+ coordinates.positionInWindow().y
+ )
+ val boundsInWindow = coordinates.boundsInWindow()
+ viewModel.notifications.onBoundsChanged(
+ left = boundsInWindow.left,
+ top = boundsInWindow.top,
+ right = boundsInWindow.right,
+ bottom = boundsInWindow.bottom,
+ )
+ }
)
}
}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/GoneScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/GoneScene.kt
index 747faab..770d654 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/GoneScene.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/GoneScene.kt
@@ -18,13 +18,10 @@
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
-import androidx.compose.ui.unit.dp
import com.android.compose.animation.scene.SceneScope
import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.notifications.ui.composable.HeadsUpNotificationSpace
import com.android.systemui.notifications.ui.composable.Notifications
import com.android.systemui.scene.shared.model.Direction
import com.android.systemui.scene.shared.model.Edge
@@ -66,12 +63,6 @@
override fun SceneScope.Content(
modifier: Modifier,
) {
- Box(modifier = modifier) {
- Box(modifier = Modifier.fillMaxSize().element(Notifications.Elements.NotificationScrim))
- HeadsUpNotificationSpace(
- viewModel = notificationsViewModel,
- modifier = Modifier.padding(16.dp).fillMaxSize(),
- )
- }
+ Box(modifier = Modifier.fillMaxSize().element(Notifications.Elements.NotificationScrim))
}
}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/TransitionSceneKeys.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/TransitionSceneKeys.kt
index 5336bf6..0c66701 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/TransitionSceneKeys.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/TransitionSceneKeys.kt
@@ -12,5 +12,5 @@
// TODO(b/293899074): Remove this file once we can use the scene keys from SceneTransitionLayout.
fun SceneKey.toTransitionSceneKey(): SceneTransitionSceneKey {
- return SceneTransitionSceneKey(name = toString(), identity = this)
+ return SceneTransitionSceneKey(debugName = toString(), identity = this)
}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeHeader.kt b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeHeader.kt
index e2beaee..b11edf7 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeHeader.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeHeader.kt
@@ -58,6 +58,8 @@
import com.android.systemui.common.ui.compose.windowinsets.CutoutLocation
import com.android.systemui.common.ui.compose.windowinsets.LocalDisplayCutout
import com.android.systemui.res.R
+import com.android.systemui.scene.ui.composable.QuickSettings
+import com.android.systemui.scene.ui.composable.Shade as ShadeKey
import com.android.systemui.shade.ui.viewmodel.ShadeHeaderViewModel
import com.android.systemui.statusbar.phone.StatusBarIconController
import com.android.systemui.statusbar.phone.StatusBarIconController.TintedIconManager
@@ -348,7 +350,7 @@
}
@Composable
-private fun StatusIcons(
+private fun SceneScope.StatusIcons(
viewModel: ShadeHeaderViewModel,
createTintedIconManager: (ViewGroup, StatusBarLocation) -> TintedIconManager,
statusBarIconController: StatusBarIconController,
@@ -358,7 +360,6 @@
val carrierIconSlots =
listOf(stringResource(id = com.android.internal.R.string.status_bar_mobile))
val isSingleCarrier by viewModel.isSingleCarrier.collectAsState()
- val isTransitioning by viewModel.isTransitioning.collectAsState()
AndroidView(
factory = { context ->
@@ -373,7 +374,9 @@
iconContainer
},
update = { iconContainer ->
- iconContainer.setQsExpansionTransitioning(isTransitioning)
+ iconContainer.setQsExpansionTransitioning(
+ layoutState.isTransitioningBetween(ShadeKey, QuickSettings)
+ )
if (isSingleCarrier || !useExpandedFormat) {
iconContainer.removeIgnoredSlots(carrierIconSlots)
} else {
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt
index 1545372..497fe87 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt
@@ -32,6 +32,7 @@
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.Layout
import androidx.compose.ui.layout.layout
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.dimensionResource
@@ -62,6 +63,7 @@
import com.android.systemui.util.animation.MeasurementInput
import javax.inject.Inject
import javax.inject.Named
+import kotlin.math.roundToInt
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
@@ -154,62 +156,104 @@
mediaHost: MediaHost,
modifier: Modifier = Modifier,
) {
- val localDensity = LocalDensity.current
+ val density = LocalDensity.current
val layoutWidth = remember { mutableStateOf(0) }
+ val maxNotifScrimTop = remember { mutableStateOf(0f) }
Box(
modifier =
modifier.element(Shade.Elements.Scrim).background(MaterialTheme.colorScheme.scrim),
)
Box {
- Column(
- horizontalAlignment = Alignment.CenterHorizontally,
- modifier = Modifier.fillMaxWidth().clickable(onClick = { viewModel.onContentClicked() })
- ) {
- CollapsedShadeHeader(
- viewModel = viewModel.shadeHeaderViewModel,
- createTintedIconManager = createTintedIconManager,
- createBatteryMeterViewController = createBatteryMeterViewController,
- statusBarIconController = statusBarIconController,
- modifier = Modifier.padding(horizontal = Shade.Dimensions.HorizontalPadding)
- )
- QuickSettings(
- modifier = Modifier.height(130.dp),
- viewModel.qsSceneAdapter,
- )
-
- if (viewModel.isMediaVisible()) {
- val mediaHeight = dimensionResource(R.dimen.qs_media_session_height_expanded)
- MediaCarousel(
- modifier =
- Modifier.height(mediaHeight).fillMaxWidth().layout { measurable, constraints
- ->
- val placeable = measurable.measure(constraints)
-
- // Notify controller to size the carousel for the current space
- mediaHost.measurementInput =
- MeasurementInput(placeable.width, placeable.height)
- mediaCarouselController.setSceneContainerSize(
- placeable.width,
- placeable.height
+ Layout(
+ contents =
+ listOf(
+ {
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally,
+ modifier =
+ Modifier.fillMaxWidth()
+ .clickable(onClick = { viewModel.onContentClicked() })
+ ) {
+ CollapsedShadeHeader(
+ viewModel = viewModel.shadeHeaderViewModel,
+ createTintedIconManager = createTintedIconManager,
+ createBatteryMeterViewController = createBatteryMeterViewController,
+ statusBarIconController = statusBarIconController,
+ modifier =
+ Modifier.padding(
+ horizontal = Shade.Dimensions.HorizontalPadding
+ )
+ )
+ QuickSettings(
+ modifier = Modifier.height(130.dp),
+ viewModel.qsSceneAdapter,
)
- layout(placeable.width, placeable.height) {
- placeable.placeRelative(0, 0)
- }
- },
- mediaHost = mediaHost,
- layoutWidth = layoutWidth.value,
- layoutHeight = with(localDensity) { mediaHeight.toPx() }.toInt(),
- carouselController = mediaCarouselController,
- )
- }
+ if (viewModel.isMediaVisible()) {
+ val mediaHeight =
+ dimensionResource(R.dimen.qs_media_session_height_expanded)
+ MediaCarousel(
+ modifier =
+ Modifier.height(mediaHeight).fillMaxWidth().layout {
+ measurable,
+ constraints ->
+ val placeable = measurable.measure(constraints)
- Spacer(modifier = Modifier.height(16.dp))
- NotificationScrollingStack(
- viewModel = viewModel.notifications,
- modifier = Modifier.fillMaxWidth().weight(1f),
- )
+ // Notify controller to size the carousel for the
+ // current space
+ mediaHost.measurementInput =
+ MeasurementInput(placeable.width, placeable.height)
+ mediaCarouselController.setSceneContainerSize(
+ placeable.width,
+ placeable.height
+ )
+
+ layout(placeable.width, placeable.height) {
+ placeable.placeRelative(0, 0)
+ }
+ },
+ mediaHost = mediaHost,
+ layoutWidth = layoutWidth.value,
+ layoutHeight = with(density) { mediaHeight.toPx() }.toInt(),
+ carouselController = mediaCarouselController,
+ )
+ }
+
+ Spacer(modifier = Modifier.height(16.dp))
+ }
+ },
+ {
+ NotificationScrollingStack(
+ viewModel = viewModel.notifications,
+ maxScrimTop = { maxNotifScrimTop.value },
+ )
+ },
+ )
+ ) { measurables, constraints ->
+ check(measurables.size == 2)
+ check(measurables[0].size == 1)
+ check(measurables[1].size == 1)
+
+ val quickSettingsPlaceable = measurables[0][0].measure(constraints)
+
+ val notificationsMeasurable = measurables[1][0]
+ val notificationsScrimMaxHeight =
+ constraints.maxHeight - ShadeHeader.Dimensions.CollapsedHeight.roundToPx()
+ val notificationsPlaceable =
+ notificationsMeasurable.measure(
+ constraints.copy(
+ minHeight = notificationsScrimMaxHeight,
+ maxHeight = notificationsScrimMaxHeight
+ )
+ )
+
+ maxNotifScrimTop.value = quickSettingsPlaceable.height.toFloat()
+
+ layout(constraints.maxWidth, constraints.maxHeight) {
+ quickSettingsPlaceable.placeRelative(x = 0, y = 0)
+ notificationsPlaceable.placeRelative(x = 0, y = maxNotifScrimTop.value.roundToInt())
+ }
}
}
}
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateToScene.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateToScene.kt
index 7d3b0fb..7057545 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateToScene.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateToScene.kt
@@ -61,7 +61,7 @@
// The transition is already finished (progress ~= 1): no need to animate. We
// finish the current transition early to make sure that the current state
// change is committed.
- layoutState.finishTransition(transitionState, transitionState.currentScene)
+ layoutState.finishTransition(transitionState, target)
null
} else {
// The transition is in progress: start the canned animation at the same
@@ -78,7 +78,7 @@
if (progress.absoluteValue < ProgressVisibilityThreshold) {
// The transition is at progress ~= 0: no need to animate.We finish the current
// transition early to make sure that the current state change is committed.
- layoutState.finishTransition(transitionState, transitionState.currentScene)
+ layoutState.finishTransition(transitionState, target)
null
} else {
// TODO(b/290184746): Also take the current velocity into account.
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 9d4b69c..c6851954 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
@@ -42,16 +42,16 @@
/** Key for a scene. */
class SceneKey(
- name: String,
+ debugName: String,
identity: Any = Object(),
-) : Key(name, identity), UserActionResult {
+) : Key(debugName, identity), UserActionResult {
@VisibleForTesting
// TODO(b/240432457): Make internal once PlatformComposeSceneTransitionLayoutTestsUtils can
// access internal members.
- val testTag: String = "scene:$name"
+ val testTag: String = "scene:$debugName"
/** The unique [ElementKey] identifying this scene's root element. */
- val rootElementKey = ElementKey(name, identity)
+ val rootElementKey = ElementKey(debugName, identity)
// Implementation of [UserActionResult].
override val toScene: SceneKey = this
@@ -64,7 +64,7 @@
/** Key for an element. */
class ElementKey(
- name: String,
+ debugName: String,
identity: Any = Object(),
/**
@@ -72,11 +72,11 @@
* or compose MovableElements.
*/
val scenePicker: ElementScenePicker = DefaultElementScenePicker,
-) : Key(name, identity), ElementMatcher {
+) : Key(debugName, identity), ElementMatcher {
@VisibleForTesting
// TODO(b/240432457): Make internal once PlatformComposeSceneTransitionLayoutTestsUtils can
// access internal members.
- val testTag: String = "element:$name"
+ val testTag: String = "element:$debugName"
override fun matches(key: ElementKey, scene: SceneKey): Boolean {
return key == this
@@ -99,7 +99,7 @@
}
/** Key for a shared value of an element. */
-class ValueKey(name: String, identity: Any = Object()) : Key(name, identity) {
+class ValueKey(debugName: String, identity: Any = Object()) : Key(debugName, identity) {
override fun toString(): String {
return "ValueKey(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 8552aaf..529fc03 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
@@ -40,12 +40,15 @@
import androidx.compose.ui.node.CompositionLocalConsumerModifierNode
import androidx.compose.ui.node.DelegatingNode
import androidx.compose.ui.node.ModifierNodeElement
+import androidx.compose.ui.node.ObserverModifierNode
import androidx.compose.ui.node.PointerInputModifierNode
import androidx.compose.ui.node.currentValueOf
+import androidx.compose.ui.node.observeReads
import androidx.compose.ui.platform.LocalViewConfiguration
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.Velocity
import androidx.compose.ui.util.fastForEach
+import kotlin.math.sign
/**
* Make an element draggable in the given [orientation].
@@ -65,7 +68,7 @@
internal fun Modifier.multiPointerDraggable(
orientation: Orientation,
enabled: () -> Boolean,
- startDragImmediately: () -> Boolean,
+ startDragImmediately: (startedPosition: Offset) -> Boolean,
onDragStarted: (startedPosition: Offset, overSlop: Float, pointersDown: Int) -> Unit,
onDragDelta: (delta: Float) -> Unit,
onDragStopped: (velocity: Float) -> Unit,
@@ -84,7 +87,7 @@
private data class MultiPointerDraggableElement(
private val orientation: Orientation,
private val enabled: () -> Boolean,
- private val startDragImmediately: () -> Boolean,
+ private val startDragImmediately: (startedPosition: Offset) -> Boolean,
private val onDragStarted:
(startedPosition: Offset, overSlop: Float, pointersDown: Int) -> Unit,
private val onDragDelta: (Float) -> Unit,
@@ -113,14 +116,19 @@
internal class MultiPointerDraggableNode(
orientation: Orientation,
enabled: () -> Boolean,
- var startDragImmediately: () -> Boolean,
+ var startDragImmediately: (startedPosition: Offset) -> Boolean,
var onDragStarted: (startedPosition: Offset, overSlop: Float, pointersDown: Int) -> Unit,
var onDragDelta: (Float) -> Unit,
var onDragStopped: (velocity: Float) -> Unit,
-) : PointerInputModifierNode, DelegatingNode(), CompositionLocalConsumerModifierNode {
+) :
+ PointerInputModifierNode,
+ DelegatingNode(),
+ CompositionLocalConsumerModifierNode,
+ ObserverModifierNode {
private val pointerInputHandler: suspend PointerInputScope.() -> Unit = { pointerInput() }
private val delegate = delegate(SuspendingPointerInputModifierNode(pointerInputHandler))
private val velocityTracker = VelocityTracker()
+ private var previousEnabled: Boolean = false
var enabled: () -> Boolean = enabled
set(value) {
@@ -140,6 +148,21 @@
}
}
+ override fun onAttach() {
+ previousEnabled = enabled()
+ onObservedReadsChanged()
+ }
+
+ override fun onObservedReadsChanged() {
+ observeReads {
+ val newEnabled = enabled()
+ if (newEnabled != previousEnabled) {
+ delegate.resetPointerInputHandler()
+ }
+ previousEnabled = newEnabled
+ }
+ }
+
override fun onCancelPointerInput() = delegate.onCancelPointerInput()
override fun onPointerEvent(
@@ -201,7 +224,7 @@
*/
private suspend fun PointerInputScope.detectDragGestures(
orientation: Orientation,
- startDragImmediately: () -> Boolean,
+ startDragImmediately: (startedPosition: Offset) -> Boolean,
onDragStart: (startedPosition: Offset, overSlop: Float, pointersDown: Int) -> Unit,
onDragEnd: () -> Unit,
onDragCancel: () -> Unit,
@@ -211,7 +234,7 @@
val initialDown = awaitFirstDown(requireUnconsumed = false, pass = PointerEventPass.Initial)
var overSlop = 0f
val drag =
- if (startDragImmediately()) {
+ if (startDragImmediately(initialDown.position)) {
initialDown.consume()
initialDown
} else {
@@ -223,12 +246,31 @@
// TODO(b/291055080): Replace by await[Orientation]PointerSlopOrCancellation once
// it is public.
- when (orientation) {
- Orientation.Horizontal ->
- awaitHorizontalTouchSlopOrCancellation(down.id, onSlopReached)
- Orientation.Vertical ->
- awaitVerticalTouchSlopOrCancellation(down.id, onSlopReached)
+ val drag =
+ when (orientation) {
+ Orientation.Horizontal ->
+ awaitHorizontalTouchSlopOrCancellation(down.id, onSlopReached)
+ Orientation.Vertical ->
+ awaitVerticalTouchSlopOrCancellation(down.id, onSlopReached)
+ }
+
+ // Make sure that overSlop is not 0f. This can happen when the user drags by exactly
+ // the touch slop. However, the overSlop we pass to onDragStarted() is used to
+ // compute the direction we are dragging in, so overSlop should never be 0f unless
+ // we intercept an ongoing swipe transition (i.e. startDragImmediately() returned
+ // true).
+ if (drag != null && overSlop == 0f) {
+ val deltaOffset = drag.position - initialDown.position
+ val delta =
+ when (orientation) {
+ Orientation.Horizontal -> deltaOffset.x
+ Orientation.Vertical -> deltaOffset.y
+ }
+ check(delta != 0f) { "delta is equal to 0" }
+ overSlop = delta.sign
}
+
+ drag
}
if (drag != null) {
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 aed04f6..35754d6 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
@@ -20,8 +20,7 @@
import android.util.Log
import androidx.compose.animation.core.Animatable
-import androidx.compose.animation.core.Spring
-import androidx.compose.animation.core.spring
+import androidx.compose.animation.core.SpringSpec
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
@@ -54,7 +53,20 @@
}
private fun updateTransition(newTransition: SwipeTransition, force: Boolean = false) {
- if (isDrivingTransition || force) layoutState.startTransition(newTransition)
+ if (isDrivingTransition || force) {
+ layoutState.startTransition(newTransition)
+
+ // Initialize SwipeTransition.swipeSpec. Note that this must be called right after
+ // layoutState.startTransition() is called, because it computes the
+ // layoutState.transformationSpec().
+ newTransition.swipeSpec =
+ layoutState.transformationSpec.swipeSpec ?: layoutState.transitions.defaultSwipeSpec
+ } else {
+ // We were not driving the transition and we don't force the update, so the spec won't
+ // be used and it doesn't matter which one we set here.
+ newTransition.swipeSpec = SceneTransitions.DefaultSwipeSpec
+ }
+
swipeTransition = newTransition
}
@@ -84,8 +96,35 @@
private var upOrLeftResult: UserActionResult? = null
private var downOrRightResult: UserActionResult? = null
+ /**
+ * Whether we should immediately intercept a gesture.
+ *
+ * Note: if this returns true, then [onDragStarted] will be called with overSlop equal to 0f,
+ * indicating that the transition should be intercepted.
+ */
+ internal fun shouldImmediatelyIntercept(startedPosition: Offset?): Boolean {
+ // We don't intercept the touch if we are not currently driving the transition.
+ if (!isDrivingTransition) {
+ return false
+ }
+
+ // Only intercept the current transition if one of the 2 swipes results is also a transition
+ // between the same pair of scenes.
+ val fromScene = swipeTransition._currentScene
+ val swipes = computeSwipes(fromScene, startedPosition, pointersDown = 1)
+ val (upOrLeft, downOrRight) = computeSwipesResults(fromScene, swipes)
+ return (upOrLeft != null &&
+ swipeTransition.isTransitioningBetween(fromScene.key, upOrLeft.toScene)) ||
+ (downOrRight != null &&
+ swipeTransition.isTransitioningBetween(fromScene.key, downOrRight.toScene))
+ }
+
internal fun onDragStarted(pointersDown: Int, startedPosition: Offset?, overSlop: Float) {
- if (isDrivingTransition) {
+ if (overSlop == 0f) {
+ check(isDrivingTransition) {
+ "onDragStarted() called while isDrivingTransition=false overSlop=0f"
+ }
+
// This [transition] was already driving the animation: simply take over it.
// Stop animating and start from where the current offset.
swipeTransition.cancelOffsetAnimation()
@@ -93,9 +132,6 @@
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.
@@ -110,9 +146,10 @@
val fromScene = layoutImpl.scene(transitionState.currentScene)
updateSwipes(fromScene, startedPosition, pointersDown)
- val (targetScene, distance) =
- findTargetSceneAndDistance(fromScene, overSlop, updateSwipesResults = true) ?: return
- updateTransition(SwipeTransition(fromScene, targetScene, distance), force = true)
+ val result =
+ findUserActionResult(fromScene, directionOffset = overSlop, updateSwipesResults = true)
+ ?: return
+ updateTransition(SwipeTransition(fromScene, result), force = true)
}
private fun updateSwipes(fromScene: Scene, startedPosition: Offset?, pointersDown: Int) {
@@ -188,8 +225,8 @@
computeFromSceneConsideringAcceleratedSwipe(swipeTransition)
val isNewFromScene = fromScene.key != swipeTransition.fromScene
- val (targetScene, distance) =
- findTargetSceneAndDistance(
+ val result =
+ findUserActionResult(
fromScene,
swipeTransition.dragOffset,
updateSwipesResults = isNewFromScene,
@@ -200,9 +237,9 @@
}
swipeTransition.dragOffset += acceleratedOffset
- if (isNewFromScene || targetScene.key != swipeTransition.toScene) {
+ if (isNewFromScene || result.toScene != swipeTransition.toScene) {
updateTransition(
- SwipeTransition(fromScene, targetScene, distance).apply {
+ SwipeTransition(fromScene, result).apply {
this.dragOffset = swipeTransition.dragOffset
}
)
@@ -211,7 +248,7 @@
private fun updateSwipesResults(fromScene: Scene) {
val (upOrLeftResult, downOrRightResult) =
- swipesResults(
+ computeSwipesResults(
fromScene,
this.swipes ?: error("updateSwipes() should be called before updateSwipesResults()")
)
@@ -220,7 +257,7 @@
this.downOrRightResult = downOrRightResult
}
- private fun swipesResults(
+ private fun computeSwipesResults(
fromScene: Scene,
swipes: Swipes
): Pair<UserActionResult?, UserActionResult?> {
@@ -270,7 +307,7 @@
}
/**
- * Returns the target scene and distance from [fromScene] in the direction [directionOffset].
+ * Returns the [UserActionResult] from [fromScene] in the direction of [directionOffset].
*
* @param fromScene the scene from which we look for the target
* @param directionOffset signed float that indicates the direction. Positive is down or right
@@ -286,66 +323,56 @@
* [directionOffset] is 0f and both direction are available, it will default to
* [upOrLeftResult].
*/
- private inline fun findTargetSceneAndDistance(
+ private fun findUserActionResult(
fromScene: Scene,
directionOffset: Float,
updateSwipesResults: Boolean,
- ): Pair<Scene, Float>? {
+ ): UserActionResult? {
if (updateSwipesResults) updateSwipesResults(fromScene)
- // Compute the target scene depending on the current offset.
return when {
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)
- )
- }
+ upOrLeftResult
+ else -> downOrRightResult
}
}
/**
- * A strict version of [findTargetSceneAndDistance] that will return null when there is no Scene
- * in [directionOffset] direction
+ * A strict version of [findUserActionResult] that will return null when there is no Scene in
+ * [directionOffset] direction
*/
- private inline fun findTargetSceneAndDistanceStrict(
- fromScene: Scene,
- directionOffset: Float,
- ): Pair<Scene, Float>? {
+ private fun findUserActionResultStrict(directionOffset: Float): UserActionResult? {
return when {
- directionOffset > 0f ->
- upOrLeftResult?.let { result ->
- Pair(
- layoutImpl.scene(result.toScene),
- -fromScene.getAbsoluteDistance(result.distance),
- )
- }
- directionOffset < 0f ->
- downOrRightResult?.let { result ->
- Pair(
- layoutImpl.scene(result.toScene),
- fromScene.getAbsoluteDistance(result.distance),
- )
- }
+ directionOffset > 0f -> upOrLeftResult
+ directionOffset < 0f -> downOrRightResult
else -> null
}
}
+ private fun computeAbsoluteDistance(
+ fromScene: Scene,
+ result: UserActionResult,
+ ): Float {
+ return if (result == upOrLeftResult) {
+ -fromScene.getAbsoluteDistance(result.distance)
+ } else {
+ check(result == downOrRightResult)
+ fromScene.getAbsoluteDistance(result.distance)
+ }
+ }
+
internal fun onDragStopped(velocity: Float, canChangeScene: Boolean) {
// The state was changed since the drag started; don't do anything.
if (!isDrivingTransition) {
return
}
+ // Important: Make sure that all the code here references the current transition when
+ // [onDragStopped] is called, otherwise the callbacks (like onAnimationCompleted()) might
+ // incorrectly finish a new transition that replaced this one.
+ val swipeTransition = this.swipeTransition
+
fun animateTo(targetScene: Scene, targetOffset: Float) {
// If the effective current scene changed, it should be reflected right now in the
// current scene state, even before the settle animation is ongoing. That way all the
@@ -399,8 +426,8 @@
if (startFromIdlePosition) {
// If there is a target scene, we start the overscroll animation.
- val (targetScene, distance) =
- findTargetSceneAndDistanceStrict(fromScene, velocity)
+ val result =
+ findUserActionResultStrict(velocity)
?: run {
// We will not animate
layoutState.finishTransition(swipeTransition, idleScene = fromScene.key)
@@ -408,7 +435,7 @@
}
updateTransition(
- SwipeTransition(fromScene, targetScene, distance).apply {
+ SwipeTransition(fromScene, result).apply {
_currentScene = swipeTransition._currentScene
}
)
@@ -455,6 +482,14 @@
}
}
+ private fun SwipeTransition(fromScene: Scene, result: UserActionResult): SwipeTransition {
+ return SwipeTransition(
+ fromScene,
+ layoutImpl.scene(result.toScene),
+ computeAbsoluteDistance(fromScene, result),
+ )
+ }
+
internal class SwipeTransition(
val _fromScene: Scene,
val _toScene: Scene,
@@ -462,7 +497,7 @@
* The signed distance between [fromScene] and [toScene]. It is negative if [fromScene] is
* above or to the left of [toScene].
*/
- val distance: Float
+ val distance: Float,
) : TransitionState.Transition(_fromScene.key, _toScene.key) {
var _currentScene by mutableStateOf(_fromScene)
override val currentScene: SceneKey
@@ -495,6 +530,9 @@
/** Job to check that there is at most one offset animation in progress. */
private var offsetAnimationJob: Job? = null
+ /** The spec to use when animating this transition to either [fromScene] or [toScene]. */
+ lateinit var swipeSpec: SpringSpec<Float>
+
/** Ends any previous [offsetAnimationJob] and runs the new [job]. */
private fun startOffsetAnimation(job: () -> Job) {
cancelOffsetAnimation()
@@ -516,13 +554,6 @@
}
}
- // TODO(b/290184746): Make this spring spec configurable.
- private val animationSpec =
- spring(
- stiffness = Spring.StiffnessMediumLow,
- visibilityThreshold = OffsetVisibilityThreshold
- )
-
fun animateOffset(
// TODO(b/317063114) The CoroutineScope should be removed.
coroutineScope: CoroutineScope,
@@ -546,7 +577,7 @@
offsetAnimatable.animateTo(
targetValue = targetOffset,
- animationSpec = animationSpec,
+ animationSpec = swipeSpec,
initialVelocity = initialVelocity,
)
@@ -651,6 +682,7 @@
}
val source = this
+ var isIntercepting = false
return PriorityNestedScrollConnection(
orientation = orientation,
@@ -658,7 +690,9 @@
canChangeScene = offsetBeforeStart == 0f
val canInterceptSwipeTransition =
- canChangeScene && gestureHandler.isDrivingTransition && offsetAvailable != 0f
+ canChangeScene &&
+ offsetAvailable != 0f &&
+ gestureHandler.shouldImmediatelyIntercept(startedPosition = null)
if (!canInterceptSwipeTransition) return@PriorityNestedScrollConnection false
val swipeTransition = gestureHandler.swipeTransition
@@ -676,7 +710,12 @@
}
// Start only if we cannot consume this event
- !shouldSnapToIdle
+ val canStart = !shouldSnapToIdle
+ if (canStart) {
+ isIntercepting = true
+ }
+
+ canStart
},
canStartPostScroll = { offsetAvailable, offsetBeforeStart ->
val behavior: NestedScrollBehavior =
@@ -688,24 +727,31 @@
val isZeroOffset = offsetBeforeStart == 0f
- when (behavior) {
- NestedScrollBehavior.DuringTransitionBetweenScenes -> {
- canChangeScene = false // unused: added for consistency
- false
+ val canStart =
+ when (behavior) {
+ NestedScrollBehavior.DuringTransitionBetweenScenes -> {
+ canChangeScene = false // unused: added for consistency
+ false
+ }
+ NestedScrollBehavior.EdgeNoPreview -> {
+ canChangeScene = isZeroOffset
+ isZeroOffset && hasNextScene(offsetAvailable)
+ }
+ NestedScrollBehavior.EdgeWithPreview -> {
+ canChangeScene = isZeroOffset
+ hasNextScene(offsetAvailable)
+ }
+ NestedScrollBehavior.EdgeAlways -> {
+ canChangeScene = true
+ hasNextScene(offsetAvailable)
+ }
}
- NestedScrollBehavior.EdgeNoPreview -> {
- canChangeScene = isZeroOffset
- isZeroOffset && hasNextScene(offsetAvailable)
- }
- NestedScrollBehavior.EdgeWithPreview -> {
- canChangeScene = isZeroOffset
- hasNextScene(offsetAvailable)
- }
- NestedScrollBehavior.EdgeAlways -> {
- canChangeScene = true
- hasNextScene(offsetAvailable)
- }
+
+ if (canStart) {
+ isIntercepting = false
}
+
+ canStart
},
canStartPostFling = { velocityAvailable ->
val behavior: NestedScrollBehavior =
@@ -717,7 +763,13 @@
// We could start an overscroll animation
canChangeScene = false
- behavior.canStartOnPostFling && hasNextScene(velocityAvailable)
+
+ val canStart = behavior.canStartOnPostFling && hasNextScene(velocityAvailable)
+ if (canStart) {
+ isIntercepting = false
+ }
+
+ canStart
},
canContinueScroll = { true },
canScrollOnFling = false,
@@ -726,7 +778,7 @@
gestureHandler.onDragStarted(
pointersDown = 1,
startedPosition = null,
- overSlop = offsetAvailable,
+ overSlop = if (isIntercepting) 0f else offsetAvailable,
)
},
onScroll = { offsetAvailable ->
@@ -761,4 +813,6 @@
* The number of pixels below which there won't be a visible difference in the transition and from
* which the animation can stop.
*/
-private const val OffsetVisibilityThreshold = 0.5f
+// TODO(b/290184746): Have a better default visibility threshold which takes the swipe distance into
+// account instead.
+internal const val OffsetVisibilityThreshold = 0.5f
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitions.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitions.kt
index 3a55567..ac423b7 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitions.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitions.kt
@@ -17,7 +17,10 @@
package com.android.compose.animation.scene
import androidx.compose.animation.core.AnimationSpec
+import androidx.compose.animation.core.Spring
+import androidx.compose.animation.core.SpringSpec
import androidx.compose.animation.core.snap
+import androidx.compose.animation.core.spring
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.util.fastForEach
@@ -36,6 +39,7 @@
/** The transitions configuration of a [SceneTransitionLayout]. */
class SceneTransitions
internal constructor(
+ internal val defaultSwipeSpec: SpringSpec<Float>,
internal val transitionSpecs: List<TransitionSpecImpl>,
) {
private val cache = mutableMapOf<SceneKey, MutableMap<SceneKey, TransitionSpecImpl>>()
@@ -91,7 +95,13 @@
TransitionSpecImpl(from, to, TransformationSpec.EmptyProvider)
companion object {
- val Empty = SceneTransitions(transitionSpecs = emptyList())
+ internal val DefaultSwipeSpec =
+ spring(
+ stiffness = Spring.StiffnessMediumLow,
+ visibilityThreshold = OffsetVisibilityThreshold,
+ )
+
+ val Empty = SceneTransitions(DefaultSwipeSpec, transitionSpecs = emptyList())
}
}
@@ -125,15 +135,30 @@
}
interface TransformationSpec {
- /** The [AnimationSpec] used to animate the associated transition progress. */
+ /**
+ * The [AnimationSpec] used to animate the associated transition progress from `0` to `1` when
+ * the transition is triggered (i.e. it is not gesture-based).
+ */
val progressSpec: AnimationSpec<Float>
+ /**
+ * The [SpringSpec] used to animate the associated transition progress when the transition was
+ * started by a swipe and is now animating back to a scene because the user lifted their finger.
+ *
+ * If `null`, then the [SceneTransitions.defaultSwipeSpec] will be used.
+ */
+ val swipeSpec: SpringSpec<Float>?
+
/** The list of [Transformation] applied to elements during this transition. */
val transformations: List<Transformation>
companion object {
internal val Empty =
- TransformationSpecImpl(progressSpec = snap(), transformations = emptyList())
+ TransformationSpecImpl(
+ progressSpec = snap(),
+ swipeSpec = null,
+ transformations = emptyList(),
+ )
internal val EmptyProvider = { Empty }
}
}
@@ -151,6 +176,7 @@
val reverse = transformationSpec.invoke()
TransformationSpecImpl(
progressSpec = reverse.progressSpec,
+ swipeSpec = reverse.swipeSpec,
transformations = reverse.transformations.map { it.reversed() }
)
}
@@ -166,6 +192,7 @@
*/
internal class TransformationSpecImpl(
override val progressSpec: AnimationSpec<Float>,
+ override val swipeSpec: SpringSpec<Float>?,
override val transformations: List<Transformation>,
) : TransformationSpec {
private val cache = mutableMapOf<ElementKey, MutableMap<SceneKey, ElementTransformations>>()
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 b9c4ac0..61f4978 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
@@ -19,6 +19,7 @@
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.runtime.Stable
import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.pointer.PointerEvent
import androidx.compose.ui.input.pointer.PointerEventPass
import androidx.compose.ui.node.DelegatingNode
@@ -94,13 +95,10 @@
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 startDragImmediately(startedPosition: Offset): Boolean {
+ // Immediately start the drag if the user can't swipe in the other direction and the gesture
+ // handler can intercept it.
+ return !canOppositeSwipe() && gestureHandler.shouldImmediatelyIntercept(startedPosition)
}
private fun canOppositeSwipe(): Boolean {
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 a764a527..04e0937 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
@@ -17,6 +17,7 @@
package com.android.compose.animation.scene
import androidx.compose.animation.core.AnimationSpec
+import androidx.compose.animation.core.SpringSpec
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
@@ -31,6 +32,12 @@
@TransitionDsl
interface SceneTransitionsBuilder {
/**
+ * The default [AnimationSpec] used when after the user lifts their finger after starting a
+ * swipe to transition, to animate back into one of the 2 scenes we are transitioning to.
+ */
+ var defaultSwipeSpec: SpringSpec<Float>
+
+ /**
* Define the default animation to be played when transitioning [to] the specified scene, from
* any scene. For the animation specification to apply only when transitioning between two
* specific scenes, use [from] instead.
@@ -64,12 +71,20 @@
@TransitionDsl
interface TransitionBuilder : PropertyTransformationBuilder {
/**
- * The [AnimationSpec] used to animate the progress of this transition from `0` to `1` when
- * performing programmatic (not input pointer tracking) animations.
+ * The [AnimationSpec] used to animate the associated transition progress from `0` to `1` when
+ * the transition is triggered (i.e. it is not gesture-based).
*/
var spec: AnimationSpec<Float>
/**
+ * The [SpringSpec] used to animate the associated transition progress when the transition was
+ * started by a swipe and is now animating back to a scene because the user lifted their finger.
+ *
+ * If `null`, then the [SceneTransitionsBuilder.defaultSwipeSpec] will be used.
+ */
+ var swipeSpec: SpringSpec<Float>?
+
+ /**
* Define a progress-based range for the transformations inside [builder].
*
* For instance, the following will fade `Foo` during the first half of the transition then it
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDslImpl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDslImpl.kt
index b96f9be..df186a1 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDslImpl.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDslImpl.kt
@@ -19,6 +19,7 @@
import androidx.compose.animation.core.AnimationSpec
import androidx.compose.animation.core.DurationBasedAnimationSpec
import androidx.compose.animation.core.Spring
+import androidx.compose.animation.core.SpringSpec
import androidx.compose.animation.core.VectorConverter
import androidx.compose.animation.core.spring
import androidx.compose.ui.geometry.Offset
@@ -40,10 +41,12 @@
builder: SceneTransitionsBuilder.() -> Unit,
): SceneTransitions {
val impl = SceneTransitionsBuilderImpl().apply(builder)
- return SceneTransitions(impl.transitionSpecs)
+ return SceneTransitions(impl.defaultSwipeSpec, impl.transitionSpecs)
}
private class SceneTransitionsBuilderImpl : SceneTransitionsBuilder {
+ override var defaultSwipeSpec: SpringSpec<Float> = SceneTransitions.DefaultSwipeSpec
+
val transitionSpecs = mutableListOf<TransitionSpecImpl>()
override fun to(to: SceneKey, builder: TransitionBuilder.() -> Unit): TransitionSpec {
@@ -67,6 +70,7 @@
val impl = TransitionBuilderImpl().apply(builder)
return TransformationSpecImpl(
progressSpec = impl.spec,
+ swipeSpec = impl.swipeSpec,
transformations = impl.transformations,
)
}
@@ -80,6 +84,7 @@
internal class TransitionBuilderImpl : TransitionBuilder {
val transformations = mutableListOf<Transformation>()
override var spec: AnimationSpec<Float> = spring(stiffness = Spring.StiffnessLow)
+ override var swipeSpec: SpringSpec<Float>? = null
private var range: TransformationRange? = null
private var reversed = false
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 88363ad..e8cc0ec 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
@@ -21,7 +21,6 @@
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
-import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.Velocity
@@ -118,9 +117,6 @@
fun up(fractionOfScreen: Float) =
if (fractionOfScreen < 0f) error("use down()") else -down(fractionOfScreen)
- // Float tolerance for comparisons
- val tolerance = 0.00001f
-
// Offset y: 10% of the screen
val offsetY10 = Offset(x = 0f, y = down(0.1f))
@@ -150,31 +146,42 @@
fromScene: SceneKey? = null,
toScene: SceneKey? = null,
progress: Float? = null,
+ isUserInputOngoing: Boolean? = null
) {
+ val transition = transitionState
assertWithMessage("transitionState must be Transition")
- .that(transitionState is Transition)
+ .that(transition is Transition)
.isTrue()
+ transition as Transition
+
if (currentScene != null)
assertWithMessage("currentScene does not match")
- .that(transitionState.currentScene)
+ .that(transition.currentScene)
.isEqualTo(currentScene)
+
if (fromScene != null)
assertWithMessage("fromScene does not match")
- .that((transitionState as? Transition)?.fromScene)
+ .that(transition.fromScene)
.isEqualTo(fromScene)
+
if (toScene != null)
assertWithMessage("toScene does not match")
- .that((transitionState as? Transition)?.toScene)
+ .that(transition.toScene)
.isEqualTo(toScene)
+
if (progress != null)
assertWithMessage("progress does not match")
- .that((transitionState as? Transition)?.progress)
- .isWithin(tolerance)
+ .that(transition.progress)
+ .isWithin(0f) // returns true when comparing 0.0f with -0.0f
.of(progress)
+
+ if (isUserInputOngoing != null)
+ assertWithMessage("isUserInputOngoing does not match")
+ .that(transition.isUserInputOngoing)
+ .isEqualTo(isUserInputOngoing)
}
}
- @OptIn(ExperimentalTestApi::class)
private fun runGestureTest(block: suspend TestGestureScope.() -> Unit) {
runMonotonicClockTest { TestGestureScope(coroutineScope = this).block() }
}
@@ -248,7 +255,7 @@
@Test
fun onDragReversedDirection_changeToScene() = runGestureTest {
// Drag A -> B with progress 0.6
- draggable.onDragStarted(overSlop = up(0.6f))
+ draggable.onDragStarted(overSlop = -60f)
assertTransition(
currentScene = SceneA,
fromScene = SceneA,
@@ -257,7 +264,7 @@
)
// Reverse direction such that A -> C now with 0.4
- draggable.onDelta(down(1f))
+ draggable.onDelta(100f)
assertTransition(
currentScene = SceneA,
fromScene = SceneA,
@@ -287,7 +294,7 @@
navigateToSceneC()
// We are on SceneC which has no action in Down direction
- draggable.onDragStarted(down(0.1f))
+ draggable.onDragStarted(10f)
assertTransition(
currentScene = SceneC,
fromScene = SceneC,
@@ -296,7 +303,7 @@
)
// Reverse drag direction, it will consume the previous drag
- draggable.onDelta(up(0.1f))
+ draggable.onDelta(-10f)
assertTransition(
currentScene = SceneC,
fromScene = SceneC,
@@ -305,7 +312,7 @@
)
// Continue reverse drag direction, it should record progress to Scene B
- draggable.onDelta(up(0.1f))
+ draggable.onDelta(-10f)
assertTransition(
currentScene = SceneC,
fromScene = SceneC,
@@ -557,53 +564,53 @@
) {
val nestedScroll = nestedScrollConnection(nestedScrollBehavior = EdgeWithPreview)
// start scene transition
- nestedScroll.scroll(available = Offset(0f, SCREEN_SIZE * firstScroll))
+ nestedScroll.scroll(available = Offset(0f, firstScroll))
// stop scene transition (start the "stop animation")
nestedScroll.onPreFling(available = Velocity.Zero)
// a pre scroll event, that could be intercepted by SceneGestureHandler
- nestedScroll.onPreScroll(Offset(0f, SCREEN_SIZE * secondScroll), NestedScrollSource.Drag)
+ nestedScroll.onPreScroll(Offset(0f, secondScroll), NestedScrollSource.Drag)
}
@Test
fun scrollAndFling_scrollLessThanInterceptable_goToIdleOnCurrentScene() = runGestureTest {
- val first = transitionInterceptionThreshold - tolerance
- val second = 0.01f
+ val firstScroll = (transitionInterceptionThreshold - 0.0001f) * SCREEN_SIZE
+ val secondScroll = 1f
- preScrollAfterSceneTransition(firstScroll = first, secondScroll = second)
+ preScrollAfterSceneTransition(firstScroll = firstScroll, secondScroll = secondScroll)
assertIdle(SceneA)
}
@Test
fun scrollAndFling_scrollMinInterceptable_interceptPreScrollEvents() = runGestureTest {
- val first = transitionInterceptionThreshold + tolerance
- val second = 0.01f
+ val firstScroll = (transitionInterceptionThreshold + 0.0001f) * SCREEN_SIZE
+ val secondScroll = 1f
- preScrollAfterSceneTransition(firstScroll = first, secondScroll = second)
+ preScrollAfterSceneTransition(firstScroll = firstScroll, secondScroll = secondScroll)
- assertTransition(progress = first + second)
+ assertTransition(progress = (firstScroll + secondScroll) / SCREEN_SIZE)
}
@Test
fun scrollAndFling_scrollMaxInterceptable_interceptPreScrollEvents() = runGestureTest {
- val first = 1f - transitionInterceptionThreshold - tolerance
- val second = 0.01f
+ val firstScroll = -(1f - transitionInterceptionThreshold - 0.0001f) * SCREEN_SIZE
+ val secondScroll = -1f
- preScrollAfterSceneTransition(firstScroll = first, secondScroll = second)
+ preScrollAfterSceneTransition(firstScroll = firstScroll, secondScroll = secondScroll)
- assertTransition(progress = first + second)
+ assertTransition(progress = -(firstScroll + secondScroll) / SCREEN_SIZE)
}
@Test
fun scrollAndFling_scrollMoreThanInterceptable_goToIdleOnNextScene() = runGestureTest {
- val first = 1f - transitionInterceptionThreshold + tolerance
- val second = 0.01f
+ val firstScroll = -(1f - transitionInterceptionThreshold + 0.0001f) * SCREEN_SIZE
+ val secondScroll = -0.01f
- preScrollAfterSceneTransition(firstScroll = first, secondScroll = second)
+ preScrollAfterSceneTransition(firstScroll = firstScroll, secondScroll = secondScroll)
- assertIdle(SceneC)
+ assertIdle(SceneB)
}
@Test
@@ -745,29 +752,75 @@
@Test
fun startNestedScrollWhileDragging() = runGestureTest {
val nestedScroll = nestedScrollConnection(nestedScrollBehavior = EdgeAlways)
- draggable.onDragStarted(overSlop = down(0.1f))
+
+ // Start a drag and then stop it, given that
+ draggable.onDragStarted(overSlop = up(0.1f))
+
assertTransition(currentScene = SceneA)
assertThat(progress).isEqualTo(0.1f)
// now we can intercept the scroll events
- nestedScroll.scroll(available = offsetY10)
+ nestedScroll.scroll(available = -offsetY10)
assertThat(progress).isEqualTo(0.2f)
// this should be ignored, we are scrolling now!
- draggable.onDragStopped(velocityThreshold)
+ draggable.onDragStopped(-velocityThreshold)
assertTransition(currentScene = SceneA)
- nestedScroll.scroll(available = offsetY10)
+ nestedScroll.scroll(available = -offsetY10)
assertThat(progress).isEqualTo(0.3f)
- nestedScroll.scroll(available = offsetY10)
+ nestedScroll.scroll(available = -offsetY10)
assertThat(progress).isEqualTo(0.4f)
- nestedScroll.onPreFling(available = Velocity(0f, velocityThreshold))
- assertTransition(currentScene = SceneC)
+ nestedScroll.onPreFling(available = Velocity(0f, -velocityThreshold))
+ assertTransition(currentScene = SceneB)
// wait for the stop animation
advanceUntilIdle()
- assertIdle(currentScene = SceneC)
+ assertIdle(currentScene = SceneB)
+ }
+
+ @Test
+ fun interceptTransition() = runGestureTest {
+ // Start at scene C.
+ navigateToSceneC()
+
+ // Swipe up from the middle to transition to scene B.
+ val middle = Offset(SCREEN_SIZE / 2f, SCREEN_SIZE / 2f)
+ draggable.onDragStarted(startedPosition = middle, overSlop = up(0.1f))
+ assertTransition(
+ currentScene = SceneC,
+ fromScene = SceneC,
+ toScene = SceneB,
+ isUserInputOngoing = true,
+ )
+
+ val firstTransition = transitionState
+
+ // During the current gesture, start a new gesture, still in the middle of the screen. We
+ // should intercept it. Because it is intercepted, the overSlop passed to onDragStarted()
+ // should be 0f.
+ assertThat(sceneGestureHandler.shouldImmediatelyIntercept(middle)).isTrue()
+ draggable.onDragStarted(startedPosition = middle, overSlop = 0f)
+
+ // We should have intercepted the transition, so the transition should be the same object.
+ assertTransition(currentScene = SceneC, fromScene = SceneC, toScene = SceneB)
+ assertThat(transitionState).isSameInstanceAs(firstTransition)
+
+ // Start a new gesture from the bottom of the screen. Because swiping up from the bottom of
+ // C leads to scene A (and not B), the previous transitions is *not* intercepted and we
+ // instead animate from C to A.
+ val bottom = Offset(SCREEN_SIZE / 2, SCREEN_SIZE)
+ assertThat(sceneGestureHandler.shouldImmediatelyIntercept(bottom)).isFalse()
+ draggable.onDragStarted(startedPosition = bottom, overSlop = up(0.1f))
+
+ assertTransition(
+ currentScene = SceneC,
+ fromScene = SceneC,
+ toScene = SceneA,
+ isUserInputOngoing = true,
+ )
+ assertThat(transitionState).isNotSameInstanceAs(firstTransition)
}
}
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutStateTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutStateTest.kt
index 48825fb..9a25d43 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutStateTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutStateTest.kt
@@ -91,6 +91,16 @@
}
@Test
+ fun setTargetScene_transitionToOriginalScene() = runMonotonicClockTest {
+ val state = MutableSceneTransitionLayoutState(TestScenes.SceneA)
+ assertThat(state.setTargetScene(TestScenes.SceneB, coroutineScope = this)).isNotNull()
+
+ // Progress is 0f, so we don't animate at all and directly snap back to A.
+ assertThat(state.setTargetScene(TestScenes.SceneA, coroutineScope = this)).isNull()
+ assertThat(state.transitionState).isEqualTo(TransitionState.Idle(TestScenes.SceneA))
+ }
+
+ @Test
fun setTargetScene_coroutineScopeCancelled() = runMonotonicClockTest {
val state = MutableSceneTransitionLayoutState(TestScenes.SceneA)
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 9403358..af1a5b8 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
@@ -21,6 +21,9 @@
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.platform.LocalViewConfiguration
@@ -63,7 +66,10 @@
/** The content under test. */
@Composable
- private fun TestContent(layoutState: SceneTransitionLayoutState) {
+ private fun TestContent(
+ layoutState: SceneTransitionLayoutState,
+ swipesEnabled: () -> Boolean = { true },
+ ) {
SceneTransitionLayout(
state = layoutState,
modifier = Modifier.size(LayoutWidth, LayoutHeight).testTag(TestElements.Foo.debugName),
@@ -71,28 +77,35 @@
scene(
TestScenes.SceneA,
userActions =
- mapOf(
- Swipe.Left to TestScenes.SceneB,
- Swipe.Down to TestScenes.SceneC,
- ),
+ if (swipesEnabled())
+ mapOf(
+ Swipe.Left to TestScenes.SceneB,
+ Swipe.Down to TestScenes.SceneC,
+ Swipe.Up to TestScenes.SceneB,
+ )
+ else emptyMap(),
) {
Box(Modifier.fillMaxSize())
}
scene(
TestScenes.SceneB,
- userActions = mapOf(Swipe.Right to TestScenes.SceneA),
+ userActions =
+ if (swipesEnabled()) mapOf(Swipe.Right to TestScenes.SceneA) else emptyMap(),
) {
Box(Modifier.fillMaxSize())
}
scene(
TestScenes.SceneC,
userActions =
- mapOf(
- Swipe.Down to TestScenes.SceneA,
- Swipe(SwipeDirection.Down, pointerCount = 2) to TestScenes.SceneB,
- Swipe(SwipeDirection.Right, fromSource = Edge.Left) to TestScenes.SceneB,
- Swipe(SwipeDirection.Down, fromSource = Edge.Top) to TestScenes.SceneB,
- ),
+ if (swipesEnabled())
+ mapOf(
+ Swipe.Down to TestScenes.SceneA,
+ Swipe(SwipeDirection.Down, pointerCount = 2) to TestScenes.SceneB,
+ Swipe(SwipeDirection.Right, fromSource = Edge.Left) to
+ TestScenes.SceneB,
+ Swipe(SwipeDirection.Down, fromSource = Edge.Top) to TestScenes.SceneB,
+ )
+ else emptyMap(),
) {
Box(Modifier.fillMaxSize())
}
@@ -357,7 +370,7 @@
// detected as a drag event.
var touchSlop = 0f
- val layoutState = MutableSceneTransitionLayoutState(TestScenes.SceneA)
+ val layoutState = layoutState()
val verticalSwipeDistance = 50.dp
assertThat(verticalSwipeDistance).isNotEqualTo(LayoutHeight)
@@ -392,4 +405,91 @@
assertThat(transition).isNotNull()
assertThat(transition!!.progress).isEqualTo(0.5f)
}
+
+ @Test
+ fun swipeByTouchSlop() {
+ val layoutState = layoutState()
+ var touchSlop = 0f
+ rule.setContent {
+ touchSlop = LocalViewConfiguration.current.touchSlop
+ TestContent(layoutState)
+ }
+
+ // Swipe down by exactly touchSlop, so that the drag overSlop is 0f.
+ rule.onRoot().performTouchInput {
+ down(middle)
+ moveBy(Offset(0f, touchSlop), delayMillis = 1_000)
+ }
+
+ // We should still correctly compute that we are swiping down to scene C.
+ var transition = layoutState.currentTransition
+ assertThat(transition).isNotNull()
+ assertThat(transition?.toScene).isEqualTo(TestScenes.SceneC)
+
+ // Release the finger, animating back to scene A.
+ rule.onRoot().performTouchInput { up() }
+ rule.waitForIdle()
+ assertThat(layoutState.currentTransition).isNull()
+ assertThat(layoutState.transitionState.currentScene).isEqualTo(TestScenes.SceneA)
+
+ // Swipe up by exactly touchSlop, so that the drag overSlop is 0f.
+ rule.onRoot().performTouchInput {
+ down(middle)
+ moveBy(Offset(0f, -touchSlop), delayMillis = 1_000)
+ }
+
+ // We should still correctly compute that we are swiping up to scene B.
+ transition = layoutState.currentTransition
+ assertThat(transition).isNotNull()
+ assertThat(transition?.toScene).isEqualTo(TestScenes.SceneB)
+
+ // Release the finger, animating back to scene A.
+ rule.onRoot().performTouchInput { up() }
+ rule.waitForIdle()
+ assertThat(layoutState.currentTransition).isNull()
+ assertThat(layoutState.transitionState.currentScene).isEqualTo(TestScenes.SceneA)
+
+ // Swipe left by exactly touchSlop, so that the drag overSlop is 0f.
+ rule.onRoot().performTouchInput {
+ down(middle)
+ moveBy(Offset(-touchSlop, 0f), delayMillis = 1_000)
+ }
+
+ // We should still correctly compute that we are swiping down to scene B.
+ transition = layoutState.currentTransition
+ assertThat(transition).isNotNull()
+ assertThat(transition?.toScene).isEqualTo(TestScenes.SceneB)
+ }
+
+ @Test
+ fun swipeEnabledLater() {
+ val layoutState = MutableSceneTransitionLayoutState(TestScenes.SceneA)
+ var swipesEnabled by mutableStateOf(false)
+ var touchSlop = 0f
+ rule.setContent {
+ touchSlop = LocalViewConfiguration.current.touchSlop
+ TestContent(layoutState, swipesEnabled = { swipesEnabled })
+ }
+
+ // Drag down from the middle. This should not do anything, because swipes are disabled.
+ rule.onRoot().performTouchInput {
+ down(middle)
+ moveBy(Offset(0f, touchSlop), delayMillis = 1_000)
+ }
+ assertThat(layoutState.currentTransition).isNull()
+
+ // Release finger.
+ rule.onRoot().performTouchInput { up() }
+
+ // Enable swipes.
+ swipesEnabled = true
+ rule.waitForIdle()
+
+ // Drag down from the middle. Now it should start a transition.
+ rule.onRoot().performTouchInput {
+ down(middle)
+ moveBy(Offset(0f, touchSlop), delayMillis = 1_000)
+ }
+ assertThat(layoutState.currentTransition).isNotNull()
+ }
}
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/TransitionDslTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/TransitionDslTest.kt
index ef72992..140baca 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/TransitionDslTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/TransitionDslTest.kt
@@ -18,6 +18,7 @@
import androidx.compose.animation.core.SpringSpec
import androidx.compose.animation.core.TweenSpec
+import androidx.compose.animation.core.spring
import androidx.compose.animation.core.tween
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.android.compose.animation.scene.transformation.Transformation
@@ -188,6 +189,40 @@
)
}
+ @Test
+ fun springSpec() {
+ val defaultSpec = spring<Float>(stiffness = 1f)
+ val specFromAToC = spring<Float>(stiffness = 2f)
+ val transitions = transitions {
+ defaultSwipeSpec = defaultSpec
+
+ from(TestScenes.SceneA, to = TestScenes.SceneB) {
+ // Default swipe spec.
+ }
+ from(TestScenes.SceneA, to = TestScenes.SceneC) { swipeSpec = specFromAToC }
+ }
+
+ assertThat(transitions.defaultSwipeSpec).isSameInstanceAs(defaultSpec)
+
+ // A => B does not have a custom spec.
+ assertThat(
+ transitions
+ .transitionSpec(from = TestScenes.SceneA, to = TestScenes.SceneB)
+ .transformationSpec()
+ .swipeSpec
+ )
+ .isNull()
+
+ // A => C has a custom swipe spec.
+ assertThat(
+ transitions
+ .transitionSpec(from = TestScenes.SceneA, to = TestScenes.SceneC)
+ .transformationSpec()
+ .swipeSpec
+ )
+ .isSameInstanceAs(specFromAToC)
+ }
+
companion object {
private val TRANSFORMATION_RANGE =
Correspondence.transforming<Transformation, TransformationRange?>(
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/customization/data/content/CustomizationProviderClient.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/customization/data/content/CustomizationProviderClient.kt
index e050604..34c4dfb 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/customization/data/content/CustomizationProviderClient.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/customization/data/content/CustomizationProviderClient.kt
@@ -33,6 +33,7 @@
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
+import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.withContext
@@ -518,6 +519,7 @@
awaitClose { context.contentResolver.unregisterContentObserver(observer) }
}
.onStart { emit(Unit) }
+ .flowOn(backgroundDispatcher)
}
private fun String.toIntent(
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/SysuiTestCaseSelfTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/SysuiTestCaseSelfTest.kt
index be6bb9c..107293e 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/SysuiTestCaseSelfTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/SysuiTestCaseSelfTest.kt
@@ -24,6 +24,7 @@
@SmallTest
@RunWith(AndroidJUnit4::class)
+@android.platform.test.annotations.EnabledOnRavenwood
class SysuiTestCaseSelfTest : SysuiTestCase() {
private val contextBeforeSetup = context
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
index 9287edf..c2f0c6f 100644
--- 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
@@ -31,6 +31,7 @@
@SmallTest
@RunWith(AndroidJUnit4::class)
+@android.platform.test.annotations.EnabledOnRavenwood
class AccessibilityQsShortcutsRepositoryImplTest : SysuiTestCase() {
private val testDispatcher = StandardTestDispatcher()
private val testScope = TestScope(testDispatcher)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/ColorCorrectionRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/ColorCorrectionRepositoryImplTest.kt
index c86c747..b6605ed 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/ColorCorrectionRepositoryImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/ColorCorrectionRepositoryImplTest.kt
@@ -37,6 +37,7 @@
@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@RunWith(AndroidJUnit4::class)
+@android.platform.test.annotations.EnabledOnRavenwood
class ColorCorrectionRepositoryImplTest : SysuiTestCase() {
private val testUser1 = UserHandle.of(1)!!
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/ColorInversionRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/ColorInversionRepositoryImplTest.kt
index 4853529..30eb782 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/ColorInversionRepositoryImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/ColorInversionRepositoryImplTest.kt
@@ -37,6 +37,7 @@
@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@RunWith(AndroidJUnit4::class)
+@android.platform.test.annotations.EnabledOnRavenwood
class ColorInversionRepositoryImplTest : SysuiTestCase() {
private val testUser1 = UserHandle.of(1)!!
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
index ce22e28..ed3b4c0 100644
--- 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
@@ -31,6 +31,7 @@
@SmallTest
@RunWith(AndroidJUnit4::class)
+@android.platform.test.annotations.EnabledOnRavenwood
class UserA11yQsShortcutsRepositoryTest : SysuiTestCase() {
private val secureSettings = FakeSettings()
private val testDispatcher = StandardTestDispatcher()
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/FaceHelpMessageDeferralTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/FaceHelpMessageDeferralTest.kt
index 39f0d57..0596205 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/FaceHelpMessageDeferralTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/FaceHelpMessageDeferralTest.kt
@@ -34,6 +34,7 @@
@SmallTest
@RunWith(AndroidJUnit4::class)
+@android.platform.test.annotations.EnabledOnRavenwood
class FaceHelpMessageDeferralTest : SysuiTestCase() {
val threshold = .75f
@Mock lateinit var logger: BiometricMessageDeferralLogger
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/data/repository/EmergencyServicesRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/data/repository/EmergencyServicesRepositoryImplTest.kt
index a67b093..d317aeb 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/data/repository/EmergencyServicesRepositoryImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/data/repository/EmergencyServicesRepositoryImplTest.kt
@@ -37,6 +37,7 @@
@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@RunWith(AndroidJUnit4::class)
+@android.platform.test.annotations.EnabledOnRavenwood
class EmergencyServicesRepositoryImplTest : SysuiTestCase() {
private val kosmos = testKosmos()
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerCallbackInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerCallbackInteractorTest.kt
index 4aea4f3..db83b3b 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerCallbackInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerCallbackInteractorTest.kt
@@ -29,6 +29,7 @@
@SmallTest
@RunWith(AndroidJUnit4::class)
+@android.platform.test.annotations.EnabledOnRavenwood
class PrimaryBouncerCallbackInteractorTest : SysuiTestCase() {
private val mPrimaryBouncerCallbackInteractor = PrimaryBouncerCallbackInteractor()
@Mock
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt
index c193d14..fbb5415 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt
@@ -16,6 +16,7 @@
package com.android.systemui.bouncer.ui.viewmodel
+import android.content.pm.UserInfo
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
@@ -26,18 +27,27 @@
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.coroutines.collectValues
import com.android.systemui.deviceentry.data.repository.fakeDeviceEntryRepository
+import com.android.systemui.inputmethod.data.model.InputMethodModel
+import com.android.systemui.inputmethod.data.repository.fakeInputMethodRepository
+import com.android.systemui.inputmethod.domain.interactor.inputMethodInteractor
import com.android.systemui.kosmos.testScope
import com.android.systemui.res.R
import com.android.systemui.scene.domain.interactor.sceneInteractor
import com.android.systemui.scene.shared.model.SceneKey
import com.android.systemui.scene.shared.model.SceneModel
import com.android.systemui.testKosmos
+import com.android.systemui.user.data.model.SelectedUserModel
+import com.android.systemui.user.data.model.SelectionStatus
+import com.android.systemui.user.data.repository.fakeUserRepository
+import com.android.systemui.user.domain.interactor.selectedUserInteractor
import com.google.common.truth.Truth.assertThat
+import java.util.UUID
import kotlin.time.Duration.Companion.seconds
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.advanceTimeBy
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Before
@@ -51,19 +61,22 @@
private val kosmos = testKosmos()
private val testScope = kosmos.testScope
- private val authenticationInteractor = kosmos.authenticationInteractor
+ private val authenticationInteractor by lazy { kosmos.authenticationInteractor }
private val sceneInteractor by lazy { kosmos.sceneInteractor }
private val bouncerInteractor by lazy { kosmos.bouncerInteractor }
+ private val selectedUserInteractor by lazy { kosmos.selectedUserInteractor }
+ private val inputMethodInteractor by lazy { kosmos.inputMethodInteractor }
private val bouncerViewModel by lazy { kosmos.bouncerViewModel }
private val isInputEnabled = MutableStateFlow(true)
- private val underTest by lazy {
+ private val underTest =
PasswordBouncerViewModel(
viewModelScope = testScope.backgroundScope,
+ isInputEnabled = isInputEnabled.asStateFlow(),
interactor = bouncerInteractor,
- isInputEnabled.asStateFlow(),
+ inputMethodInteractor = inputMethodInteractor,
+ selectedUserInteractor = selectedUserInteractor,
)
- }
@Before
fun setUp() {
@@ -270,6 +283,52 @@
assertThat(isTextFieldFocusRequested).isTrue()
}
+ @Test
+ fun isImeSwitcherButtonVisible() =
+ testScope.runTest {
+ val selectedUserId by collectLastValue(selectedUserInteractor.selectedUser)
+ selectUser(USER_INFOS.first())
+
+ enableInputMethodsForUser(checkNotNull(selectedUserId))
+
+ // Assert initial value, before the UI subscribes.
+ assertThat(underTest.isImeSwitcherButtonVisible.value).isFalse()
+
+ // Subscription starts; verify a fresh value is fetched.
+ val isImeSwitcherButtonVisible by collectLastValue(underTest.isImeSwitcherButtonVisible)
+ assertThat(isImeSwitcherButtonVisible).isTrue()
+
+ // Change the user, verify a fresh value is fetched.
+ selectUser(USER_INFOS.last())
+
+ assertThat(
+ inputMethodInteractor.hasMultipleEnabledImesOrSubtypes(
+ checkNotNull(selectedUserId)
+ )
+ )
+ .isFalse()
+ assertThat(isImeSwitcherButtonVisible).isFalse()
+
+ // Enable IMEs and add another subscriber; verify a fresh value is fetched.
+ enableInputMethodsForUser(checkNotNull(selectedUserId))
+ val collector2 by collectLastValue(underTest.isImeSwitcherButtonVisible)
+ assertThat(collector2).isTrue()
+ }
+
+ @Test
+ fun onImeSwitcherButtonClicked() =
+ testScope.runTest {
+ val displayId = 7
+ assertThat(kosmos.fakeInputMethodRepository.inputMethodPickerShownDisplayId)
+ .isNotEqualTo(displayId)
+
+ underTest.onImeSwitcherButtonClicked(displayId)
+ runCurrent()
+
+ assertThat(kosmos.fakeInputMethodRepository.inputMethodPickerShownDisplayId)
+ .isEqualTo(displayId)
+ }
+
private fun TestScope.switchToScene(toScene: SceneKey) {
val currentScene by collectLastValue(sceneInteractor.desiredScene)
val bouncerShown = currentScene?.key != SceneKey.Bouncer && toScene == SceneKey.Bouncer
@@ -310,8 +369,45 @@
runCurrent()
}
+ private fun TestScope.selectUser(userInfo: UserInfo) {
+ kosmos.fakeUserRepository.selectedUser.value =
+ SelectedUserModel(
+ userInfo = userInfo,
+ selectionStatus = SelectionStatus.SELECTION_COMPLETE
+ )
+ advanceTimeBy(PasswordBouncerViewModel.DELAY_TO_FETCH_IMES)
+ }
+
+ private suspend fun enableInputMethodsForUser(userId: Int) {
+ kosmos.fakeInputMethodRepository.setEnabledInputMethods(
+ userId,
+ createInputMethodWithSubtypes(auxiliarySubtypes = 0, nonAuxiliarySubtypes = 0),
+ createInputMethodWithSubtypes(auxiliarySubtypes = 0, nonAuxiliarySubtypes = 1),
+ )
+ assertThat(inputMethodInteractor.hasMultipleEnabledImesOrSubtypes(userId)).isTrue()
+ }
+
+ private fun createInputMethodWithSubtypes(
+ auxiliarySubtypes: Int,
+ nonAuxiliarySubtypes: Int,
+ ): InputMethodModel {
+ return InputMethodModel(
+ imeId = UUID.randomUUID().toString(),
+ subtypes =
+ List(auxiliarySubtypes + nonAuxiliarySubtypes) {
+ InputMethodModel.Subtype(subtypeId = it, isAuxiliary = it < auxiliarySubtypes)
+ }
+ )
+ }
+
companion object {
private const val ENTER_YOUR_PASSWORD = "Enter your password"
private const val WRONG_PASSWORD = "Wrong password"
+
+ private val USER_INFOS =
+ listOf(
+ UserInfo(100, "First user", 0),
+ UserInfo(101, "Second user", 0),
+ )
}
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PinInputViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PinInputViewModelTest.kt
index 55016bb..25a287c 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PinInputViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PinInputViewModelTest.kt
@@ -24,6 +24,7 @@
*/
@SmallTest
@RunWith(AndroidJUnit4::class)
+@android.platform.test.annotations.EnabledOnRavenwood
class PinInputViewModelTest : SysuiTestCase() {
@Test
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalSceneStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalSceneStartableTest.kt
new file mode 100644
index 0000000..a8fe16b
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalSceneStartableTest.kt
@@ -0,0 +1,252 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS 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
+
+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.communal.shared.model.CommunalSceneKey
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.dock.DockManager
+import com.android.systemui.dock.dockManager
+import com.android.systemui.dock.fakeDockManager
+import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
+import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.kosmos.applicationCoroutineScope
+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.TestScope
+import kotlinx.coroutines.test.advanceTimeBy
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class CommunalSceneStartableTest : SysuiTestCase() {
+ private val kosmos = testKosmos()
+
+ private lateinit var underTest: CommunalSceneStartable
+
+ @Before
+ fun setUp() =
+ with(kosmos) {
+ underTest =
+ CommunalSceneStartable(
+ dockManager = dockManager,
+ communalInteractor = communalInteractor,
+ keyguardTransitionInteractor = keyguardTransitionInteractor,
+ applicationScope = applicationCoroutineScope,
+ bgScope = applicationCoroutineScope,
+ )
+ .apply { start() }
+ }
+
+ @Test
+ fun keyguardGoesAway_forceBlankScene() =
+ with(kosmos) {
+ testScope.runTest {
+ val scene by collectLastValue(communalInteractor.desiredScene)
+
+ communalInteractor.onSceneChanged(CommunalSceneKey.Communal)
+ assertThat(scene).isEqualTo(CommunalSceneKey.Communal)
+
+ fakeKeyguardTransitionRepository.sendTransitionSteps(
+ from = KeyguardState.PRIMARY_BOUNCER,
+ to = KeyguardState.GONE,
+ testScope = this
+ )
+
+ assertThat(scene).isEqualTo(CommunalSceneKey.Blank)
+ }
+ }
+
+ @Test
+ fun deviceDreaming_forceBlankScene() =
+ with(kosmos) {
+ testScope.runTest {
+ val scene by collectLastValue(communalInteractor.desiredScene)
+
+ communalInteractor.onSceneChanged(CommunalSceneKey.Communal)
+ assertThat(scene).isEqualTo(CommunalSceneKey.Communal)
+
+ fakeKeyguardTransitionRepository.sendTransitionSteps(
+ from = KeyguardState.GLANCEABLE_HUB,
+ to = KeyguardState.DREAMING,
+ testScope = this
+ )
+
+ assertThat(scene).isEqualTo(CommunalSceneKey.Blank)
+ }
+ }
+
+ @Test
+ fun deviceDocked_forceCommunalScene() =
+ with(kosmos) {
+ testScope.runTest {
+ val scene by collectLastValue(communalInteractor.desiredScene)
+ assertThat(scene).isEqualTo(CommunalSceneKey.Blank)
+
+ updateDocked(true)
+ fakeKeyguardTransitionRepository.sendTransitionSteps(
+ from = KeyguardState.GONE,
+ to = KeyguardState.LOCKSCREEN,
+ testScope = this
+ )
+ assertThat(scene).isEqualTo(CommunalSceneKey.Communal)
+
+ fakeKeyguardTransitionRepository.sendTransitionSteps(
+ from = KeyguardState.GLANCEABLE_HUB,
+ to = KeyguardState.DREAMING,
+ testScope = this
+ )
+ assertThat(scene).isEqualTo(CommunalSceneKey.Blank)
+ }
+ }
+
+ @Test
+ fun deviceDocked_doesNotForceCommunalIfTransitioningFromCommunal() =
+ with(kosmos) {
+ testScope.runTest {
+ val scene by collectLastValue(communalInteractor.desiredScene)
+ assertThat(scene).isEqualTo(CommunalSceneKey.Blank)
+
+ updateDocked(true)
+ fakeKeyguardTransitionRepository.sendTransitionSteps(
+ from = KeyguardState.GLANCEABLE_HUB,
+ to = KeyguardState.LOCKSCREEN,
+ testScope = this
+ )
+ assertThat(scene).isEqualTo(CommunalSceneKey.Blank)
+ }
+ }
+
+ @Test
+ fun deviceAsleep_forceBlankSceneAfterTimeout() =
+ with(kosmos) {
+ testScope.runTest {
+ val scene by collectLastValue(communalInteractor.desiredScene)
+ communalInteractor.onSceneChanged(CommunalSceneKey.Communal)
+ assertThat(scene).isEqualTo(CommunalSceneKey.Communal)
+
+ fakeKeyguardTransitionRepository.sendTransitionSteps(
+ from = KeyguardState.GLANCEABLE_HUB,
+ to = KeyguardState.OFF,
+ testScope = this
+ )
+ assertThat(scene).isEqualTo(CommunalSceneKey.Communal)
+
+ advanceTimeBy(CommunalSceneStartable.AWAKE_DEBOUNCE_DELAY)
+
+ assertThat(scene).isEqualTo(CommunalSceneKey.Blank)
+ }
+ }
+
+ @Test
+ fun deviceAsleep_wakesUpBeforeTimeout_noChangeInScene() =
+ with(kosmos) {
+ testScope.runTest {
+ val scene by collectLastValue(communalInteractor.desiredScene)
+ communalInteractor.onSceneChanged(CommunalSceneKey.Communal)
+ assertThat(scene).isEqualTo(CommunalSceneKey.Communal)
+
+ fakeKeyguardTransitionRepository.sendTransitionSteps(
+ from = KeyguardState.GLANCEABLE_HUB,
+ to = KeyguardState.OFF,
+ testScope = this
+ )
+ assertThat(scene).isEqualTo(CommunalSceneKey.Communal)
+ advanceTimeBy(CommunalSceneStartable.AWAKE_DEBOUNCE_DELAY / 2)
+ assertThat(scene).isEqualTo(CommunalSceneKey.Communal)
+
+ fakeKeyguardTransitionRepository.sendTransitionSteps(
+ from = KeyguardState.OFF,
+ to = KeyguardState.GLANCEABLE_HUB,
+ testScope = this
+ )
+
+ advanceTimeBy(CommunalSceneStartable.AWAKE_DEBOUNCE_DELAY)
+ assertThat(scene).isEqualTo(CommunalSceneKey.Communal)
+ }
+ }
+
+ @Test
+ fun dockingOnLockscreen_forcesCommunal() =
+ with(kosmos) {
+ testScope.runTest {
+ communalInteractor.onSceneChanged(CommunalSceneKey.Blank)
+ val scene by collectLastValue(communalInteractor.desiredScene)
+
+ // device is docked while on the lockscreen
+ fakeKeyguardTransitionRepository.sendTransitionSteps(
+ from = KeyguardState.GLANCEABLE_HUB,
+ to = KeyguardState.LOCKSCREEN,
+ testScope = this
+ )
+ updateDocked(true)
+
+ assertThat(scene).isEqualTo(CommunalSceneKey.Blank)
+ advanceTimeBy(CommunalSceneStartable.DOCK_DEBOUNCE_DELAY)
+ assertThat(scene).isEqualTo(CommunalSceneKey.Communal)
+ }
+ }
+
+ @Test
+ fun dockingOnLockscreen_doesNotForceCommunalIfDreamStarts() =
+ with(kosmos) {
+ testScope.runTest {
+ communalInteractor.onSceneChanged(CommunalSceneKey.Blank)
+ val scene by collectLastValue(communalInteractor.desiredScene)
+
+ // device is docked while on the lockscreen
+ fakeKeyguardTransitionRepository.sendTransitionSteps(
+ from = KeyguardState.GLANCEABLE_HUB,
+ to = KeyguardState.LOCKSCREEN,
+ testScope = this
+ )
+ updateDocked(true)
+
+ assertThat(scene).isEqualTo(CommunalSceneKey.Blank)
+ advanceTimeBy(CommunalSceneStartable.DOCK_DEBOUNCE_DELAY / 2)
+ assertThat(scene).isEqualTo(CommunalSceneKey.Blank)
+
+ // dream starts shortly after docking
+ fakeKeyguardTransitionRepository.sendTransitionSteps(
+ from = KeyguardState.LOCKSCREEN,
+ to = KeyguardState.DREAMING,
+ testScope = this
+ )
+ advanceTimeBy(CommunalSceneStartable.DOCK_DEBOUNCE_DELAY)
+ assertThat(scene).isEqualTo(CommunalSceneKey.Blank)
+ }
+ }
+
+ private fun TestScope.updateDocked(docked: Boolean) =
+ with(kosmos) {
+ runCurrent()
+ fakeDockManager.setIsDocked(docked)
+ fakeDockManager.setDockEvent(DockManager.STATE_DOCKED)
+ runCurrent()
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalMediaRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalMediaRepositoryImplTest.kt
index 92b75cb..76b0d4a 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalMediaRepositoryImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalMediaRepositoryImplTest.kt
@@ -40,6 +40,7 @@
@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@RunWith(AndroidJUnit4::class)
+@android.platform.test.annotations.EnabledOnRavenwood
class CommunalMediaRepositoryImplTest : SysuiTestCase() {
@Mock private lateinit var mediaDataManager: MediaDataManager
@Mock private lateinit var mediaData: MediaData
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalPrefsRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalPrefsRepositoryImplTest.kt
index 820bfbf..d15e15e 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalPrefsRepositoryImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalPrefsRepositoryImplTest.kt
@@ -39,6 +39,7 @@
@SmallTest
@RunWith(AndroidJUnit4::class)
+@android.platform.test.annotations.EnabledOnRavenwood
class CommunalPrefsRepositoryImplTest : SysuiTestCase() {
private lateinit var underTest: CommunalPrefsRepositoryImpl
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalTutorialRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalTutorialRepositoryImplTest.kt
index c4a8582..0c66bbb 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalTutorialRepositoryImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalTutorialRepositoryImplTest.kt
@@ -38,6 +38,7 @@
@SmallTest
@RunWith(AndroidJUnit4::class)
+@android.platform.test.annotations.EnabledOnRavenwood
class CommunalTutorialRepositoryImplTest : SysuiTestCase() {
private val kosmos = testKosmos()
private val testScope = kosmos.testScope
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/widgets/CommunalAppWidgetHostTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/widgets/CommunalAppWidgetHostTest.kt
index 3aa99c4..89a4c04 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/widgets/CommunalAppWidgetHostTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/widgets/CommunalAppWidgetHostTest.kt
@@ -23,16 +23,27 @@
import com.android.systemui.SysuiTestCase
import com.android.systemui.communal.widgets.CommunalAppWidgetHost
import com.android.systemui.communal.widgets.CommunalAppWidgetHostView
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.log.logcatLogBuffer
+import com.android.systemui.testKosmos
import com.android.systemui.util.mockito.mock
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
import org.junit.runner.RunWith
+@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@RunWithLooper(setAsMainLooper = true)
@RunWith(AndroidJUnit4::class)
class CommunalAppWidgetHostTest : SysuiTestCase() {
+ private val kosmos = testKosmos()
+ private val testScope = kosmos.testScope
private lateinit var testableLooper: TestableLooper
private lateinit var underTest: CommunalAppWidgetHost
@@ -43,9 +54,11 @@
underTest =
CommunalAppWidgetHost(
context = context,
+ backgroundScope = kosmos.applicationCoroutineScope,
hostId = 116,
interactionHandler = mock(),
- looper = testableLooper.looper
+ looper = testableLooper.looper,
+ logBuffer = logcatLogBuffer("CommunalAppWidgetHostTest"),
)
}
@@ -64,4 +77,23 @@
assertThat(view).isNotNull()
assertThat(view.appWidgetId).isEqualTo(appWidgetId)
}
+
+ @Test
+ fun appWidgetIdToRemove_emit() =
+ testScope.runTest {
+ val appWidgetIdToRemove by collectLastValue(underTest.appWidgetIdToRemove)
+
+ // Nothing should be emitted yet
+ assertThat(appWidgetIdToRemove).isNull()
+
+ underTest.onAppWidgetRemoved(appWidgetId = 1)
+ runCurrent()
+
+ assertThat(appWidgetIdToRemove).isEqualTo(1)
+
+ underTest.onAppWidgetRemoved(appWidgetId = 2)
+ runCurrent()
+
+ assertThat(appWidgetIdToRemove).isEqualTo(2)
+ }
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartableTest.kt
index a3654b6..032d76f 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartableTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartableTest.kt
@@ -21,14 +21,21 @@
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.communal.data.repository.fakeCommunalRepository
+import com.android.systemui.communal.data.repository.fakeCommunalWidgetRepository
import com.android.systemui.communal.domain.interactor.communalInteractor
+import com.android.systemui.communal.shared.model.CommunalWidgetContentModel
+import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.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 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.flow.MutableSharedFlow
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Before
@@ -47,6 +54,8 @@
@Mock private lateinit var appWidgetHost: CommunalAppWidgetHost
+ private lateinit var appWidgetIdToRemove: MutableSharedFlow<Int>
+
private lateinit var underTest: CommunalAppWidgetHostStartable
@Before
@@ -54,6 +63,9 @@
MockitoAnnotations.initMocks(this)
kosmos.fakeUserRepository.setUserInfos(listOf(MAIN_USER_INFO))
+ appWidgetIdToRemove = MutableSharedFlow()
+ whenever(appWidgetHost.appWidgetIdToRemove).thenReturn(appWidgetIdToRemove)
+
underTest =
CommunalAppWidgetHostStartable(
appWidgetHost,
@@ -120,6 +132,38 @@
}
}
+ @Test
+ fun removeAppWidgetReportedByHost() =
+ with(kosmos) {
+ testScope.runTest {
+ // Set up communal widgets
+ val widget1 =
+ mock<CommunalWidgetContentModel> { whenever(this.appWidgetId).thenReturn(1) }
+ val widget2 =
+ mock<CommunalWidgetContentModel> { whenever(this.appWidgetId).thenReturn(2) }
+ val widget3 =
+ mock<CommunalWidgetContentModel> { whenever(this.appWidgetId).thenReturn(3) }
+ fakeCommunalWidgetRepository.setCommunalWidgets(listOf(widget1, widget2, widget3))
+
+ underTest.start()
+
+ // Assert communal widgets has 3
+ val communalWidgets by
+ collectLastValue(fakeCommunalWidgetRepository.communalWidgets)
+ assertThat(communalWidgets).containsExactly(widget1, widget2, widget3)
+
+ // Report app widget 1 to remove and assert widget removed
+ appWidgetIdToRemove.emit(1)
+ runCurrent()
+ assertThat(communalWidgets).containsExactly(widget2, widget3)
+
+ // Report app widget 3 to remove and assert widget removed
+ appWidgetIdToRemove.emit(3)
+ runCurrent()
+ assertThat(communalWidgets).containsExactly(widget2)
+ }
+ }
+
private suspend fun setCommunalAvailable(available: Boolean) =
with(kosmos) {
fakeKeyguardRepository.setIsEncryptedOrLockdown(false)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/data/repository/DeviceEntryRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/data/repository/DeviceEntryRepositoryTest.kt
index b54c5bd..9536084 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/data/repository/DeviceEntryRepositoryTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/data/repository/DeviceEntryRepositoryTest.kt
@@ -32,6 +32,7 @@
@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@RunWith(AndroidJUnit4::class)
+@android.platform.test.annotations.EnabledOnRavenwood
class DeviceEntryRepositoryTest : SysuiTestCase() {
@Mock private lateinit var lockPatternUtils: LockPatternUtils
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractorTest.kt
index 32943a1..51db451 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractorTest.kt
@@ -33,6 +33,7 @@
@SmallTest
@RunWith(AndroidJUnit4::class)
+@android.platform.test.annotations.EnabledOnRavenwood
class DeviceUnlockedInteractorTest : SysuiTestCase() {
private val kosmos = testKosmos()
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/ui/binder/LiftToRunFaceAuthBinderTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/ui/binder/LiftToRunFaceAuthBinderTest.kt
new file mode 100644
index 0000000..e9e85c9
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/ui/binder/LiftToRunFaceAuthBinderTest.kt
@@ -0,0 +1,200 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS 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.ui.binder
+
+import android.content.packageManager
+import android.content.pm.PackageManager
+import android.hardware.Sensor
+import android.hardware.TriggerEventListener
+import android.testing.TestableLooper
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.bouncer.data.repository.keyguardBouncerRepository
+import com.android.systemui.deviceentry.ui.binder.liftToRunFaceAuthBinder
+import com.android.systemui.keyguard.data.repository.biometricSettingsRepository
+import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.power.data.repository.fakePowerRepository
+import com.android.systemui.testKosmos
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.capture
+import com.android.systemui.util.mockito.whenever
+import com.android.systemui.util.sensors.asyncSensorManager
+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.ArgumentCaptor
+import org.mockito.Captor
+import org.mockito.Mock
+import org.mockito.Mockito.clearInvocations
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@ExperimentalCoroutinesApi
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
+class LiftToRunFaceAuthBinderTest : SysuiTestCase() {
+ private val kosmos = testKosmos()
+ private val testScope = kosmos.testScope
+ private val sensorManager = kosmos.asyncSensorManager
+ private val powerRepository = kosmos.fakePowerRepository
+ private val keyguardRepository = kosmos.fakeKeyguardRepository
+ private val bouncerRepository = kosmos.keyguardBouncerRepository
+ private val biometricSettingsRepository = kosmos.biometricSettingsRepository
+ private val packageManager = kosmos.packageManager
+
+ @Captor private lateinit var triggerEventListenerCaptor: ArgumentCaptor<TriggerEventListener>
+ @Mock private lateinit var mockSensor: Sensor
+
+ private val underTest = kosmos.liftToRunFaceAuthBinder
+
+ @Before
+ fun setup() {
+ MockitoAnnotations.initMocks(this)
+ whenever(packageManager.hasSystemFeature(PackageManager.FEATURE_FACE)).thenReturn(true)
+ whenever(sensorManager.getDefaultSensor(Sensor.TYPE_PICK_UP_GESTURE)).thenReturn(mockSensor)
+ }
+
+ @Test
+ fun doNotListenForGesture() =
+ testScope.runTest {
+ start()
+ verifyNeverRequestsTriggerSensor()
+ }
+
+ @Test
+ fun awakeKeyguard_listenForGesture() =
+ testScope.runTest {
+ start()
+ givenAwakeKeyguard(true)
+ runCurrent()
+ verifyRequestTriggerSensor()
+ }
+
+ @Test
+ fun faceNotEnrolled_listenForGesture() =
+ testScope.runTest {
+ start()
+ givenAwakeKeyguard(true)
+ biometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(false)
+ runCurrent()
+ verifyNeverRequestsTriggerSensor()
+ }
+
+ @Test
+ fun notInteractive_doNotListenForGesture() =
+ testScope.runTest {
+ start()
+ givenAwakeKeyguard(true)
+ powerRepository.setInteractive(false)
+ runCurrent()
+ verifyNeverRequestsTriggerSensor()
+ }
+
+ @Test
+ fun primaryBouncer_listenForGesture() =
+ testScope.runTest {
+ start()
+ givenAwakeKeyguard(false)
+ givenPrimaryBouncerShowing()
+ runCurrent()
+ verifyRequestTriggerSensor()
+ }
+
+ @Test
+ fun alternateBouncer_listenForGesture() =
+ testScope.runTest {
+ start()
+ givenAwakeKeyguard(false)
+ givenAlternateBouncerShowing()
+ runCurrent()
+ verifyRequestTriggerSensor()
+ }
+
+ @Test
+ fun restartListeningForGestureAfterSensorTrigger() =
+ testScope.runTest {
+ start()
+ givenAwakeKeyguard(true)
+ runCurrent()
+ verifyRequestTriggerSensor()
+ clearInvocations(sensorManager)
+
+ triggerEventListenerCaptor.value.onTrigger(null)
+ runCurrent()
+ verifyRequestTriggerSensor()
+ }
+
+ @Test
+ fun cancelTriggerSensor_keyguardNotAwakeAnymore() =
+ testScope.runTest {
+ start()
+ givenAwakeKeyguard(true)
+ runCurrent()
+ verifyRequestTriggerSensor()
+
+ givenAwakeKeyguard(false)
+ runCurrent()
+ verifyCancelTriggerSensor()
+ }
+
+ private fun start() {
+ underTest.start()
+ biometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(true)
+ givenAwakeKeyguard(false)
+ givenBouncerNotShowing()
+ }
+
+ private fun givenAwakeKeyguard(isAwake: Boolean) {
+ powerRepository.setInteractive(isAwake)
+ keyguardRepository.setKeyguardShowing(isAwake)
+ keyguardRepository.setKeyguardOccluded(false)
+ }
+
+ private fun givenPrimaryBouncerShowing() {
+ bouncerRepository.setPrimaryShow(true)
+ bouncerRepository.setAlternateVisible(false)
+ }
+
+ private fun givenBouncerNotShowing() {
+ bouncerRepository.setPrimaryShow(false)
+ bouncerRepository.setAlternateVisible(false)
+ }
+
+ private fun givenAlternateBouncerShowing() {
+ bouncerRepository.setPrimaryShow(false)
+ bouncerRepository.setAlternateVisible(true)
+ }
+
+ private fun verifyRequestTriggerSensor() {
+ verify(sensorManager).requestTriggerSensor(capture(triggerEventListenerCaptor), any())
+ }
+
+ private fun verifyNeverRequestsTriggerSensor() {
+ verify(sensorManager, never()).requestTriggerSensor(any(), any())
+ }
+
+ private fun verifyCancelTriggerSensor() {
+ verify(sensorManager).cancelTriggerSensor(any(), any())
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dock/DockManagerFakeKosmos.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/dock/DockManagerFakeKosmos.kt
new file mode 100644
index 0000000..06275fa
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dock/DockManagerFakeKosmos.kt
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.dock
+
+import com.android.systemui.kosmos.Kosmos
+
+val Kosmos.dockManager: DockManager by Kosmos.Fixture { fakeDockManager }
+val Kosmos.fakeDockManager: DockManagerFake by Kosmos.Fixture { DockManagerFake() }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayCallbackControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayCallbackControllerTest.kt
index 2c6c793..d9dcfdc 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayCallbackControllerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayCallbackControllerTest.kt
@@ -31,6 +31,7 @@
@SmallTest
@RunWith(AndroidJUnit4::class)
+@android.platform.test.annotations.EnabledOnRavenwood
class DreamOverlayCallbackControllerTest : SysuiTestCase() {
@Mock private lateinit var callback: DreamOverlayCallbackController.Callback
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayNotificationCountProviderTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayNotificationCountProviderTest.java
index d379dc6..5ae8595 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayNotificationCountProviderTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayNotificationCountProviderTest.java
@@ -39,6 +39,7 @@
@SmallTest
@RunWith(AndroidJUnit4.class)
+@android.platform.test.annotations.EnabledOnRavenwood
public class DreamOverlayNotificationCountProviderTest extends SysuiTestCase {
@Mock
NotificationListener mNotificationListener;
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayStateControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayStateControllerTest.java
index 8bf878c..b46f2aa 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayStateControllerTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayStateControllerTest.java
@@ -50,6 +50,7 @@
@SmallTest
@RunWith(AndroidJUnit4.class)
+@android.platform.test.annotations.EnabledOnRavenwood
public class DreamOverlayStateControllerTest extends SysuiTestCase {
@Mock
DreamOverlayStateController.Callback mCallback;
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayStatusBarItemsProviderTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayStatusBarItemsProviderTest.java
index 7ff345c..ad353ce 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayStatusBarItemsProviderTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayStatusBarItemsProviderTest.java
@@ -37,6 +37,7 @@
@SmallTest
@RunWith(AndroidJUnit4.class)
+@android.platform.test.annotations.EnabledOnRavenwood
public class DreamOverlayStatusBarItemsProviderTest extends SysuiTestCase {
@Mock
DreamOverlayStatusBarItemsProvider.Callback mCallback;
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/conditions/AssistantAttentionConditionTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/conditions/AssistantAttentionConditionTest.java
index e0c6ab2..cb5702ad 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/conditions/AssistantAttentionConditionTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/conditions/AssistantAttentionConditionTest.java
@@ -42,6 +42,7 @@
@SmallTest
@RunWith(AndroidJUnit4.class)
+@android.platform.test.annotations.EnabledOnRavenwood
public class AssistantAttentionConditionTest extends SysuiTestCase {
@Mock
Condition.Callback mCallback;
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/conditions/DreamConditionTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/conditions/DreamConditionTest.java
index 480754c..96d3c93 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/conditions/DreamConditionTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/conditions/DreamConditionTest.java
@@ -45,6 +45,7 @@
@SmallTest
@RunWith(AndroidJUnit4.class)
+@android.platform.test.annotations.EnabledOnRavenwood
public class DreamConditionTest extends SysuiTestCase {
@Mock
Condition.Callback mCallback;
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/HomeControlsComponentInteractorKosmos.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/HomeControlsComponentInteractorKosmos.kt
new file mode 100644
index 0000000..efccf7a
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/HomeControlsComponentInteractorKosmos.kt
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.systemui.dreams.homecontrols
+
+import com.android.systemui.controls.dagger.ControlsComponent
+import com.android.systemui.controls.management.ControlsListingController
+import com.android.systemui.controls.panels.AuthorizedPanelsRepository
+import com.android.systemui.controls.panels.selectedComponentRepository
+import com.android.systemui.dreams.homecontrols.domain.interactor.HomeControlsComponentInteractor
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.user.data.repository.fakeUserRepository
+import com.android.systemui.util.mockito.mock
+
+val Kosmos.homeControlsComponentInteractor by
+ Kosmos.Fixture {
+ HomeControlsComponentInteractor(
+ selectedComponentRepository = selectedComponentRepository,
+ controlsComponent,
+ authorizedPanelsRepository = authorizedPanelsRepository,
+ userRepository = fakeUserRepository,
+ bgScope = applicationCoroutineScope,
+ )
+ }
+
+val Kosmos.controlsComponent by Kosmos.Fixture<ControlsComponent> { mock() }
+val Kosmos.controlsListingController by Kosmos.Fixture<ControlsListingController> { mock() }
+val Kosmos.authorizedPanelsRepository by Kosmos.Fixture<AuthorizedPanelsRepository> { mock() }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/HomeControlsComponentInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/HomeControlsComponentInteractorTest.kt
new file mode 100644
index 0000000..ce74a90
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/HomeControlsComponentInteractorTest.kt
@@ -0,0 +1,255 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS 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.dreams.homecontrols
+
+import android.content.ComponentName
+import android.content.Context
+import android.content.pm.ApplicationInfo
+import android.content.pm.ServiceInfo
+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.controls.ControlsServiceInfo
+import com.android.systemui.controls.dagger.ControlsComponent
+import com.android.systemui.controls.management.ControlsListingController
+import com.android.systemui.controls.panels.AuthorizedPanelsRepository
+import com.android.systemui.controls.panels.FakeSelectedComponentRepository
+import com.android.systemui.controls.panels.SelectedComponentRepository
+import com.android.systemui.controls.panels.selectedComponentRepository
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.dreams.homecontrols.domain.interactor.HomeControlsComponentInteractor
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.testKosmos
+import com.android.systemui.user.data.repository.FakeUserRepository
+import com.android.systemui.user.data.repository.fakeUserRepository
+import com.android.systemui.util.mockito.whenever
+import com.android.systemui.util.mockito.withArgCaptor
+import com.google.common.truth.Truth.assertThat
+import java.util.Optional
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class HomeControlsComponentInteractorTest : SysuiTestCase() {
+
+ private val kosmos = testKosmos()
+
+ private lateinit var controlsComponent: ControlsComponent
+ private lateinit var controlsListingController: ControlsListingController
+ private lateinit var authorizedPanelsRepository: AuthorizedPanelsRepository
+ private lateinit var underTest: HomeControlsComponentInteractor
+ private lateinit var userRepository: FakeUserRepository
+ private lateinit var selectedComponentRepository: FakeSelectedComponentRepository
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+ userRepository = kosmos.fakeUserRepository
+ userRepository.setUserInfos(listOf(PRIMARY_USER, ANOTHER_USER))
+
+ controlsComponent = kosmos.controlsComponent
+ authorizedPanelsRepository = kosmos.authorizedPanelsRepository
+ controlsListingController = kosmos.controlsListingController
+ selectedComponentRepository = kosmos.selectedComponentRepository
+
+ selectedComponentRepository.setCurrentUserHandle(PRIMARY_USER.userHandle)
+ whenever(controlsComponent.getControlsListingController())
+ .thenReturn(Optional.of(controlsListingController))
+
+ underTest =
+ HomeControlsComponentInteractor(
+ selectedComponentRepository,
+ controlsComponent,
+ authorizedPanelsRepository,
+ userRepository,
+ kosmos.applicationCoroutineScope,
+ )
+ }
+
+ @Test
+ fun testPanelComponentReturnsComponentNameForSelectedItemByUser() =
+ with(kosmos) {
+ testScope.runTest {
+ whenever(authorizedPanelsRepository.getAuthorizedPanels())
+ .thenReturn(setOf(TEST_PACKAGE_PANEL))
+ userRepository.setSelectedUserInfo(PRIMARY_USER)
+ selectedComponentRepository.setSelectedComponent(TEST_SELECTED_COMPONENT_PANEL)
+ val actualValue by collectLastValue(underTest.panelComponent)
+ assertThat(actualValue).isNull()
+ runServicesUpdate()
+ assertThat(actualValue).isEqualTo(TEST_COMPONENT_PANEL)
+ }
+ }
+
+ @Test
+ fun testPanelComponentReturnsComponentNameAsInitialValueWithoutServiceUpdate() =
+ with(kosmos) {
+ testScope.runTest {
+ whenever(authorizedPanelsRepository.getAuthorizedPanels())
+ .thenReturn(setOf(TEST_PACKAGE_PANEL))
+ userRepository.setSelectedUserInfo(PRIMARY_USER)
+ selectedComponentRepository.setSelectedComponent(TEST_SELECTED_COMPONENT_PANEL)
+ whenever(controlsListingController.getCurrentServices())
+ .thenReturn(
+ listOf(ControlsServiceInfo(TEST_COMPONENT_PANEL, "panel", hasPanel = true))
+ )
+ val actualValue by collectLastValue(underTest.panelComponent)
+ assertThat(actualValue).isEqualTo(TEST_COMPONENT_PANEL)
+ }
+ }
+
+ @Test
+ fun testPanelComponentReturnsNullForHomeControlsThatDoesNotSupportPanel() =
+ with(kosmos) {
+ testScope.runTest {
+ whenever(authorizedPanelsRepository.getAuthorizedPanels())
+ .thenReturn(setOf(TEST_PACKAGE_PANEL))
+ userRepository.setSelectedUserInfo(PRIMARY_USER)
+ selectedComponentRepository.setSelectedComponent(TEST_SELECTED_COMPONENT_NON_PANEL)
+ val actualValue by collectLastValue(underTest.panelComponent)
+ assertThat(actualValue).isNull()
+ runServicesUpdate(false)
+ assertThat(actualValue).isNull()
+ }
+ }
+
+ @Test
+ fun testPanelComponentReturnsNullWhenPanelIsUnauthorized() =
+ with(kosmos) {
+ testScope.runTest {
+ whenever(authorizedPanelsRepository.getAuthorizedPanels()).thenReturn(setOf())
+ userRepository.setSelectedUserInfo(PRIMARY_USER)
+ selectedComponentRepository.setSelectedComponent(TEST_SELECTED_COMPONENT_PANEL)
+ val actualValue by collectLastValue(underTest.panelComponent)
+ assertThat(actualValue).isNull()
+ runServicesUpdate()
+ assertThat(actualValue).isNull()
+ }
+ }
+
+ @Test
+ fun testPanelComponentReturnsComponentNameForDifferentUsers() =
+ with(kosmos) {
+ testScope.runTest {
+ whenever(authorizedPanelsRepository.getAuthorizedPanels())
+ .thenReturn(setOf(TEST_PACKAGE_PANEL))
+ userRepository.setSelectedUserInfo(ANOTHER_USER)
+ selectedComponentRepository.setSelectedComponent(TEST_SELECTED_COMPONENT_NON_PANEL)
+ selectedComponentRepository.setCurrentUserHandle(ANOTHER_USER.userHandle)
+ selectedComponentRepository.setSelectedComponent(TEST_SELECTED_COMPONENT_PANEL)
+
+ val actualValue by collectLastValue(underTest.panelComponent)
+ assertThat(actualValue).isNull()
+ runServicesUpdate()
+ assertThat(actualValue).isEqualTo(TEST_COMPONENT_PANEL)
+ }
+ }
+
+ @Test
+ fun testPanelComponentReturnsNullWhenControlsComponentReturnsNullForListingController() =
+ with(kosmos) {
+ testScope.runTest {
+ whenever(authorizedPanelsRepository.getAuthorizedPanels())
+ .thenReturn(setOf(TEST_PACKAGE_PANEL))
+ whenever(controlsComponent.getControlsListingController())
+ .thenReturn(Optional.empty())
+ userRepository.setSelectedUserInfo(PRIMARY_USER)
+ selectedComponentRepository.setSelectedComponent(TEST_SELECTED_COMPONENT_PANEL)
+ val actualValue by collectLastValue(underTest.panelComponent)
+ assertThat(actualValue).isNull()
+ }
+ }
+
+ private fun runServicesUpdate(hasPanelBoolean: Boolean = true) {
+ val listings =
+ listOf(ControlsServiceInfo(TEST_COMPONENT_PANEL, "panel", hasPanel = hasPanelBoolean))
+ val callback = withArgCaptor { verify(controlsListingController).addCallback(capture()) }
+ callback.onServicesUpdated(listings)
+ }
+
+ private fun ControlsServiceInfo(
+ componentName: ComponentName,
+ label: CharSequence,
+ hasPanel: Boolean
+ ): ControlsServiceInfo {
+ val serviceInfo =
+ ServiceInfo().apply {
+ applicationInfo = ApplicationInfo()
+ packageName = componentName.packageName
+ name = componentName.className
+ }
+ return FakeControlsServiceInfo(context, serviceInfo, label, hasPanel)
+ }
+
+ private class FakeControlsServiceInfo(
+ context: Context,
+ serviceInfo: ServiceInfo,
+ private val label: CharSequence,
+ hasPanel: Boolean
+ ) : ControlsServiceInfo(context, serviceInfo) {
+
+ init {
+ if (hasPanel) {
+ panelActivity = serviceInfo.componentName
+ }
+ }
+
+ override fun loadLabel(): CharSequence {
+ return label
+ }
+ }
+
+ companion object {
+ private const val PRIMARY_USER_ID = 0
+ private val PRIMARY_USER =
+ UserInfo(
+ /* id= */ PRIMARY_USER_ID,
+ /* name= */ "primary user",
+ /* flags= */ UserInfo.FLAG_PRIMARY
+ )
+
+ private const val ANOTHER_USER_ID = 1
+ private val ANOTHER_USER =
+ UserInfo(
+ /* id= */ ANOTHER_USER_ID,
+ /* name= */ "another user",
+ /* flags= */ UserInfo.FLAG_PRIMARY
+ )
+ private const val TEST_PACKAGE = "pkg"
+ private val TEST_COMPONENT = ComponentName(TEST_PACKAGE, "service")
+ private const val TEST_PACKAGE_PANEL = "pkg.panel"
+ private val TEST_COMPONENT_PANEL = ComponentName(TEST_PACKAGE_PANEL, "service")
+ private val TEST_SELECTED_COMPONENT_PANEL =
+ SelectedComponentRepository.SelectedComponent(
+ TEST_PACKAGE_PANEL,
+ TEST_COMPONENT_PANEL,
+ true
+ )
+ private val TEST_SELECTED_COMPONENT_NON_PANEL =
+ SelectedComponentRepository.SelectedComponent(
+ TEST_PACKAGE_PANEL,
+ TEST_COMPONENT_PANEL,
+ false
+ )
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/HomeControlsDreamServiceTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/HomeControlsDreamServiceTest.kt
new file mode 100644
index 0000000..d28b6bf
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/HomeControlsDreamServiceTest.kt
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS 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.dreams.homecontrols
+
+import android.app.Activity
+import android.content.ComponentName
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.controls.dagger.ControlsComponent
+import com.android.systemui.controls.management.ControlsListingController
+import com.android.systemui.controls.settings.FakeControlsSettingsRepository
+import com.android.systemui.dreams.homecontrols.domain.interactor.HomeControlsComponentInteractor
+import com.android.systemui.log.LogBuffer
+import com.android.systemui.log.core.FakeLogBuffer.Factory.Companion.create
+import com.android.systemui.testKosmos
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.whenever
+import java.util.Optional
+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
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class HomeControlsDreamServiceTest : SysuiTestCase() {
+
+ private val kosmos = testKosmos()
+
+ private lateinit var controlsSettingsRepository: FakeControlsSettingsRepository
+ @Mock private lateinit var taskFragmentComponentFactory: TaskFragmentComponent.Factory
+ @Mock private lateinit var taskFragmentComponent: TaskFragmentComponent
+ @Mock private lateinit var activity: Activity
+ private val logBuffer: LogBuffer = create()
+
+ private lateinit var underTest: HomeControlsDreamService
+ private lateinit var homeControlsComponentInteractor: HomeControlsComponentInteractor
+ private lateinit var fakeDreamActivityProvider: DreamActivityProvider
+ private lateinit var controlsComponent: ControlsComponent
+ private lateinit var controlsListingController: ControlsListingController
+
+ @Before
+ fun setup() {
+ MockitoAnnotations.initMocks(this)
+ whenever(taskFragmentComponentFactory.create(any(), any(), any(), any()))
+ .thenReturn(taskFragmentComponent)
+
+ controlsSettingsRepository = FakeControlsSettingsRepository()
+ controlsSettingsRepository.setAllowActionOnTrivialControlsInLockscreen(true)
+
+ controlsComponent = kosmos.controlsComponent
+ controlsListingController = kosmos.controlsListingController
+
+ whenever(controlsComponent.getControlsListingController())
+ .thenReturn(Optional.of(controlsListingController))
+
+ homeControlsComponentInteractor = kosmos.homeControlsComponentInteractor
+
+ fakeDreamActivityProvider = DreamActivityProvider { activity }
+ underTest =
+ HomeControlsDreamService(
+ controlsSettingsRepository,
+ taskFragmentComponentFactory,
+ homeControlsComponentInteractor,
+ fakeDreamActivityProvider,
+ logBuffer
+ )
+ }
+
+ @Test
+ fun testOnAttachedToWindowCreatesTaskFragmentComponent() {
+ underTest.onAttachedToWindow()
+ verify(taskFragmentComponentFactory).create(any(), any(), any(), any())
+ }
+
+ @Test
+ fun testOnDetachedFromWindowDestroyTaskFragmentComponent() {
+ underTest.onAttachedToWindow()
+ underTest.onDetachedFromWindow()
+ verify(taskFragmentComponent).destroy()
+ }
+
+ @Test
+ fun testNotCreatingTaskFragmentComponentWhenActivityIsNull() {
+ fakeDreamActivityProvider = DreamActivityProvider { null }
+ underTest =
+ HomeControlsDreamService(
+ controlsSettingsRepository,
+ taskFragmentComponentFactory,
+ homeControlsComponentInteractor,
+ fakeDreamActivityProvider,
+ logBuffer
+ )
+
+ underTest.onAttachedToWindow()
+ verify(taskFragmentComponentFactory, never()).create(any(), any(), any(), any())
+ }
+
+ companion object {
+ private const val TEST_PACKAGE_PANEL = "pkg.panel"
+ private val TEST_COMPONENT_PANEL = ComponentName(TEST_PACKAGE_PANEL, "service")
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/HomeControlsDreamStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/HomeControlsDreamStartableTest.kt
new file mode 100644
index 0000000..6610e70
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/HomeControlsDreamStartableTest.kt
@@ -0,0 +1,202 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS 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.dreams.homecontrols
+
+import android.content.ComponentName
+import android.content.Context
+import android.content.pm.ApplicationInfo
+import android.content.pm.PackageManager
+import android.content.pm.ServiceInfo
+import android.content.pm.UserInfo
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
+import android.service.controls.flags.Flags.FLAG_HOME_PANEL_DREAM
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.controls.ControlsServiceInfo
+import com.android.systemui.controls.dagger.ControlsComponent
+import com.android.systemui.controls.management.ControlsListingController
+import com.android.systemui.controls.panels.AuthorizedPanelsRepository
+import com.android.systemui.controls.panels.SelectedComponentRepository
+import com.android.systemui.controls.panels.selectedComponentRepository
+import com.android.systemui.dreams.homecontrols.domain.interactor.HomeControlsComponentInteractor
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.testKosmos
+import com.android.systemui.user.data.repository.FakeUserRepository
+import com.android.systemui.user.data.repository.fakeUserRepository
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.eq
+import com.android.systemui.util.mockito.whenever
+import java.util.Optional
+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
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class HomeControlsDreamStartableTest : SysuiTestCase() {
+
+ private val kosmos = testKosmos()
+
+ @Mock private lateinit var packageManager: PackageManager
+
+ private lateinit var homeControlsComponentInteractor: HomeControlsComponentInteractor
+ private lateinit var selectedComponentRepository: SelectedComponentRepository
+ private lateinit var authorizedPanelsRepository: AuthorizedPanelsRepository
+ private lateinit var userRepository: FakeUserRepository
+ private lateinit var controlsComponent: ControlsComponent
+ private lateinit var controlsListingController: ControlsListingController
+
+ private lateinit var startable: HomeControlsDreamStartable
+ private val componentName = ComponentName(context, HomeControlsDreamService::class.java)
+ private val testScope = kosmos.testScope
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+
+ selectedComponentRepository = kosmos.selectedComponentRepository
+ authorizedPanelsRepository = kosmos.authorizedPanelsRepository
+ userRepository = kosmos.fakeUserRepository
+ controlsComponent = kosmos.controlsComponent
+ controlsListingController = kosmos.controlsListingController
+
+ userRepository.setUserInfos(listOf(PRIMARY_USER))
+
+ whenever(authorizedPanelsRepository.getAuthorizedPanels())
+ .thenReturn(setOf(TEST_PACKAGE_PANEL))
+
+ whenever(controlsComponent.getControlsListingController())
+ .thenReturn(Optional.of(controlsListingController))
+ whenever(controlsListingController.getCurrentServices())
+ .thenReturn(listOf(ControlsServiceInfo(TEST_COMPONENT_PANEL, "panel", hasPanel = true)))
+
+ homeControlsComponentInteractor = kosmos.homeControlsComponentInteractor
+
+ startable =
+ HomeControlsDreamStartable(
+ mContext,
+ packageManager,
+ homeControlsComponentInteractor,
+ kosmos.applicationCoroutineScope
+ )
+ }
+
+ @Test
+ @EnableFlags(FLAG_HOME_PANEL_DREAM)
+ fun testStartEnablesHomeControlsDreamServiceWhenPanelComponentIsNotNull() =
+ testScope.runTest {
+ userRepository.setSelectedUserInfo(PRIMARY_USER)
+ selectedComponentRepository.setSelectedComponent(TEST_SELECTED_COMPONENT_PANEL)
+ startable.start()
+ runCurrent()
+ verify(packageManager)
+ .setComponentEnabledSetting(
+ eq(componentName),
+ eq(PackageManager.COMPONENT_ENABLED_STATE_ENABLED),
+ eq(PackageManager.DONT_KILL_APP)
+ )
+ }
+
+ @Test
+ @EnableFlags(FLAG_HOME_PANEL_DREAM)
+ fun testStartDisablesHomeControlsDreamServiceWhenPanelComponentIsNull() =
+ testScope.runTest {
+ selectedComponentRepository.setSelectedComponent(TEST_SELECTED_COMPONENT_NON_PANEL)
+ startable.start()
+ runCurrent()
+ verify(packageManager)
+ .setComponentEnabledSetting(
+ eq(componentName),
+ eq(PackageManager.COMPONENT_ENABLED_STATE_DISABLED),
+ eq(PackageManager.DONT_KILL_APP)
+ )
+ }
+
+ @Test
+ @DisableFlags(FLAG_HOME_PANEL_DREAM)
+ fun testStartDoesNotRunDreamServiceWhenFlagIsDisabled() =
+ testScope.runTest {
+ selectedComponentRepository.setSelectedComponent(TEST_SELECTED_COMPONENT_NON_PANEL)
+ startable.start()
+ runCurrent()
+ verify(packageManager, never()).setComponentEnabledSetting(any(), any(), any())
+ }
+
+ private fun ControlsServiceInfo(
+ componentName: ComponentName,
+ label: CharSequence,
+ hasPanel: Boolean
+ ): ControlsServiceInfo {
+ val serviceInfo =
+ ServiceInfo().apply {
+ applicationInfo = ApplicationInfo()
+ packageName = componentName.packageName
+ name = componentName.className
+ }
+ return FakeControlsServiceInfo(context, serviceInfo, label, hasPanel)
+ }
+
+ private class FakeControlsServiceInfo(
+ context: Context,
+ serviceInfo: ServiceInfo,
+ private val label: CharSequence,
+ hasPanel: Boolean
+ ) : ControlsServiceInfo(context, serviceInfo) {
+
+ init {
+ if (hasPanel) {
+ panelActivity = serviceInfo.componentName
+ }
+ }
+
+ override fun loadLabel(): CharSequence {
+ return label
+ }
+ }
+
+ companion object {
+ private const val PRIMARY_USER_ID = 0
+ private val PRIMARY_USER =
+ UserInfo(
+ /* id= */ PRIMARY_USER_ID,
+ /* name= */ "primary user",
+ /* flags= */ UserInfo.FLAG_PRIMARY
+ )
+ private const val TEST_PACKAGE_PANEL = "pkg.panel"
+ private val TEST_COMPONENT_PANEL = ComponentName(TEST_PACKAGE_PANEL, "service")
+ private val TEST_SELECTED_COMPONENT_PANEL =
+ SelectedComponentRepository.SelectedComponent(
+ TEST_PACKAGE_PANEL,
+ TEST_COMPONENT_PANEL,
+ true
+ )
+ private val TEST_SELECTED_COMPONENT_NON_PANEL =
+ SelectedComponentRepository.SelectedComponent(
+ TEST_PACKAGE_PANEL,
+ TEST_COMPONENT_PANEL,
+ false
+ )
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/touch/scrim/BouncerlessScrimControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/touch/scrim/BouncerlessScrimControllerTest.java
index 017fdbe..97052a8 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/touch/scrim/BouncerlessScrimControllerTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/touch/scrim/BouncerlessScrimControllerTest.java
@@ -39,6 +39,7 @@
@SmallTest
@RunWith(AndroidJUnit4.class)
+@android.platform.test.annotations.EnabledOnRavenwood
public class BouncerlessScrimControllerTest extends SysuiTestCase {
@Mock
BouncerlessScrimController.Callback mCallback;
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/touch/scrim/ScrimManagerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/touch/scrim/ScrimManagerTest.java
index 4ee4a60..ebbcf98 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/touch/scrim/ScrimManagerTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/touch/scrim/ScrimManagerTest.java
@@ -39,6 +39,7 @@
@SmallTest
@RunWith(AndroidJUnit4.class)
+@android.platform.test.annotations.EnabledOnRavenwood
public class ScrimManagerTest extends SysuiTestCase {
@Mock
ScrimController mBouncerlessScrimController;
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/fold/ui/helper/FoldPostureTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/fold/ui/helper/FoldPostureTest.kt
index 61b2057..db52a45 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/fold/ui/helper/FoldPostureTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/fold/ui/helper/FoldPostureTest.kt
@@ -28,6 +28,7 @@
@SmallTest
@RunWith(AndroidJUnit4::class)
+@android.platform.test.annotations.EnabledOnRavenwood
class FoldPostureTest : SysuiTestCase() {
@Test
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/inputmethod/data/repository/InputMethodRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/inputmethod/data/repository/InputMethodRepositoryTest.kt
new file mode 100644
index 0000000..857cdce
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/inputmethod/data/repository/InputMethodRepositoryTest.kt
@@ -0,0 +1,124 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.inputmethod.data.repository
+
+import android.os.UserHandle
+import android.view.inputmethod.InputMethodInfo
+import android.view.inputmethod.InputMethodManager
+import android.view.inputmethod.InputMethodSubtype
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.eq
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.flow.count
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyBoolean
+import org.mockito.Mock
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class InputMethodRepositoryTest : SysuiTestCase() {
+
+ @Mock private lateinit var inputMethodManager: InputMethodManager
+
+ private val kosmos = Kosmos()
+ private val testScope = kosmos.testScope
+
+ private lateinit var underTest: InputMethodRepository
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+
+ whenever(inputMethodManager.getEnabledInputMethodSubtypeList(eq(null), anyBoolean()))
+ .thenReturn(listOf())
+
+ underTest =
+ InputMethodRepositoryImpl(
+ backgroundDispatcher = kosmos.testDispatcher,
+ inputMethodManager = inputMethodManager,
+ )
+ }
+
+ @Test
+ fun enabledInputMethods_noImes_emptyFlow() =
+ testScope.runTest {
+ whenever(inputMethodManager.getEnabledInputMethodListAsUser(eq(USER_HANDLE)))
+ .thenReturn(listOf())
+ whenever(inputMethodManager.getEnabledInputMethodSubtypeList(any(), anyBoolean()))
+ .thenReturn(listOf())
+
+ assertThat(underTest.enabledInputMethods(USER_ID, fetchSubtypes = true).count())
+ .isEqualTo(0)
+ }
+
+ @Test
+ fun selectedInputMethodSubtypes_returnsSubtypeList() =
+ testScope.runTest {
+ val subtypeId = 123
+ val isAuxiliary = true
+ whenever(inputMethodManager.getEnabledInputMethodListAsUser(eq(USER_HANDLE)))
+ .thenReturn(listOf(mock<InputMethodInfo>()))
+ whenever(inputMethodManager.getEnabledInputMethodSubtypeList(any(), anyBoolean()))
+ .thenReturn(listOf())
+ whenever(inputMethodManager.getEnabledInputMethodSubtypeList(eq(null), anyBoolean()))
+ .thenReturn(
+ listOf(
+ InputMethodSubtype.InputMethodSubtypeBuilder()
+ .setSubtypeId(subtypeId)
+ .setIsAuxiliary(isAuxiliary)
+ .build()
+ )
+ )
+
+ val result = underTest.selectedInputMethodSubtypes()
+ assertThat(result).hasSize(1)
+ assertThat(result.first().subtypeId).isEqualTo(subtypeId)
+ assertThat(result.first().isAuxiliary).isEqualTo(isAuxiliary)
+ }
+
+ @Test
+ fun showImePicker_forwardsDisplayId() =
+ testScope.runTest {
+ val displayId = 7
+
+ underTest.showInputMethodPicker(displayId, /* showAuxiliarySubtypes = */ true)
+
+ verify(inputMethodManager)
+ .showInputMethodPickerFromSystem(
+ /* showAuxiliarySubtypes = */ eq(true),
+ /* displayId = */ eq(displayId)
+ )
+ }
+
+ companion object {
+ private const val USER_ID = 100
+ private val USER_HANDLE = UserHandle.of(USER_ID)
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/inputmethod/domain/interactor/InputMethodInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/inputmethod/domain/interactor/InputMethodInteractorTest.kt
new file mode 100644
index 0000000..d23ff2a
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/inputmethod/domain/interactor/InputMethodInteractorTest.kt
@@ -0,0 +1,157 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.inputmethod.domain.interactor
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.inputmethod.data.model.InputMethodModel
+import com.android.systemui.inputmethod.data.repository.fakeInputMethodRepository
+import com.android.systemui.inputmethod.data.repository.inputMethodRepository
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testScope
+import com.google.common.truth.Truth.assertThat
+import java.util.UUID
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class InputMethodInteractorTest : SysuiTestCase() {
+
+ private val kosmos = Kosmos()
+ private val testScope = kosmos.testScope
+ private val fakeInputMethodRepository = kosmos.fakeInputMethodRepository
+
+ private val underTest = InputMethodInteractor(repository = kosmos.inputMethodRepository)
+
+ @Test
+ fun hasMultipleEnabledImesOrSubtypes_noImes_returnsFalse() =
+ testScope.runTest {
+ fakeInputMethodRepository.setEnabledInputMethods(USER_ID)
+
+ assertThat(underTest.hasMultipleEnabledImesOrSubtypes(USER_ID)).isFalse()
+ }
+
+ @Test
+ fun hasMultipleEnabledImesOrSubtypes_noMatches_returnsFalse() =
+ testScope.runTest {
+ fakeInputMethodRepository.setEnabledInputMethods(
+ USER_ID,
+ createInputMethodWithSubtypes(auxiliarySubtypes = 1, nonAuxiliarySubtypes = 0),
+ createInputMethodWithSubtypes(auxiliarySubtypes = 3, nonAuxiliarySubtypes = 0),
+ )
+
+ assertThat(underTest.hasMultipleEnabledImesOrSubtypes(USER_ID)).isFalse()
+ }
+
+ @Test
+ fun hasMultipleEnabledImesOrSubtypes_oneMatch_returnsFalse() =
+ testScope.runTest {
+ fakeInputMethodRepository.setEnabledInputMethods(
+ USER_ID,
+ createInputMethodWithSubtypes(auxiliarySubtypes = 0, nonAuxiliarySubtypes = 0),
+ )
+
+ assertThat(underTest.hasMultipleEnabledImesOrSubtypes(USER_ID)).isFalse()
+ }
+
+ @Test
+ fun hasMultipleEnabledImesOrSubtypes_twoMatches_returnsTrue() =
+ testScope.runTest {
+ fakeInputMethodRepository.setEnabledInputMethods(
+ USER_ID,
+ createInputMethodWithSubtypes(auxiliarySubtypes = 0, nonAuxiliarySubtypes = 1),
+ createInputMethodWithSubtypes(auxiliarySubtypes = 0, nonAuxiliarySubtypes = 0),
+ )
+
+ assertThat(underTest.hasMultipleEnabledImesOrSubtypes(USER_ID)).isTrue()
+ }
+
+ @Test
+ fun hasMultipleEnabledImesOrSubtypes_oneWithNonAux_returnsFalse() =
+ testScope.runTest {
+ fakeInputMethodRepository.setEnabledInputMethods(
+ USER_ID,
+ createInputMethodWithSubtypes(auxiliarySubtypes = 0, nonAuxiliarySubtypes = 2),
+ )
+
+ assertThat(underTest.hasMultipleEnabledImesOrSubtypes(USER_ID)).isFalse()
+ }
+
+ @Test
+ fun hasMultipleEnabledImesOrSubtypes_twoWithAux_returnsFalse() =
+ testScope.runTest {
+ fakeInputMethodRepository.setEnabledInputMethods(
+ USER_ID,
+ createInputMethodWithSubtypes(auxiliarySubtypes = 3, nonAuxiliarySubtypes = 0),
+ createInputMethodWithSubtypes(auxiliarySubtypes = 5, nonAuxiliarySubtypes = 0),
+ )
+
+ assertThat(underTest.hasMultipleEnabledImesOrSubtypes(USER_ID)).isFalse()
+ }
+
+ @Test
+ fun hasMultipleEnabledImesOrSubtypes_selectedHasOneSubtype_returnsFalse() =
+ testScope.runTest {
+ fakeInputMethodRepository.selectedInputMethodSubtypes =
+ listOf(InputMethodModel.Subtype(1, isAuxiliary = false))
+
+ assertThat(underTest.hasMultipleEnabledImesOrSubtypes(USER_ID)).isFalse()
+ }
+
+ @Test
+ fun hasMultipleEnabledImesOrSubtypes_selectedHasTwoSubtypes_returnsTrue() =
+ testScope.runTest {
+ fakeInputMethodRepository.selectedInputMethodSubtypes =
+ listOf(
+ InputMethodModel.Subtype(subtypeId = 1, isAuxiliary = false),
+ InputMethodModel.Subtype(subtypeId = 2, isAuxiliary = false),
+ )
+
+ assertThat(underTest.hasMultipleEnabledImesOrSubtypes(USER_ID)).isTrue()
+ }
+
+ @Test
+ fun showImePicker_shownOnCorrectId() =
+ testScope.runTest {
+ val displayId = 7
+
+ underTest.showInputMethodPicker(displayId, showAuxiliarySubtypes = false)
+
+ assertThat(fakeInputMethodRepository.inputMethodPickerShownDisplayId)
+ .isEqualTo(displayId)
+ }
+
+ private fun createInputMethodWithSubtypes(
+ auxiliarySubtypes: Int,
+ nonAuxiliarySubtypes: Int,
+ ): InputMethodModel {
+ return InputMethodModel(
+ imeId = UUID.randomUUID().toString(),
+ subtypes =
+ List(auxiliarySubtypes + nonAuxiliarySubtypes) {
+ InputMethodModel.Subtype(subtypeId = it, isAuxiliary = it < auxiliarySubtypes)
+ }
+ )
+ }
+
+ companion object {
+ private const val USER_ID = 100
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/CameraQuickAffordanceConfigTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/CameraQuickAffordanceConfigTest.kt
index e20d3af..ee3e241 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/CameraQuickAffordanceConfigTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/CameraQuickAffordanceConfigTest.kt
@@ -43,6 +43,7 @@
@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@RunWith(AndroidJUnit4::class)
+@android.platform.test.annotations.EnabledOnRavenwood
class CameraQuickAffordanceConfigTest : SysuiTestCase() {
@Mock private lateinit var cameraGestureHelper: CameraGestureHelper
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/FlashlightQuickAffordanceConfigTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/FlashlightQuickAffordanceConfigTest.kt
index 4ae144c..77e0f4e 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/FlashlightQuickAffordanceConfigTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/FlashlightQuickAffordanceConfigTest.kt
@@ -42,6 +42,7 @@
@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@RunWith(AndroidJUnit4::class)
+@android.platform.test.annotations.EnabledOnRavenwood
class FlashlightQuickAffordanceConfigTest : LeakCheckedTest() {
@Mock private lateinit var context: Context
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfigTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfigTest.kt
index 521dea3..ca64cec 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfigTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfigTest.kt
@@ -40,6 +40,7 @@
@SmallTest
@RunWith(AndroidJUnit4::class)
+@android.platform.test.annotations.EnabledOnRavenwood
class QrCodeScannerKeyguardQuickAffordanceConfigTest : SysuiTestCase() {
@Mock private lateinit var controller: QRCodeScannerController
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/DeviceEntryFingerprintAuthRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/DeviceEntryFingerprintAuthRepositoryTest.kt
index 6b7d263..558e7e6 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/DeviceEntryFingerprintAuthRepositoryTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/DeviceEntryFingerprintAuthRepositoryTest.kt
@@ -50,6 +50,7 @@
@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@RunWith(AndroidJUnit4::class)
+@android.platform.test.annotations.EnabledOnRavenwood
class DeviceEntryFingerprintAuthRepositoryTest : SysuiTestCase() {
@Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor
@Mock private lateinit var authController: AuthController
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/DevicePostureRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/DevicePostureRepositoryTest.kt
index ae6c5b7..a0b8542 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/DevicePostureRepositoryTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/DevicePostureRepositoryTest.kt
@@ -40,6 +40,7 @@
@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@RunWith(AndroidJUnit4::class)
+@android.platform.test.annotations.EnabledOnRavenwood
class DevicePostureRepositoryTest : SysuiTestCase() {
private lateinit var underTest: DevicePostureRepository
private lateinit var testScope: TestScope
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 c01c79d..128b465 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
@@ -63,6 +63,7 @@
@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@RunWith(AndroidJUnit4::class)
+@android.platform.test.annotations.EnabledOnRavenwood
class KeyguardRepositoryImplTest : SysuiTestCase() {
@Mock private lateinit var statusBarStateController: StatusBarStateController
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/TrustRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/TrustRepositoryTest.kt
index ee47c58f4..5f0f24d 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/TrustRepositoryTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/TrustRepositoryTest.kt
@@ -46,6 +46,7 @@
@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@RunWith(AndroidJUnit4::class)
+@android.platform.test.annotations.EnabledOnRavenwood
class TrustRepositoryTest : SysuiTestCase() {
@Mock private lateinit var trustManager: TrustManager
@Captor private lateinit var listener: ArgumentCaptor<TrustManager.TrustListener>
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt
index 34f703b..db414b7 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt
@@ -27,6 +27,7 @@
import com.android.systemui.common.shared.model.ContentDescription
import com.android.systemui.common.shared.model.Icon
import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.coroutines.collectValues
import com.android.systemui.dock.DockManager
import com.android.systemui.dock.DockManagerFake
import com.android.systemui.flags.FakeFeatureFlags
@@ -49,16 +50,17 @@
import com.android.systemui.res.R
import com.android.systemui.settings.UserFileManager
import com.android.systemui.settings.UserTracker
+import com.android.systemui.shade.domain.interactor.ShadeInteractor
import com.android.systemui.shade.domain.interactor.shadeInteractor
import com.android.systemui.shared.keyguard.shared.model.KeyguardQuickAffordanceSlots
import com.android.systemui.statusbar.policy.KeyguardStateController
-import com.android.systemui.testKosmos
import com.android.systemui.util.FakeSharedPreferences
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
import com.android.systemui.util.settings.FakeSettings
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runCurrent
@@ -82,6 +84,7 @@
@Mock private lateinit var activityStarter: ActivityStarter
@Mock private lateinit var launchAnimator: DialogLaunchAnimator
@Mock private lateinit var devicePolicyManager: DevicePolicyManager
+ @Mock private lateinit var shadeInteractor: ShadeInteractor
@Mock private lateinit var logger: KeyguardQuickAffordancesMetricsLogger
private lateinit var underTest: KeyguardQuickAffordanceInteractor
@@ -95,8 +98,6 @@
private lateinit var dockManager: DockManagerFake
private lateinit var biometricSettingsRepository: FakeBiometricSettingsRepository
- private val kosmos = testKosmos()
-
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
@@ -183,7 +184,7 @@
underTest =
KeyguardQuickAffordanceInteractor(
keyguardInteractor = withDeps.keyguardInteractor,
- shadeInteractor = kosmos.shadeInteractor,
+ shadeInteractor = shadeInteractor,
lockPatternUtils = lockPatternUtils,
keyguardStateController = keyguardStateController,
userTracker = userTracker,
@@ -198,6 +199,8 @@
backgroundDispatcher = testDispatcher,
appContext = context,
)
+
+ whenever(shadeInteractor.anyExpansion).thenReturn(MutableStateFlow(0f))
}
@Test
@@ -344,6 +347,25 @@
}
@Test
+ fun quickAffordance_updateOncePerShadeExpansion() =
+ testScope.runTest {
+ val shadeExpansion = MutableStateFlow(0f)
+ whenever(shadeInteractor.anyExpansion).thenReturn(shadeExpansion)
+
+ val collectedValue by
+ collectValues(
+ underTest.quickAffordance(KeyguardQuickAffordancePosition.BOTTOM_START)
+ )
+
+ val initialSize = collectedValue.size
+ for (i in 0..10) {
+ shadeExpansion.value = i / 10f
+ }
+
+ assertThat(collectedValue.size).isEqualTo(initialSize + 1)
+ }
+
+ @Test
fun quickAffordanceAlwaysVisible_notVisible_restrictedByPolicyManager() =
testScope.runTest {
whenever(devicePolicyManager.getKeyguardDisabledFeatures(null, userTracker.userId))
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorTest.kt
index 6828041..9368097 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorTest.kt
@@ -45,6 +45,7 @@
@SmallTest
@RunWith(AndroidJUnit4::class)
@kotlinx.coroutines.ExperimentalCoroutinesApi
+@android.platform.test.annotations.EnabledOnRavenwood
class KeyguardTransitionInteractorTest : SysuiTestCase() {
val kosmos = testKosmos()
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/preview/KeyguardRemotePreviewManagerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/preview/KeyguardRemotePreviewManagerTest.kt
index e850456..4695ea4 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/preview/KeyguardRemotePreviewManagerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/preview/KeyguardRemotePreviewManagerTest.kt
@@ -13,6 +13,7 @@
@SmallTest
@RunWith(AndroidJUnit4::class)
+@android.platform.test.annotations.EnabledOnRavenwood
class KeyguardRemotePreviewManagerTest : SysuiTestCase() {
private val testDispatcher = StandardTestDispatcher()
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelTest.kt
index d4dd2ac..0b80ff8 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelTest.kt
@@ -36,6 +36,7 @@
@SmallTest
@RunWith(AndroidJUnit4::class)
+@android.platform.test.annotations.EnabledOnRavenwood
class LockscreenContentViewModelTest : SysuiTestCase() {
private val kosmos: Kosmos = testKosmos()
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModelTest.kt
index 30ac344..6fc5be1 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModelTest.kt
@@ -19,7 +19,7 @@
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
-import com.android.systemui.bouncer.domain.interactor.primaryBouncerInteractor
+import com.android.systemui.bouncer.domain.interactor.mockPrimaryBouncerInteractor
import com.android.systemui.coroutines.collectValues
import com.android.systemui.flags.Flags
import com.android.systemui.flags.fakeFeatureFlagsClassic
@@ -52,11 +52,9 @@
val testScope = kosmos.testScope
val keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository
- val primaryBouncerInteractor = kosmos.primaryBouncerInteractor
+ val primaryBouncerInteractor = kosmos.mockPrimaryBouncerInteractor
val sysuiStatusBarStateController = kosmos.sysuiStatusBarStateController
- val underTest by lazy {
- kosmos.primaryBouncerToGoneTransitionViewModel
- }
+ val underTest by lazy { kosmos.primaryBouncerToGoneTransitionViewModel }
@Before
fun setUp() {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/data/repository/SceneContainerRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/data/repository/SceneContainerRepositoryTest.kt
index b267720..2ad872c 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/data/repository/SceneContainerRepositoryTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/data/repository/SceneContainerRepositoryTest.kt
@@ -40,6 +40,7 @@
@SmallTest
@RunWith(AndroidJUnit4::class)
+@android.platform.test.annotations.EnabledOnRavenwood
class SceneContainerRepositoryTest : SysuiTestCase() {
private val kosmos = testKosmos().apply { fakeSceneContainerFlags.enabled = true }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/data/repository/WindowRootViewVisibilityRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/data/repository/WindowRootViewVisibilityRepositoryTest.kt
index 7ae501d..13b5b54 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/data/repository/WindowRootViewVisibilityRepositoryTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/data/repository/WindowRootViewVisibilityRepositoryTest.kt
@@ -30,6 +30,7 @@
@SmallTest
@RunWith(AndroidJUnit4::class)
+@android.platform.test.annotations.EnabledOnRavenwood
class WindowRootViewVisibilityRepositoryTest : SysuiTestCase() {
private val iStatusBarService = mock<IStatusBarService>()
private val executor = FakeExecutor(FakeSystemClock())
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt
index bf99a86..942fbc2 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt
@@ -174,105 +174,6 @@
}
@Test
- fun transitioning_idle_false() =
- testScope.runTest {
- val transitionState =
- MutableStateFlow<ObservableTransitionState>(
- ObservableTransitionState.Idle(SceneKey.Shade)
- )
- val transitioning by
- collectLastValue(underTest.transitioning(SceneKey.Shade, SceneKey.Lockscreen))
- underTest.setTransitionState(transitionState)
-
- assertThat(transitioning).isFalse()
- }
-
- @Test
- fun transitioning_wrongFromScene_false() =
- testScope.runTest {
- val transitionState =
- MutableStateFlow<ObservableTransitionState>(
- ObservableTransitionState.Transition(
- fromScene = SceneKey.Gone,
- toScene = SceneKey.Lockscreen,
- progress = flowOf(0.5f),
- isInitiatedByUserInput = false,
- isUserInputOngoing = flowOf(false),
- )
- )
- val transitioning by
- collectLastValue(underTest.transitioning(SceneKey.Shade, SceneKey.Lockscreen))
- underTest.setTransitionState(transitionState)
-
- assertThat(transitioning).isFalse()
- }
-
- @Test
- fun transitioning_wrongToScene_false() =
- testScope.runTest {
- val transitionState =
- MutableStateFlow<ObservableTransitionState>(
- ObservableTransitionState.Transition(
- fromScene = SceneKey.Shade,
- toScene = SceneKey.QuickSettings,
- progress = flowOf(0.5f),
- isInitiatedByUserInput = false,
- isUserInputOngoing = flowOf(false),
- )
- )
- underTest.setTransitionState(transitionState)
-
- assertThat(underTest.transitioning(SceneKey.Shade, SceneKey.Lockscreen).value).isFalse()
- }
-
- @Test
- fun transitioning_correctFromAndToScenes_true() =
- testScope.runTest {
- val transitionState =
- MutableStateFlow<ObservableTransitionState>(
- ObservableTransitionState.Transition(
- fromScene = SceneKey.Shade,
- toScene = SceneKey.Lockscreen,
- progress = flowOf(0.5f),
- isInitiatedByUserInput = false,
- isUserInputOngoing = flowOf(false),
- )
- )
- val transitioning by
- collectLastValue(underTest.transitioning(SceneKey.Shade, SceneKey.Lockscreen))
- underTest.setTransitionState(transitionState)
-
- assertThat(transitioning).isTrue()
- }
-
- @Test
- fun transitioning_updates() =
- testScope.runTest {
- val transitionState =
- MutableStateFlow<ObservableTransitionState>(
- ObservableTransitionState.Idle(SceneKey.Shade)
- )
- val transitioning by
- collectLastValue(underTest.transitioning(SceneKey.Shade, SceneKey.Lockscreen))
- underTest.setTransitionState(transitionState)
-
- assertThat(transitioning).isFalse()
-
- transitionState.value =
- ObservableTransitionState.Transition(
- fromScene = SceneKey.Shade,
- toScene = SceneKey.Lockscreen,
- progress = flowOf(0.5f),
- isInitiatedByUserInput = false,
- isUserInputOngoing = flowOf(false),
- )
- assertThat(transitioning).isTrue()
-
- transitionState.value = ObservableTransitionState.Idle(SceneKey.Lockscreen)
- assertThat(transitioning).isFalse()
- }
-
- @Test
fun isTransitionUserInputOngoing_idle_false() =
testScope.runTest {
val transitionState =
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelTest.kt
index e9a2a3b..c0aaab3 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelTest.kt
@@ -9,8 +9,6 @@
import com.android.systemui.flags.Flags
import com.android.systemui.kosmos.testScope
import com.android.systemui.scene.domain.interactor.sceneInteractor
-import com.android.systemui.scene.shared.model.ObservableTransitionState
-import com.android.systemui.scene.shared.model.SceneKey
import com.android.systemui.statusbar.pipeline.airplane.data.repository.FakeAirplaneModeRepository
import com.android.systemui.statusbar.pipeline.airplane.domain.interactor.AirplaneModeInteractor
import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel
@@ -22,8 +20,6 @@
import com.android.systemui.testKosmos
import com.android.systemui.util.mockito.mock
import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
@@ -74,74 +70,6 @@
}
@Test
- fun isTransitioning_idle_false() =
- testScope.runTest {
- val isTransitioning by collectLastValue(underTest.isTransitioning)
- sceneInteractor.setTransitionState(
- MutableStateFlow(ObservableTransitionState.Idle(SceneKey.Shade))
- )
-
- assertThat(isTransitioning).isFalse()
- }
-
- @Test
- fun isTransitioning_shadeToQs_true() =
- testScope.runTest {
- val isTransitioning by collectLastValue(underTest.isTransitioning)
- sceneInteractor.setTransitionState(
- MutableStateFlow(
- ObservableTransitionState.Transition(
- fromScene = SceneKey.Shade,
- toScene = SceneKey.QuickSettings,
- progress = MutableStateFlow(0.5f),
- isInitiatedByUserInput = false,
- isUserInputOngoing = flowOf(false),
- )
- )
- )
-
- assertThat(isTransitioning).isTrue()
- }
-
- @Test
- fun isTransitioning_qsToShade_true() =
- testScope.runTest {
- val isTransitioning by collectLastValue(underTest.isTransitioning)
- sceneInteractor.setTransitionState(
- MutableStateFlow(
- ObservableTransitionState.Transition(
- fromScene = SceneKey.QuickSettings,
- toScene = SceneKey.Shade,
- progress = MutableStateFlow(0.5f),
- isInitiatedByUserInput = false,
- isUserInputOngoing = flowOf(false),
- )
- )
- )
-
- assertThat(isTransitioning).isTrue()
- }
-
- @Test
- fun isTransitioning_otherTransition_false() =
- testScope.runTest {
- val isTransitioning by collectLastValue(underTest.isTransitioning)
- sceneInteractor.setTransitionState(
- MutableStateFlow(
- ObservableTransitionState.Transition(
- fromScene = SceneKey.Gone,
- toScene = SceneKey.Shade,
- progress = MutableStateFlow(0.5f),
- isInitiatedByUserInput = false,
- isUserInputOngoing = flowOf(false),
- )
- )
- )
-
- assertThat(isTransitioning).isFalse()
- }
-
- @Test
fun mobileSubIds_update() =
testScope.runTest {
val mobileSubIds by collectLastValue(underTest.mobileSubIds)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/smartspace/BcSmartspaceConfigProviderTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/smartspace/BcSmartspaceConfigProviderTest.kt
index cb83e7c..bbbee90 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/smartspace/BcSmartspaceConfigProviderTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/smartspace/BcSmartspaceConfigProviderTest.kt
@@ -30,6 +30,7 @@
@SmallTest
@RunWith(AndroidJUnit4::class)
+@android.platform.test.annotations.EnabledOnRavenwood
class BcSmartspaceConfigProviderTest : SysuiTestCase() {
@Mock private lateinit var featureFlags: FeatureFlags
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/smartspace/LockscreenPreconditionTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/smartspace/LockscreenPreconditionTest.kt
index 0b5aea7..089eb43e 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/smartspace/LockscreenPreconditionTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/smartspace/LockscreenPreconditionTest.kt
@@ -37,6 +37,7 @@
@SmallTest
@RunWith(AndroidJUnit4::class)
@TestableLooper.RunWithLooper
+@android.platform.test.annotations.EnabledOnRavenwood
class LockscreenPreconditionTest : SysuiTestCase() {
@Mock
private lateinit var deviceProvisionedController: DeviceProvisionedController
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/smartspace/LockscreenTargetFilterTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/smartspace/LockscreenTargetFilterTest.kt
index bf33010..6616786 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/smartspace/LockscreenTargetFilterTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/smartspace/LockscreenTargetFilterTest.kt
@@ -52,6 +52,7 @@
@SmallTest
@TestableLooper.RunWithLooper
@RunWith(AndroidJUnit4::class)
+@android.platform.test.annotations.EnabledOnRavenwood
class LockscreenTargetFilterTest : SysuiTestCase() {
@Mock private lateinit var secureSettings: SecureSettings
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/data/repository/RemoteInputRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/data/repository/RemoteInputRepositoryImplTest.kt
index 8a0400d..f7a8858 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/data/repository/RemoteInputRepositoryImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/data/repository/RemoteInputRepositoryImplTest.kt
@@ -38,6 +38,7 @@
@SmallTest
@RunWith(AndroidJUnit4::class)
+@android.platform.test.annotations.EnabledOnRavenwood
class RemoteInputRepositoryImplTest : SysuiTestCase() {
@Mock private lateinit var remoteInputManager: NotificationRemoteInputManager
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/domain/interactor/RemoteInputInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/domain/interactor/RemoteInputInteractorTest.kt
index 12469dd..60da53c 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/domain/interactor/RemoteInputInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/domain/interactor/RemoteInputInteractorTest.kt
@@ -34,6 +34,7 @@
@SmallTest
@RunWith(AndroidJUnit4::class)
+@android.platform.test.annotations.EnabledOnRavenwood
class RemoteInputInteractorTest : SysuiTestCase() {
private val kosmos = testKosmos()
private val testScope = kosmos.testScope
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationStackAppearanceIntegrationTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationStackAppearanceIntegrationTest.kt
index 6a2e317..4d7d5d3 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationStackAppearanceIntegrationTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationStackAppearanceIntegrationTest.kt
@@ -87,21 +87,21 @@
}
@Test
- fun updateShadeExpansion() =
+ fun shadeExpansion_goneToShade() =
testScope.runTest {
+ val transitionState =
+ MutableStateFlow<ObservableTransitionState>(
+ ObservableTransitionState.Idle(scene = SceneKey.Gone)
+ )
+ sceneInteractor.setTransitionState(transitionState)
val expandFraction by collectLastValue(appearanceViewModel.expandFraction)
assertThat(expandFraction).isEqualTo(0f)
- val transitionState =
- MutableStateFlow<ObservableTransitionState>(
- ObservableTransitionState.Idle(scene = SceneKey.Lockscreen)
- )
- sceneInteractor.setTransitionState(transitionState)
sceneInteractor.changeScene(SceneModel(SceneKey.Shade), "reason")
val transitionProgress = MutableStateFlow(0f)
transitionState.value =
ObservableTransitionState.Transition(
- fromScene = SceneKey.Lockscreen,
+ fromScene = SceneKey.Gone,
toScene = SceneKey.Shade,
progress = transitionProgress,
isInitiatedByUserInput = false,
@@ -118,4 +118,49 @@
sceneInteractor.onSceneChanged(SceneModel(SceneKey.Shade), "reason")
assertThat(expandFraction).isWithin(0.01f).of(1f)
}
+
+ @Test
+ fun shadeExpansion_idleOnLockscreen() =
+ testScope.runTest {
+ val transitionState =
+ MutableStateFlow<ObservableTransitionState>(
+ ObservableTransitionState.Idle(scene = SceneKey.Lockscreen)
+ )
+ sceneInteractor.setTransitionState(transitionState)
+ val expandFraction by collectLastValue(appearanceViewModel.expandFraction)
+ assertThat(expandFraction).isEqualTo(1f)
+ }
+
+ @Test
+ fun shadeExpansion_shadeToQs() =
+ testScope.runTest {
+ val transitionState =
+ MutableStateFlow<ObservableTransitionState>(
+ ObservableTransitionState.Idle(scene = SceneKey.Shade)
+ )
+ sceneInteractor.setTransitionState(transitionState)
+ val expandFraction by collectLastValue(appearanceViewModel.expandFraction)
+ assertThat(expandFraction).isEqualTo(1f)
+
+ sceneInteractor.changeScene(SceneModel(SceneKey.QuickSettings), "reason")
+ val transitionProgress = MutableStateFlow(0f)
+ transitionState.value =
+ ObservableTransitionState.Transition(
+ fromScene = SceneKey.Shade,
+ toScene = SceneKey.QuickSettings,
+ progress = transitionProgress,
+ isInitiatedByUserInput = false,
+ isUserInputOngoing = flowOf(false),
+ )
+ val steps = 10
+ repeat(steps) { repetition ->
+ val progress = (1f / steps) * (repetition + 1)
+ transitionProgress.value = progress
+ runCurrent()
+ assertThat(expandFraction).isEqualTo(1f)
+ }
+
+ sceneInteractor.onSceneChanged(SceneModel(SceneKey.QuickSettings), "reason")
+ assertThat(expandFraction).isEqualTo(1f)
+ }
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractorTest.kt
index c7411cd..ffe6e6d 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractorTest.kt
@@ -30,6 +30,7 @@
@SmallTest
@RunWith(AndroidJUnit4::class)
+@android.platform.test.annotations.EnabledOnRavenwood
class NotificationStackAppearanceInteractorTest : SysuiTestCase() {
private val kosmos = Kosmos()
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/DisabledWifiRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/DisabledWifiRepositoryTest.kt
index ce00250..18825a2 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/DisabledWifiRepositoryTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/DisabledWifiRepositoryTest.kt
@@ -28,6 +28,7 @@
@SmallTest
@RunWith(AndroidJUnit4::class)
+@android.platform.test.annotations.EnabledOnRavenwood
class DisabledWifiRepositoryTest : SysuiTestCase() {
private lateinit var underTest: DisabledWifiRepository
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractorImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractorImplTest.kt
index 7fbbfc7..84c728c 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractorImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractorImplTest.kt
@@ -43,6 +43,7 @@
@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
@SmallTest
@RunWith(AndroidJUnit4::class)
+@android.platform.test.annotations.EnabledOnRavenwood
class WifiInteractorImplTest : SysuiTestCase() {
private lateinit var underTest: WifiInteractor
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/data/repository/UserSetupRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/data/repository/UserSetupRepositoryTest.kt
index ebc81be..4c8bbe0 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/data/repository/UserSetupRepositoryTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/data/repository/UserSetupRepositoryTest.kt
@@ -45,6 +45,7 @@
@SmallTest
@RunWith(AndroidJUnit4::class)
+@android.platform.test.annotations.EnabledOnRavenwood
class UserSetupRepositoryTest : SysuiTestCase() {
private val kosmos = testKosmos()
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/domain/interactor/UserSetupInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/domain/interactor/UserSetupInteractorTest.kt
index 26c0f80..7ec0a61 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/domain/interactor/UserSetupInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/domain/interactor/UserSetupInteractorTest.kt
@@ -30,6 +30,7 @@
@SmallTest
@RunWith(AndroidJUnit4::class)
+@android.platform.test.annotations.EnabledOnRavenwood
class UserSetupInteractorTest : SysuiTestCase() {
private val kosmos = testKosmos()
private val testScope = kosmos.testScope
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
index b4a0a37..96d1c0d 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/util/kotlin/BooleanFlowOperatorsTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/util/kotlin/BooleanFlowOperatorsTest.kt
@@ -34,6 +34,7 @@
@SmallTest
@RunWith(AndroidJUnit4::class)
+@android.platform.test.annotations.EnabledOnRavenwood
class BooleanFlowOperatorsTest : SysuiTestCase() {
val kosmos = testKosmos()
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QSTile.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QSTile.java
index 5d85fba..c9e2989 100644
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QSTile.java
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QSTile.java
@@ -264,15 +264,18 @@
}
}
- @ProvidesInterface(version = BooleanState.VERSION)
- public static class BooleanState extends State {
+ /**
+ * Distinguished from [BooleanState] for use-case purposes such as allowing null secondary label
+ */
+ @ProvidesInterface(version = AdapterState.VERSION)
+ class AdapterState extends State {
public static final int VERSION = 1;
public boolean value;
public boolean forceExpandIcon;
@Override
public boolean copyTo(State other) {
- final BooleanState o = (BooleanState) other;
+ final AdapterState o = (AdapterState) other;
final boolean changed = super.copyTo(other)
|| o.value != value
|| o.forceExpandIcon != forceExpandIcon;
@@ -291,6 +294,18 @@
@Override
public State copy() {
+ AdapterState state = new AdapterState();
+ copyTo(state);
+ return state;
+ }
+ }
+
+ @ProvidesInterface(version = BooleanState.VERSION)
+ class BooleanState extends AdapterState {
+ public static final int VERSION = 1;
+
+ @Override
+ public State copy() {
BooleanState state = new BooleanState();
copyTo(state);
return state;
diff --git a/packages/SystemUI/res/drawable-nodpi/homecontrols_sq.png b/packages/SystemUI/res/drawable-nodpi/homecontrols_sq.png
new file mode 100644
index 0000000..00b461b
--- /dev/null
+++ b/packages/SystemUI/res/drawable-nodpi/homecontrols_sq.png
Binary files differ
diff --git a/packages/SystemUI/res/drawable/accessibility_window_magnification_drag_handle_background_change_inset.xml b/packages/SystemUI/res/drawable/accessibility_window_magnification_drag_handle_background_change_inset.xml
new file mode 100644
index 0000000..02486bf
--- /dev/null
+++ b/packages/SystemUI/res/drawable/accessibility_window_magnification_drag_handle_background_change_inset.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright (C) 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<inset xmlns:android="http://schemas.android.com/apk/res/android"
+ android:drawable="@drawable/accessibility_window_magnification_drag_handle_background_change"
+ android:insetBottom="@dimen/magnification_inner_border_margin"
+ android:insetLeft="@dimen/magnification_inner_border_margin"
+ android:insetRight="@dimen/magnification_inner_border_margin"
+ android:insetTop="@dimen/magnification_inner_border_margin" />
diff --git a/packages/SystemUI/res/drawable/accessibility_window_magnification_drag_handle_background_inset.xml b/packages/SystemUI/res/drawable/accessibility_window_magnification_drag_handle_background_inset.xml
new file mode 100644
index 0000000..bfb7c47
--- /dev/null
+++ b/packages/SystemUI/res/drawable/accessibility_window_magnification_drag_handle_background_inset.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright (C) 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<inset xmlns:android="http://schemas.android.com/apk/res/android"
+ android:drawable="@drawable/accessibility_window_magnification_drag_handle_background"
+ android:insetBottom="@dimen/magnification_inner_border_margin"
+ android:insetLeft="@dimen/magnification_inner_border_margin"
+ android:insetRight="@dimen/magnification_inner_border_margin"
+ android:insetTop="@dimen/magnification_inner_border_margin" />
diff --git a/packages/SystemUI/res/layout/window_magnifier_view.xml b/packages/SystemUI/res/layout/window_magnifier_view.xml
index a8a048d..6286f34 100644
--- a/packages/SystemUI/res/layout/window_magnifier_view.xml
+++ b/packages/SystemUI/res/layout/window_magnifier_view.xml
@@ -117,12 +117,11 @@
android:id="@+id/drag_handle"
android:layout_width="@dimen/magnification_drag_view_size"
android:layout_height="@dimen/magnification_drag_view_size"
- android:layout_margin="@dimen/magnification_inner_border_margin"
android:layout_gravity="right|bottom"
android:padding="@dimen/magnifier_drag_handle_padding"
android:scaleType="center"
android:src="@drawable/ic_move_magnification"
- android:background="@drawable/accessibility_window_magnification_drag_handle_background"/>
+ android:background="@drawable/accessibility_window_magnification_drag_handle_background_inset"/>
<ImageView
android:id="@+id/close_button"
diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml
index 17719d1..7a83070 100644
--- a/packages/SystemUI/res/values/config.xml
+++ b/packages/SystemUI/res/values/config.xml
@@ -991,7 +991,7 @@
<!-- Component name for Home Panel Dream -->
<string name="config_homePanelDreamComponent" translatable="false">
- @null
+ com.android.systemui/com.android.systemui.dreams.homecontrols.HomeControlsDreamService
</string>
<!--
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index 4209c1f..fa89fcd 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -1249,7 +1249,7 @@
<dimen name="magnification_drag_corner_margin">8dp</dimen>
<dimen name="magnification_frame_move_short">5dp</dimen>
<dimen name="magnification_frame_move_long">25dp</dimen>
- <dimen name="magnification_drag_view_size">36dp</dimen>
+ <dimen name="magnification_drag_view_size">70dp</dimen>
<dimen name="magnification_controls_size">90dp</dimen>
<dimen name="magnification_switch_button_size">56dp</dimen>
<dimen name="magnification_switch_button_padding">6dp</dimen>
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 8fa4a9e..8971859 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -1748,6 +1748,9 @@
<!-- Notification: Snooze panel: Snooze undo button label. [CHAR LIMIT=50]-->
<string name="snooze_undo">Undo</string>
+ <!-- Notification: Snooze panel: Snooze undo content description for a11y. [CHAR LIMIT=NONE]-->
+ <string name="snooze_undo_content_description">Undo notification snooze</string>
+
<!-- Notification: Snooze panel: message indicating how long the notification was snoozed for. [CHAR LIMIT=100]-->
<string name="snoozed_for_time">Snoozed for <xliff:g id="time_amount" example="15 minutes">%1$s</xliff:g></string>
@@ -3311,4 +3314,8 @@
<string name="keyboard_backlight_dialog_title">Keyboard backlight</string>
<!-- Content description for keyboard backlight brightness value [CHAR LIMIT=NONE] -->
<string name="keyboard_backlight_value">Level %1$d of %2$d</string>
+ <!-- Label for home control panel [CHAR LIMIT=30] -->
+ <string name="home_controls_dream_label">Home Controls</string>
+ <!-- Description for home control panel [CHAR LIMIT=50] -->
+ <string name="home_controls_dream_description">Quickly access your home controls as a screensaver</string>
</resources>
diff --git a/packages/SystemUI/res/xml/home_controls_dream_metadata.xml b/packages/SystemUI/res/xml/home_controls_dream_metadata.xml
new file mode 100644
index 0000000..eb7c79e
--- /dev/null
+++ b/packages/SystemUI/res/xml/home_controls_dream_metadata.xml
@@ -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.
+ -->
+<dream xmlns:android="http://schemas.android.com/apk/res/android"
+ android:showClockAndComplications="false"
+ android:previewImage="@drawable/homecontrols_sq"
+ />
\ No newline at end of file
diff --git a/packages/SystemUI/shared/Android.bp b/packages/SystemUI/shared/Android.bp
index 326c7ef..4e04af6 100644
--- a/packages/SystemUI/shared/Android.bp
+++ b/packages/SystemUI/shared/Android.bp
@@ -53,6 +53,7 @@
"SystemUIPluginLib",
"SystemUIUnfoldLib",
"SystemUISharedLib-Keyguard",
+ "WindowManager-Shell-shared",
"tracinglib",
"androidx.dynamicanimation_dynamicanimation",
"androidx.concurrent_concurrent-futures",
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationRunnerCompat.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationRunnerCompat.java
index 88b9c02..a78080f 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationRunnerCompat.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationRunnerCompat.java
@@ -38,7 +38,7 @@
import android.window.IRemoteTransitionFinishedCallback;
import android.window.TransitionInfo;
-import com.android.wm.shell.util.CounterRotator;
+import com.android.wm.shell.shared.CounterRotator;
public abstract class RemoteAnimationRunnerCompat extends IRemoteAnimationRunner.Stub {
private static final String TAG = "RemoteAnimRunnerCompat";
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationTargetCompat.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationTargetCompat.java
index f094102..e4d9243 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationTargetCompat.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationTargetCompat.java
@@ -22,7 +22,7 @@
import android.window.TransitionInfo;
import android.window.TransitionInfo.Change;
-import com.android.wm.shell.util.TransitionUtil;
+import com.android.wm.shell.shared.TransitionUtil;
import java.util.ArrayList;
import java.util.function.Predicate;
@@ -40,7 +40,15 @@
*/
public static RemoteAnimationTarget[] wrapApps(TransitionInfo info,
SurfaceControl.Transaction t, ArrayMap<SurfaceControl, SurfaceControl> leashMap) {
- return wrap(info, t, leashMap, new TransitionUtil.LeafTaskFilter());
+ // LeafTaskFilter is order-dependent, so the same object needs to be used for all Change
+ // objects. That's why it's constructed here and captured by the lambda instead of building
+ // a new one ad hoc every time.
+ TransitionUtil.LeafTaskFilter taskFilter = new TransitionUtil.LeafTaskFilter();
+ return wrap(info, t, leashMap, (change) -> {
+ // Intra-task activity -> activity transitions should be categorized as apps.
+ if (change.getActivityComponent() != null) return true;
+ return taskFilter.test(change);
+ });
}
/**
@@ -53,8 +61,12 @@
*/
public static RemoteAnimationTarget[] wrapNonApps(TransitionInfo info, boolean wallpapers,
SurfaceControl.Transaction t, ArrayMap<SurfaceControl, SurfaceControl> leashMap) {
- return wrap(info, t, leashMap, (change) -> (wallpapers
- ? TransitionUtil.isWallpaper(change) : TransitionUtil.isNonApp(change)));
+ return wrap(info, t, leashMap, (change) -> {
+ // Intra-task activity -> activity transitions should be categorized as apps.
+ if (change.getActivityComponent() != null) return false;
+ return wallpapers
+ ? TransitionUtil.isWallpaper(change) : TransitionUtil.isNonApp(change);
+ });
}
private static RemoteAnimationTarget[] wrap(TransitionInfo info,
diff --git a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt
index 82410fd..91e6b62 100644
--- a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt
+++ b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt
@@ -39,7 +39,7 @@
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.dagger.qualifiers.DisplaySpecific
import com.android.systemui.dagger.qualifiers.Main
-import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.FeatureFlagsClassic
import com.android.systemui.flags.Flags.REGION_SAMPLING
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
@@ -47,7 +47,6 @@
import com.android.systemui.keyguard.shared.model.TransitionState
import com.android.systemui.lifecycle.repeatWhenAttached
import com.android.systemui.log.core.Logger
-import com.android.systemui.log.core.LogLevel.DEBUG
import com.android.systemui.plugins.clocks.ClockController
import com.android.systemui.plugins.clocks.ClockFaceController
import com.android.systemui.plugins.clocks.ClockMessageBuffers
@@ -92,7 +91,7 @@
@Main private val mainExecutor: DelayableExecutor,
@Background private val bgExecutor: Executor,
private val clockBuffers: ClockMessageBuffers,
- private val featureFlags: FeatureFlags,
+ private val featureFlags: FeatureFlagsClassic,
private val zenModeController: ZenModeController,
) {
var loggers = listOf(
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java
index ad30317..28013c6 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java
@@ -196,6 +196,9 @@
mSmallClockFrame = findViewById(R.id.lockscreen_clock_view);
mLargeClockFrame = findViewById(R.id.lockscreen_clock_view_large);
mStatusArea = findViewById(R.id.keyguard_status_area);
+ } else {
+ removeView(findViewById(R.id.lockscreen_clock_view));
+ removeView(findViewById(R.id.lockscreen_clock_view_large));
}
onConfigChanged();
}
@@ -263,6 +266,9 @@
}
void updateClockTargetRegions() {
+ if (migrateClocksToBlueprint()) {
+ return;
+ }
if (mClock != null) {
if (mSmallClockFrame.isLaidOut()) {
Rect targetRegion = getSmallClockRegion(mSmallClockFrame);
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java
index de7c12d..2db3795 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java
@@ -246,8 +246,11 @@
protected void onInit() {
mKeyguardSliceViewController.init();
- mSmallClockFrame = mView.findViewById(R.id.lockscreen_clock_view);
- mLargeClockFrame = mView.findViewById(R.id.lockscreen_clock_view_large);
+ if (!migrateClocksToBlueprint()) {
+ mSmallClockFrame = mView.findViewById(R.id.lockscreen_clock_view);
+ mLargeClockFrame = mView.findViewById(R.id.lockscreen_clock_view_large);
+ }
+
if (!mOnlyClock) {
mDumpManager.unregisterDumpable(getClass().getSimpleName()); // unregister previous
@@ -526,13 +529,15 @@
*/
void updatePosition(int x, float scale, AnimationProperties props, boolean animate) {
x = getCurrentLayoutDirection() == View.LAYOUT_DIRECTION_RTL ? -x : x;
+ if (!migrateClocksToBlueprint()) {
+ PropertyAnimator.setProperty(mSmallClockFrame, AnimatableProperty.TRANSLATION_X,
+ x, props, animate);
+ PropertyAnimator.setProperty(mLargeClockFrame, AnimatableProperty.SCALE_X,
+ scale, props, animate);
+ PropertyAnimator.setProperty(mLargeClockFrame, AnimatableProperty.SCALE_Y,
+ scale, props, animate);
- PropertyAnimator.setProperty(mSmallClockFrame, AnimatableProperty.TRANSLATION_X,
- x, props, animate);
- PropertyAnimator.setProperty(mLargeClockFrame, AnimatableProperty.SCALE_X,
- scale, props, animate);
- PropertyAnimator.setProperty(mLargeClockFrame, AnimatableProperty.SCALE_Y,
- scale, props, animate);
+ }
if (mStatusArea != null) {
PropertyAnimator.setProperty(mStatusArea, KeyguardStatusAreaView.TRANSLATE_X_AOD,
@@ -550,6 +555,10 @@
return 0;
}
+ if (migrateClocksToBlueprint()) {
+ return 0;
+ }
+
if (mLargeClockFrame.getVisibility() == View.VISIBLE) {
// This gets the expected clock bottom if mLargeClockFrame had a top margin, but it's
// top margin only contributed to height and didn't move the top of the view (as this
@@ -581,6 +590,9 @@
}
boolean isClockTopAligned() {
+ if (migrateClocksToBlueprint()) {
+ return mKeyguardClockInteractor.getClockSize().getValue() == LARGE;
+ }
return mLargeClockFrame.getVisibility() != View.VISIBLE;
}
diff --git a/packages/SystemUI/src/com/android/keyguard/logging/KeyguardLogger.kt b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardLogger.kt
index bddf3b0..d2ad096 100644
--- a/packages/SystemUI/src/com/android/keyguard/logging/KeyguardLogger.kt
+++ b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardLogger.kt
@@ -97,6 +97,26 @@
)
}
+ fun logUpdateLockScreenUserLockedMsg(
+ userId: Int,
+ userUnlocked: Boolean,
+ encryptedOrLockdown: Boolean,
+ ) {
+ buffer.log(
+ KeyguardIndicationController.TAG,
+ LogLevel.DEBUG,
+ {
+ int1 = userId
+ bool1 = userUnlocked
+ bool2 = encryptedOrLockdown
+ },
+ {
+ "updateLockScreenUserLockedMsg userId=$int1 " +
+ "userUnlocked:$bool1 encryptedOrLockdown:$bool2"
+ }
+ )
+ }
+
fun logUpdateBatteryIndication(
powerIndication: String,
pluggedIn: Boolean,
diff --git a/packages/SystemUI/src/com/android/systemui/GuestResumeSessionReceiver.java b/packages/SystemUI/src/com/android/systemui/GuestResumeSessionReceiver.java
index 4372826..0f5f869 100644
--- a/packages/SystemUI/src/com/android/systemui/GuestResumeSessionReceiver.java
+++ b/packages/SystemUI/src/com/android/systemui/GuestResumeSessionReceiver.java
@@ -20,12 +20,11 @@
import android.content.Context;
import android.content.DialogInterface;
import android.content.pm.UserInfo;
-import android.os.HandlerExecutor;
-import android.os.HandlerThread;
import android.os.UserHandle;
import androidx.annotation.NonNull;
+import com.android.systemui.res.R;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.logging.UiEventLogger;
import com.android.systemui.GuestResetOrExitSessionReceiver.ResetSessionDialogFactory;
@@ -33,7 +32,6 @@
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.qs.QSUserSwitcherEvent;
-import com.android.systemui.res.R;
import com.android.systemui.settings.UserTracker;
import com.android.systemui.statusbar.phone.SystemUIDialog;
import com.android.systemui.statusbar.policy.UserSwitcherController;
@@ -63,7 +61,6 @@
private final SecureSettings mSecureSettings;
private final ResetSessionDialogFactory mResetSessionDialogFactory;
private final GuestSessionNotification mGuestSessionNotification;
- private final HandlerThread mHandlerThread;
@VisibleForTesting
public final UserTracker.Callback mUserChangedCallback =
@@ -114,16 +111,13 @@
mSecureSettings = secureSettings;
mGuestSessionNotification = guestSessionNotification;
mResetSessionDialogFactory = resetSessionDialogFactory;
- mHandlerThread = new HandlerThread("GuestResumeSessionReceiver");
- mHandlerThread.start();
}
/**
* Register this receiver with the {@link BroadcastDispatcher}
*/
public void register() {
- mUserTracker.addCallback(mUserChangedCallback,
- new HandlerExecutor(mHandlerThread.getThreadHandler()));
+ mUserTracker.addCallback(mUserChangedCallback, mMainExecutor);
}
private void cancelDialog() {
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/Magnification.java b/packages/SystemUI/src/com/android/systemui/accessibility/Magnification.java
index 3ca95e1..5171a1f 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/Magnification.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/Magnification.java
@@ -19,6 +19,7 @@
import static android.provider.Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN;
import static android.provider.Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW;
import static android.view.WindowManager.LayoutParams.TYPE_ACCESSIBILITY_MAGNIFICATION_OVERLAY;
+import static android.view.WindowManager.LayoutParams.TYPE_ACCESSIBILITY_OVERLAY;
import static com.android.systemui.accessibility.AccessibilityLogger.MagnificationSettingsEvent;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_MAGNIFICATION_OVERLAP;
@@ -28,10 +29,12 @@
import android.content.Context;
import android.graphics.Rect;
import android.hardware.display.DisplayManager;
+import android.os.Binder;
import android.os.Handler;
import android.util.SparseArray;
import android.view.Display;
import android.view.SurfaceControl;
+import android.view.SurfaceControlViewHost;
import android.view.WindowManagerGlobal;
import android.view.accessibility.AccessibilityManager;
import android.view.accessibility.IMagnificationConnection;
@@ -40,6 +43,7 @@
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.graphics.SfVsyncFrameCallbackProvider;
import com.android.systemui.CoreStartable;
+import com.android.systemui.Flags;
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.model.SysUiState;
@@ -49,6 +53,7 @@
import com.android.systemui.util.settings.SecureSettings;
import java.io.PrintWriter;
+import java.util.function.Supplier;
import javax.inject.Inject;
@@ -101,19 +106,28 @@
@Override
protected WindowMagnificationController createInstance(Display display) {
final Context windowContext = mContext.createWindowContext(display,
- TYPE_ACCESSIBILITY_MAGNIFICATION_OVERLAY, /* options */ null);
+ Flags.createWindowlessWindowMagnifier()
+ ? TYPE_ACCESSIBILITY_OVERLAY
+ : TYPE_ACCESSIBILITY_MAGNIFICATION_OVERLAY,
+ /* options */ null);
windowContext.setTheme(com.android.systemui.res.R.style.Theme_SystemUI);
+
+ Supplier<SurfaceControlViewHost> scvhSupplier = () ->
+ Flags.createWindowlessWindowMagnifier() ? new SurfaceControlViewHost(mContext,
+ mContext.getDisplay(), new Binder(), TAG) : null;
+
return new WindowMagnificationController(
windowContext,
mHandler,
new WindowMagnificationAnimationController(windowContext),
- new SfVsyncFrameCallbackProvider(),
- null,
+ /* mirrorWindowControl= */ null,
new SurfaceControl.Transaction(),
mWindowMagnifierCallback,
mSysUiState,
- WindowManagerGlobal::getWindowSession,
- mSecureSettings);
+ mSecureSettings,
+ scvhSupplier,
+ new SfVsyncFrameCallbackProvider(),
+ WindowManagerGlobal::getWindowSession);
}
}
@@ -140,7 +154,7 @@
@Override
protected MagnificationSettingsController createInstance(Display display) {
final Context windowContext = mContext.createWindowContext(display,
- TYPE_ACCESSIBILITY_MAGNIFICATION_OVERLAY, /* options */ null);
+ TYPE_ACCESSIBILITY_OVERLAY, /* options */ null);
windowContext.setTheme(com.android.systemui.res.R.style.Theme_SystemUI);
return new MagnificationSettingsController(
windowContext,
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationController.java b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationController.java
index dde9f48..d65cd5c 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationController.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationController.java
@@ -63,23 +63,27 @@
import android.view.MotionEvent;
import android.view.Surface;
import android.view.SurfaceControl;
+import android.view.SurfaceControlViewHost;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.View;
import android.view.WindowManager;
import android.view.WindowManagerGlobal;
import android.view.WindowMetrics;
+import android.view.accessibility.AccessibilityManager;
import android.view.accessibility.AccessibilityNodeInfo;
import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
import android.view.accessibility.IRemoteMagnificationAnimationCallback;
import android.widget.FrameLayout;
import android.widget.ImageView;
+import androidx.annotation.UiThread;
import androidx.core.math.MathUtils;
import com.android.internal.accessibility.common.MagnificationConstants;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.graphics.SfVsyncFrameCallbackProvider;
+import com.android.systemui.Flags;
import com.android.systemui.model.SysUiState;
import com.android.systemui.res.R;
import com.android.systemui.util.settings.SecureSettings;
@@ -158,6 +162,15 @@
private int mMagnificationFrameOffsetX = 0;
private int mMagnificationFrameOffsetY = 0;
+ @Nullable private Supplier<SurfaceControlViewHost> mScvhSupplier;
+
+ /**
+ * SurfaceControlViewHost is used to control the position of the window containing
+ * {@link #mMirrorView}. Using SurfaceControlViewHost instead of a regular window enables
+ * changing the window's position and setting {@link #mMirrorSurface}'s geometry atomically.
+ */
+ private SurfaceControlViewHost mSurfaceControlViewHost;
+
// The root of the mirrored content
private SurfaceControl mMirrorSurface;
@@ -236,21 +249,21 @@
@UiContext Context context,
@NonNull Handler handler,
@NonNull WindowMagnificationAnimationController animationController,
- SfVsyncFrameCallbackProvider sfVsyncFrameProvider,
MirrorWindowControl mirrorWindowControl,
SurfaceControl.Transaction transaction,
@NonNull WindowMagnifierCallback callback,
SysUiState sysUiState,
- @NonNull Supplier<IWindowSession> globalWindowSessionSupplier,
- SecureSettings secureSettings) {
+ SecureSettings secureSettings,
+ Supplier<SurfaceControlViewHost> scvhSupplier,
+ SfVsyncFrameCallbackProvider sfVsyncFrameProvider,
+ Supplier<IWindowSession> globalWindowSessionSupplier) {
mContext = context;
mHandler = handler;
mAnimationController = animationController;
- mGlobalWindowSessionSupplier = globalWindowSessionSupplier;
mAnimationController.setWindowMagnificationController(this);
- mSfVsyncFrameProvider = sfVsyncFrameProvider;
mWindowMagnifierCallback = callback;
mSysUiState = sysUiState;
+ mScvhSupplier = scvhSupplier;
mConfiguration = new Configuration(context.getResources().getConfiguration());
mWindowMagnificationSizePrefs = new WindowMagnificationSizePrefs(mContext);
@@ -288,22 +301,78 @@
mTransaction = transaction;
mGestureDetector =
new MagnificationGestureDetector(mContext, handler, this);
+ mWindowInsetChangeRunnable = this::onWindowInsetChanged;
+ mGlobalWindowSessionSupplier = globalWindowSessionSupplier;
+ mSfVsyncFrameProvider = sfVsyncFrameProvider;
// Initialize listeners.
- mMirrorViewRunnable = () -> {
- if (mMirrorView != null) {
- final Rect oldViewBounds = new Rect(mMirrorViewBounds);
- mMirrorView.getBoundsOnScreen(mMirrorViewBounds);
- if (oldViewBounds.width() != mMirrorViewBounds.width()
- || oldViewBounds.height() != mMirrorViewBounds.height()) {
- mMirrorView.setSystemGestureExclusionRects(Collections.singletonList(
- new Rect(0, 0, mMirrorViewBounds.width(), mMirrorViewBounds.height())));
+ if (Flags.createWindowlessWindowMagnifier()) {
+ mMirrorViewRunnable = new Runnable() {
+ final Rect mPreviousBounds = new Rect();
+
+ @Override
+ public void run() {
+ if (mMirrorView != null) {
+ if (mPreviousBounds.width() != mMirrorViewBounds.width()
+ || mPreviousBounds.height() != mMirrorViewBounds.height()) {
+ mMirrorView.setSystemGestureExclusionRects(Collections.singletonList(
+ new Rect(0, 0, mMirrorViewBounds.width(),
+ mMirrorViewBounds.height())));
+ mPreviousBounds.set(mMirrorViewBounds);
+ }
+ updateSystemUIStateIfNeeded();
+ mWindowMagnifierCallback.onWindowMagnifierBoundsChanged(
+ mDisplayId, mMirrorViewBounds);
+ }
}
- updateSystemUIStateIfNeeded();
- mWindowMagnifierCallback.onWindowMagnifierBoundsChanged(
- mDisplayId, mMirrorViewBounds);
- }
- };
+ };
+
+ mMirrorSurfaceViewLayoutChangeListener =
+ (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) ->
+ mMirrorView.post(this::applyTapExcludeRegion);
+
+ mMirrorViewGeometryVsyncCallback = null;
+ } else {
+ mMirrorViewRunnable = () -> {
+ if (mMirrorView != null) {
+ final Rect oldViewBounds = new Rect(mMirrorViewBounds);
+ mMirrorView.getBoundsOnScreen(mMirrorViewBounds);
+ if (oldViewBounds.width() != mMirrorViewBounds.width()
+ || oldViewBounds.height() != mMirrorViewBounds.height()) {
+ mMirrorView.setSystemGestureExclusionRects(Collections.singletonList(
+ new Rect(0, 0,
+ mMirrorViewBounds.width(), mMirrorViewBounds.height())));
+ }
+ updateSystemUIStateIfNeeded();
+ mWindowMagnifierCallback.onWindowMagnifierBoundsChanged(
+ mDisplayId, mMirrorViewBounds);
+ }
+ };
+
+ mMirrorSurfaceViewLayoutChangeListener =
+ (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) ->
+ mMirrorView.post(this::applyTapExcludeRegion);
+
+ mMirrorViewGeometryVsyncCallback =
+ l -> {
+ if (isActivated() && mMirrorSurface != null && calculateSourceBounds(
+ mMagnificationFrame, mScale)) {
+ // The final destination for the magnification surface should be at 0,0
+ // since the ViewRootImpl's position will change
+ mTmpRect.set(0, 0, mMagnificationFrame.width(),
+ mMagnificationFrame.height());
+ mTransaction.setGeometry(mMirrorSurface, mSourceBounds, mTmpRect,
+ Surface.ROTATION_0).apply();
+
+ // Notify source bounds change when the magnifier is not animating.
+ if (!mAnimationController.isAnimating()) {
+ mWindowMagnifierCallback.onSourceBoundsChanged(mDisplayId,
+ mSourceBounds);
+ }
+ }
+ };
+ }
+
mMirrorViewLayoutChangeListener =
(v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
if (!mHandler.hasCallbacks(mMirrorViewRunnable)) {
@@ -311,34 +380,11 @@
}
};
- mMirrorSurfaceViewLayoutChangeListener =
- (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) ->
- mMirrorView.post(this::applyTapExcludeRegion);
-
- mMirrorViewGeometryVsyncCallback =
- l -> {
- if (isActivated() && mMirrorSurface != null && calculateSourceBounds(
- mMagnificationFrame, mScale)) {
- // The final destination for the magnification surface should be at 0,0
- // since the ViewRootImpl's position will change
- mTmpRect.set(0, 0, mMagnificationFrame.width(),
- mMagnificationFrame.height());
- mTransaction.setGeometry(mMirrorSurface, mSourceBounds, mTmpRect,
- Surface.ROTATION_0).apply();
-
- // Notify source bounds change when the magnifier is not animating.
- if (!mAnimationController.isAnimating()) {
- mWindowMagnifierCallback.onSourceBoundsChanged(mDisplayId,
- mSourceBounds);
- }
- }
- };
mUpdateStateDescriptionRunnable = () -> {
if (isActivated()) {
mMirrorView.setStateDescription(formatStateDescription(mScale));
}
};
- mWindowInsetChangeRunnable = this::onWindowInsetChanged;
}
private void setupMagnificationSizeScaleOptions() {
@@ -448,13 +494,21 @@
if (mMirrorView != null) {
mHandler.removeCallbacks(mMirrorViewRunnable);
mMirrorView.removeOnLayoutChangeListener(mMirrorViewLayoutChangeListener);
- mWm.removeView(mMirrorView);
+ if (!Flags.createWindowlessWindowMagnifier()) {
+ mWm.removeView(mMirrorView);
+ }
mMirrorView = null;
}
if (mMirrorWindowControl != null) {
mMirrorWindowControl.destroyControl();
}
+
+ if (mSurfaceControlViewHost != null) {
+ mSurfaceControlViewHost.release();
+ mSurfaceControlViewHost = null;
+ }
+
mMirrorViewBounds.setEmpty();
mSourceBounds.setEmpty();
updateSystemUIStateIfNeeded();
@@ -551,7 +605,11 @@
if (!isActivated()) return;
LayoutParams params = (LayoutParams) mMirrorView.getLayoutParams();
params.accessibilityTitle = getAccessibilityWindowTitle();
- mWm.updateViewLayout(mMirrorView, params);
+ if (Flags.createWindowlessWindowMagnifier()) {
+ mSurfaceControlViewHost.relayout(params);
+ } else {
+ mWm.updateViewLayout(mMirrorView, params);
+ }
}
/**
@@ -602,6 +660,11 @@
}
private void createMirrorWindow() {
+ if (Flags.createWindowlessWindowMagnifier()) {
+ createWindowlessMirrorWindow();
+ return;
+ }
+
// The window should be the size the mirrored surface will be but also add room for the
// border and the drag handle.
int windowWidth = mMagnificationFrame.width() + 2 * mMirrorSurfaceMargin;
@@ -652,6 +715,68 @@
addDragTouchListeners();
}
+ private void createWindowlessMirrorWindow() {
+ // The window should be the size the mirrored surface will be but also add room for the
+ // border and the drag handle.
+ int windowWidth = mMagnificationFrame.width() + 2 * mMirrorSurfaceMargin;
+ int windowHeight = mMagnificationFrame.height() + 2 * mMirrorSurfaceMargin;
+
+ // TODO delete TYPE_ACCESSIBILITY_MAGNIFICATION_OVERLAY, it shouldn't be needed anymore
+
+ LayoutParams params = new LayoutParams(
+ windowWidth, windowHeight,
+ LayoutParams.TYPE_ACCESSIBILITY_OVERLAY,
+ LayoutParams.FLAG_NOT_TOUCH_MODAL
+ | LayoutParams.FLAG_NOT_FOCUSABLE,
+ PixelFormat.TRANSPARENT);
+ params.receiveInsetsIgnoringZOrder = true;
+ params.setTitle(mContext.getString(R.string.magnification_window_title));
+ params.accessibilityTitle = getAccessibilityWindowTitle();
+ params.setTrustedOverlay();
+
+ mMirrorView = LayoutInflater.from(mContext).inflate(R.layout.window_magnifier_view, null);
+ mMirrorSurfaceView = mMirrorView.findViewById(R.id.surface_view);
+
+ mMirrorBorderView = mMirrorView.findViewById(R.id.magnification_inner_border);
+
+ // Allow taps to go through to the mirror SurfaceView below.
+ mMirrorSurfaceView.addOnLayoutChangeListener(mMirrorSurfaceViewLayoutChangeListener);
+
+ mMirrorView.addOnLayoutChangeListener(mMirrorViewLayoutChangeListener);
+ mMirrorView.setAccessibilityDelegate(new MirrorWindowA11yDelegate());
+ mMirrorView.setOnApplyWindowInsetsListener((v, insets) -> {
+ if (!mHandler.hasCallbacks(mWindowInsetChangeRunnable)) {
+ mHandler.post(mWindowInsetChangeRunnable);
+ }
+ return v.onApplyWindowInsets(insets);
+ });
+
+ mSurfaceControlViewHost = mScvhSupplier.get();
+ mSurfaceControlViewHost.setView(mMirrorView, params);
+ SurfaceControl surfaceControl = mSurfaceControlViewHost
+ .getSurfacePackage().getSurfaceControl();
+
+ int x = mMagnificationFrame.left - mMirrorSurfaceMargin;
+ int y = mMagnificationFrame.top - mMirrorSurfaceMargin;
+ mTransaction
+ .setCrop(surfaceControl, new Rect(0, 0, windowWidth, windowHeight))
+ .setPosition(surfaceControl, x, y)
+ .setLayer(surfaceControl, Integer.MAX_VALUE)
+ .show(surfaceControl)
+ .apply();
+
+ mMirrorViewBounds.set(x, y, x + windowWidth, y + windowHeight);
+
+ AccessibilityManager accessibilityManager = mContext
+ .getSystemService(AccessibilityManager.class);
+ accessibilityManager.attachAccessibilityOverlayToDisplay(mDisplayId, surfaceControl);
+
+ SurfaceHolder holder = mMirrorSurfaceView.getHolder();
+ holder.addCallback(this);
+ holder.setFormat(PixelFormat.RGBA_8888);
+ addDragTouchListeners();
+ }
+
private void onWindowInsetChanged() {
if (updateSystemGestureInsetsTop()) {
updateSystemUIStateIfNeeded();
@@ -659,6 +784,11 @@
}
private void applyTapExcludeRegion() {
+ if (Flags.createWindowlessWindowMagnifier()) {
+ applyTouchableRegion();
+ return;
+ }
+
// Sometimes this can get posted and run after deleteWindowMagnification() is called.
if (mMirrorView == null) return;
@@ -709,6 +839,51 @@
return regionInsideDragBorder;
}
+ private void applyTouchableRegion() {
+ // Sometimes this can get posted and run after deleteWindowMagnification() is called.
+ if (mMirrorView == null) return;
+
+ var surfaceControl = mSurfaceControlViewHost.getRootSurfaceControl();
+ surfaceControl.setTouchableRegion(calculateTouchableRegion());
+ }
+
+ private Region calculateTouchableRegion() {
+ Region touchableRegion = new Region(0, 0, mMirrorView.getWidth(), mMirrorView.getHeight());
+
+ Region regionInsideDragBorder = new Region(mBorderDragSize, mBorderDragSize,
+ mMirrorView.getWidth() - mBorderDragSize,
+ mMirrorView.getHeight() - mBorderDragSize);
+ touchableRegion.op(regionInsideDragBorder, Region.Op.DIFFERENCE);
+
+ Rect dragArea = new Rect();
+ mDragView.getHitRect(dragArea);
+
+ Rect topLeftArea = new Rect();
+ mTopLeftCornerView.getHitRect(topLeftArea);
+
+ Rect topRightArea = new Rect();
+ mTopRightCornerView.getHitRect(topRightArea);
+
+ Rect bottomLeftArea = new Rect();
+ mBottomLeftCornerView.getHitRect(bottomLeftArea);
+
+ Rect bottomRightArea = new Rect();
+ mBottomRightCornerView.getHitRect(bottomRightArea);
+
+ Rect closeArea = new Rect();
+ mCloseView.getHitRect(closeArea);
+
+ // Add touchable regions for drag and close
+ touchableRegion.op(dragArea, Region.Op.UNION);
+ touchableRegion.op(topLeftArea, Region.Op.UNION);
+ touchableRegion.op(topRightArea, Region.Op.UNION);
+ touchableRegion.op(bottomLeftArea, Region.Op.UNION);
+ touchableRegion.op(bottomRightArea, Region.Op.UNION);
+ touchableRegion.op(closeArea, Region.Op.UNION);
+
+ return touchableRegion;
+ }
+
private String getAccessibilityWindowTitle() {
return mResources.getString(com.android.internal.R.string.android_system_label);
}
@@ -852,8 +1027,84 @@
* {@link #mMagnificationFrame}.
*/
private void modifyWindowMagnification(boolean computeWindowSize) {
- mSfVsyncFrameProvider.postFrameCallback(mMirrorViewGeometryVsyncCallback);
- updateMirrorViewLayout(computeWindowSize);
+ if (Flags.createWindowlessWindowMagnifier()) {
+ updateMirrorSurfaceGeometry();
+ updateWindowlessMirrorViewLayout(computeWindowSize);
+ } else {
+ mSfVsyncFrameProvider.postFrameCallback(mMirrorViewGeometryVsyncCallback);
+ updateMirrorViewLayout(computeWindowSize);
+ }
+ }
+
+ /**
+ * Updates {@link #mMirrorSurface}'s geometry. This modifies {@link #mTransaction} but does not
+ * apply it.
+ */
+ @UiThread
+ private void updateMirrorSurfaceGeometry() {
+ if (isActivated() && mMirrorSurface != null
+ && calculateSourceBounds(mMagnificationFrame, mScale)) {
+ // The final destination for the magnification surface should be at 0,0
+ // since the ViewRootImpl's position will change
+ mTmpRect.set(0, 0, mMagnificationFrame.width(), mMagnificationFrame.height());
+ mTransaction.setGeometry(mMirrorSurface, mSourceBounds, mTmpRect, Surface.ROTATION_0);
+
+ // Notify source bounds change when the magnifier is not animating.
+ if (!mAnimationController.isAnimating()) {
+ mWindowMagnifierCallback.onSourceBoundsChanged(mDisplayId, mSourceBounds);
+ }
+ }
+ }
+
+ /**
+ * Updates the position of {@link mSurfaceControlViewHost} and layout params of MirrorView based
+ * on the position and size of {@link #mMagnificationFrame}.
+ *
+ * @param computeWindowSize set to {@code true} to compute window size with
+ * {@link #mMagnificationFrame}.
+ */
+ @UiThread
+ private void updateWindowlessMirrorViewLayout(boolean computeWindowSize) {
+ if (!isActivated()) {
+ return;
+ }
+
+ final int width = mMagnificationFrame.width() + 2 * mMirrorSurfaceMargin;
+ final int height = mMagnificationFrame.height() + 2 * mMirrorSurfaceMargin;
+
+ final int minX = -mOuterBorderSize;
+ final int maxX = mWindowBounds.right - width + mOuterBorderSize;
+ final int x = MathUtils.clamp(mMagnificationFrame.left - mMirrorSurfaceMargin, minX, maxX);
+
+ final int minY = -mOuterBorderSize;
+ final int maxY = mWindowBounds.bottom - height + mOuterBorderSize;
+ final int y = MathUtils.clamp(mMagnificationFrame.top - mMirrorSurfaceMargin, minY, maxY);
+
+ if (computeWindowSize) {
+ LayoutParams params = (LayoutParams) mMirrorView.getLayoutParams();
+ params.width = width;
+ params.height = height;
+ mSurfaceControlViewHost.relayout(params);
+ mTransaction.setCrop(mSurfaceControlViewHost.getSurfacePackage().getSurfaceControl(),
+ new Rect(0, 0, width, height));
+ }
+
+ mMirrorViewBounds.set(x, y, x + width, y + height);
+ mTransaction.setPosition(
+ mSurfaceControlViewHost.getSurfacePackage().getSurfaceControl(), x, y);
+ if (computeWindowSize) {
+ mSurfaceControlViewHost.getRootSurfaceControl().applyTransactionOnDraw(mTransaction);
+ } else {
+ mTransaction.apply();
+ }
+
+ // If they are not dragging the handle, we can move the drag handle immediately without
+ // disruption. But if they are dragging it, we avoid moving until the end of the drag.
+ if (!mIsDragging) {
+ mMirrorView.post(this::maybeRepositionButton);
+ }
+
+ mMirrorViewRunnable.run();
}
/**
@@ -1094,7 +1345,7 @@
/**
* Enables window magnification with specified parameters. If the given scale is <strong>less
- * than or equal to 1.0f<strong>, then
+ * than or equal to 1.0f</strong>, then
* {@link WindowMagnificationController#deleteWindowMagnification()} will be called instead to
* be consistent with the behavior of display magnification.
*
@@ -1110,7 +1361,7 @@
/**
* Enables window magnification with specified parameters. If the given scale is <strong>less
- * than 1.0f<strong>, then
+ * than 1.0f</strong>, then
* {@link WindowMagnificationController#deleteWindowMagnification()} will be called instead to
* be consistent with the behavior of display magnification.
*
@@ -1426,10 +1677,8 @@
final FrameLayout.LayoutParams layoutParams =
(FrameLayout.LayoutParams) mDragView.getLayoutParams();
- mMirrorView.getBoundsOnScreen(mTmpRect);
-
final int newGravity;
- if (mTmpRect.right >= screenEdgeX) {
+ if (mMirrorViewBounds.right >= screenEdgeX) {
newGravity = Gravity.BOTTOM | Gravity.LEFT;
} else {
newGravity = Gravity.BOTTOM | Gravity.RIGHT;
@@ -1449,8 +1698,8 @@
mSettingsPanelVisibility = settingsPanelIsShown;
mDragView.setBackground(mContext.getResources().getDrawable(settingsPanelIsShown
- ? R.drawable.accessibility_window_magnification_drag_handle_background_change
- : R.drawable.accessibility_window_magnification_drag_handle_background));
+ ? R.drawable.accessibility_window_magnification_drag_handle_background_change_inset
+ : R.drawable.accessibility_window_magnification_drag_handle_background_inset));
PorterDuffColorFilter filter = new PorterDuffColorFilter(
mContext.getColor(settingsPanelIsShown
diff --git a/packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt b/packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt
index 49f34f1..454ed27 100644
--- a/packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt
@@ -39,7 +39,7 @@
import com.android.systemui.scene.shared.flag.SceneContainerFlags
import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionsRepository
import com.android.systemui.user.data.repository.UserRepository
-import com.android.systemui.util.kotlin.pairwise
+import com.android.systemui.util.kotlin.onSubscriberAdded
import com.android.systemui.util.time.SystemClock
import dagger.Binds
import dagger.Module
@@ -54,7 +54,6 @@
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
-import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onStart
@@ -355,10 +354,7 @@
userRepository.selectedUserInfo.map { it.id }.distinctUntilChanged(),
// Emits a value only when the number of downstream subscribers of this flow
// increases.
- flow.subscriptionCount.pairwise(initialValue = 0).filter { (previous, current)
- ->
- current > previous
- },
+ flow.onSubscriberAdded(),
) { selectedUserId, _ ->
selectedUserId
}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/LogContextInteractor.kt b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/LogContextInteractor.kt
index 70be0f2..a742c0e 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/LogContextInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/LogContextInteractor.kt
@@ -19,22 +19,15 @@
import android.hardware.biometrics.AuthenticateOptions
import android.hardware.biometrics.IBiometricContextListener
import android.util.Log
-import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
-import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
-import com.android.systemui.keyguard.WakefulnessLifecycle
+import com.android.systemui.display.data.repository.DeviceStateRepository
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
import com.android.systemui.keyguard.shared.model.KeyguardState
-import com.android.systemui.unfold.updates.FOLD_UPDATE_FINISH_CLOSED
-import com.android.systemui.unfold.updates.FOLD_UPDATE_FINISH_FULL_OPEN
-import com.android.systemui.unfold.updates.FOLD_UPDATE_FINISH_HALF_OPEN
-import com.android.systemui.unfold.updates.FoldStateProvider
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancel
-import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.catch
@@ -80,15 +73,11 @@
@Inject
constructor(
@Application private val applicationScope: CoroutineScope,
- private val foldProvider: FoldStateProvider,
+ deviceStateRepository: DeviceStateRepository,
keyguardTransitionInteractor: KeyguardTransitionInteractor,
udfpsOverlayInteractor: UdfpsOverlayInteractor,
) : LogContextInteractor {
- init {
- applicationScope.launch { foldProvider.start() }
- }
-
override val displayState =
keyguardTransitionInteractor.startedKeyguardTransitionStep.map {
when (it.to) {
@@ -123,32 +112,21 @@
.distinctUntilChanged()
override val foldState: Flow<Int> =
- conflatedCallbackFlow {
- val callback =
- object : FoldStateProvider.FoldUpdatesListener {
- override fun onHingeAngleUpdate(angle: Float) {}
-
- override fun onFoldUpdate(@FoldStateProvider.FoldUpdate update: Int) {
- val loggedState =
- when (update) {
- FOLD_UPDATE_FINISH_HALF_OPEN ->
- IBiometricContextListener.FoldState.HALF_OPENED
- FOLD_UPDATE_FINISH_FULL_OPEN ->
- IBiometricContextListener.FoldState.FULLY_OPENED
- FOLD_UPDATE_FINISH_CLOSED ->
- IBiometricContextListener.FoldState.FULLY_CLOSED
- else -> null
- }
- if (loggedState != null) {
- trySendWithFailureLogging(loggedState, TAG)
- }
- }
- }
-
- foldProvider.addCallback(callback)
- trySendWithFailureLogging(IBiometricContextListener.FoldState.UNKNOWN, TAG)
- awaitClose { foldProvider.removeCallback(callback) }
+ deviceStateRepository.state
+ .map {
+ when (it) {
+ DeviceStateRepository.DeviceState.UNFOLDED,
+ DeviceStateRepository.DeviceState.REAR_DISPLAY,
+ DeviceStateRepository.DeviceState.CONCURRENT_DISPLAY ->
+ IBiometricContextListener.FoldState.FULLY_OPENED
+ DeviceStateRepository.DeviceState.FOLDED ->
+ IBiometricContextListener.FoldState.FULLY_CLOSED
+ DeviceStateRepository.DeviceState.HALF_FOLDED ->
+ IBiometricContextListener.FoldState.HALF_OPENED
+ else -> IBiometricContextListener.FoldState.UNKNOWN
+ }
}
+ .distinctUntilChanged()
.shareIn(applicationScope, started = SharingStarted.Eagerly, replay = 1)
override fun addBiometricContextListener(listener: IBiometricContextListener): Job {
@@ -178,6 +156,3 @@
private const val TAG = "ContextRepositoryImpl"
}
}
-
-private val WakefulnessLifecycle.isAwake: Boolean
- get() = wakefulness == WakefulnessLifecycle.WAKEFULNESS_AWAKE
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/data/repository/BouncerRepository.kt b/packages/SystemUI/src/com/android/systemui/bouncer/data/repository/BouncerRepository.kt
index 7265c0c..d849b3a 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/data/repository/BouncerRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/data/repository/BouncerRepository.kt
@@ -21,8 +21,6 @@
import com.android.systemui.flags.Flags
import javax.inject.Inject
import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.asStateFlow
/** Provides access to bouncer-related application state. */
@SysUISingleton
@@ -31,15 +29,10 @@
constructor(
private val flags: FeatureFlagsClassic,
) {
- private val _message = MutableStateFlow<String?>(null)
/** The user-facing message to show in the bouncer. */
- val message: StateFlow<String?> = _message.asStateFlow()
+ val message = MutableStateFlow<String?>(null)
/** Whether the user switcher should be displayed within the bouncer UI on large screens. */
val isUserSwitcherVisible: Boolean
get() = flags.isEnabled(Flags.FULL_SCREEN_USER_SWITCHER)
-
- fun setMessage(message: String?) {
- _message.value = message
- }
}
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractor.kt b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractor.kt
index c8ce245..d8be1af 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractor.kt
@@ -120,7 +120,7 @@
}
fun setMessage(message: String?) {
- repository.setMessage(message)
+ repository.message.value = message
}
/**
@@ -129,13 +129,13 @@
*/
fun resetMessage() {
applicationScope.launch {
- repository.setMessage(promptMessage(authenticationInteractor.getAuthenticationMethod()))
+ setMessage(promptMessage(authenticationInteractor.getAuthenticationMethod()))
}
}
/** Removes the user-facing message. */
fun clearMessage() {
- repository.setMessage(null)
+ setMessage(null)
}
/**
@@ -196,7 +196,7 @@
* message without having the attempt trigger lockout.
*/
private suspend fun showWrongInputMessage() {
- repository.setMessage(wrongInputMessage(authenticationInteractor.getAuthenticationMethod()))
+ setMessage(wrongInputMessage(authenticationInteractor.getAuthenticationMethod()))
}
/** Notifies that the input method editor (software keyboard) has been hidden by the user. */
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModel.kt
index 4d686a1..4466cbb 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModel.kt
@@ -34,7 +34,9 @@
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.inputmethod.domain.interactor.InputMethodInteractor
import com.android.systemui.scene.shared.flag.SceneContainerFlags
+import com.android.systemui.user.domain.interactor.SelectedUserInteractor
import com.android.systemui.user.ui.viewmodel.UserActionViewModel
import com.android.systemui.user.ui.viewmodel.UserSwitcherViewModel
import com.android.systemui.user.ui.viewmodel.UserViewModel
@@ -66,8 +68,10 @@
@Application private val applicationScope: CoroutineScope,
@Main private val mainDispatcher: CoroutineDispatcher,
private val bouncerInteractor: BouncerInteractor,
+ private val inputMethodInteractor: InputMethodInteractor,
private val simBouncerInteractor: SimBouncerInteractor,
private val authenticationInteractor: AuthenticationInteractor,
+ private val selectedUserInteractor: SelectedUserInteractor,
flags: SceneContainerFlags,
selectedUser: Flow<UserViewModel>,
users: Flow<List<UserViewModel>>,
@@ -346,8 +350,10 @@
is AuthenticationMethodModel.Password ->
PasswordBouncerViewModel(
viewModelScope = newViewModelScope,
- interactor = bouncerInteractor,
isInputEnabled = isInputEnabled,
+ interactor = bouncerInteractor,
+ inputMethodInteractor = inputMethodInteractor,
+ selectedUserInteractor = selectedUserInteractor,
)
is AuthenticationMethodModel.Pattern ->
PatternBouncerViewModel(
@@ -467,11 +473,13 @@
@Application applicationScope: CoroutineScope,
@Main mainDispatcher: CoroutineDispatcher,
bouncerInteractor: BouncerInteractor,
+ imeInteractor: InputMethodInteractor,
simBouncerInteractor: SimBouncerInteractor,
+ actionButtonInteractor: BouncerActionButtonInteractor,
authenticationInteractor: AuthenticationInteractor,
+ selectedUserInteractor: SelectedUserInteractor,
flags: SceneContainerFlags,
userSwitcherViewModel: UserSwitcherViewModel,
- actionButtonInteractor: BouncerActionButtonInteractor,
clock: SystemClock,
devicePolicyManager: DevicePolicyManager,
): BouncerViewModel {
@@ -480,8 +488,10 @@
applicationScope = applicationScope,
mainDispatcher = mainDispatcher,
bouncerInteractor = bouncerInteractor,
+ inputMethodInteractor = imeInteractor,
simBouncerInteractor = simBouncerInteractor,
authenticationInteractor = authenticationInteractor,
+ selectedUserInteractor = selectedUserInteractor,
flags = flags,
selectedUser = userSwitcherViewModel.selectedUser,
users = userSwitcherViewModel.users,
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModel.kt
index 5c9c997..1c8b84d 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModel.kt
@@ -16,23 +16,32 @@
package com.android.systemui.bouncer.ui.viewmodel
+import androidx.annotation.VisibleForTesting
import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
import com.android.systemui.bouncer.domain.interactor.BouncerInteractor
+import com.android.systemui.inputmethod.domain.interactor.InputMethodInteractor
import com.android.systemui.res.R
+import com.android.systemui.user.domain.interactor.SelectedUserInteractor
+import com.android.systemui.util.kotlin.onSubscriberAdded
+import kotlin.time.Duration.Companion.milliseconds
import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.delay
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.onEach
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
/** Holds UI state and handles user input for the password bouncer UI. */
class PasswordBouncerViewModel(
viewModelScope: CoroutineScope,
- interactor: BouncerInteractor,
isInputEnabled: StateFlow<Boolean>,
+ interactor: BouncerInteractor,
+ private val inputMethodInteractor: InputMethodInteractor,
+ private val selectedUserInteractor: SelectedUserInteractor,
) :
AuthMethodBouncerViewModel(
viewModelScope = viewModelScope,
@@ -49,6 +58,9 @@
override val lockoutMessageId = R.string.kg_too_many_failed_password_attempts_dialog_message
+ /** Informs the UI whether the input method switcher button should be visible. */
+ val isImeSwitcherButtonVisible: StateFlow<Boolean> = imeSwitcherRefreshingFlow()
+
/** Whether the text field element currently has focus. */
private val isTextFieldFocused = MutableStateFlow(false)
@@ -87,6 +99,13 @@
_password.value = newPassword
}
+ /** Notifies that the user clicked the button to change the input method. */
+ fun onImeSwitcherButtonClicked(displayId: Int) {
+ viewModelScope.launch {
+ inputMethodInteractor.showInputMethodPicker(displayId, showAuxiliarySubtypes = false)
+ }
+ }
+
/** Notifies that the user has pressed the key for attempting to authenticate the password. */
fun onAuthenticateKeyPressed() {
if (_password.value.isNotEmpty()) {
@@ -103,4 +122,35 @@
fun onTextFieldFocusChanged(isFocused: Boolean) {
isTextFieldFocused.value = isFocused
}
+
+ /**
+ * Whether the input method switcher button should be displayed in the password bouncer UI. The
+ * value may be stale at the moment of subscription to this flow, but it is guaranteed to be
+ * shortly updated with a fresh value.
+ *
+ * Note: Each added subscription triggers an IPC call in the background, so this should only be
+ * subscribed to by the UI once in its lifecycle (i.e. when the bouncer is shown).
+ */
+ private fun imeSwitcherRefreshingFlow(): StateFlow<Boolean> {
+ val isImeSwitcherButtonVisible = MutableStateFlow(value = false)
+ viewModelScope.launch {
+ // Re-fetch the currently-enabled IMEs whenever the selected user changes, and whenever
+ // the UI subscribes to the `isImeSwitcherButtonVisible` flow.
+ combine(
+ // InputMethodManagerService sometimes takes some time to update its internal
+ // state when the selected user changes. As a workaround, delay fetching the IME
+ // info.
+ selectedUserInteractor.selectedUser.onEach { delay(DELAY_TO_FETCH_IMES) },
+ isImeSwitcherButtonVisible.onSubscriberAdded()
+ ) { selectedUserId, _ ->
+ inputMethodInteractor.hasMultipleEnabledImesOrSubtypes(selectedUserId)
+ }
+ .collect { isImeSwitcherButtonVisible.value = it }
+ }
+ return isImeSwitcherButtonVisible.asStateFlow()
+ }
+
+ companion object {
+ @VisibleForTesting val DELAY_TO_FETCH_IMES = 300.milliseconds
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/CommunalSceneStartable.kt b/packages/SystemUI/src/com/android/systemui/communal/CommunalSceneStartable.kt
new file mode 100644
index 0000000..f7ba5a4
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/communal/CommunalSceneStartable.kt
@@ -0,0 +1,110 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.communal
+
+import com.android.systemui.CoreStartable
+import com.android.systemui.communal.domain.interactor.CommunalInteractor
+import com.android.systemui.communal.shared.model.CommunalSceneKey
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.dock.DockManager
+import com.android.systemui.dock.retrieveIsDocked
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.shared.model.TransitionStep
+import com.android.systemui.util.kotlin.sample
+import javax.inject.Inject
+import kotlin.time.Duration.Companion.seconds
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.FlowPreview
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.debounce
+import kotlinx.coroutines.flow.filterNotNull
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.mapLatest
+import kotlinx.coroutines.flow.onEach
+
+/**
+ * A [CoreStartable] responsible for automatically navigating between communal scenes when certain
+ * conditions are met.
+ */
+@OptIn(ExperimentalCoroutinesApi::class, FlowPreview::class)
+@SysUISingleton
+class CommunalSceneStartable
+@Inject
+constructor(
+ private val dockManager: DockManager,
+ private val communalInteractor: CommunalInteractor,
+ private val keyguardTransitionInteractor: KeyguardTransitionInteractor,
+ @Application private val applicationScope: CoroutineScope,
+ @Background private val bgScope: CoroutineScope,
+) : CoreStartable {
+ override fun start() {
+ // Handle automatically switching based on keyguard state.
+ keyguardTransitionInteractor.startedKeyguardTransitionStep
+ .mapLatest(::determineSceneAfterTransition)
+ .filterNotNull()
+ // TODO(b/322787129): Also set a custom transition animation here to avoid the regular
+ // slide-in animation when setting the scene programmatically
+ .onEach { nextScene -> communalInteractor.onSceneChanged(nextScene) }
+ .launchIn(applicationScope)
+
+ // Handle automatically switching to communal when docked.
+ dockManager
+ .retrieveIsDocked()
+ // Allow some time after docking to ensure the dream doesn't start. If the dream
+ // starts, then we don't want to automatically transition to glanceable hub.
+ .debounce(DOCK_DEBOUNCE_DELAY)
+ .sample(keyguardTransitionInteractor.startedKeyguardState, ::Pair)
+ .onEach { (docked, lastStartedState) ->
+ if (docked && lastStartedState == KeyguardState.LOCKSCREEN) {
+ communalInteractor.onSceneChanged(CommunalSceneKey.Communal)
+ }
+ }
+ .launchIn(bgScope)
+ }
+
+ private suspend fun determineSceneAfterTransition(
+ lastStartedTransition: TransitionStep,
+ ): CommunalSceneKey? {
+ val to = lastStartedTransition.to
+ val from = lastStartedTransition.from
+ val docked = dockManager.isDocked
+
+ return when {
+ to == KeyguardState.DREAMING -> CommunalSceneKey.Blank
+ docked && to == KeyguardState.LOCKSCREEN && from != KeyguardState.GLANCEABLE_HUB -> {
+ CommunalSceneKey.Communal
+ }
+ to == KeyguardState.GONE -> CommunalSceneKey.Blank
+ !docked && !KeyguardState.deviceIsAwakeInState(to) -> {
+ // If the user taps the screen and wakes the device within this timeout, we don't
+ // want to dismiss the hub
+ delay(AWAKE_DEBOUNCE_DELAY)
+ CommunalSceneKey.Blank
+ }
+ else -> null
+ }
+ }
+
+ companion object {
+ val AWAKE_DEBOUNCE_DELAY = 5.seconds
+ val DOCK_DEBOUNCE_DELAY = 1.seconds
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryModule.kt b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryModule.kt
index ab0a2d0d..d7f163b 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryModule.kt
@@ -26,6 +26,7 @@
import com.android.systemui.communal.widgets.WidgetInteractionHandler
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.log.LogBuffer
import com.android.systemui.log.dagger.CommunalLog
@@ -35,6 +36,7 @@
import dagger.Provides
import java.util.Optional
import javax.inject.Named
+import kotlinx.coroutines.CoroutineScope
@Module
interface CommunalWidgetRepositoryModule {
@@ -52,10 +54,19 @@
@Provides
fun provideCommunalAppWidgetHost(
@Application context: Context,
+ @Background backgroundScope: CoroutineScope,
interactionHandler: WidgetInteractionHandler,
@Main looper: Looper,
+ @CommunalLog logBuffer: LogBuffer,
): CommunalAppWidgetHost {
- return CommunalAppWidgetHost(context, APP_WIDGET_HOST_ID, interactionHandler, looper)
+ return CommunalAppWidgetHost(
+ context,
+ backgroundScope,
+ APP_WIDGET_HOST_ID,
+ interactionHandler,
+ looper,
+ logBuffer,
+ )
}
@SysUISingleton
diff --git a/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHost.kt b/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHost.kt
index 61db026..5f1d89e 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHost.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHost.kt
@@ -22,14 +22,31 @@
import android.content.Context
import android.os.Looper
import android.widget.RemoteViews
+import com.android.systemui.log.LogBuffer
+import com.android.systemui.log.core.Logger
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.SharedFlow
+import kotlinx.coroutines.flow.asSharedFlow
+import kotlinx.coroutines.launch
/** Communal app widget host that creates a [CommunalAppWidgetHostView]. */
class CommunalAppWidgetHost(
context: Context,
+ private val backgroundScope: CoroutineScope,
hostId: Int,
interactionHandler: RemoteViews.InteractionHandler,
- looper: Looper
+ looper: Looper,
+ logBuffer: LogBuffer,
) : AppWidgetHost(context, hostId, interactionHandler, looper) {
+
+ private val logger = Logger(logBuffer, TAG)
+
+ private val _appWidgetIdToRemove = MutableSharedFlow<Int>()
+
+ /** App widget ids that have been removed and no longer available. */
+ val appWidgetIdToRemove: SharedFlow<Int> = _appWidgetIdToRemove.asSharedFlow()
+
override fun onCreateView(
context: Context,
appWidgetId: Int,
@@ -52,4 +69,15 @@
// `createView`, but we are sure that the hostView is `CommunalAppWidgetHostView`
return createView(context, appWidgetId, appWidget) as CommunalAppWidgetHostView
}
+
+ override fun onAppWidgetRemoved(appWidgetId: Int) {
+ backgroundScope.launch {
+ logger.i({ "App widget removed from system: $int1" }) { int1 = appWidgetId }
+ _appWidgetIdToRemove.emit(appWidgetId)
+ }
+ }
+
+ companion object {
+ private const val TAG = "CommunalAppWidgetHost"
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartable.kt b/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartable.kt
index 586df32..fb9abeb 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartable.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartable.kt
@@ -40,6 +40,7 @@
@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.
@@ -47,6 +48,10 @@
.filter { (previous, new) -> previous != new }
.onEach { (_, shouldListen) -> updateAppWidgetHostActive(shouldListen) }
.launchIn(bgScope)
+
+ appWidgetHost.appWidgetIdToRemove
+ .onEach { appWidgetId -> communalInteractor.deleteWidget(appWidgetId) }
+ .launchIn(bgScope)
}
private suspend fun updateAppWidgetHostActive(active: Boolean) =
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 ad1327e..54c709d 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt
@@ -29,6 +29,7 @@
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
import com.android.internal.logging.UiEventLogger
import com.android.systemui.communal.shared.log.CommunalUiEvent
+import com.android.systemui.communal.shared.model.CommunalSceneKey
import com.android.systemui.communal.ui.viewmodel.CommunalEditModeViewModel
import com.android.systemui.compose.ComposeFacade.setCommunalEditWidgetActivityContent
import javax.inject.Inject
@@ -126,6 +127,7 @@
},
onEditDone = {
try {
+ communalViewModel.onSceneChanged(CommunalSceneKey.Communal)
checkNotNull(windowManagerService).lockNow(/* options */ null)
finish()
} catch (e: RemoteException) {
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/GlobalModule.java b/packages/SystemUI/src/com/android/systemui/dagger/GlobalModule.java
index 5c38264..3072f74 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/GlobalModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/GlobalModule.java
@@ -24,6 +24,7 @@
import com.android.systemui.plugins.PluginsModule;
import com.android.systemui.unfold.UnfoldTransitionModule;
import com.android.systemui.util.concurrency.GlobalConcurrencyModule;
+import com.android.systemui.util.kotlin.GlobalCoroutinesModule;
import dagger.Module;
import dagger.Provides;
@@ -47,6 +48,7 @@
AndroidInternalsModule.class,
FrameworkServicesModule.class,
GlobalConcurrencyModule.class,
+ GlobalCoroutinesModule.class,
UnfoldTransitionModule.class,
PluginsModule.class,
})
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java
index 50f861f..e9d1e94 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java
@@ -63,6 +63,7 @@
import com.android.systemui.statusbar.policy.SensorPrivacyController;
import com.android.systemui.statusbar.policy.SensorPrivacyControllerImpl;
import com.android.systemui.toast.ToastModule;
+import com.android.systemui.unfold.UnfoldTransitionModule;
import com.android.systemui.volume.dagger.VolumeModule;
import com.android.systemui.wallpapers.dagger.WallpaperModule;
@@ -107,6 +108,7 @@
ShadeModule.class,
StartCentralSurfacesModule.class,
SceneContainerFrameworkModule.class,
+ UnfoldTransitionModule.Startables.class,
ToastModule.class,
VolumeModule.class,
WallpaperModule.class
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt
index 95233f7..5ee2045 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt
@@ -24,12 +24,14 @@
import com.android.systemui.back.domain.interactor.BackActionInteractor
import com.android.systemui.biometrics.BiometricNotificationService
import com.android.systemui.clipboardoverlay.ClipboardListener
+import com.android.systemui.communal.CommunalSceneStartable
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
import com.android.systemui.dreams.DreamMonitor
+import com.android.systemui.dreams.homecontrols.HomeControlsDreamStartable
import com.android.systemui.globalactions.GlobalActionsComponent
import com.android.systemui.keyboard.KeyboardUI
import com.android.systemui.keyboard.PhysicalKeyboardCoreStartable
@@ -50,7 +52,6 @@
import com.android.systemui.statusbar.ImmersiveModeConfirmation
import com.android.systemui.statusbar.gesture.GesturePointerEventListener
import com.android.systemui.statusbar.notification.InstantAppNotifier
-import com.android.systemui.statusbar.phone.KeyguardLiftController
import com.android.systemui.statusbar.phone.ScrimController
import com.android.systemui.statusbar.phone.StatusBarHeadsUpChangeListener
import com.android.systemui.stylus.StylusUsiPowerStartable
@@ -224,12 +225,6 @@
@ClassKey(WMShell::class)
abstract fun bindWMShell(sysui: WMShell): CoreStartable
- /** Inject into KeyguardLiftController. */
- @Binds
- @IntoMap
- @ClassKey(KeyguardLiftController::class)
- abstract fun bindKeyguardLiftController(sysui: KeyguardLiftController): CoreStartable
-
/** Inject into MediaTttSenderCoordinator. */
@Binds
@IntoMap
@@ -328,8 +323,18 @@
@Binds
@IntoMap
+ @ClassKey(CommunalSceneStartable::class)
+ abstract fun bindCommunalSceneStartable(impl: CommunalSceneStartable): CoreStartable
+
+ @Binds
+ @IntoMap
@ClassKey(CommunalAppWidgetHostStartable::class)
abstract fun bindCommunalAppWidgetHostStartable(
impl: CommunalAppWidgetHostStartable
): CoreStartable
+
+ @Binds
+ @IntoMap
+ @ClassKey(HomeControlsDreamStartable::class)
+ abstract fun bindHomeControlsDreamStartable(impl: HomeControlsDreamStartable): CoreStartable
}
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
index c1870c0..efcbd47 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
@@ -57,6 +57,7 @@
import com.android.systemui.flags.FeatureFlags;
import com.android.systemui.flags.FlagDependenciesModule;
import com.android.systemui.flags.FlagsModule;
+import com.android.systemui.inputmethod.InputMethodModule;
import com.android.systemui.keyboard.KeyboardModule;
import com.android.systemui.keyevent.data.repository.KeyEventRepositoryModule;
import com.android.systemui.keyguard.ui.view.layout.blueprints.KeyguardBlueprintModule;
@@ -134,7 +135,7 @@
import com.android.systemui.util.EventLogModule;
import com.android.systemui.util.concurrency.SysUIConcurrencyModule;
import com.android.systemui.util.dagger.UtilModule;
-import com.android.systemui.util.kotlin.CoroutinesModule;
+import com.android.systemui.util.kotlin.SysUICoroutinesModule;
import com.android.systemui.util.reference.ReferenceModule;
import com.android.systemui.util.sensors.SensorModule;
import com.android.systemui.util.settings.SettingsUtilModule;
@@ -183,7 +184,6 @@
ConfigurationControllerModule.class,
ConnectivityModule.class,
ControlsModule.class,
- CoroutinesModule.class,
DemoModeModule.class,
DeviceEntryModule.class,
DisableFlagsModule.class,
@@ -194,6 +194,7 @@
FlagsModule.class,
FlagDependenciesModule.class,
FooterActionsModule.class,
+ InputMethodModule.class,
KeyEventRepositoryModule.class,
KeyboardModule.class,
KeyguardBlueprintModule.class,
@@ -229,6 +230,7 @@
StatusBarWindowModule.class,
SystemPropertiesFlagsModule.class,
SysUIConcurrencyModule.class,
+ SysUICoroutinesModule.class,
SysUIUnfoldModule.class,
TelephonyRepositoryModule.class,
TemporaryDisplayModule.class,
diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/ui/binder/LiftToRunFaceAuthBinder.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/ui/binder/LiftToRunFaceAuthBinder.kt
new file mode 100644
index 0000000..1fd7d00
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/deviceentry/ui/binder/LiftToRunFaceAuthBinder.kt
@@ -0,0 +1,143 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS 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.ui.binder
+
+import android.content.pm.PackageManager
+import android.hardware.Sensor
+import android.hardware.TriggerEvent
+import android.hardware.TriggerEventListener
+import com.android.keyguard.ActiveUnlockConfig
+import com.android.keyguard.KeyguardUpdateMonitor
+import com.android.systemui.CoreStartable
+import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor
+import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFaceAuthInteractor
+import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
+import com.android.systemui.power.domain.interactor.PowerInteractor
+import com.android.systemui.util.Assert
+import com.android.systemui.util.sensors.AsyncSensorManager
+import java.io.PrintWriter
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.filterNot
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.launch
+
+/**
+ * Triggers face auth and active unlock on lift when the device is showing the lock screen or
+ * bouncer. Only initialized if face auth is supported on the device. Not to be confused with the
+ * lift to wake gesture which is handled by {@link com.android.server.policy.PhoneWindowManager}.
+ */
+@SysUISingleton
+class LiftToRunFaceAuthBinder
+@Inject
+constructor(
+ @Application private val scope: CoroutineScope,
+ private val packageManager: PackageManager,
+ private val asyncSensorManager: AsyncSensorManager,
+ private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
+ keyguardInteractor: KeyguardInteractor,
+ primaryBouncerInteractor: PrimaryBouncerInteractor,
+ alternateBouncerInteractor: AlternateBouncerInteractor,
+ private val deviceEntryFaceAuthInteractor: DeviceEntryFaceAuthInteractor,
+ powerInteractor: PowerInteractor,
+) : CoreStartable {
+
+ private var pickupSensor: Sensor? = null
+ private val isListening: MutableStateFlow<Boolean> = MutableStateFlow(false)
+ private val stoppedListening: Flow<Unit> = isListening.filterNot { it }.map {} // map to Unit
+
+ private val onAwakeKeyguard: Flow<Boolean> =
+ combine(
+ powerInteractor.isInteractive,
+ keyguardInteractor.isKeyguardVisible,
+ ) { isInteractive, isKeyguardVisible ->
+ isInteractive && isKeyguardVisible
+ }
+ private val bouncerShowing: Flow<Boolean> =
+ combine(
+ primaryBouncerInteractor.isShowing,
+ alternateBouncerInteractor.isVisible,
+ ) { primaryBouncerShowing, alternateBouncerShowing ->
+ primaryBouncerShowing || alternateBouncerShowing
+ }
+ private val listenForPickupSensor: Flow<Boolean> =
+ combine(
+ stoppedListening,
+ bouncerShowing,
+ onAwakeKeyguard,
+ ) { _, bouncerShowing, onAwakeKeyguard ->
+ (onAwakeKeyguard || bouncerShowing) &&
+ deviceEntryFaceAuthInteractor.isFaceAuthEnabledAndEnrolled()
+ }
+
+ override fun start() {
+ if (packageManager.hasSystemFeature(PackageManager.FEATURE_FACE)) {
+ init()
+ }
+ }
+
+ private fun init() {
+ pickupSensor = asyncSensorManager.getDefaultSensor(Sensor.TYPE_PICK_UP_GESTURE)
+ scope.launch {
+ listenForPickupSensor.collect { listenForPickupSensor ->
+ updateListeningState(listenForPickupSensor)
+ }
+ }
+ }
+
+ private val listener: TriggerEventListener =
+ object : TriggerEventListener() {
+ override fun onTrigger(event: TriggerEvent?) {
+ Assert.isMainThread()
+ deviceEntryFaceAuthInteractor.onDeviceLifted()
+ keyguardUpdateMonitor.requestActiveUnlock(
+ ActiveUnlockConfig.ActiveUnlockRequestOrigin.WAKE,
+ "KeyguardLiftController"
+ )
+
+ // Not listening anymore since trigger events unregister themselves
+ isListening.value = false
+ }
+ }
+
+ override fun dump(pw: PrintWriter, args: Array<out String>) {
+ pw.println("LiftToRunFaceAuthBinder:")
+ pw.println(" pickupSensor: $pickupSensor")
+ pw.println(" isListening: ${isListening.value}")
+ }
+
+ private fun updateListeningState(shouldListen: Boolean) {
+ if (pickupSensor == null) {
+ return
+ }
+ if (shouldListen != isListening.value) {
+ isListening.value = shouldListen
+
+ if (shouldListen) {
+ asyncSensorManager.requestTriggerSensor(listener, pickupSensor)
+ } else {
+ asyncSensorManager.cancelTriggerSensor(listener, pickupSensor)
+ }
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamModule.java b/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamModule.java
index 0656933..ba74742 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamModule.java
@@ -17,6 +17,7 @@
package com.android.systemui.dreams.dagger;
import android.annotation.Nullable;
+import android.app.Service;
import android.content.ComponentName;
import android.content.Context;
import android.content.pm.PackageManager;
@@ -30,12 +31,18 @@
import com.android.systemui.dreams.DreamOverlayNotificationCountProvider;
import com.android.systemui.dreams.DreamOverlayService;
import com.android.systemui.dreams.complication.dagger.ComplicationComponent;
+import com.android.systemui.dreams.homecontrols.DreamActivityProvider;
+import com.android.systemui.dreams.homecontrols.DreamActivityProviderImpl;
+import com.android.systemui.dreams.homecontrols.HomeControlsDreamService;
import com.android.systemui.dreams.touch.scrim.dagger.ScrimModule;
import com.android.systemui.res.R;
import com.android.systemui.touch.TouchInsetManager;
+import dagger.Binds;
import dagger.Module;
import dagger.Provides;
+import dagger.multibindings.ClassKey;
+import dagger.multibindings.IntoMap;
import java.util.Optional;
import java.util.concurrent.Executor;
@@ -88,6 +95,15 @@
}
/**
+ * Provides Home Controls Dream Service
+ */
+ @Binds
+ @IntoMap
+ @ClassKey(HomeControlsDreamService.class)
+ Service bindHomeControlsDreamService(
+ HomeControlsDreamService service);
+
+ /**
* Provides a touch inset manager for dreams.
*/
@Provides
@@ -151,4 +167,9 @@
static String providesDreamOverlayWindowTitle(@Main Resources resources) {
return resources.getString(R.string.app_label);
}
+
+ /** Provides activity for dream service */
+ @Binds
+ DreamActivityProvider bindActivityProvider(DreamActivityProviderImpl impl);
+
}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/DreamActivityProvider.kt b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/DreamActivityProvider.kt
new file mode 100644
index 0000000..b35b7f5
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/DreamActivityProvider.kt
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.systemui.dreams.homecontrols
+
+import android.app.Activity
+import android.service.dreams.DreamService
+
+fun interface DreamActivityProvider {
+ /**
+ * Provides abstraction for getting the activity associated with a dream service, so that the
+ * activity can be mocked in tests.
+ */
+ fun getActivity(dreamService: DreamService): Activity?
+}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/DreamActivityProviderImpl.kt b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/DreamActivityProviderImpl.kt
new file mode 100644
index 0000000..0854e93
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/DreamActivityProviderImpl.kt
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.systemui.dreams.homecontrols
+
+import android.app.Activity
+import android.service.dreams.DreamService
+import javax.inject.Inject
+
+class DreamActivityProviderImpl @Inject constructor() : DreamActivityProvider {
+ override fun getActivity(dreamService: DreamService): Activity {
+ return dreamService.activity
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/HomeControlsDreamService.kt b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/HomeControlsDreamService.kt
new file mode 100644
index 0000000..e04a505
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/HomeControlsDreamService.kt
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.dreams.homecontrols
+
+import android.content.Intent
+import android.service.controls.ControlsProviderService
+import android.service.dreams.DreamService
+import android.window.TaskFragmentInfo
+import com.android.systemui.controls.settings.ControlsSettingsRepository
+import com.android.systemui.dreams.DreamLogger
+import com.android.systemui.dreams.homecontrols.domain.interactor.HomeControlsComponentInteractor
+import com.android.systemui.log.LogBuffer
+import com.android.systemui.log.dagger.DreamLog
+import javax.inject.Inject
+
+class HomeControlsDreamService
+@Inject
+constructor(
+ private val controlsSettingsRepository: ControlsSettingsRepository,
+ private val taskFragmentFactory: TaskFragmentComponent.Factory,
+ private val homeControlsComponentInteractor: HomeControlsComponentInteractor,
+ private val dreamActivityProvider: DreamActivityProvider,
+ @DreamLog logBuffer: LogBuffer
+) : DreamService() {
+ private lateinit var taskFragmentComponent: TaskFragmentComponent
+
+ private val logger = DreamLogger(logBuffer, "HomeControlsDreamService")
+
+ override fun onAttachedToWindow() {
+ super.onAttachedToWindow()
+ val activity = dreamActivityProvider.getActivity(this)
+ if (activity == null) {
+ finish()
+ return
+ }
+ taskFragmentComponent =
+ taskFragmentFactory
+ .create(
+ activity = activity,
+ onCreateCallback = this::onTaskFragmentCreated,
+ onInfoChangedCallback = this::onTaskFragmentInfoChanged,
+ hide = { finish() }
+ )
+ .apply { createTaskFragment() }
+ }
+
+ private fun onTaskFragmentInfoChanged(taskFragmentInfo: TaskFragmentInfo) {
+ if (taskFragmentInfo.isEmpty) {
+ logger.d("Finishing dream due to TaskFragment being empty")
+ finish()
+ }
+ }
+
+ private fun onTaskFragmentCreated(taskFragmentInfo: TaskFragmentInfo) {
+ val setting = controlsSettingsRepository.allowActionOnTrivialControlsInLockscreen.value
+ val componentName = homeControlsComponentInteractor.panelComponent.value
+ logger.d("Starting embedding $componentName")
+ val intent =
+ Intent().apply {
+ component = componentName
+ putExtra(ControlsProviderService.EXTRA_LOCKSCREEN_ALLOW_TRIVIAL_CONTROLS, setting)
+ putExtra(
+ ControlsProviderService.EXTRA_CONTROLS_SURFACE,
+ ControlsProviderService.CONTROLS_SURFACE_DREAM
+ )
+ }
+ taskFragmentComponent.startActivityInTaskFragment(intent)
+ }
+
+ override fun onDetachedFromWindow() {
+ super.onDetachedFromWindow()
+ taskFragmentComponent.destroy()
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/HomeControlsDreamStartable.kt b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/HomeControlsDreamStartable.kt
new file mode 100644
index 0000000..6cd94c6
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/HomeControlsDreamStartable.kt
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.dreams.homecontrols
+
+import android.content.ComponentName
+import android.content.Context
+import android.content.pm.PackageManager
+import android.service.controls.flags.Flags.homePanelDream
+import com.android.systemui.CoreStartable
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.dreams.homecontrols.domain.interactor.HomeControlsComponentInteractor
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+
+class HomeControlsDreamStartable
+@Inject
+constructor(
+ private val context: Context,
+ private val packageManager: PackageManager,
+ private val homeControlsComponentInteractor: HomeControlsComponentInteractor,
+ @Background private val bgScope: CoroutineScope,
+) : CoreStartable {
+
+ private val componentName = ComponentName(context, HomeControlsDreamService::class.java)
+
+ override fun start() {
+ if (!homePanelDream()) return
+ bgScope.launch {
+ homeControlsComponentInteractor.panelComponent.collect { selectedPanelComponent ->
+ setEnableHomeControlPanel(selectedPanelComponent != null)
+ }
+ }
+ }
+
+ private fun setEnableHomeControlPanel(enabled: Boolean) {
+ val packageState =
+ if (enabled) {
+ PackageManager.COMPONENT_ENABLED_STATE_ENABLED
+ } else {
+ PackageManager.COMPONENT_ENABLED_STATE_DISABLED
+ }
+ packageManager.setComponentEnabledSetting(
+ componentName,
+ packageState,
+ PackageManager.DONT_KILL_APP
+ )
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/TaskFragmentComponent.kt b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/TaskFragmentComponent.kt
new file mode 100644
index 0000000..6f7dcb1
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/TaskFragmentComponent.kt
@@ -0,0 +1,166 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS 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.dreams.homecontrols
+
+import android.app.Activity
+import android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW
+import android.content.Intent
+import android.graphics.Rect
+import android.os.Binder
+import android.window.TaskFragmentCreationParams
+import android.window.TaskFragmentInfo
+import android.window.TaskFragmentOperation
+import android.window.TaskFragmentOperation.OP_TYPE_DELETE_TASK_FRAGMENT
+import android.window.TaskFragmentOperation.OP_TYPE_REORDER_TO_TOP_OF_TASK
+import android.window.TaskFragmentOrganizer
+import android.window.TaskFragmentOrganizer.TASK_FRAGMENT_TRANSIT_CHANGE
+import android.window.TaskFragmentOrganizer.TASK_FRAGMENT_TRANSIT_CLOSE
+import android.window.TaskFragmentOrganizer.TASK_FRAGMENT_TRANSIT_OPEN
+import android.window.TaskFragmentTransaction
+import android.window.TaskFragmentTransaction.TYPE_ACTIVITY_REPARENTED_TO_TASK
+import android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_APPEARED
+import android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_ERROR
+import android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_INFO_CHANGED
+import android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_PARENT_INFO_CHANGED
+import android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_VANISHED
+import android.window.WindowContainerTransaction
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.util.concurrency.DelayableExecutor
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+
+typealias FragmentInfoCallback = (TaskFragmentInfo) -> Unit
+
+/** Wrapper around TaskFragmentOrganizer for managing a task fragment within an activity */
+class TaskFragmentComponent
+@AssistedInject
+constructor(
+ @Assisted private val activity: Activity,
+ @Assisted("onCreateCallback") private val onCreateCallback: FragmentInfoCallback,
+ @Assisted("onInfoChangedCallback") private val onInfoChangedCallback: FragmentInfoCallback,
+ @Assisted private val hide: () -> Unit,
+ @Main private val executor: DelayableExecutor,
+) {
+
+ @AssistedFactory
+ fun interface Factory {
+ fun create(
+ activity: Activity,
+ @Assisted("onCreateCallback") onCreateCallback: FragmentInfoCallback,
+ @Assisted("onInfoChangedCallback") onInfoChangedCallback: FragmentInfoCallback,
+ hide: () -> Unit
+ ): TaskFragmentComponent
+ }
+
+ private val fragmentToken = Binder()
+ private val organizer: TaskFragmentOrganizer =
+ object : TaskFragmentOrganizer(executor) {
+
+ override fun onTransactionReady(transaction: TaskFragmentTransaction) {
+ handleTransactionReady(transaction)
+ }
+ }
+ .apply { registerOrganizer(true /* isSystemOrganizer */) }
+
+ private fun handleTransactionReady(transaction: TaskFragmentTransaction) {
+ val resultT = WindowContainerTransaction()
+
+ for (change in transaction.changes) {
+ change.taskFragmentInfo?.let { taskFragmentInfo ->
+ if (taskFragmentInfo.fragmentToken == fragmentToken) {
+ when (change.type) {
+ TYPE_TASK_FRAGMENT_APPEARED -> {
+ resultT.addTaskFragmentOperation(
+ fragmentToken,
+ TaskFragmentOperation.Builder(OP_TYPE_REORDER_TO_TOP_OF_TASK)
+ .build()
+ )
+
+ onCreateCallback(taskFragmentInfo)
+ }
+ TYPE_TASK_FRAGMENT_INFO_CHANGED -> {
+ onInfoChangedCallback(taskFragmentInfo)
+ }
+ TYPE_TASK_FRAGMENT_VANISHED -> {
+ hide()
+ }
+ TYPE_TASK_FRAGMENT_PARENT_INFO_CHANGED -> {}
+ TYPE_TASK_FRAGMENT_ERROR -> {
+ hide()
+ }
+ TYPE_ACTIVITY_REPARENTED_TO_TASK -> {}
+ else ->
+ throw IllegalArgumentException(
+ "Unknown TaskFragmentEvent=" + change.type
+ )
+ }
+ }
+ }
+ }
+ organizer.onTransactionHandled(
+ transaction.transactionToken,
+ resultT,
+ TASK_FRAGMENT_TRANSIT_CHANGE,
+ false
+ )
+ }
+
+ /** Creates the task fragment */
+ fun createTaskFragment() {
+ val taskBounds = Rect(activity.resources.configuration.windowConfiguration.bounds)
+ val fragmentOptions =
+ TaskFragmentCreationParams.Builder(
+ organizer.organizerToken,
+ fragmentToken,
+ activity.activityToken!!
+ )
+ .setInitialRelativeBounds(taskBounds)
+ .setWindowingMode(WINDOWING_MODE_MULTI_WINDOW)
+ .build()
+ organizer.applyTransaction(
+ WindowContainerTransaction().createTaskFragment(fragmentOptions),
+ TASK_FRAGMENT_TRANSIT_CHANGE,
+ false
+ )
+ }
+
+ private fun WindowContainerTransaction.startActivity(intent: Intent) =
+ this.startActivityInTaskFragment(fragmentToken, activity.activityToken!!, intent, null)
+
+ /** Starts the provided activity in the fragment and move it to the background */
+ fun startActivityInTaskFragment(intent: Intent) {
+ organizer.applyTransaction(
+ WindowContainerTransaction().startActivity(intent),
+ TASK_FRAGMENT_TRANSIT_OPEN,
+ false
+ )
+ }
+
+ /** Destroys the task fragment */
+ fun destroy() {
+ organizer.applyTransaction(
+ WindowContainerTransaction()
+ .addTaskFragmentOperation(
+ fragmentToken,
+ TaskFragmentOperation.Builder(OP_TYPE_DELETE_TASK_FRAGMENT).build()
+ ),
+ TASK_FRAGMENT_TRANSIT_CLOSE,
+ false
+ )
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/domain/interactor/HomeControlsComponentInteractor.kt b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/domain/interactor/HomeControlsComponentInteractor.kt
new file mode 100644
index 0000000..91e0547
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/domain/interactor/HomeControlsComponentInteractor.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.dreams.homecontrols.domain.interactor
+
+import android.content.ComponentName
+import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.controls.ControlsServiceInfo
+import com.android.systemui.controls.dagger.ControlsComponent
+import com.android.systemui.controls.management.ControlsListingController
+import com.android.systemui.controls.panels.AuthorizedPanelsRepository
+import com.android.systemui.controls.panels.SelectedComponentRepository
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.user.data.repository.UserRepository
+import com.android.systemui.util.kotlin.getOrNull
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.emptyFlow
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onStart
+import kotlinx.coroutines.flow.stateIn
+
+@SysUISingleton
+@OptIn(ExperimentalCoroutinesApi::class)
+class HomeControlsComponentInteractor
+@Inject
+constructor(
+ private val selectedComponentRepository: SelectedComponentRepository,
+ private val controlsComponent: ControlsComponent,
+ private val authorizedPanelsRepository: AuthorizedPanelsRepository,
+ userRepository: UserRepository,
+ @Background private val bgScope: CoroutineScope
+) {
+ private val controlsListingController =
+ controlsComponent.getControlsListingController().getOrNull()
+
+ /** Gets the current user's selected panel, or null if there isn't one */
+ private val selectedItem: Flow<SelectedComponentRepository.SelectedComponent?> =
+ userRepository.selectedUserInfo
+ .flatMapLatest { user ->
+ selectedComponentRepository.selectedComponentFlow(user.userHandle)
+ }
+ .map { if (it?.isPanel == true) it else null }
+
+ /** Gets all the available panels which are authorized by the user */
+ private fun allPanelItem(): Flow<List<PanelComponent>> {
+ if (controlsListingController == null) {
+ return emptyFlow()
+ }
+ return conflatedCallbackFlow {
+ val listener =
+ object : ControlsListingController.ControlsListingCallback {
+ override fun onServicesUpdated(serviceInfos: List<ControlsServiceInfo>) {
+ trySend(serviceInfos)
+ }
+ }
+ controlsListingController.addCallback(listener)
+ awaitClose { controlsListingController.removeCallback(listener) }
+ }
+ .onStart { emit(controlsListingController.getCurrentServices()) }
+ .map { serviceInfos ->
+ val authorizedPanels = authorizedPanelsRepository.getAuthorizedPanels()
+ serviceInfos.mapNotNull {
+ val panelActivity = it.panelActivity
+ if (it.componentName.packageName in authorizedPanels && panelActivity != null) {
+ PanelComponent(it.componentName, panelActivity)
+ } else {
+ null
+ }
+ }
+ }
+ }
+ val panelComponent: StateFlow<ComponentName?> =
+ combine(allPanelItem(), selectedItem) { items, selected ->
+ val item =
+ items.firstOrNull { it.componentName == selected?.componentName }
+ ?: items.firstOrNull()
+ item?.panelActivity
+ }
+ .stateIn(bgScope, SharingStarted.WhileSubscribed(), null)
+
+ data class PanelComponent(val componentName: ComponentName, val panelActivity: ComponentName)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt b/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt
index 1540423..df0566e 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt
@@ -29,6 +29,8 @@
import com.android.systemui.keyguard.shared.KeyguardShadeMigrationNssl
import com.android.systemui.scene.shared.flag.SceneContainerFlag
import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor
+import com.android.systemui.statusbar.notification.interruption.VisualInterruptionRefactor
+import com.android.systemui.statusbar.notification.shared.NotificationAvalancheSuppression
import com.android.systemui.statusbar.notification.shared.NotificationIconContainerRefactor
import com.android.systemui.statusbar.notification.shared.NotificationsLiveDataStoreRefactor
import javax.inject.Inject
@@ -45,6 +47,7 @@
// Internal notification frontend dependencies
NotificationsLiveDataStoreRefactor.token dependsOn NotificationIconContainerRefactor.token
FooterViewRefactor.token dependsOn NotificationIconContainerRefactor.token
+ NotificationAvalancheSuppression.token dependsOn VisualInterruptionRefactor.token
// Internal keyguard dependencies
KeyguardShadeMigrationNssl.token dependsOn keyguardBottomAreaRefactor
diff --git a/packages/SystemUI/src/com/android/systemui/inputmethod/InputMethodModule.kt b/packages/SystemUI/src/com/android/systemui/inputmethod/InputMethodModule.kt
new file mode 100644
index 0000000..bac48f1
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/inputmethod/InputMethodModule.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.inputmethod
+
+import com.android.systemui.inputmethod.data.repository.InputMethodRepositoryModule
+import dagger.Module
+
+/** Module for providing objects exposed by the input method package. */
+@Module(
+ includes =
+ [
+ InputMethodRepositoryModule::class,
+ ],
+)
+object InputMethodModule
diff --git a/packages/SystemUI/src/com/android/systemui/inputmethod/data/model/InputMethodModel.kt b/packages/SystemUI/src/com/android/systemui/inputmethod/data/model/InputMethodModel.kt
new file mode 100644
index 0000000..bdc18b3
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/inputmethod/data/model/InputMethodModel.kt
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS 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.inputmethod.data.model
+
+/**
+ * Models an input method editor (IME).
+ *
+ * @see android.view.inputmethod.InputMethodInfo
+ */
+data class InputMethodModel(
+ /** A unique ID for this input method. */
+ val imeId: String,
+ /** The subtypes of this IME (may be empty). */
+ val subtypes: List<Subtype>,
+) {
+ /**
+ * A Subtype can describe locale (e.g. en_US, fr_FR...) and mode (e.g. voice, keyboard), and is
+ * used for IME switch and settings.
+ *
+ * @see android.view.inputmethod.InputMethodSubtype
+ */
+ data class Subtype(
+ /** A unique ID for this IME subtype. */
+ val subtypeId: Int,
+ /**
+ * Whether this subtype is auxiliary. An auxiliary subtype will not be shown in the list of
+ * enabled IMEs for choosing the current IME in Settings, but it will be shown in the list
+ * of IMEs in the IME switcher to allow the user to tentatively switch to this subtype while
+ * an IME is shown.
+ *
+ * The intent of this flag is to allow for IMEs that are invoked in a one-shot way as
+ * auxiliary input mode, and return to the previous IME once it is finished (e.g. voice
+ * input).
+ */
+ val isAuxiliary: Boolean,
+ )
+}
diff --git a/packages/SystemUI/src/com/android/systemui/inputmethod/data/repository/InputMethodRepository.kt b/packages/SystemUI/src/com/android/systemui/inputmethod/data/repository/InputMethodRepository.kt
new file mode 100644
index 0000000..5f316c4
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/inputmethod/data/repository/InputMethodRepository.kt
@@ -0,0 +1,139 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS 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.inputmethod.data.repository
+
+import android.annotation.SuppressLint
+import android.os.UserHandle
+import android.view.inputmethod.InputMethodInfo
+import android.view.inputmethod.InputMethodManager
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.inputmethod.data.model.InputMethodModel
+import dagger.Binds
+import dagger.Module
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.asFlow
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.withContext
+
+/** Provides access to input-method related application state in the bouncer. */
+interface InputMethodRepository {
+ /**
+ * Creates and returns a new `Flow` of installed input methods that are enabled for the
+ * specified user.
+ *
+ * @param fetchSubtypes Whether to fetch the IME Subtypes as well (requires an additional IPC
+ * call for each IME, avoid if not needed).
+ * @see InputMethodManager.getEnabledInputMethodListAsUser
+ */
+ suspend fun enabledInputMethods(userId: Int, fetchSubtypes: Boolean): Flow<InputMethodModel>
+
+ /** Returns enabled subtypes for the currently selected input method. */
+ suspend fun selectedInputMethodSubtypes(): List<InputMethodModel.Subtype>
+
+ /**
+ * Shows the system's input method picker dialog.
+ *
+ * @param displayId The display ID on which to show the dialog.
+ * @param showAuxiliarySubtypes Whether to show auxiliary input method subtypes in the list of
+ * enabled IMEs.
+ * @see InputMethodManager.showInputMethodPickerFromSystem
+ */
+ suspend fun showInputMethodPicker(displayId: Int, showAuxiliarySubtypes: Boolean)
+}
+
+@SysUISingleton
+class InputMethodRepositoryImpl
+@Inject
+constructor(
+ @Background private val backgroundDispatcher: CoroutineDispatcher,
+ private val inputMethodManager: InputMethodManager,
+) : InputMethodRepository {
+
+ override suspend fun enabledInputMethods(
+ userId: Int,
+ fetchSubtypes: Boolean
+ ): Flow<InputMethodModel> {
+ return withContext(backgroundDispatcher) {
+ inputMethodManager.getEnabledInputMethodListAsUser(UserHandle.of(userId))
+ }
+ .asFlow()
+ .map { inputMethodInfo ->
+ InputMethodModel(
+ imeId = inputMethodInfo.id,
+ subtypes =
+ if (fetchSubtypes) {
+ enabledInputMethodSubtypes(
+ inputMethodInfo,
+ allowsImplicitlyEnabledSubtypes = true
+ )
+ } else {
+ listOf()
+ }
+ )
+ }
+ }
+
+ override suspend fun selectedInputMethodSubtypes(): List<InputMethodModel.Subtype> {
+ return enabledInputMethodSubtypes(
+ inputMethodInfo = null, // Fetch subtypes for the currently-selected IME.
+ allowsImplicitlyEnabledSubtypes = false
+ )
+ }
+
+ @SuppressLint("MissingPermission")
+ override suspend fun showInputMethodPicker(displayId: Int, showAuxiliarySubtypes: Boolean) {
+ withContext(backgroundDispatcher) {
+ inputMethodManager.showInputMethodPickerFromSystem(showAuxiliarySubtypes, displayId)
+ }
+ }
+
+ /**
+ * Returns a list of enabled input method subtypes for the specified input method info.
+ *
+ * @param inputMethodInfo The [InputMethodInfo] whose subtypes list will be returned. If `null`,
+ * returns enabled subtypes for the currently selected [InputMethodInfo].
+ * @param allowsImplicitlyEnabledSubtypes Whether to allow to return the implicitly enabled
+ * subtypes. If an input method info doesn't have enabled subtypes, the framework will
+ * implicitly enable subtypes according to the current system language.
+ * @see InputMethodManager.getEnabledInputMethodSubtypeList
+ */
+ private suspend fun enabledInputMethodSubtypes(
+ inputMethodInfo: InputMethodInfo?,
+ allowsImplicitlyEnabledSubtypes: Boolean
+ ): List<InputMethodModel.Subtype> {
+ return withContext(backgroundDispatcher) {
+ inputMethodManager.getEnabledInputMethodSubtypeList(
+ inputMethodInfo,
+ allowsImplicitlyEnabledSubtypes
+ )
+ }
+ .map {
+ InputMethodModel.Subtype(
+ subtypeId = it.subtypeId,
+ isAuxiliary = it.isAuxiliary,
+ )
+ }
+ }
+}
+
+@Module
+interface InputMethodRepositoryModule {
+ @Binds fun repository(impl: InputMethodRepositoryImpl): InputMethodRepository
+}
diff --git a/packages/SystemUI/src/com/android/systemui/inputmethod/domain/interactor/InputMethodInteractor.kt b/packages/SystemUI/src/com/android/systemui/inputmethod/domain/interactor/InputMethodInteractor.kt
new file mode 100644
index 0000000..c54aa7f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/inputmethod/domain/interactor/InputMethodInteractor.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.inputmethod.domain.interactor
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.inputmethod.data.repository.InputMethodRepository
+import javax.inject.Inject
+import kotlinx.coroutines.flow.count
+import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.take
+
+/** Hosts application business logic related to input methods (e.g. software keyboard). */
+@SysUISingleton
+class InputMethodInteractor
+@Inject
+constructor(
+ private val repository: InputMethodRepository,
+) {
+ /**
+ * Returns whether there are multiple enabled input methods to choose from for password input.
+ *
+ * Method adapted from `com.android.inputmethod.latin.Utils`.
+ */
+ suspend fun hasMultipleEnabledImesOrSubtypes(userId: Int): Boolean {
+ // Count IMEs that either have no subtypes, or have at least one non-auxiliary subtype.
+ val matchingInputMethods =
+ repository
+ .enabledInputMethods(userId, fetchSubtypes = true)
+ .filter { ime -> ime.subtypes.isEmpty() || ime.subtypes.any { !it.isAuxiliary } }
+ .take(2) // Short-circuit if we find at least 2 matching IMEs.
+
+ return matchingInputMethods.count() > 1 || repository.selectedInputMethodSubtypes().size > 1
+ }
+
+ /** Shows the system's input method picker dialog. */
+ suspend fun showInputMethodPicker(displayId: Int, showAuxiliarySubtypes: Boolean) {
+ repository.showInputMethodPicker(displayId, showAuxiliarySubtypes)
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/PhysicalKeyboardCoreStartable.kt b/packages/SystemUI/src/com/android/systemui/keyboard/PhysicalKeyboardCoreStartable.kt
index 26e83a3..f16a3bd 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/PhysicalKeyboardCoreStartable.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/PhysicalKeyboardCoreStartable.kt
@@ -17,7 +17,7 @@
package com.android.systemui.keyboard
-import com.android.hardware.input.Flags.keyboardA11yStickyKeysFlag
+import android.hardware.input.InputSettings
import com.android.systemui.CoreStartable
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.flags.FeatureFlags
@@ -40,7 +40,7 @@
if (featureFlags.isEnabled(Flags.KEYBOARD_BACKLIGHT_INDICATOR)) {
keyboardBacklightDialogCoordinator.get().startListening()
}
- if (keyboardA11yStickyKeysFlag()) {
+ if (InputSettings.isAccessibilityStickyKeysFeatureEnabled()) {
stickyKeysIndicatorCoordinator.get().startListening()
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java
index f10b87e..b5f9c69 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java
@@ -85,10 +85,10 @@
import com.android.systemui.power.domain.interactor.PowerInteractor;
import com.android.systemui.power.shared.model.ScreenPowerState;
import com.android.systemui.settings.DisplayTracker;
+import com.android.wm.shell.shared.CounterRotator;
+import com.android.wm.shell.shared.TransitionUtil;
import com.android.wm.shell.transition.ShellTransitions;
import com.android.wm.shell.transition.Transitions;
-import com.android.wm.shell.util.CounterRotator;
-import com.android.wm.shell.util.TransitionUtil;
import java.util.ArrayList;
import java.util.Map;
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt
index afef875..7f43fac 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt
@@ -41,6 +41,7 @@
import com.android.systemui.keyguard.ui.view.layout.KeyguardBlueprintCommandListener
import com.android.systemui.keyguard.ui.viewmodel.AodAlphaViewModel
import com.android.systemui.keyguard.ui.viewmodel.KeyguardBlueprintViewModel
+import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel
import com.android.systemui.keyguard.ui.viewmodel.KeyguardIndicationAreaViewModel
import com.android.systemui.keyguard.ui.viewmodel.KeyguardRootViewModel
import com.android.systemui.keyguard.ui.viewmodel.OccludingAppDeviceEntryMessageViewModel
@@ -86,6 +87,7 @@
private val vibratorHelper: VibratorHelper,
private val falsingManager: FalsingManager,
private val aodAlphaViewModel: AodAlphaViewModel,
+ private val keyguardClockViewModel: KeyguardClockViewModel,
) : CoreStartable {
private var rootViewHandle: DisposableHandle? = null
@@ -113,7 +115,11 @@
initializeViews()
if (!SceneContainerFlag.isEnabled) {
- KeyguardBlueprintViewBinder.bind(keyguardRootView, keyguardBlueprintViewModel)
+ KeyguardBlueprintViewBinder.bind(
+ keyguardRootView,
+ keyguardBlueprintViewModel,
+ keyguardClockViewModel
+ )
}
keyguardBlueprintCommandListener.start()
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java b/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java
index 13e3835..e16f8dc 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java
@@ -51,7 +51,7 @@
import com.android.systemui.keyguard.KeyguardViewMediator;
import com.android.systemui.keyguard.WindowManagerLockscreenVisibilityManager;
import com.android.systemui.keyguard.data.quickaffordance.KeyguardDataQuickAffordanceModule;
-import com.android.systemui.keyguard.data.repository.KeyguardFaceAuthModule;
+import com.android.systemui.keyguard.data.repository.DeviceEntryFaceAuthModule;
import com.android.systemui.keyguard.data.repository.KeyguardRepositoryModule;
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor;
import com.android.systemui.keyguard.domain.interactor.StartKeyguardTransitionModule;
@@ -104,7 +104,7 @@
FalsingModule.class,
KeyguardDataQuickAffordanceModule.class,
KeyguardRepositoryModule.class,
- KeyguardFaceAuthModule.class,
+ DeviceEntryFaceAuthModule.class,
KeyguardDisplayModule.class,
StartKeyguardTransitionModule.class,
ResourceTrimmerModule.class,
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardFaceAuthModule.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthModule.kt
similarity index 85%
rename from packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardFaceAuthModule.kt
rename to packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthModule.kt
index fede479..4cd544f 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardFaceAuthModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthModule.kt
@@ -23,6 +23,7 @@
import com.android.systemui.deviceentry.data.repository.DeviceEntryFaceAuthRepositoryImpl
import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFaceAuthInteractor
import com.android.systemui.deviceentry.domain.interactor.SystemUIDeviceEntryFaceAuthInteractor
+import com.android.systemui.deviceentry.ui.binder.LiftToRunFaceAuthBinder
import com.android.systemui.log.table.TableLogBuffer
import com.android.systemui.log.table.TableLogBufferFactory
import dagger.Binds
@@ -32,7 +33,7 @@
import dagger.multibindings.IntoMap
@Module
-interface KeyguardFaceAuthModule {
+interface DeviceEntryFaceAuthModule {
@Binds
fun deviceEntryFaceAuthRepository(
impl: DeviceEntryFaceAuthRepositoryImpl
@@ -41,13 +42,20 @@
@Binds
@IntoMap
@ClassKey(SystemUIDeviceEntryFaceAuthInteractor::class)
- fun bind(impl: SystemUIDeviceEntryFaceAuthInteractor): CoreStartable
+ fun bindSystemUIDeviceEntryFaceAuthInteractor(
+ impl: SystemUIDeviceEntryFaceAuthInteractor
+ ): CoreStartable
@Binds
fun keyguardFaceAuthInteractor(
impl: SystemUIDeviceEntryFaceAuthInteractor
): DeviceEntryFaceAuthInteractor
+ @Binds
+ @IntoMap
+ @ClassKey(LiftToRunFaceAuthBinder::class)
+ fun bindLiftToRunFaceAuthBinder(impl: LiftToRunFaceAuthBinder): CoreStartable
+
companion object {
@Provides
@SysUISingleton
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardBlueprintRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardBlueprintRepository.kt
index 48b6634..9381830 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardBlueprintRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardBlueprintRepository.kt
@@ -23,6 +23,8 @@
import com.android.systemui.keyguard.shared.model.KeyguardBlueprint
import com.android.systemui.keyguard.ui.view.layout.blueprints.DefaultKeyguardBlueprint.Companion.DEFAULT
import com.android.systemui.keyguard.ui.view.layout.blueprints.KeyguardBlueprintModule
+import com.android.systemui.keyguard.ui.view.layout.blueprints.transitions.IntraBlueprintTransitionType
+import com.android.systemui.keyguard.ui.view.layout.blueprints.transitions.IntraBlueprintTransitionType.DefaultTransition
import java.io.PrintWriter
import java.util.TreeMap
import javax.inject.Inject
@@ -54,6 +56,8 @@
TreeMap<String, KeyguardBlueprint>().apply { putAll(blueprints.associateBy { it.id }) }
val blueprint: MutableStateFlow<KeyguardBlueprint> = MutableStateFlow(blueprintIdMap[DEFAULT]!!)
val refreshBluePrint: MutableSharedFlow<Unit> = MutableSharedFlow(extraBufferCapacity = 1)
+ val refreshBlueprintTransition: MutableSharedFlow<IntraBlueprintTransitionType> =
+ MutableSharedFlow(extraBufferCapacity = 1)
val configurationChange: Flow<Unit> = configurationRepository.onAnyConfigurationChange
/**
@@ -103,7 +107,12 @@
/** Re-emits the last emitted blueprint value if possible. */
fun refreshBlueprint() {
+ refreshBlueprintWithTransition(DefaultTransition)
+ }
+
+ fun refreshBlueprintWithTransition(type: IntraBlueprintTransitionType = DefaultTransition) {
refreshBluePrint.tryEmit(Unit)
+ refreshBlueprintTransition.tryEmit(type)
}
/** Prints all available blueprints to the PrintWriter. */
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractor.kt
index 9d38be9..71d941a 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractor.kt
@@ -28,6 +28,7 @@
import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.keyguard.shared.model.TransitionModeOnCanceled
import com.android.systemui.power.domain.interactor.PowerInteractor
+import com.android.systemui.util.kotlin.Utils.Companion.sample as sampleMultiple
import com.android.systemui.util.kotlin.sample
import javax.inject.Inject
import kotlin.time.Duration.Companion.milliseconds
@@ -64,6 +65,7 @@
listenForHubToAlternateBouncer()
listenForHubToOccluded()
listenForHubToGone()
+ listenForHubToDreaming()
}
override fun getDefaultAnimatorForTransitionsToState(toState: KeyguardState): ValueAnimator {
@@ -131,6 +133,23 @@
}
}
+ private fun listenForHubToDreaming() {
+ val invalidFromStates = setOf(KeyguardState.AOD, KeyguardState.DOZING)
+ scope.launch("$TAG#listenForHubToDreaming") {
+ keyguardInteractor.isAbleToDream
+ .sampleMultiple(startedKeyguardTransitionStep, finishedKeyguardState)
+ .collect { (isAbleToDream, lastStartedTransition, finishedKeyguardState) ->
+ val isOnHub = finishedKeyguardState == KeyguardState.GLANCEABLE_HUB
+ val isTransitionInterruptible =
+ lastStartedTransition.to == KeyguardState.GLANCEABLE_HUB &&
+ !invalidFromStates.contains(lastStartedTransition.from)
+ if (isAbleToDream && (isOnHub || isTransitionInterruptible)) {
+ startTransitionTo(KeyguardState.DREAMING)
+ }
+ }
+ }
+ }
+
private fun listenForHubToOccluded() {
scope.launch {
keyguardInteractor.isKeyguardOccluded.sample(startedKeyguardState, ::Pair).collect {
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 3965648..deb70b7 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
@@ -405,7 +405,9 @@
interpolator = Interpolators.LINEAR
duration =
when (toState) {
- KeyguardState.DREAMING -> TO_DREAMING_DURATION
+ // Adds 100ms to the overall delay to workaround legacy setOccluded calls
+ // being delayed in KeyguardViewMediator
+ KeyguardState.DREAMING -> TO_DREAMING_DURATION + 100.milliseconds
KeyguardState.OCCLUDED -> TO_OCCLUDED_DURATION
KeyguardState.AOD -> TO_AOD_DURATION
KeyguardState.DOZING -> TO_DOZING_DURATION
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractor.kt
index ba44e68..566e006 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractor.kt
@@ -24,12 +24,13 @@
import com.android.systemui.keyguard.shared.model.KeyguardBlueprint
import com.android.systemui.keyguard.ui.view.layout.blueprints.DefaultKeyguardBlueprint
import com.android.systemui.keyguard.ui.view.layout.blueprints.SplitShadeKeyguardBlueprint
+import com.android.systemui.keyguard.ui.view.layout.blueprints.transitions.IntraBlueprintTransitionType
+import com.android.systemui.keyguard.ui.view.layout.blueprints.transitions.IntraBlueprintTransitionType.DefaultTransition
import com.android.systemui.statusbar.policy.SplitShadeStateController
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.merge
+import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.launch
@@ -48,13 +49,15 @@
*
* This flow can also emit the same blueprint value if refreshBlueprint is emitted.
*/
- val blueprint: Flow<KeyguardBlueprint> =
- merge(
- keyguardBlueprintRepository.blueprint,
- keyguardBlueprintRepository.refreshBluePrint.map {
- keyguardBlueprintRepository.blueprint.value
- }
- )
+ val blueprint: Flow<KeyguardBlueprint> = keyguardBlueprintRepository.blueprint
+
+ val blueprintWithTransition =
+ combine(
+ keyguardBlueprintRepository.refreshBluePrint,
+ keyguardBlueprintRepository.refreshBlueprintTransition
+ ) { _, source ->
+ source
+ }
init {
applicationScope.launch {
@@ -107,6 +110,10 @@
keyguardBlueprintRepository.refreshBlueprint()
}
+ fun refreshBlueprintWithTransition(type: IntraBlueprintTransitionType = DefaultTransition) {
+ keyguardBlueprintRepository.refreshBlueprintWithTransition(type)
+ }
+
fun getCurrentBlueprint(): KeyguardBlueprint {
return keyguardBlueprintRepository.blueprint.value
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt
index 3888345..a1f9425 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt
@@ -56,6 +56,7 @@
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
@@ -102,10 +103,10 @@
quickAffordanceAlwaysVisible(position),
keyguardInteractor.isDozing,
keyguardInteractor.isKeyguardShowing,
- shadeInteractor.anyExpansion,
+ shadeInteractor.anyExpansion.map { it < 1.0f }.distinctUntilChanged(),
biometricSettingsRepository.isCurrentUserInLockdown,
- ) { affordance, isDozing, isKeyguardShowing, qsExpansion, isUserInLockdown ->
- if (!isDozing && isKeyguardShowing && (qsExpansion < 1.0f) && !isUserInLockdown) {
+ ) { affordance, isDozing, isKeyguardShowing, isQuickSettingsVisible, isUserInLockdown ->
+ if (!isDozing && isKeyguardShowing && isQuickSettingsVisible && !isUserInLockdown) {
affordance
} else {
KeyguardQuickAffordanceModel.Hidden
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/Edge.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/Edge.kt
new file mode 100644
index 0000000..a0f9be6
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/Edge.kt
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.systemui.keyguard.shared.model
+
+/** FROM -> TO keyguard transition. null values are allowed to signify FROM -> *, or * -> TO */
+data class Edge(
+ val from: KeyguardState?,
+ val to: KeyguardState?,
+)
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlow.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlow.kt
index 4abda74..00b7989 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlow.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlow.kt
@@ -22,6 +22,7 @@
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
+import com.android.systemui.keyguard.shared.model.Edge
import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.keyguard.shared.model.TransitionState
import com.android.systemui.keyguard.shared.model.TransitionState.CANCELED
@@ -174,11 +175,6 @@
}
}
- data class Edge(
- val from: KeyguardState?,
- val to: KeyguardState?,
- )
-
data class StateToValue(
val transitionState: TransitionState,
val value: Float?,
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 703bb87..873cc84 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
@@ -19,6 +19,7 @@
import android.annotation.SuppressLint
import android.content.res.ColorStateList
+import android.util.StateSet
import android.view.HapticFeedbackConstants
import android.view.View
import androidx.lifecycle.Lifecycle
@@ -113,6 +114,8 @@
fgIconView.repeatWhenAttached {
repeatOnLifecycle(Lifecycle.State.STARTED) {
+ // Start with an empty state
+ fgIconView.setImageState(StateSet.NOTHING, /* merge */ false)
launch {
fgViewModel.viewModel.collect { viewModel ->
fgIconView.setImageState(
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBlueprintViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBlueprintViewBinder.kt
index 8d6493f..404046b 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBlueprintViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBlueprintViewBinder.kt
@@ -26,7 +26,10 @@
import androidx.lifecycle.repeatOnLifecycle
import com.android.systemui.Flags.keyguardBottomAreaRefactor
import com.android.systemui.keyguard.ui.view.layout.blueprints.transitions.BaseBlueprintTransition
+import com.android.systemui.keyguard.ui.view.layout.blueprints.transitions.IntraBlueprintTransition
+import com.android.systemui.keyguard.ui.view.layout.blueprints.transitions.IntraBlueprintTransitionType
import com.android.systemui.keyguard.ui.viewmodel.KeyguardBlueprintViewModel
+import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel
import com.android.systemui.lifecycle.repeatWhenAttached
import kotlinx.coroutines.launch
@@ -34,7 +37,11 @@
companion object {
private const val TAG = "KeyguardBlueprintViewBinder"
- fun bind(constraintLayout: ConstraintLayout, viewModel: KeyguardBlueprintViewModel) {
+ fun bind(
+ constraintLayout: ConstraintLayout,
+ viewModel: KeyguardBlueprintViewModel,
+ clockViewModel: KeyguardClockViewModel
+ ) {
constraintLayout.repeatWhenAttached {
repeatOnLifecycle(Lifecycle.State.CREATED) {
launch {
@@ -42,6 +49,7 @@
val prevBluePrint = viewModel.currentBluePrint
Trace.beginSection("KeyguardBlueprint#applyBlueprint")
Log.d(TAG, "applying blueprint: $blueprint")
+ TransitionManager.endTransitions(constraintLayout)
val cs =
ConstraintSet().apply {
@@ -54,11 +62,28 @@
}
// Apply transition.
- if (!keyguardBottomAreaRefactor() && prevBluePrint != null &&
- prevBluePrint != blueprint) {
+ if (
+ !keyguardBottomAreaRefactor() &&
+ prevBluePrint != null &&
+ prevBluePrint != blueprint
+ ) {
TransitionManager.beginDelayedTransition(
constraintLayout,
- BaseBlueprintTransition()
+ BaseBlueprintTransition(clockViewModel)
+ .addTransition(
+ IntraBlueprintTransition(
+ IntraBlueprintTransitionType.NoTransition,
+ clockViewModel
+ )
+ )
+ )
+ } else {
+ TransitionManager.beginDelayedTransition(
+ constraintLayout,
+ IntraBlueprintTransition(
+ IntraBlueprintTransitionType.NoTransition,
+ clockViewModel
+ )
)
}
@@ -71,6 +96,25 @@
Trace.endSection()
}
}
+
+ launch {
+ viewModel.blueprintWithTransition.collect { source ->
+ TransitionManager.endTransitions(constraintLayout)
+
+ val cs =
+ ConstraintSet().apply {
+ clone(constraintLayout)
+ viewModel.currentBluePrint?.applyConstraints(this)
+ }
+
+ TransitionManager.beginDelayedTransition(
+ constraintLayout,
+ IntraBlueprintTransition(source, clockViewModel)
+ )
+ cs.applyTo(constraintLayout)
+ Trace.endSection()
+ }
+ }
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinder.kt
index 3c3ebdf..f0e89f9 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinder.kt
@@ -16,34 +16,30 @@
package com.android.systemui.keyguard.ui.binder
-import android.animation.Animator
-import android.animation.ValueAnimator
-import android.transition.Transition
import android.transition.TransitionManager
import android.transition.TransitionSet
-import android.transition.TransitionValues
-import android.view.ViewGroup
+import android.util.Log
+import android.view.View.INVISIBLE
import androidx.annotation.VisibleForTesting
import androidx.constraintlayout.helper.widget.Layer
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.constraintlayout.widget.ConstraintSet
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.repeatOnLifecycle
-import com.android.app.animation.Interpolators
import com.android.keyguard.KeyguardClockSwitch.LARGE
import com.android.keyguard.KeyguardClockSwitch.SMALL
import com.android.systemui.Flags.migrateClocksToBlueprint
import com.android.systemui.keyguard.domain.interactor.KeyguardBlueprintInteractor
import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor
+import com.android.systemui.keyguard.ui.view.layout.blueprints.transitions.IntraBlueprintTransitionType
import com.android.systemui.keyguard.ui.view.layout.sections.ClockSection
import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel
import com.android.systemui.lifecycle.repeatWhenAttached
import com.android.systemui.plugins.clocks.ClockController
import com.android.systemui.res.R
+import com.android.systemui.shared.clocks.DEFAULT_CLOCK_ID
import kotlinx.coroutines.launch
-private const val KEYGUARD_STATUS_VIEW_CUSTOM_CLOCK_MOVE_DURATION_MS = 1000L
-
object KeyguardClockViewBinder {
@JvmStatic
fun bind(
@@ -67,32 +63,56 @@
viewModel.clock = currentClock
addClockViews(currentClock, keyguardRootView)
updateBurnInLayer(keyguardRootView, viewModel)
- blueprintInteractor.refreshBlueprint()
+ applyConstraints(clockSection, keyguardRootView, true)
}
}
launch {
if (!migrateClocksToBlueprint()) return@launch
viewModel.clockSize.collect {
updateBurnInLayer(keyguardRootView, viewModel)
- blueprintInteractor.refreshBlueprint()
+ blueprintInteractor.refreshBlueprintWithTransition(
+ IntraBlueprintTransitionType.ClockSize
+ )
}
}
launch {
if (!migrateClocksToBlueprint()) return@launch
- viewModel.clockShouldBeCentered.collect {
+ viewModel.clockShouldBeCentered.collect { clockShouldBeCentered ->
+ Log.d(
+ "ClockViewBinder",
+ "Sherry clockShouldBeCentered $clockShouldBeCentered"
+ )
viewModel.clock?.let {
- if (it.largeClock.config.hasCustomPositionUpdatedAnimation) {
- playClockCenteringAnimation(clockSection, keyguardRootView, it)
+ // Weather clock also has hasCustomPositionUpdatedAnimation as true
+ // TODO(b/323020908): remove ID check
+ if (
+ it.largeClock.config.hasCustomPositionUpdatedAnimation &&
+ it.config.id == DEFAULT_CLOCK_ID
+ ) {
+ blueprintInteractor.refreshBlueprintWithTransition(
+ IntraBlueprintTransitionType.DefaultClockStepping
+ )
} else {
- blueprintInteractor.refreshBlueprint()
+ blueprintInteractor.refreshBlueprintWithTransition(
+ IntraBlueprintTransitionType.DefaultTransition
+ )
}
}
}
}
launch {
if (!migrateClocksToBlueprint()) return@launch
- viewModel.isAodIconsVisible.collect {
- applyConstraints(clockSection, keyguardRootView, true)
+ viewModel.isAodIconsVisible.collect { isAodIconsVisible ->
+ viewModel.clock?.let {
+ // Weather clock also has hasCustomPositionUpdatedAnimation as true
+ if (
+ viewModel.useLargeClock && it.config.id == "DIGITAL_CLOCK_WEATHER"
+ ) {
+ blueprintInteractor.refreshBlueprintWithTransition(
+ IntraBlueprintTransitionType.DefaultTransition
+ )
+ }
+ }
}
}
}
@@ -155,7 +175,9 @@
}
// small clock should either be a single view or container with id
// `lockscreen_clock_view`
- clock.smallClock.layout.views.forEach { rootView.addView(it) }
+ clock.smallClock.layout.views.forEach {
+ rootView.addView(it).apply { it.visibility = INVISIBLE }
+ }
clock.largeClock.layout.views.forEach { rootView.addView(it) }
}
}
@@ -173,74 +195,4 @@
}
constraintSet.applyTo(rootView)
}
-
- private fun playClockCenteringAnimation(
- clockSection: ClockSection,
- keyguardRootView: ConstraintLayout,
- clock: ClockController,
- ) {
- // Find the clock, so we can exclude it from this transition.
- val clockView = clock.largeClock.view
- val set = TransitionSet()
- val adapter = SplitShadeTransitionAdapter(clock)
- adapter.setInterpolator(Interpolators.LINEAR)
- adapter.setDuration(KEYGUARD_STATUS_VIEW_CUSTOM_CLOCK_MOVE_DURATION_MS)
- adapter.addTarget(clockView)
- set.addTransition(adapter)
- applyConstraints(clockSection, keyguardRootView, true, set)
- }
-
- internal class SplitShadeTransitionAdapter
- @VisibleForTesting
- constructor(private val clock: ClockController) : Transition() {
- private fun captureValues(transitionValues: TransitionValues) {
- transitionValues.values[PROP_BOUNDS_LEFT] = transitionValues.view.left
- val locationInWindowTmp = IntArray(2)
- transitionValues.view.getLocationInWindow(locationInWindowTmp)
- transitionValues.values[PROP_X_IN_WINDOW] = locationInWindowTmp[0]
- }
-
- override fun captureEndValues(transitionValues: TransitionValues) {
- captureValues(transitionValues)
- }
-
- override fun captureStartValues(transitionValues: TransitionValues) {
- captureValues(transitionValues)
- }
-
- override fun createAnimator(
- sceneRoot: ViewGroup,
- startValues: TransitionValues?,
- endValues: TransitionValues?
- ): Animator? {
- if (startValues == null || endValues == null) {
- return null
- }
- val anim = ValueAnimator.ofFloat(0f, 1f)
- val fromLeft = startValues.values[PROP_BOUNDS_LEFT] as Int
- val fromWindowX = startValues.values[PROP_X_IN_WINDOW] as Int
- val toWindowX = endValues.values[PROP_X_IN_WINDOW] as Int
- // Using windowX, to determine direction, instead of left, as in RTL the difference of
- // toLeft - fromLeft is always positive, even when moving left.
- val direction = if (toWindowX - fromWindowX > 0) 1 else -1
- anim.addUpdateListener { animation: ValueAnimator ->
- clock.largeClock.animations.onPositionUpdated(
- fromLeft,
- direction,
- animation.animatedFraction
- )
- }
- return anim
- }
-
- override fun getTransitionProperties(): Array<String> {
- return TRANSITION_PROPERTIES
- }
-
- companion object {
- private const val PROP_BOUNDS_LEFT = "splitShadeTransitionAdapter:boundsLeft"
- private const val PROP_X_IN_WINDOW = "splitShadeTransitionAdapter:xInWindow"
- private val TRANSITION_PROPERTIES = arrayOf(PROP_BOUNDS_LEFT, PROP_X_IN_WINDOW)
- }
- }
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSmartspaceViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSmartspaceViewBinder.kt
index 10392e3..08a2b9c 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSmartspaceViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSmartspaceViewBinder.kt
@@ -49,14 +49,15 @@
clockViewModel,
smartspaceViewModel
)
- blueprintInteractor.refreshBlueprint()
+ blueprintInteractor.refreshBlueprintWithTransition()
}
}
launch {
+ if (!migrateClocksToBlueprint()) return@launch
smartspaceViewModel.bcSmartspaceVisibility.collect {
updateBCSmartspaceInBurnInLayer(keyguardRootView, clockViewModel)
- blueprintInteractor.refreshBlueprint()
+ blueprintInteractor.refreshBlueprintWithTransition()
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/PreviewKeyguardBlueprintViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/PreviewKeyguardBlueprintViewBinder.kt
deleted file mode 100644
index 2feaa2e..0000000
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/PreviewKeyguardBlueprintViewBinder.kt
+++ /dev/null
@@ -1,82 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- *
- */
-
-package com.android.systemui.keyguard.ui.binder
-
-import android.os.Trace
-import androidx.constraintlayout.widget.ConstraintLayout
-import androidx.constraintlayout.widget.ConstraintSet
-import androidx.lifecycle.Lifecycle
-import androidx.lifecycle.repeatOnLifecycle
-import com.android.systemui.keyguard.ui.viewmodel.KeyguardBlueprintViewModel
-import com.android.systemui.lifecycle.repeatWhenAttached
-import kotlinx.coroutines.DisposableHandle
-import kotlinx.coroutines.launch
-
-/**
- * Binds the existing blueprint to the constraint layout that previews keyguard.
- *
- * This view binder should only inflate and add relevant views and apply the constraints. Actual
- * data binding should be done in {@link KeyguardPreviewRenderer}
- */
-class PreviewKeyguardBlueprintViewBinder {
- companion object {
-
- /**
- * Binds the existing blueprint to the constraint layout that previews keyguard.
- *
- * @param constraintLayout The root view to bind to
- * @param viewModel The instance of the view model that contains flows we collect on.
- * @param finishedAddViewCallback Called when we have finished inflating the views.
- */
- fun bind(
- constraintLayout: ConstraintLayout,
- viewModel: KeyguardBlueprintViewModel,
- finishedAddViewCallback: () -> Unit
- ): DisposableHandle {
- return constraintLayout.repeatWhenAttached {
- repeatOnLifecycle(Lifecycle.State.CREATED) {
- launch {
- viewModel.blueprint.collect { blueprint ->
- val prevBluePrint = viewModel.currentBluePrint
- Trace.beginSection("PreviewKeyguardBlueprint#applyBlueprint")
-
- ConstraintSet().apply {
- clone(constraintLayout)
- val emptyLayout = ConstraintSet.Layout()
- knownIds.forEach { getConstraint(it).layout.copyFrom(emptyLayout) }
- blueprint.applyConstraints(this)
- // Add and remove views of sections that are not contained by the
- // other.
- blueprint.replaceViews(
- prevBluePrint,
- constraintLayout,
- bindData = false
- )
- applyTo(constraintLayout)
- }
-
- viewModel.currentBluePrint = blueprint
- finishedAddViewCallback.invoke()
- Trace.endSection()
- }
- }
- }
- }
- }
- }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt
index 841bad4..a0c0095 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt
@@ -41,6 +41,7 @@
import android.widget.FrameLayout
import android.widget.TextView
import androidx.constraintlayout.widget.ConstraintLayout
+import androidx.constraintlayout.widget.ConstraintSet
import androidx.core.view.isInvisible
import com.android.keyguard.ClockEventController
import com.android.keyguard.KeyguardClockSwitch
@@ -54,15 +55,12 @@
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.dagger.qualifiers.Main
-import com.android.systemui.flags.FeatureFlagsClassic
-import com.android.systemui.keyguard.shared.KeyguardShadeMigrationNssl
import com.android.systemui.keyguard.ui.binder.KeyguardPreviewClockViewBinder
import com.android.systemui.keyguard.ui.binder.KeyguardPreviewSmartspaceViewBinder
import com.android.systemui.keyguard.ui.binder.KeyguardQuickAffordanceViewBinder
import com.android.systemui.keyguard.ui.binder.KeyguardRootViewBinder
-import com.android.systemui.keyguard.ui.binder.PreviewKeyguardBlueprintViewBinder
import com.android.systemui.keyguard.ui.view.KeyguardRootView
-import com.android.systemui.keyguard.ui.viewmodel.KeyguardBlueprintViewModel
+import com.android.systemui.keyguard.ui.view.layout.sections.DefaultShortcutsSection
import com.android.systemui.keyguard.ui.viewmodel.KeyguardBottomAreaViewModel
import com.android.systemui.keyguard.ui.viewmodel.KeyguardPreviewClockViewModel
import com.android.systemui.keyguard.ui.viewmodel.KeyguardPreviewSmartspaceViewModel
@@ -124,19 +122,18 @@
private val broadcastDispatcher: BroadcastDispatcher,
private val lockscreenSmartspaceController: LockscreenSmartspaceController,
private val udfpsOverlayInteractor: UdfpsOverlayInteractor,
- private val featureFlags: FeatureFlagsClassic,
private val falsingManager: FalsingManager,
private val vibratorHelper: VibratorHelper,
private val indicationController: KeyguardIndicationController,
private val keyguardRootViewModel: KeyguardRootViewModel,
@Assisted bundle: Bundle,
- private val keyguardBlueprintViewModel: KeyguardBlueprintViewModel,
private val occludingAppDeviceEntryMessageViewModel: OccludingAppDeviceEntryMessageViewModel,
private val chipbarCoordinator: ChipbarCoordinator,
private val screenOffAnimationController: ScreenOffAnimationController,
private val shadeInteractor: ShadeInteractor,
private val secureSettings: SecureSettings,
private val communalTutorialViewModel: CommunalTutorialIndicatorViewModel,
+ private val defaultShortcutsSection: DefaultShortcutsSection,
) {
val hostToken: IBinder? = bundle.getBinder(KEY_HOST_TOKEN)
private val width: Int = bundle.getInt(KEY_VIEW_WIDTH)
@@ -393,32 +390,32 @@
setUpUdfps(previewContext, rootView)
- disposables.add(
- PreviewKeyguardBlueprintViewBinder.bind(keyguardRootView, keyguardBlueprintViewModel) {
- if (keyguardBottomAreaRefactor()) {
- setupShortcuts(keyguardRootView)
- }
+ if (keyguardBottomAreaRefactor()) {
+ setupShortcuts(keyguardRootView)
+ }
- if (!shouldHideClock) {
- setUpClock(previewContext, rootView)
- KeyguardPreviewClockViewBinder.bind(
- largeClockHostView,
- smallClockHostView,
- clockViewModel,
- )
- }
+ if (!shouldHideClock) {
+ setUpClock(previewContext, rootView)
+ KeyguardPreviewClockViewBinder.bind(
+ largeClockHostView,
+ smallClockHostView,
+ clockViewModel,
+ )
+ }
- setUpSmartspace(previewContext, rootView)
- smartSpaceView?.let {
- KeyguardPreviewSmartspaceViewBinder.bind(it, smartspaceViewModel)
- }
-
- setupCommunalTutorialIndicator(keyguardRootView)
- }
- )
+ setUpSmartspace(previewContext, rootView)
+ smartSpaceView?.let { KeyguardPreviewSmartspaceViewBinder.bind(it, smartspaceViewModel) }
+ setupCommunalTutorialIndicator(keyguardRootView)
}
private fun setupShortcuts(keyguardRootView: ConstraintLayout) {
+ // Add shortcuts
+ val cs = ConstraintSet()
+ cs.clone(keyguardRootView)
+ defaultShortcutsSection.addViews(keyguardRootView)
+ defaultShortcutsSection.applyConstraints(cs)
+ cs.applyTo(keyguardRootView)
+
keyguardRootView.findViewById<LaunchableImageView?>(R.id.start_button)?.let { imageView ->
shortcutsBindings.add(
KeyguardQuickAffordanceViewBinder.bind(
@@ -476,53 +473,40 @@
}
private fun setUpClock(previewContext: Context, parentView: ViewGroup) {
- largeClockHostView =
- if (KeyguardShadeMigrationNssl.isEnabled) {
- parentView.requireViewById<FrameLayout>(R.id.lockscreen_clock_view_large)
- } else {
- val hostView = FrameLayout(previewContext)
- hostView.layoutParams =
- FrameLayout.LayoutParams(
- FrameLayout.LayoutParams.MATCH_PARENT,
- FrameLayout.LayoutParams.MATCH_PARENT,
- )
- parentView.addView(hostView)
- hostView
- }
+ val resources = parentView.resources
+ largeClockHostView = FrameLayout(previewContext)
+ largeClockHostView.layoutParams =
+ FrameLayout.LayoutParams(
+ FrameLayout.LayoutParams.MATCH_PARENT,
+ FrameLayout.LayoutParams.MATCH_PARENT,
+ )
+ parentView.addView(largeClockHostView)
largeClockHostView.isInvisible = true
- smallClockHostView =
- if (KeyguardShadeMigrationNssl.isEnabled) {
- parentView.requireViewById<FrameLayout>(R.id.lockscreen_clock_view)
- } else {
- val resources = parentView.resources
- val hostView = FrameLayout(previewContext)
- val layoutParams =
- FrameLayout.LayoutParams(
- FrameLayout.LayoutParams.WRAP_CONTENT,
- resources.getDimensionPixelSize(
- com.android.systemui.customization.R.dimen.small_clock_height
- )
- )
- layoutParams.topMargin =
- KeyguardPreviewSmartspaceViewModel.getStatusBarHeight(resources) +
- resources.getDimensionPixelSize(
- com.android.systemui.customization.R.dimen.small_clock_padding_top
- )
- hostView.layoutParams = layoutParams
-
- hostView.setPaddingRelative(
- resources.getDimensionPixelSize(
- com.android.systemui.customization.R.dimen.clock_padding_start
- ),
- 0,
- 0,
- 0
+ smallClockHostView = FrameLayout(previewContext)
+ val layoutParams =
+ FrameLayout.LayoutParams(
+ FrameLayout.LayoutParams.WRAP_CONTENT,
+ resources.getDimensionPixelSize(
+ com.android.systemui.customization.R.dimen.small_clock_height
)
- hostView.clipChildren = false
- parentView.addView(hostView)
- hostView
- }
+ )
+ layoutParams.topMargin =
+ KeyguardPreviewSmartspaceViewModel.getStatusBarHeight(resources) +
+ resources.getDimensionPixelSize(
+ com.android.systemui.customization.R.dimen.small_clock_padding_top
+ )
+ smallClockHostView.layoutParams = layoutParams
+ smallClockHostView.setPaddingRelative(
+ resources.getDimensionPixelSize(
+ com.android.systemui.customization.R.dimen.clock_padding_start
+ ),
+ 0,
+ 0,
+ 0
+ )
+ smallClockHostView.clipChildren = false
+ parentView.addView(smallClockHostView)
smallClockHostView.isInvisible = true
// TODO (b/283465254): Move the listeners to KeyguardClockRepository
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/transitions/BaseBlueprintTransition.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/transitions/BaseBlueprintTransition.kt
index fd530b7..9c9df80 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/transitions/BaseBlueprintTransition.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/transitions/BaseBlueprintTransition.kt
@@ -19,30 +19,32 @@
import android.animation.Animator
import android.animation.ObjectAnimator
import android.transition.ChangeBounds
+import android.transition.Transition
import android.transition.TransitionSet
import android.transition.TransitionValues
import android.transition.Visibility
import android.view.View
import android.view.ViewGroup
import androidx.constraintlayout.helper.widget.Layer
+import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel
import com.android.systemui.plugins.BcSmartspaceDataPlugin.SmartspaceView
-import com.android.systemui.res.R
-class BaseBlueprintTransition : TransitionSet() {
+class BaseBlueprintTransition(val clockViewModel: KeyguardClockViewModel) : TransitionSet() {
init {
ordering = ORDERING_SEQUENTIAL
addTransition(AlphaOutVisibility())
.addTransition(ChangeBounds())
.addTransition(AlphaInVisibility())
excludeTarget(Layer::class.java, /* exclude= */ true)
- excludeClockAndSmartspaceViews()
+ excludeClockAndSmartspaceViews(this)
}
- private fun excludeClockAndSmartspaceViews() {
- excludeTarget(R.id.lockscreen_clock_view, true)
- excludeTarget(R.id.lockscreen_clock_view_large, true)
- excludeTarget(SmartspaceView::class.java, true)
- // TODO(b/319468190): need to exclude views from large weather clock
+ private fun excludeClockAndSmartspaceViews(transition: Transition) {
+ transition.excludeTarget(SmartspaceView::class.java, true)
+ clockViewModel.clock?.let { clock ->
+ clock.largeClock.layout.views.forEach { view -> transition.excludeTarget(view, true) }
+ clock.smallClock.layout.views.forEach { view -> transition.excludeTarget(view, true) }
+ }
}
class AlphaOutVisibility : Visibility() {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/transitions/IntraBlueprintTransition.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/transitions/IntraBlueprintTransition.kt
new file mode 100644
index 0000000..524aa1a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/transitions/IntraBlueprintTransition.kt
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.ui.view.layout.blueprints.transitions
+
+import android.transition.TransitionSet
+import com.android.systemui.keyguard.ui.view.layout.sections.transitions.ClockSizeTransition
+import com.android.systemui.keyguard.ui.view.layout.sections.transitions.DefaultClockSteppingTransition
+import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel
+
+enum class IntraBlueprintTransitionType {
+ ClockSize,
+ ClockCenter,
+ DefaultClockStepping,
+ DefaultTransition,
+ AodNotifIconsTransition,
+ // When transition between blueprint, we don't need any duration or interpolator but we need
+ // all elements go to correct state
+ NoTransition,
+}
+
+class IntraBlueprintTransition(
+ type: IntraBlueprintTransitionType,
+ clockViewModel: KeyguardClockViewModel
+) : TransitionSet() {
+ init {
+ ordering = ORDERING_TOGETHER
+ if (type == IntraBlueprintTransitionType.DefaultClockStepping)
+ addTransition(clockViewModel.clock?.let { DefaultClockSteppingTransition(it) })
+ addTransition(ClockSizeTransition(type, clockViewModel))
+ }
+}
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 fe4f07d..b1178f5 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
@@ -166,10 +166,7 @@
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(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)
constrainWidth(R.id.lockscreen_clock_view_large, WRAP_CONTENT)
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/NotificationStackScrollLayoutSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/NotificationStackScrollLayoutSection.kt
index 400d0dc..a651c10 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/NotificationStackScrollLayoutSection.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/NotificationStackScrollLayoutSection.kt
@@ -87,7 +87,8 @@
return
}
// This moves the existing NSSL view to a different parent, as the controller is a
- // singleton and recreating it has other bad side effects
+ // singleton and recreating it has other bad side effects.
+ // In the SceneContainer, this is done by the NotificationSection composable.
notificationPanelView.findViewById<View?>(R.id.notification_stack_scroller)?.let {
(it.parent as ViewGroup).removeView(it)
sharedNotificationContainer.addNotificationStackScrollLayout(it)
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/transitions/ClockSizeTransition.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/transitions/ClockSizeTransition.kt
new file mode 100644
index 0000000..99565b1
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/transitions/ClockSizeTransition.kt
@@ -0,0 +1,196 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS 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.ui.view.layout.sections.transitions
+
+import android.animation.Animator
+import android.animation.ObjectAnimator
+import android.transition.ChangeBounds
+import android.transition.TransitionSet
+import android.transition.TransitionValues
+import android.transition.Visibility
+import android.view.View
+import android.view.ViewGroup
+import com.android.app.animation.Interpolators
+import com.android.systemui.keyguard.ui.view.layout.blueprints.transitions.IntraBlueprintTransitionType
+import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel
+import com.android.systemui.res.R
+import com.android.systemui.shared.R as sharedR
+
+const val CLOCK_OUT_MILLIS = 133L
+const val CLOCK_IN_MILLIS = 167L
+val CLOCK_IN_INTERPOLATOR = Interpolators.LINEAR_OUT_SLOW_IN
+const val CLOCK_IN_START_DELAY_MILLIS = 133L
+val CLOCK_OUT_INTERPOLATOR = Interpolators.LINEAR
+
+class ClockSizeTransition(
+ val type: IntraBlueprintTransitionType,
+ clockViewModel: KeyguardClockViewModel
+) : TransitionSet() {
+ init {
+ ordering = ORDERING_TOGETHER
+ addTransition(ClockOutTransition(clockViewModel, type))
+ addTransition(ClockInTransition(clockViewModel, type))
+ addTransition(SmartspaceChangeBounds(clockViewModel, type))
+ addTransition(ClockInChangeBounds(clockViewModel, type))
+ addTransition(ClockOutChangeBounds(clockViewModel, type))
+ }
+
+ class ClockInTransition(viewModel: KeyguardClockViewModel, type: IntraBlueprintTransitionType) :
+ Visibility() {
+ init {
+ mode = MODE_IN
+ if (type != IntraBlueprintTransitionType.NoTransition) {
+ duration = CLOCK_IN_MILLIS
+ startDelay = CLOCK_IN_START_DELAY_MILLIS
+ interpolator = Interpolators.LINEAR_OUT_SLOW_IN
+ } else {
+ duration = 0
+ startDelay = 0
+ }
+
+ addTarget(sharedR.id.bc_smartspace_view)
+ addTarget(sharedR.id.date_smartspace_view)
+ addTarget(sharedR.id.weather_smartspace_view)
+ if (viewModel.useLargeClock) {
+ viewModel.clock?.let { it.largeClock.layout.views.forEach { addTarget(it) } }
+ } else {
+ addTarget(R.id.lockscreen_clock_view)
+ }
+ }
+
+ override fun onAppear(
+ sceneRoot: ViewGroup?,
+ view: View,
+ startValues: TransitionValues?,
+ endValues: TransitionValues?
+ ): Animator {
+ return ObjectAnimator.ofFloat(view, "alpha", 1f).also {
+ it.duration = duration
+ it.startDelay = startDelay
+ it.interpolator = interpolator
+ it.addUpdateListener { view.alpha = it.animatedValue as Float }
+ it.start()
+ }
+ }
+ }
+
+ class ClockOutTransition(
+ viewModel: KeyguardClockViewModel,
+ type: IntraBlueprintTransitionType
+ ) : Visibility() {
+ init {
+ mode = MODE_OUT
+ if (type != IntraBlueprintTransitionType.NoTransition) {
+ duration = CLOCK_OUT_MILLIS
+ interpolator = CLOCK_OUT_INTERPOLATOR
+ } else {
+ duration = 0
+ }
+
+ addTarget(sharedR.id.bc_smartspace_view)
+ addTarget(sharedR.id.date_smartspace_view)
+ addTarget(sharedR.id.weather_smartspace_view)
+ if (viewModel.useLargeClock) {
+ addTarget(R.id.lockscreen_clock_view)
+ } else {
+ viewModel.clock?.let { it.largeClock.layout.views.forEach { addTarget(it) } }
+ }
+ }
+
+ override fun onDisappear(
+ sceneRoot: ViewGroup?,
+ view: View,
+ startValues: TransitionValues?,
+ endValues: TransitionValues?
+ ): Animator {
+ return ObjectAnimator.ofFloat(view, "alpha", 0f).also {
+ it.duration = duration
+ it.interpolator = interpolator
+ it.addUpdateListener { view.alpha = it.animatedValue as Float }
+ it.start()
+ }
+ }
+ }
+
+ class ClockInChangeBounds(
+ viewModel: KeyguardClockViewModel,
+ type: IntraBlueprintTransitionType
+ ) : ChangeBounds() {
+ init {
+ if (type != IntraBlueprintTransitionType.NoTransition) {
+ duration = CLOCK_IN_MILLIS
+ startDelay = CLOCK_IN_START_DELAY_MILLIS
+ interpolator = CLOCK_IN_INTERPOLATOR
+ } else {
+ duration = 0
+ startDelay = 0
+ }
+
+ if (viewModel.useLargeClock) {
+ viewModel.clock?.let { it.largeClock.layout.views.forEach { addTarget(it) } }
+ } else {
+ addTarget(R.id.lockscreen_clock_view)
+ }
+ }
+ }
+
+ class ClockOutChangeBounds(
+ viewModel: KeyguardClockViewModel,
+ type: IntraBlueprintTransitionType
+ ) : ChangeBounds() {
+ init {
+ if (type != IntraBlueprintTransitionType.NoTransition) {
+ duration = CLOCK_OUT_MILLIS
+ interpolator = CLOCK_OUT_INTERPOLATOR
+ } else {
+ duration = 0
+ }
+ if (viewModel.useLargeClock) {
+ addTarget(R.id.lockscreen_clock_view)
+ } else {
+ viewModel.clock?.let { it.largeClock.layout.views.forEach { addTarget(it) } }
+ }
+ }
+ }
+
+ class SmartspaceChangeBounds(
+ viewModel: KeyguardClockViewModel,
+ val type: IntraBlueprintTransitionType = IntraBlueprintTransitionType.DefaultTransition
+ ) : ChangeBounds() {
+ init {
+ if (type != IntraBlueprintTransitionType.NoTransition) {
+ duration =
+ if (viewModel.useLargeClock) {
+ STATUS_AREA_MOVE_UP_MILLIS
+ } else {
+ STATUS_AREA_MOVE_DOWN_MILLIS
+ }
+ interpolator = Interpolators.EMPHASIZED
+ } else {
+ duration = 0
+ }
+ addTarget(sharedR.id.date_smartspace_view)
+ addTarget(sharedR.id.weather_smartspace_view)
+ addTarget(sharedR.id.bc_smartspace_view)
+ }
+
+ companion object {
+ const val STATUS_AREA_MOVE_UP_MILLIS = 967L
+ const val STATUS_AREA_MOVE_DOWN_MILLIS = 467L
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/transitions/DefaultClockSteppingTransition.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/transitions/DefaultClockSteppingTransition.kt
new file mode 100644
index 0000000..c35dad7
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/transitions/DefaultClockSteppingTransition.kt
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.ui.view.layout.sections.transitions
+
+import android.animation.Animator
+import android.animation.ValueAnimator
+import android.transition.Transition
+import android.transition.TransitionValues
+import android.view.ViewGroup
+import com.android.app.animation.Interpolators
+import com.android.systemui.plugins.clocks.ClockController
+
+class DefaultClockSteppingTransition(private val clock: ClockController) : Transition() {
+ init {
+ interpolator = Interpolators.LINEAR
+ duration = KEYGUARD_STATUS_VIEW_CUSTOM_CLOCK_MOVE_DURATION_MS
+ addTarget(clock.largeClock.view)
+ }
+ private fun captureValues(transitionValues: TransitionValues) {
+ transitionValues.values[PROP_BOUNDS_LEFT] = transitionValues.view.left
+ val locationInWindowTmp = IntArray(2)
+ transitionValues.view.getLocationInWindow(locationInWindowTmp)
+ transitionValues.values[PROP_X_IN_WINDOW] = locationInWindowTmp[0]
+ }
+
+ override fun captureEndValues(transitionValues: TransitionValues) {
+ captureValues(transitionValues)
+ }
+
+ override fun captureStartValues(transitionValues: TransitionValues) {
+ captureValues(transitionValues)
+ }
+
+ override fun createAnimator(
+ sceneRoot: ViewGroup,
+ startValues: TransitionValues?,
+ endValues: TransitionValues?
+ ): Animator? {
+ if (startValues == null || endValues == null) {
+ return null
+ }
+ val anim = ValueAnimator.ofFloat(0f, 1f)
+ val fromLeft = startValues.values[PROP_BOUNDS_LEFT] as Int
+ val fromWindowX = startValues.values[PROP_X_IN_WINDOW] as Int
+ val toWindowX = endValues.values[PROP_X_IN_WINDOW] as Int
+ // Using windowX, to determine direction, instead of left, as in RTL the difference of
+ // toLeft - fromLeft is always positive, even when moving left.
+ val direction = if (toWindowX - fromWindowX > 0) 1 else -1
+ anim.addUpdateListener { animation: ValueAnimator ->
+ clock.largeClock.animations.onPositionUpdated(
+ fromLeft,
+ direction,
+ animation.animatedFraction
+ )
+ }
+ return anim
+ }
+
+ override fun getTransitionProperties(): Array<String> {
+ return TRANSITION_PROPERTIES
+ }
+
+ companion object {
+ private const val PROP_BOUNDS_LEFT = "splitShadeTransitionAdapter:boundsLeft"
+ private const val PROP_X_IN_WINDOW = "splitShadeTransitionAdapter:xInWindow"
+ private val TRANSITION_PROPERTIES = arrayOf(PROP_BOUNDS_LEFT, PROP_X_IN_WINDOW)
+ private const val KEYGUARD_STATUS_VIEW_CUSTOM_CLOCK_MOVE_DURATION_MS = 1000L
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBlueprintViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBlueprintViewModel.kt
index e2bfc36..d22856b 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBlueprintViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBlueprintViewModel.kt
@@ -26,4 +26,5 @@
constructor(keyguardBlueprintInteractor: KeyguardBlueprintInteractor) {
var currentBluePrint: KeyguardBlueprint? = null
val blueprint = keyguardBlueprintInteractor.blueprint
+ val blueprintWithTransition = keyguardBlueprintInteractor.blueprintWithTransition
}
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/resume/MediaResumeListener.kt b/packages/SystemUI/src/com/android/systemui/media/controls/resume/MediaResumeListener.kt
index a3029b2..23ee00d 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/resume/MediaResumeListener.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/resume/MediaResumeListener.kt
@@ -146,7 +146,7 @@
null,
UserHandle.ALL
)
- userTracker.addCallback(userTrackerCallback, backgroundExecutor)
+ userTracker.addCallback(userTrackerCallback, mainExecutor)
loadSavedComponents()
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaHierarchyManager.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaHierarchyManager.kt
index 523414c..c33ab12 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaHierarchyManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaHierarchyManager.kt
@@ -586,7 +586,7 @@
coroutineScope.launch {
communalInteractor.isCommunalShowing.collect { value ->
isCommunalShowing = value
- updateDesiredLocation(forceNoAnimation = true)
+ updateDesiredLocation()
}
}
}
@@ -1149,13 +1149,17 @@
when {
mediaFlags.isSceneContainerEnabled() -> desiredLocation
dreamOverlayActive && dreamMediaComplicationActive -> LOCATION_DREAM_OVERLAY
+
+ // UMO should show in communal unless the shade is expanding or visible.
+ isCommunalShowing && qsExpansion == 0.0f -> LOCATION_COMMUNAL_HUB
(qsExpansion > 0.0f || inSplitShade) && !onLockscreen -> LOCATION_QS
qsExpansion > 0.4f && onLockscreen -> LOCATION_QS
onLockscreen && isSplitShadeExpanding() -> LOCATION_QS
onLockscreen && isTransformingToFullShadeAndInQQS() -> LOCATION_QQS
- // TODO(b/311234666): revisit logic once interactions between the hub and
- // shade/keyguard state are finalized
- isCommunalShowing && communalInteractor.isCommunalEnabled -> LOCATION_COMMUNAL_HUB
+
+ // Communal does not have its own StatusBarState so it should always have higher
+ // priority for the UMO over the lockscreen.
+ isCommunalShowing -> LOCATION_COMMUNAL_HUB
onLockscreen && allowMediaPlayerOnLockScreen -> LOCATION_LOCKSCREEN
else -> LOCATION_QQS
}
diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/MediaProjectionCaptureTarget.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/MediaProjectionCaptureTarget.kt
index 11d0be5..a618490 100644
--- a/packages/SystemUI/src/com/android/systemui/mediaprojection/MediaProjectionCaptureTarget.kt
+++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/MediaProjectionCaptureTarget.kt
@@ -16,7 +16,7 @@
package com.android.systemui.mediaprojection
-import android.os.IBinder
+import android.app.ActivityOptions.LaunchCookie
import android.os.Parcel
import android.os.Parcelable
@@ -24,12 +24,12 @@
* Class that represents an area that should be captured. Currently it has only a launch cookie that
* represents a task but we potentially could add more identifiers e.g. for a pair of tasks.
*/
-data class MediaProjectionCaptureTarget(val launchCookie: IBinder?) : Parcelable {
+data class MediaProjectionCaptureTarget(val launchCookie: LaunchCookie?) : Parcelable {
- constructor(parcel: Parcel) : this(parcel.readStrongBinder())
+ constructor(parcel: Parcel) : this(LaunchCookie.readFromParcel(parcel))
override fun writeToParcel(dest: Parcel, flags: Int) {
- dest.writeStrongBinder(launchCookie)
+ LaunchCookie.writeToParcel(launchCookie, dest)
}
override fun describeContents(): Int = 0
diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorActivity.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorActivity.kt
index 50e9e751..4685c5a 100644
--- a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorActivity.kt
+++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorActivity.kt
@@ -16,6 +16,7 @@
package com.android.systemui.mediaprojection.appselector
import android.app.ActivityOptions
+import android.app.ActivityOptions.LaunchCookie
import android.content.Intent
import android.content.res.Configuration
import android.content.res.Resources
@@ -24,9 +25,7 @@
import android.media.projection.MediaProjectionManager.EXTRA_MEDIA_PROJECTION
import android.media.projection.ReviewGrantedConsentResult.RECORD_CANCEL
import android.media.projection.ReviewGrantedConsentResult.RECORD_CONTENT_TASK
-import android.os.Binder
import android.os.Bundle
-import android.os.IBinder
import android.os.ResultReceiver
import android.os.UserHandle
import android.util.Log
@@ -163,9 +162,9 @@
val intent = createIntent(targetInfo)
- val launchToken: IBinder = Binder("media_projection_launch_token")
+ val launchCookie = LaunchCookie("media_projection_launch_token")
val activityOptions = ActivityOptions.makeBasic()
- activityOptions.launchCookie = launchToken
+ activityOptions.setLaunchCookie(launchCookie)
val userHandle = mMultiProfilePagerAdapter.activeListAdapter.userHandle
@@ -175,7 +174,7 @@
// is created and ready to be captured.
val activityStarted =
activityLauncher.startActivityAsUser(intent, userHandle, activityOptions.toBundle()) {
- returnSelectedApp(launchToken)
+ returnSelectedApp(launchCookie)
}
// Rely on the ActivityManager to pop up a dialog regarding app suspension
@@ -233,7 +232,7 @@
}
}
- override fun returnSelectedApp(launchCookie: IBinder) {
+ override fun returnSelectedApp(launchCookie: LaunchCookie) {
taskSelected = true
if (intent.hasExtra(EXTRA_CAPTURE_REGION_RESULT_RECEIVER)) {
// The client requested to return the result in the result receiver instead of
@@ -255,7 +254,7 @@
val mediaProjectionBinder = intent.getIBinderExtra(EXTRA_MEDIA_PROJECTION)
val projection = IMediaProjection.Stub.asInterface(mediaProjectionBinder)
- projection.launchCookie = launchCookie
+ projection.setLaunchCookie(launchCookie)
val intent = Intent()
intent.putExtra(EXTRA_MEDIA_PROJECTION, projection.asBinder())
diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorResultHandler.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorResultHandler.kt
index 93c3bce..f204b3e 100644
--- a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorResultHandler.kt
+++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorResultHandler.kt
@@ -1,6 +1,6 @@
package com.android.systemui.mediaprojection.appselector
-import android.os.IBinder
+import android.app.ActivityOptions.LaunchCookie
/**
* Interface that allows to continue the media projection flow and return the selected app
@@ -11,5 +11,5 @@
* Return selected app to the original caller of the media projection app picker.
* @param launchCookie launch cookie of the launched activity of the target app
*/
- fun returnSelectedApp(launchCookie: IBinder)
+ fun returnSelectedApp(launchCookie: LaunchCookie)
}
diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/view/MediaProjectionRecentsViewController.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/view/MediaProjectionRecentsViewController.kt
index ba837db..a811065 100644
--- a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/view/MediaProjectionRecentsViewController.kt
+++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/view/MediaProjectionRecentsViewController.kt
@@ -17,10 +17,10 @@
package com.android.systemui.mediaprojection.appselector.view
import android.app.ActivityOptions
+import android.app.ActivityOptions.LaunchCookie
import android.app.ComponentOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED
import android.app.IActivityTaskManager
import android.graphics.Rect
-import android.os.Binder
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
@@ -121,7 +121,7 @@
}
override fun onRecentAppClicked(task: RecentTask, view: View) {
- val launchCookie = Binder()
+ val launchCookie = LaunchCookie()
val activityOptions =
ActivityOptions.makeScaleUpAnimation(
view,
@@ -132,7 +132,7 @@
)
activityOptions.pendingIntentBackgroundActivityStartMode =
MODE_BACKGROUND_ACTIVITY_START_ALLOWED
- activityOptions.launchCookie = launchCookie
+ activityOptions.setLaunchCookie(launchCookie)
activityOptions.launchDisplayId = task.displayId
activityTaskManager.startActivityFromRecents(task.taskId, activityOptions.toBundle())
diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionActivity.java b/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionActivity.java
index 039372d..8b034b2 100644
--- a/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionActivity.java
+++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionActivity.java
@@ -28,6 +28,7 @@
import android.annotation.Nullable;
import android.app.Activity;
import android.app.ActivityManager;
+import android.app.ActivityOptions.LaunchCookie;
import android.app.AlertDialog;
import android.app.StatusBarManager;
import android.content.Context;
@@ -146,6 +147,13 @@
final IMediaProjection projection =
MediaProjectionServiceHelper.createOrReuseProjection(mUid, mPackageName,
mReviewGrantedConsentRequired);
+
+ LaunchCookie launchCookie = launchingIntent.getParcelableExtra(
+ MediaProjectionManager.EXTRA_LAUNCH_COOKIE, LaunchCookie.class);
+ if (launchCookie != null) {
+ projection.setLaunchCookie(launchCookie);
+ }
+
// Automatically grant consent if a system-privileged component is recording.
final Intent intent = new Intent();
intent.putExtra(MediaProjectionManager.EXTRA_MEDIA_PROJECTION,
diff --git a/packages/SystemUI/src/com/android/systemui/power/PowerUI.java b/packages/SystemUI/src/com/android/systemui/power/PowerUI.java
index 21de185..958ace35 100644
--- a/packages/SystemUI/src/com/android/systemui/power/PowerUI.java
+++ b/packages/SystemUI/src/com/android/systemui/power/PowerUI.java
@@ -26,8 +26,6 @@
import android.database.ContentObserver;
import android.os.BatteryManager;
import android.os.Handler;
-import android.os.HandlerExecutor;
-import android.os.HandlerThread;
import android.os.IThermalEventListener;
import android.os.IThermalService;
import android.os.PowerManager;
@@ -97,7 +95,6 @@
private Future mLastShowWarningTask;
private boolean mEnableSkinTemperatureWarning;
private boolean mEnableUsbTemperatureAlarm;
- private final HandlerThread mHandlerThread;
private int mLowBatteryAlertCloseLevel;
private final int[] mLowBatteryReminderLevels = new int[2];
@@ -170,8 +167,6 @@
mPowerManager = powerManager;
mWakefulnessLifecycle = wakefulnessLifecycle;
mUserTracker = userTracker;
- mHandlerThread = new HandlerThread("PowerUI");
- mHandlerThread.start();
}
public void start() {
@@ -190,8 +185,7 @@
false, obs, UserHandle.USER_ALL);
updateBatteryWarningLevels();
mReceiver.init();
- mUserTracker.addCallback(mUserChangedCallback,
- new HandlerExecutor(mHandlerThread.getThreadHandler()));
+ mUserTracker.addCallback(mUserChangedCallback, mContext.getMainExecutor());
mWakefulnessLifecycle.addObserver(mWakefulnessObserver);
// Check to see if we need to let the user know that the phone previously shut down due
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt
index c5eeb2f..9fefcbd 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt
@@ -52,7 +52,7 @@
import com.android.systemui.animation.LaunchableViewDelegate
import com.android.systemui.plugins.qs.QSIconView
import com.android.systemui.plugins.qs.QSTile
-import com.android.systemui.plugins.qs.QSTile.BooleanState
+import com.android.systemui.plugins.qs.QSTile.AdapterState
import com.android.systemui.plugins.qs.QSTileView
import com.android.systemui.qs.logging.QSLogger
import com.android.systemui.qs.tileimpl.QSIconViewImpl.QS_ANIM_LENGTH
@@ -506,13 +506,12 @@
state.expandedAccessibilityClassName
}
- if (state is BooleanState) {
+ if (state is AdapterState) {
val newState = state.value
if (tileState != newState) {
tileState = newState
}
}
- //
// Labels
if (!Objects.equals(label.text, state.label)) {
@@ -625,7 +624,7 @@
customDrawableView.setImageDrawable(state.sideViewCustomDrawable)
customDrawableView.visibility = VISIBLE
chevronView.visibility = GONE
- } else if (state !is BooleanState || state.forceExpandIcon) {
+ } else if (state !is AdapterState || state.forceExpandIcon) {
customDrawableView.setImageDrawable(null)
customDrawableView.visibility = GONE
chevronView.visibility = VISIBLE
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt
index 4780a2e..5a389f3 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt
@@ -227,7 +227,7 @@
): QSTile.State =
// we have to use QSTile.BooleanState to support different side icons
// which are bound to instanceof QSTile.BooleanState in QSTileView.
- QSTile.BooleanState().apply {
+ QSTile.AdapterState().apply {
spec = config.tileSpec.spec
label = viewModelState.label
// This value is synthetic and doesn't have any meaning. It's only needed to satisfy
diff --git a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
index 4e89fbf..7d86a6a 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
@@ -513,10 +513,6 @@
notifySystemUiStateFlags(mSysUiState.getFlags());
notifyConnectionChanged();
- if (mDoneUserChanging != null) {
- mDoneUserChanging.run();
- mDoneUserChanging = null;
- }
}
@Override
@@ -571,14 +567,11 @@
}
};
- private Runnable mDoneUserChanging;
private final UserTracker.Callback mUserChangedCallback =
new UserTracker.Callback() {
@Override
- public void onUserChanging(int newUser, @NonNull Context userContext,
- @NonNull Runnable resultCallback) {
+ public void onUserChanged(int newUser, @NonNull Context userContext) {
mConnectionBackoffAttempts = 0;
- mDoneUserChanging = resultCallback;
internalConnectToCurrentUser("User changed");
}
};
diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt
index b3d2e09..b9e9fe7 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt
@@ -171,28 +171,6 @@
return repository.setVisible(isVisible)
}
- /** True if there is a transition happening from and to the specified scenes. */
- fun transitioning(from: SceneKey, to: SceneKey): StateFlow<Boolean> {
- fun transitioning(
- state: ObservableTransitionState,
- from: SceneKey,
- to: SceneKey,
- ): Boolean {
- return (state as? ObservableTransitionState.Transition)?.let {
- it.fromScene == from && it.toScene == to
- }
- ?: false
- }
-
- return transitionState
- .map { state -> transitioning(state, from, to) }
- .stateIn(
- scope = applicationScope,
- started = SharingStarted.WhileSubscribed(),
- initialValue = transitioning(transitionState.value, from, to),
- )
- }
-
/**
* Binds the given flow so the system remembers it.
*
diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootViewBinder.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootViewBinder.kt
index 93cfc5d..2b978b2 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootViewBinder.kt
@@ -97,6 +97,10 @@
val legacyView = view.requireViewById<View>(R.id.legacy_window_root)
view.addView(createVisibilityToggleView(legacyView))
+ // This moves the SharedNotificationContainer to the WindowRootView just after
+ // the SceneContainerView. This SharedNotificationContainer should contain NSSL
+ // due to the NotificationStackScrollLayoutSection (legacy) or
+ // NotificationSection (scene container) moving it there.
if (flags.flexiNotifsEnabled()) {
(sharedNotificationContainer.parent as? ViewGroup)?.removeView(
sharedNotificationContainer
diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingService.java b/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingService.java
index 7aa0dad..b5a1313 100644
--- a/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingService.java
+++ b/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingService.java
@@ -25,7 +25,6 @@
import android.content.Context;
import android.content.Intent;
import android.content.res.Resources;
-import android.graphics.Bitmap;
import android.graphics.drawable.Icon;
import android.media.MediaRecorder;
import android.net.Uri;
@@ -379,10 +378,9 @@
.addExtras(extras);
// Add thumbnail if available
- Bitmap thumbnailBitmap = recording.getThumbnail();
- if (thumbnailBitmap != null) {
+ if (recording.getThumbnail() != null) {
Notification.BigPictureStyle pictureStyle = new Notification.BigPictureStyle()
- .bigPicture(thumbnailBitmap)
+ .bigPicture(recording.getThumbnail())
.showBigPictureWhenCollapsed(true);
builder.setStyle(pictureStyle);
}
diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenMediaRecorder.java b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenMediaRecorder.java
index a170d0da..5469a4e 100644
--- a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenMediaRecorder.java
+++ b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenMediaRecorder.java
@@ -23,10 +23,12 @@
import static com.android.systemui.screenrecord.ScreenRecordingAudioSource.MIC_AND_INTERNAL;
import android.annotation.Nullable;
+import android.app.ActivityManager;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.graphics.Bitmap;
+import android.graphics.drawable.Icon;
import android.hardware.display.DisplayManager;
import android.hardware.display.VirtualDisplay;
import android.media.MediaCodec;
@@ -51,6 +53,7 @@
import android.view.Surface;
import android.view.WindowManager;
+import com.android.internal.R;
import com.android.systemui.mediaprojection.MediaProjectionCaptureTarget;
import java.io.Closeable;
@@ -361,14 +364,27 @@
Files.copy(mTempVideoFile.toPath(), os);
os.close();
if (mTempAudioFile != null) mTempAudioFile.delete();
- DisplayMetrics metrics = mContext.getResources().getDisplayMetrics();
- Size size = new Size(metrics.widthPixels, metrics.heightPixels);
- SavedRecording recording = new SavedRecording(itemUri, mTempVideoFile, size);
+ SavedRecording recording = new SavedRecording(
+ itemUri, mTempVideoFile, getRequiredThumbnailSize());
mTempVideoFile.delete();
return recording;
}
/**
+ * Returns the required {@code Size} of the thumbnail.
+ */
+ private Size getRequiredThumbnailSize() {
+ boolean isLowRam = ActivityManager.isLowRamDeviceStatic();
+ int thumbnailIconHeight = mContext.getResources().getDimensionPixelSize(isLowRam
+ ? R.dimen.notification_big_picture_max_height_low_ram
+ : R.dimen.notification_big_picture_max_height);
+ int thumbnailIconWidth = mContext.getResources().getDimensionPixelSize(isLowRam
+ ? R.dimen.notification_big_picture_max_width_low_ram
+ : R.dimen.notification_big_picture_max_width);
+ return new Size(thumbnailIconWidth, thumbnailIconHeight);
+ }
+
+ /**
* Release the resources without saving the data
*/
protected void release() {
@@ -386,13 +402,14 @@
public class SavedRecording {
private Uri mUri;
- private Bitmap mThumbnailBitmap;
+ private Icon mThumbnailIcon;
protected SavedRecording(Uri uri, File file, Size thumbnailSize) {
mUri = uri;
try {
- mThumbnailBitmap = ThumbnailUtils.createVideoThumbnail(
+ Bitmap thumbnailBitmap = ThumbnailUtils.createVideoThumbnail(
file, thumbnailSize, null);
+ mThumbnailIcon = Icon.createWithBitmap(thumbnailBitmap);
} catch (IOException e) {
Log.e(TAG, "Error creating thumbnail", e);
}
@@ -402,8 +419,8 @@
return mUri;
}
- public @Nullable Bitmap getThumbnail() {
- return mThumbnailBitmap;
+ public @Nullable Icon getThumbnail() {
+ return mThumbnailIcon;
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java
index 19a5840..ef820f3 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java
@@ -18,6 +18,7 @@
import static com.android.systemui.flags.Flags.LOCKSCREEN_WALLPAPER_DREAM_ENABLED;
import static com.android.systemui.flags.Flags.TRACKPAD_GESTURE_COMMON;
+import static com.android.systemui.statusbar.StatusBarState.KEYGUARD;
import static com.android.systemui.util.kotlin.JavaAdapterKt.collectFlow;
import android.app.StatusBarManager;
@@ -477,8 +478,9 @@
if (KeyguardShadeMigrationNssl.isEnabled()) {
// When on lockscreen, if the touch originates at the top of the screen
// go directly to QS and not the shade
- if (mQuickSettingsController.shouldQuickSettingsIntercept(
- ev.getX(), ev.getY(), 0)) {
+ if (mStatusBarStateController.getState() == KEYGUARD
+ && mQuickSettingsController.shouldQuickSettingsIntercept(
+ ev.getX(), ev.getY(), 0)) {
mShadeLogger.d("NSWVC: QS intercepted");
return true;
}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModel.kt b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModel.kt
index 51276c6..314637e 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModel.kt
@@ -22,12 +22,11 @@
import android.icu.text.DateFormat
import android.icu.text.DisplayContext
import android.os.UserHandle
-import com.android.systemui.res.R
import com.android.systemui.broadcast.BroadcastDispatcher
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.res.R
import com.android.systemui.scene.domain.interactor.SceneInteractor
-import com.android.systemui.scene.shared.model.SceneKey
import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractor
import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconsViewModel
import java.util.Date
@@ -38,7 +37,6 @@
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
-import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
@@ -57,16 +55,6 @@
val mobileIconsViewModel: MobileIconsViewModel,
broadcastDispatcher: BroadcastDispatcher,
) {
- /** True if we are transitioning between Shade and QuickSettings scenes, in either direction. */
- val isTransitioning =
- combine(
- sceneInteractor.transitioning(from = SceneKey.Shade, to = SceneKey.QuickSettings),
- sceneInteractor.transitioning(from = SceneKey.QuickSettings, to = SceneKey.Shade)
- ) { shadeToQuickSettings, quickSettingsToShade ->
- shadeToQuickSettings || quickSettingsToShade
- }
- .stateIn(applicationScope, SharingStarted.WhileSubscribed(), false)
-
/** True if there is exactly one mobile connection. */
val isSingleCarrier: StateFlow<Boolean> = mobileIconsInteractor.isSingleCarrier
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java
index ada7d3e..ffb11dd 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java
@@ -56,6 +56,7 @@
import android.view.WindowInsets.Type.InsetsType;
import android.view.WindowInsetsController.Appearance;
import android.view.WindowInsetsController.Behavior;
+import android.view.accessibility.Flags;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
@@ -305,6 +306,13 @@
default void setTopAppHidesStatusBar(boolean topAppHidesStatusBar) { }
default void addQsTile(ComponentName tile) { }
+
+ /**
+ * Add a tile to the Quick Settings Panel
+ * @param tile the ComponentName of the {@link android.service.quicksettings.TileService}
+ * @param end if true, the tile will be added at the end. If false, at the beginning.
+ */
+ default void addQsTileToFrontOrEnd(ComponentName tile, boolean end) { }
default void remQsTile(ComponentName tile) { }
default void setQsTiles(String[] tiles) {}
@@ -904,8 +912,29 @@
@Override
public void addQsTile(ComponentName tile) {
- synchronized (mLock) {
- mHandler.obtainMessage(MSG_ADD_QS_TILE, tile).sendToTarget();
+ if (Flags.a11yQsShortcut()) {
+ addQsTileToFrontOrEnd(tile, false);
+ } else {
+ synchronized (mLock) {
+ mHandler.obtainMessage(MSG_ADD_QS_TILE, tile).sendToTarget();
+ }
+ }
+ }
+
+ /**
+ * Add a tile to the Quick Settings Panel
+ * @param tile the ComponentName of the {@link android.service.quicksettings.TileService}
+ * @param end if true, the tile will be added at the end. If false, at the beginning.
+ */
+ @Override
+ public void addQsTileToFrontOrEnd(ComponentName tile, boolean end) {
+ if (Flags.a11yQsShortcut()) {
+ synchronized (mLock) {
+ SomeArgs args = SomeArgs.obtain();
+ args.arg1 = tile;
+ args.arg2 = end;
+ mHandler.obtainMessage(MSG_ADD_QS_TILE, args).sendToTarget();
+ }
}
}
@@ -1546,11 +1575,21 @@
mCallbacks.get(i).showPictureInPictureMenu();
}
break;
- case MSG_ADD_QS_TILE:
- for (int i = 0; i < mCallbacks.size(); i++) {
- mCallbacks.get(i).addQsTile((ComponentName) msg.obj);
+ case MSG_ADD_QS_TILE: {
+ if (Flags.a11yQsShortcut()) {
+ SomeArgs someArgs = (SomeArgs) msg.obj;
+ for (int i = 0; i < mCallbacks.size(); i++) {
+ mCallbacks.get(i).addQsTileToFrontOrEnd(
+ (ComponentName) someArgs.arg1, (boolean) someArgs.arg2);
+ }
+ someArgs.recycle();
+ } else {
+ for (int i = 0; i < mCallbacks.size(); i++) {
+ mCallbacks.get(i).addQsTile((ComponentName) msg.obj);
+ }
}
break;
+ }
case MSG_REMOVE_QS_TILE:
for (int i = 0; i < mCallbacks.size(); i++) {
mCallbacks.get(i).remQsTile((ComponentName) msg.obj);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java
index d6d3e67..04d9b0c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java
@@ -591,8 +591,10 @@
}
private void updateLockScreenUserLockedMsg(int userId) {
- if (!mKeyguardUpdateMonitor.isUserUnlocked(userId)
- || mKeyguardUpdateMonitor.isEncryptedOrLockdown(userId)) {
+ boolean userUnlocked = mKeyguardUpdateMonitor.isUserUnlocked(userId);
+ boolean encryptedOrLockdown = mKeyguardUpdateMonitor.isEncryptedOrLockdown(userId);
+ mKeyguardLogger.logUpdateLockScreenUserLockedMsg(userId, userUnlocked, encryptedOrLockdown);
+ if (!userUnlocked || encryptedOrLockdown) {
mRotateTextViewController.updateIndication(
INDICATION_TYPE_USER_LOCKED,
new KeyguardIndication.Builder()
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/NetworkControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/NetworkControllerImpl.java
index 28d4457..fc84973 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/NetworkControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/NetworkControllerImpl.java
@@ -35,6 +35,7 @@
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Handler;
+import android.os.HandlerExecutor;
import android.os.Looper;
import android.provider.Settings;
import android.telephony.CarrierConfigManager;
@@ -60,6 +61,7 @@
import com.android.settingslib.mobile.TelephonyIcons;
import com.android.settingslib.net.DataUsageController;
import com.android.systemui.Dumpable;
+import com.android.systemui.res.R;
import com.android.systemui.broadcast.BroadcastDispatcher;
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.dagger.qualifiers.Background;
@@ -71,7 +73,6 @@
import com.android.systemui.log.core.LogLevel;
import com.android.systemui.log.dagger.StatusBarNetworkControllerLog;
import com.android.systemui.qs.tiles.dialog.InternetDialogFactory;
-import com.android.systemui.res.R;
import com.android.systemui.settings.UserTracker;
import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags;
import com.android.systemui.statusbar.policy.ConfigurationController;
@@ -84,8 +85,6 @@
import dalvik.annotation.optimization.NeverCompile;
-import kotlin.Unit;
-
import java.io.PrintWriter;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
@@ -100,6 +99,8 @@
import javax.inject.Inject;
+import kotlin.Unit;
+
/** Platform implementation of the network controller. **/
@SysUISingleton
public class NetworkControllerImpl extends BroadcastReceiver
@@ -349,7 +350,7 @@
// AIRPLANE_MODE_CHANGED is sent at boot; we've probably already missed it
updateAirplaneMode(true /* force callback */);
mUserTracker = userTracker;
- mUserTracker.addCallback(mUserChangedCallback, mBgExecutor);
+ mUserTracker.addCallback(mUserChangedCallback, new HandlerExecutor(mMainHandler));
deviceProvisionedController.addCallback(new DeviceProvisionedListener() {
@Override
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/KeyguardNotificationVisibilityProvider.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/KeyguardNotificationVisibilityProvider.kt
index 342828c..aca8b64 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/KeyguardNotificationVisibilityProvider.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/KeyguardNotificationVisibilityProvider.kt
@@ -6,7 +6,6 @@
import android.net.Uri
import android.os.Handler
import android.os.HandlerExecutor
-import android.os.HandlerThread
import android.os.UserHandle
import android.provider.Settings
import com.android.keyguard.KeyguardUpdateMonitor
@@ -88,7 +87,6 @@
secureSettings.getUriFor(Settings.Secure.LOCK_SCREEN_SHOW_SILENT_NOTIFICATIONS)
private val onStateChangedListeners = ListenerSet<Consumer<String>>()
private var hideSilentNotificationsOnLockscreen: Boolean = false
- private val handlerThread: HandlerThread = HandlerThread("KeyguardNotificationVis")
private val userTrackerCallback = object : UserTracker.Callback {
override fun onUserChanged(newUser: Int, userContext: Context) {
@@ -156,9 +154,7 @@
notifyStateChanged("onStatusBarUpcomingStateChanged")
}
})
- handlerThread.start()
- userTracker.addCallback(userTrackerCallback,
- HandlerExecutor(handlerThread.getThreadHandler()))
+ userTracker.addCallback(userTrackerCallback, HandlerExecutor(handler))
}
override fun addOnStateChangedListener(listener: Consumer<String>) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationSnooze.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationSnooze.java
index 3443da1..99a6f6a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationSnooze.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationSnooze.java
@@ -45,13 +45,15 @@
import android.widget.LinearLayout;
import android.widget.TextView;
+import androidx.annotation.NonNull;
+
import com.android.app.animation.Interpolators;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.logging.MetricsLogger;
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
-import com.android.systemui.res.R;
import com.android.systemui.plugins.statusbar.NotificationSwipeActionHelper;
import com.android.systemui.plugins.statusbar.NotificationSwipeActionHelper.SnoozeOption;
+import com.android.systemui.res.R;
import java.util.ArrayList;
import java.util.List;
@@ -77,6 +79,9 @@
private static final LogMaker UNDO_LOG =
new LogMaker(MetricsEvent.NOTIFICATION_UNDO_SNOOZE)
.setType(MetricsEvent.TYPE_ACTION);
+
+ private static final String PARAGRAPH_SEPARATOR = "\u2029";
+
private NotificationGuts mGutsContainer;
private NotificationSwipeActionHelper mSnoozeListener;
private StatusBarNotification mSbn;
@@ -111,8 +116,7 @@
}
@VisibleForTesting
- SnoozeOption getDefaultOption()
- {
+ SnoozeOption getDefaultOption() {
return mDefaultOption;
}
@@ -130,6 +134,8 @@
mSelectedOptionText = (TextView) findViewById(R.id.snooze_option_default);
mUndoButton = (TextView) findViewById(R.id.undo);
mUndoButton.setOnClickListener(this);
+ mUndoButton.setContentDescription(
+ getContext().getString(R.string.snooze_undo_content_description));
mExpandButton = (ImageView) findViewById(R.id.expand_button);
mDivider = findViewById(R.id.divider);
mDivider.setAlpha(0f);
@@ -163,6 +169,46 @@
info.addAction(action);
}
}
+
+ mSnoozeView.setAccessibilityDelegate(new AccessibilityDelegate() {
+ @Override
+ public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) {
+ super.onInitializeAccessibilityNodeInfo(host, info);
+ // Replace "Double tap to activate" prompt with "Double tap to expand/collapse"
+ AccessibilityAction customClick = new AccessibilityAction(
+ AccessibilityNodeInfo.ACTION_CLICK, getExpandActionString());
+ info.addAction(customClick);
+ }
+ });
+ }
+
+ /**
+ * Update the content description of the snooze view based on the snooze option and whether the
+ * snooze options are expanded or not.
+ * For example, this will be something like "Collapsed\u2029Snooze for 1 hour". The paragraph
+ * separator is added to introduce a break in speech, to match what TalkBack does by default
+ * when you e.g. press on a notification.
+ */
+ private void updateContentDescription() {
+ mSnoozeView.setContentDescription(
+ getExpandStateString() + PARAGRAPH_SEPARATOR + mSelectedOptionText.getText());
+ }
+
+ /** Returns "collapse" if the snooze options are expanded, or "expand" otherwise. */
+ @NonNull
+ private String getExpandActionString() {
+ return mContext.getString(mExpanded
+ ? com.android.internal.R.string.expand_button_content_description_expanded
+ : com.android.internal.R.string.expand_button_content_description_collapsed);
+ }
+
+
+ /** Returns "expanded" if the snooze options are expanded, or "collapsed" otherwise. */
+ @NonNull
+ private String getExpandStateString() {
+ return mContext.getString(
+ (mExpanded ? com.android.internal.R.string.content_description_expanded
+ : com.android.internal.R.string.content_description_collapsed));
}
@Override
@@ -179,6 +225,8 @@
if (so.getAccessibilityAction() != null
&& so.getAccessibilityAction().getId() == action) {
setSelected(so, true);
+ mSnoozeView.sendAccessibilityEvent(
+ AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED);
return true;
}
}
@@ -290,11 +338,10 @@
int drawableId = show ? com.android.internal.R.drawable.ic_collapse_notification
: com.android.internal.R.drawable.ic_expand_notification;
mExpandButton.setImageResource(drawableId);
- mExpandButton.setContentDescription(mContext.getString(show
- ? com.android.internal.R.string.expand_button_content_description_expanded
- : com.android.internal.R.string.expand_button_content_description_collapsed));
+ mExpandButton.setContentDescription(getExpandActionString());
if (mExpanded != show) {
mExpanded = show;
+ updateContentDescription();
animateSnoozeOptions(show);
if (mGutsContainer != null) {
mGutsContainer.onHeightChanged();
@@ -335,8 +382,11 @@
}
private void setSelected(SnoozeOption option, boolean userAction) {
- mSelectedOption = option;
- mSelectedOptionText.setText(option.getConfirmation());
+ if (option != mSelectedOption) {
+ mSelectedOption = option;
+ mSelectedOptionText.setText(option.getConfirmation());
+ updateContentDescription();
+ }
showSnoozeOptions(false);
hideSelectedOption();
if (userAction) {
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 dd04531..b9afb14 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
@@ -617,7 +617,11 @@
private final ScrollAdapter mScrollAdapter = new ScrollAdapter() {
@Override
public boolean isScrolledToTop() {
- return mOwnScrollY == 0;
+ if (SceneContainerFlag.isEnabled()) {
+ return mController.isPlaceholderScrolledToTop();
+ } else {
+ return mOwnScrollY == 0;
+ }
}
@Override
@@ -1442,7 +1446,14 @@
fraction = BouncerPanelExpansionCalculator.aboutToShowBouncerProgress(fraction);
}
final float stackY = MathUtils.lerp(0, endTopPosition, fraction);
- mAmbientState.setStackY(stackY);
+ // TODO(b/322228881): Clean up scene container vs legacy behavior in NSSL
+ if (SceneContainerFlag.isEnabled()) {
+ // stackY should be driven by scene container, not NSSL
+ mAmbientState.setStackY(mTopPadding);
+ } else {
+ mAmbientState.setStackY(stackY);
+ }
+
if (mOnStackYChanged != null) {
mOnStackYChanged.accept(listenerNeedsAnimation);
}
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 49fde39..ed26677 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
@@ -1144,6 +1144,14 @@
return mStackAppearanceInteractor.getStackBounds().getValue().getTop();
}
+ /**
+ * Returns whether the notification stack is scrolled to the top; i.e., it cannot be scrolled
+ * down any further.
+ */
+ public boolean isPlaceholderScrolledToTop() {
+ return mStackAppearanceInteractor.getScrolledToTop().getValue();
+ }
+
/** Set the intrinsic height of the stack content without additional padding. */
public void setIntrinsicContentHeight(float intrinsicContentHeight) {
mStackAppearanceInteractor.setIntrinsicContentHeight(intrinsicContentHeight);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/data/repository/NotificationStackAppearanceRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/data/repository/NotificationStackAppearanceRepository.kt
index aac3c28..0197264 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/data/repository/NotificationStackAppearanceRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/data/repository/NotificationStackAppearanceRepository.kt
@@ -44,4 +44,10 @@
* screen.
*/
val contentTop = MutableStateFlow(0f)
+
+ /**
+ * Whether the notification stack is scrolled to the top; i.e., it cannot be scrolled down any
+ * further.
+ */
+ val scrolledToTop = MutableStateFlow(true)
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractor.kt
index 1dfde09..8307397 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractor.kt
@@ -42,10 +42,16 @@
* notifications, this can exceed the space available on screen to show notifications, at which
* point the notification stack should become scrollable.
*/
- val intrinsicContentHeight = repository.intrinsicContentHeight.asStateFlow()
+ val intrinsicContentHeight: StateFlow<Float> = repository.intrinsicContentHeight.asStateFlow()
/** The y-coordinate in px of top of the contents of the notification stack. */
- val contentTop = repository.contentTop.asStateFlow()
+ val contentTop: StateFlow<Float> = repository.contentTop.asStateFlow()
+
+ /**
+ * Whether the notification stack is scrolled to the top; i.e., it cannot be scrolled down any
+ * further.
+ */
+ val scrolledToTop: StateFlow<Boolean> = repository.scrolledToTop.asStateFlow()
/** Sets the position of the notification stack in the current scene. */
fun setStackBounds(bounds: NotificationContainerBounds) {
@@ -62,4 +68,9 @@
fun setContentTop(startY: Float) {
repository.contentTop.value = startY
}
+
+ /** Sets whether the notification stack is scrolled to the top. */
+ fun setScrolledToTop(scrolledToTop: Boolean) {
+ repository.scrolledToTop.value = scrolledToTop
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationStackAppearanceViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationStackAppearanceViewBinder.kt
index ed15f55..6c2cbbe 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationStackAppearanceViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationStackAppearanceViewBinder.kt
@@ -25,7 +25,6 @@
import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController
import com.android.systemui.statusbar.notification.stack.ui.view.SharedNotificationContainer
import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationStackAppearanceViewModel
-import kotlin.math.pow
import kotlin.math.roundToInt
import kotlinx.coroutines.launch
@@ -65,7 +64,9 @@
viewModel.expandFraction.collect { expandFraction ->
ambientState.expansionFraction = expandFraction
controller.expandedHeight = expandFraction * controller.view.height
- controller.setMaxAlphaForExpansion(expandFraction.pow(0.75f))
+ controller.setMaxAlphaForExpansion(
+ ((expandFraction - 0.5f) / 0.5f).coerceAtLeast(0f)
+ )
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationStackAppearanceViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationStackAppearanceViewModel.kt
index 74db583..56ff7f9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationStackAppearanceViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationStackAppearanceViewModel.kt
@@ -19,11 +19,15 @@
import com.android.systemui.common.shared.model.NotificationContainerBounds
import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.scene.domain.interactor.SceneInteractor
+import com.android.systemui.scene.shared.model.ObservableTransitionState
+import com.android.systemui.scene.shared.model.SceneKey
import com.android.systemui.shade.domain.interactor.ShadeInteractor
import com.android.systemui.statusbar.notification.stack.domain.interactor.NotificationStackAppearanceInteractor
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.combine
/** ViewModel which represents the state of the NSSL/Controller in the world of flexiglass */
@SysUISingleton
@@ -32,9 +36,40 @@
constructor(
stackAppearanceInteractor: NotificationStackAppearanceInteractor,
shadeInteractor: ShadeInteractor,
+ sceneInteractor: SceneInteractor,
) {
- /** The expansion fraction from the top of the notification shade. */
- val expandFraction: Flow<Float> = shadeInteractor.shadeExpansion
+ /**
+ * The expansion fraction of the notification stack. It should go from 0 to 1 when transitioning
+ * from Gone to Shade scenes, and remain at 1 when in Lockscreen or Shade scenes and while
+ * transitioning from Shade to QuickSettings scenes.
+ */
+ val expandFraction: Flow<Float> =
+ combine(
+ shadeInteractor.shadeExpansion,
+ sceneInteractor.transitionState,
+ ) { shadeExpansion, transitionState ->
+ when (transitionState) {
+ is ObservableTransitionState.Idle -> {
+ if (transitionState.scene == SceneKey.Lockscreen) {
+ 1f
+ } else {
+ shadeExpansion
+ }
+ }
+ is ObservableTransitionState.Transition -> {
+ if (
+ (transitionState.fromScene == SceneKey.Shade &&
+ transitionState.toScene == SceneKey.QuickSettings) ||
+ (transitionState.fromScene == SceneKey.QuickSettings &&
+ transitionState.toScene == SceneKey.Shade)
+ ) {
+ 1f
+ } else {
+ shadeExpansion
+ }
+ }
+ }
+ }
/** The bounds of the notification stack in the current scene. */
val stackBounds: Flow<NotificationContainerBounds> = stackAppearanceInteractor.stackBounds
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt
index 385f061..a436f17 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt
@@ -87,4 +87,9 @@
fun onContentTopChanged(padding: Float) {
interactor.setContentTop(padding)
}
+
+ /** Sets whether the notification stack is scrolled to the top. */
+ fun setScrolledToTop(scrolledToTop: Boolean) {
+ interactor.setScrolledToTop(scrolledToTop)
+ }
}
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 4617ce4..3915c376 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
@@ -24,13 +24,24 @@
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
+import com.android.systemui.keyguard.shared.model.Edge
import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.shared.model.KeyguardState.ALTERNATE_BOUNCER
+import com.android.systemui.keyguard.shared.model.KeyguardState.AOD
+import com.android.systemui.keyguard.shared.model.KeyguardState.DOZING
+import com.android.systemui.keyguard.shared.model.KeyguardState.DREAMING
+import com.android.systemui.keyguard.shared.model.KeyguardState.GLANCEABLE_HUB
+import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN
+import com.android.systemui.keyguard.shared.model.KeyguardState.OCCLUDED
+import com.android.systemui.keyguard.shared.model.KeyguardState.PRIMARY_BOUNCER
import com.android.systemui.keyguard.shared.model.StatusBarState.SHADE_LOCKED
import com.android.systemui.keyguard.shared.model.TransitionState.RUNNING
import com.android.systemui.keyguard.shared.model.TransitionState.STARTED
import com.android.systemui.keyguard.ui.viewmodel.AodBurnInViewModel
import com.android.systemui.keyguard.ui.viewmodel.BurnInParameters
+import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel
import com.android.systemui.keyguard.ui.viewmodel.GlanceableHubToLockscreenTransitionViewModel
+import com.android.systemui.keyguard.ui.viewmodel.LockscreenToDreamingTransitionViewModel
import com.android.systemui.keyguard.ui.viewmodel.LockscreenToGlanceableHubTransitionViewModel
import com.android.systemui.keyguard.ui.viewmodel.LockscreenToOccludedTransitionViewModel
import com.android.systemui.keyguard.ui.viewmodel.OccludedToLockscreenTransitionViewModel
@@ -51,6 +62,7 @@
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flow
+import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.onStart
@@ -69,43 +81,52 @@
communalInteractor: CommunalInteractor,
private val occludedToLockscreenTransitionViewModel: OccludedToLockscreenTransitionViewModel,
lockscreenToOccludedTransitionViewModel: LockscreenToOccludedTransitionViewModel,
+ dreamingToLockscreenTransitionViewModel: DreamingToLockscreenTransitionViewModel,
+ lockscreenToDreamingTransitionViewModel: LockscreenToDreamingTransitionViewModel,
glanceableHubToLockscreenTransitionViewModel: GlanceableHubToLockscreenTransitionViewModel,
lockscreenToGlanceableHubTransitionViewModel: LockscreenToGlanceableHubTransitionViewModel,
private val aodBurnInViewModel: AodBurnInViewModel,
) {
- private val statesForConstrainedNotifications =
- setOf(
- KeyguardState.AOD,
- KeyguardState.LOCKSCREEN,
- KeyguardState.DOZING,
- KeyguardState.ALTERNATE_BOUNCER,
- KeyguardState.PRIMARY_BOUNCER
+ private val statesForConstrainedNotifications: Set<KeyguardState> =
+ setOf(AOD, LOCKSCREEN, DOZING, ALTERNATE_BOUNCER, PRIMARY_BOUNCER)
+
+ private val edgeToAlphaViewModel =
+ mapOf<Edge?, Flow<Float>>(
+ Edge(from = LOCKSCREEN, to = DREAMING) to
+ lockscreenToDreamingTransitionViewModel.lockscreenAlpha,
+ Edge(from = DREAMING, to = LOCKSCREEN) to
+ dreamingToLockscreenTransitionViewModel.lockscreenAlpha,
+ Edge(from = LOCKSCREEN, to = OCCLUDED) to
+ lockscreenToOccludedTransitionViewModel.lockscreenAlpha,
+ Edge(from = OCCLUDED, to = LOCKSCREEN) to
+ occludedToLockscreenTransitionViewModel.lockscreenAlpha,
)
- private val lockscreenToOccludedRunning =
- keyguardTransitionInteractor
- .transition(KeyguardState.LOCKSCREEN, KeyguardState.OCCLUDED)
- .map { it.transitionState == STARTED || it.transitionState == RUNNING }
+ private val lockscreenTransitionInProgress: Flow<Edge?> =
+ keyguardTransitionInteractor.transitions
+ .map { step ->
+ if (
+ (step.transitionState == STARTED || step.transitionState == RUNNING) &&
+ (step.from == LOCKSCREEN || step.to == LOCKSCREEN)
+ ) {
+ Edge(step.from, step.to)
+ } else {
+ null
+ }
+ }
.distinctUntilChanged()
- .onStart { emit(false) }
-
- private val occludedToLockscreenRunning =
- keyguardTransitionInteractor
- .transition(KeyguardState.OCCLUDED, KeyguardState.LOCKSCREEN)
- .map { it.transitionState == STARTED || it.transitionState == RUNNING }
- .distinctUntilChanged()
- .onStart { emit(false) }
+ .onStart { emit(null) }
private val lockscreenToGlanceableHubRunning =
keyguardTransitionInteractor
- .transition(KeyguardState.LOCKSCREEN, KeyguardState.GLANCEABLE_HUB)
+ .transition(LOCKSCREEN, GLANCEABLE_HUB)
.map { it.transitionState == STARTED || it.transitionState == RUNNING }
.distinctUntilChanged()
.onStart { emit(false) }
private val glanceableHubToLockscreenRunning =
keyguardTransitionInteractor
- .transition(KeyguardState.GLANCEABLE_HUB, KeyguardState.LOCKSCREEN)
+ .transition(GLANCEABLE_HUB, LOCKSCREEN)
.map { it.transitionState == STARTED || it.transitionState == RUNNING }
.distinctUntilChanged()
.onStart { emit(false) }
@@ -141,7 +162,7 @@
statesForConstrainedNotifications.contains(it)
},
keyguardTransitionInteractor
- .transitionValue(KeyguardState.LOCKSCREEN)
+ .transitionValue(LOCKSCREEN)
.onStart { emit(0f) }
.map { it > 0 }
) { constrainedNotificationState, transitioningToOrFromLockscreen ->
@@ -242,38 +263,46 @@
initialValue = NotificationContainerBounds(),
)
+ /** As QS is expanding, fade out notifications unless in splitshade */
+ private val alphaForQsExpansion: Flow<Float> =
+ interactor.configurationBasedDimensions.flatMapLatest {
+ if (it.useSplitShade) {
+ flowOf(1f)
+ } else {
+ shadeInteractor.qsExpansion.map { 1f - it }
+ }
+ }
+
val expansionAlpha: Flow<Float> =
// Due to issues with the legacy shade, some shade expansion events are sent incorrectly,
- // such as when the shade resets. This can happen while the LOCKSCREEN<->OCCLUDED transition
+ // such as when the shade resets. This can happen while the transition to/from LOCKSCREEN
// is running. Therefore use a series of flatmaps to prevent unwanted interruptions while
// those transitions are in progress. Without this, the alpha value will produce a visible
// flicker.
- lockscreenToOccludedRunning.flatMapLatest { isLockscreenToOccludedRunning ->
- if (isLockscreenToOccludedRunning) {
- lockscreenToOccludedTransitionViewModel.lockscreenAlpha
- } else {
- occludedToLockscreenRunning.flatMapLatest { isOccludedToLockscreenRunning ->
- if (isOccludedToLockscreenRunning) {
- occludedToLockscreenTransitionViewModel.lockscreenAlpha.onStart { emit(0f) }
- } else {
+ lockscreenTransitionInProgress
+ .flatMapLatest { edge ->
+ edgeToAlphaViewModel.getOrElse(
+ edge,
+ {
isOnLockscreenWithoutShade.flatMapLatest { isOnLockscreenWithoutShade ->
combineTransform(
keyguardInteractor.keyguardAlpha,
shadeCollpaseFadeIn,
- ) { alpha, shadeCollpaseFadeIn ->
+ alphaForQsExpansion,
+ ) { alpha, shadeCollpaseFadeIn, alphaForQsExpansion ->
if (isOnLockscreenWithoutShade) {
if (!shadeCollpaseFadeIn) {
emit(alpha)
}
} else {
- emit(1f)
+ emit(alphaForQsExpansion)
}
}
}
}
- }
+ )
}
- }
+ .distinctUntilChanged()
/**
* Returns a flow of the expected alpha while running a LOCKSCREEN<->GLANCEABLE_HUB transition
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java
index 60a4606..39ca7b2 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java
@@ -179,6 +179,11 @@
}
@Override
+ public void addQsTileToFrontOrEnd(ComponentName tile, boolean end) {
+ mQSHost.addTile(tile, end);
+ }
+
+ @Override
public void remQsTile(ComponentName tile) {
mQSHost.removeTileByUser(tile);
}
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 ade417d..64fcef5 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
@@ -125,6 +125,7 @@
import com.android.systemui.charging.WirelessChargingAnimation;
import com.android.systemui.classifier.FalsingCollector;
import com.android.systemui.colorextraction.SysuiColorExtractor;
+import com.android.systemui.communal.domain.interactor.CommunalInteractor;
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.dagger.qualifiers.UiBackground;
@@ -245,6 +246,7 @@
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.Executor;
+import java.util.function.Consumer;
import javax.inject.Inject;
import javax.inject.Named;
@@ -551,6 +553,25 @@
private final WakefulnessLifecycle mWakefulnessLifecycle;
protected final PowerInteractor mPowerInteractor;
+ private final CommunalInteractor mCommunalInteractor;
+
+ /**
+ * True if the device is showing the glanceable hub. See
+ * {@link CommunalInteractor#isIdleOnCommunal()} for more details.
+ */
+ private boolean mIsIdleOnCommunal = false;
+ private final Consumer<Boolean> mIdleOnCommunalConsumer = (Boolean idleOnCommunal) -> {
+ if (idleOnCommunal == mIsIdleOnCommunal) {
+ // Ignore initial value coming through the flow.
+ return;
+ }
+
+ mIsIdleOnCommunal = idleOnCommunal;
+ // Trigger an update for the scrim state when we enter or exit glanceable hub, so that we
+ // can transition to/from ScrimState.GLANCEABLE_HUB if needed.
+ updateScrimController();
+ };
+
private boolean mNoAnimationOnNextBarModeChange;
private final SysuiStatusBarStateController mStatusBarStateController;
@@ -618,6 +639,7 @@
ScreenLifecycle screenLifecycle,
WakefulnessLifecycle wakefulnessLifecycle,
PowerInteractor powerInteractor,
+ CommunalInteractor communalInteractor,
SysuiStatusBarStateController statusBarStateController,
Optional<Bubbles> bubblesOptional,
Lazy<NoteTaskController> noteTaskControllerLazy,
@@ -722,6 +744,7 @@
mScreenLifecycle = screenLifecycle;
mWakefulnessLifecycle = wakefulnessLifecycle;
mPowerInteractor = powerInteractor;
+ mCommunalInteractor = communalInteractor;
mStatusBarStateController = statusBarStateController;
mBubblesOptional = bubblesOptional;
mNoteTaskControllerLazy = noteTaskControllerLazy;
@@ -1051,6 +1074,10 @@
//TODO(b/264502026) move the rest of the listeners here.
mDeviceStateManager.registerCallback(mMainExecutor,
new FoldStateListener(mContext, this::onFoldedStateChanged));
+
+ mJavaAdapter.alwaysCollectFlow(
+ mCommunalInteractor.isIdleOnCommunal(),
+ mIdleOnCommunalConsumer);
}
/**
@@ -2795,6 +2822,8 @@
// This will cancel the keyguardFadingAway animation if it is running. We need to do
// this as otherwise it can remain pending and leave keyguard in a weird state.
mUnlockScrimCallback.onCancelled();
+ } else if (mIsIdleOnCommunal) {
+ mScrimController.transitionTo(ScrimState.GLANCEABLE_HUB);
} else if (mKeyguardStateController.isShowing()
&& !mKeyguardStateController.isOccluded()
&& !unlocking) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardLiftController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardLiftController.kt
deleted file mode 100644
index 9f08633..0000000
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardLiftController.kt
+++ /dev/null
@@ -1,128 +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
- */
-
-package com.android.systemui.statusbar.phone
-
-import android.content.Context
-import android.content.pm.PackageManager
-import android.hardware.Sensor
-import android.hardware.TriggerEvent
-import android.hardware.TriggerEventListener
-import com.android.keyguard.ActiveUnlockConfig
-import com.android.keyguard.KeyguardUpdateMonitor
-import com.android.keyguard.KeyguardUpdateMonitorCallback
-import com.android.systemui.CoreStartable
-import com.android.systemui.Dumpable
-import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.dump.DumpManager
-import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFaceAuthInteractor
-import com.android.systemui.plugins.statusbar.StatusBarStateController
-import com.android.systemui.user.domain.interactor.SelectedUserInteractor
-import com.android.systemui.util.Assert
-import com.android.systemui.util.sensors.AsyncSensorManager
-import java.io.PrintWriter
-import javax.inject.Inject
-
-/**
- * Triggers face auth on lift when the device is showing the lock screen. Only initialized
- * if face auth is supported on the device. Not to be confused with the lift to wake gesture
- * which is handled by {@link com.android.server.policy.PhoneWindowManager}.
- */
-@SysUISingleton
-class KeyguardLiftController @Inject constructor(
- private val context: Context,
- private val statusBarStateController: StatusBarStateController,
- private val asyncSensorManager: AsyncSensorManager,
- private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
- private val deviceEntryFaceAuthInteractor: DeviceEntryFaceAuthInteractor,
- private val dumpManager: DumpManager,
- private val selectedUserInteractor: SelectedUserInteractor,
-) : Dumpable, CoreStartable {
-
- private val pickupSensor = asyncSensorManager.getDefaultSensor(Sensor.TYPE_PICK_UP_GESTURE)
- private var isListening = false
- private var bouncerVisible = false
-
- override fun start() {
- if (context.packageManager.hasSystemFeature(PackageManager.FEATURE_FACE)) {
- init()
- }
- }
-
- private fun init() {
- dumpManager.registerDumpable(this)
- statusBarStateController.addCallback(statusBarStateListener)
- keyguardUpdateMonitor.registerCallback(keyguardUpdateMonitorCallback)
- updateListeningState()
- }
-
- private val listener: TriggerEventListener = object : TriggerEventListener() {
- override fun onTrigger(event: TriggerEvent?) {
- Assert.isMainThread()
- // Not listening anymore since trigger events unregister themselves
- isListening = false
- updateListeningState()
- deviceEntryFaceAuthInteractor.onDeviceLifted()
- keyguardUpdateMonitor.requestActiveUnlock(
- ActiveUnlockConfig.ActiveUnlockRequestOrigin.WAKE,
- "KeyguardLiftController")
- }
- }
-
- private val keyguardUpdateMonitorCallback = object : KeyguardUpdateMonitorCallback() {
- override fun onKeyguardBouncerFullyShowingChanged(bouncer: Boolean) {
- bouncerVisible = bouncer
- updateListeningState()
- }
-
- override fun onKeyguardVisibilityChanged(visible: Boolean) {
- updateListeningState()
- }
- }
-
- private val statusBarStateListener = object : StatusBarStateController.StateListener {
- override fun onDozingChanged(isDozing: Boolean) {
- updateListeningState()
- }
- }
-
- override fun dump(pw: PrintWriter, args: Array<out String>) {
- pw.println("KeyguardLiftController:")
- pw.println(" pickupSensor: $pickupSensor")
- pw.println(" isListening: $isListening")
- pw.println(" bouncerVisible: $bouncerVisible")
- }
-
- private fun updateListeningState() {
- if (pickupSensor == null) {
- return
- }
- val onKeyguard = keyguardUpdateMonitor.isKeyguardVisible &&
- !statusBarStateController.isDozing
-
- val isFaceEnabled = deviceEntryFaceAuthInteractor.isFaceAuthEnabledAndEnrolled()
- val shouldListen = (onKeyguard || bouncerVisible) && isFaceEnabled
- if (shouldListen != isListening) {
- isListening = shouldListen
-
- if (shouldListen) {
- asyncSensorManager.requestTriggerSensor(listener, pickupSensor)
- } else {
- asyncSensorManager.cancelTriggerSensor(listener, pickupSensor)
- }
- }
- }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java
index 3f20eaf..6f78604 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java
@@ -17,7 +17,9 @@
package com.android.systemui.statusbar.phone;
import static com.android.systemui.keyguard.shared.model.KeyguardState.ALTERNATE_BOUNCER;
+import static com.android.systemui.keyguard.shared.model.KeyguardState.GLANCEABLE_HUB;
import static com.android.systemui.keyguard.shared.model.KeyguardState.GONE;
+import static com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN;
import static com.android.systemui.keyguard.shared.model.KeyguardState.PRIMARY_BOUNCER;
import static com.android.systemui.util.kotlin.JavaAdapterKt.collectFlow;
@@ -62,6 +64,7 @@
import com.android.systemui.keyguard.KeyguardUnlockAnimationController;
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor;
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor;
+import com.android.systemui.keyguard.shared.model.KeyguardState;
import com.android.systemui.keyguard.shared.model.ScrimAlpha;
import com.android.systemui.keyguard.shared.model.TransitionState;
import com.android.systemui.keyguard.shared.model.TransitionStep;
@@ -292,6 +295,30 @@
mScrimBehind.setViewAlpha(mBehindAlpha);
};
+ /**
+ * Consumer that fades the behind scrim in and out during the transition between the lock screen
+ * and the glanceable hub.
+ *
+ * While the lock screen is showing, the behind scrim is used to slightly darken the lock screen
+ * wallpaper underneath. Since the glanceable hub is under all of the scrims, we want to fade
+ * out the scrim so that the glanceable hub isn't darkened when it opens.
+ *
+ * {@link #applyState()} handles the scrim alphas once on the glanceable hub, this is only
+ * responsible for setting the behind alpha during the transition.
+ */
+ private final Consumer<TransitionStep> mGlanceableHubConsumer = (TransitionStep step) -> {
+ final float baseAlpha = ScrimState.KEYGUARD.getBehindAlpha();
+ final float transitionProgress = step.getValue();
+ if (step.getTo() == KeyguardState.LOCKSCREEN) {
+ // Transitioning back to lock screen, fade in behind scrim again.
+ mBehindAlpha = baseAlpha * transitionProgress;
+ } else if (step.getTo() == GLANCEABLE_HUB) {
+ // Transitioning to glanceable hub, fade out behind scrim.
+ mBehindAlpha = baseAlpha * (1 - transitionProgress);
+ }
+ mScrimBehind.setViewAlpha(mBehindAlpha);
+ };
+
Consumer<TransitionStep> mBouncerToGoneTransition;
@Inject
@@ -444,6 +471,14 @@
mBouncerToGoneTransition, mMainDispatcher);
collectFlow(behindScrim, mAlternateBouncerToGoneTransitionViewModel.getScrimAlpha(),
mScrimAlphaConsumer, mMainDispatcher);
+
+ // LOCKSCREEN<->GLANCEABLE_HUB
+ collectFlow(behindScrim,
+ mKeyguardTransitionInteractor.transition(LOCKSCREEN, GLANCEABLE_HUB),
+ mGlanceableHubConsumer, mMainDispatcher);
+ collectFlow(behindScrim,
+ mKeyguardTransitionInteractor.transition(GLANCEABLE_HUB, LOCKSCREEN),
+ mGlanceableHubConsumer, mMainDispatcher);
}
// TODO(b/270984686) recompute scrim height accurately, based on shade contents.
@@ -815,9 +850,9 @@
return;
}
mBouncerHiddenFraction = bouncerHiddenAmount;
- if (mState == ScrimState.DREAMING) {
- // Only the dreaming state requires this for the scrim calculation, so we should
- // only trigger an update if dreaming.
+ if (mState == ScrimState.DREAMING || mState == ScrimState.GLANCEABLE_HUB) {
+ // The dreaming and glanceable hub states requires this for the scrim calculation, so we
+ // should only trigger an update in those states.
applyAndDispatchState();
}
}
@@ -939,7 +974,7 @@
} else if (mState == ScrimState.AUTH_SCRIMMED_SHADE) {
mNotificationsAlpha = (float) Math.pow(getInterpolatedFraction(), 0.8f);
} else if (mState == ScrimState.KEYGUARD || mState == ScrimState.SHADE_LOCKED
- || mState == ScrimState.PULSING) {
+ || mState == ScrimState.PULSING || mState == ScrimState.GLANCEABLE_HUB) {
Pair<Integer, Float> result = calculateBackStateForState(mState);
int behindTint = result.first;
float behindAlpha = result.second;
@@ -950,6 +985,11 @@
mTransitionToFullShadeProgress);
behindTint = ColorUtils.blendARGB(behindTint, shadeResult.first,
mTransitionToFullShadeProgress);
+ } else if (mState == ScrimState.GLANCEABLE_HUB && mTransitionToFullShadeProgress == 0.0f
+ && mBouncerHiddenFraction == KeyguardBouncerConstants.EXPANSION_HIDDEN) {
+ // Behind scrim should not be visible when idle on the glanceable hub and neither
+ // bouncer nor shade are showing.
+ behindAlpha = 0f;
}
mInFrontAlpha = mState.getFrontAlpha();
if (mClipsQsScrim) {
@@ -965,6 +1005,13 @@
} else if (mState == ScrimState.SHADE_LOCKED) {
// going from KEYGUARD to SHADE_LOCKED state
mNotificationsAlpha = getInterpolatedFraction();
+ } else if (mState == ScrimState.GLANCEABLE_HUB
+ && mTransitionToFullShadeProgress == 0.0f) {
+ // Notification scrim should not be visible on the glanceable hub unless the
+ // shade is showing or transitioning in. Otherwise the notification scrim will
+ // be visible as the bouncer transitions in or after the notification shade
+ // closes.
+ mNotificationsAlpha = 0;
} else {
mNotificationsAlpha = Math.max(1.0f - getInterpolatedFraction(), mQsExpansion);
}
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 61bd112..f2a649b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimState.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimState.java
@@ -296,6 +296,21 @@
updateScrimColor(mScrimBehind, 1f /* alpha */, mBackgroundColor);
}
}
+ },
+
+ /**
+ * Device is locked or on dream and user has swiped from the right edge to enter the glanceable
+ * hub UI. From this state, the user can swipe from the left edge to go back to the lock screen
+ * or dream, as well as swipe down for the notifications and up for the bouncer.
+ */
+ GLANCEABLE_HUB {
+ @Override
+ public void prepare(ScrimState previousState) {
+ // No scrims should be visible by default in this state.
+ mBehindAlpha = 0;
+ mNotifAlpha = 0;
+ mFrontAlpha = 0;
+ }
};
boolean mBlankScreen = false;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImpl.kt
index a5b9dc1..f73d089 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImpl.kt
@@ -153,7 +153,14 @@
if (satelliteManager != null) {
// First, check that satellite is supported on this device
scope.launch {
- ensureMinUptime(systemClock, MIN_UPTIME)
+ val waitTime = ensureMinUptime(systemClock, MIN_UPTIME)
+ if (waitTime > 0) {
+ logBuffer.i({ long1 = waitTime }) {
+ "Waiting $long1 ms before checking for satellite support"
+ }
+ delay(waitTime)
+ }
+
satelliteSupport.value = satelliteManager.checkSatelliteSupported()
logBuffer.i(
@@ -259,7 +266,7 @@
}
override fun onResult(allowed: Boolean) {
- logBuffer.i { allowed.toString() }
+ logBuffer.i { "isSatelliteAllowedForCurrentLocation: $allowed" }
isSatelliteAllowedForCurrentLocation.value = allowed
}
}
@@ -308,19 +315,14 @@
// TTL for satellite polling is one hour
const val POLLING_INTERVAL_MS: Long = 1000 * 60 * 60
- // Let the system boot up and stabilize before we check for system support
- const val MIN_UPTIME: Long = 1000 * 60
+ // Let the system boot up (5s) and stabilize before we check for system support
+ const val MIN_UPTIME: Long = 1000 * 5
private const val TAG = "DeviceBasedSatelliteRepo"
- /** If our process hasn't been up for at least MIN_UPTIME, delay until we reach that time */
- private suspend fun ensureMinUptime(clock: SystemClock, uptime: Long) {
- val timeTilMinUptime =
- uptime - (clock.uptimeMillis() - android.os.Process.getStartUptimeMillis())
- if (timeTilMinUptime > 0) {
- delay(timeTilMinUptime)
- }
- }
+ /** Calculates how long we have to wait to reach MIN_UPTIME */
+ private fun ensureMinUptime(clock: SystemClock, uptime: Long): Long =
+ uptime - (clock.uptimeMillis() - android.os.Process.getStartUptimeMillis())
/** A couple of convenience logging methods rather than a whole class */
private fun LogBuffer.i(
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/domain/interactor/DeviceBasedSatelliteInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/domain/interactor/DeviceBasedSatelliteInteractor.kt
index 8779577..6e1114c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/domain/interactor/DeviceBasedSatelliteInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/domain/interactor/DeviceBasedSatelliteInteractor.kt
@@ -79,7 +79,7 @@
} else {
flowOf(false)
}
- .stateIn(scope, SharingStarted.WhileSubscribed(), false)
+ .stateIn(scope, SharingStarted.WhileSubscribed(), true)
}
/**
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModel.kt
index 0051161..1c33d3f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModel.kt
@@ -18,6 +18,7 @@
import com.android.systemui.common.shared.model.Icon
import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.statusbar.pipeline.airplane.data.repository.AirplaneModeRepository
import com.android.systemui.statusbar.pipeline.satellite.domain.interactor.DeviceBasedSatelliteInteractor
import com.android.systemui.statusbar.pipeline.satellite.ui.model.SatelliteIconModel
import javax.inject.Inject
@@ -40,6 +41,7 @@
constructor(
interactor: DeviceBasedSatelliteInteractor,
@Application scope: CoroutineScope,
+ airplaneModeRepository: AirplaneModeRepository,
) {
private val shouldShowIcon: StateFlow<Boolean> =
interactor.areAllConnectionsOutOfService
@@ -47,7 +49,11 @@
if (!allOos) {
flowOf(false)
} else {
- interactor.isSatelliteAllowed
+ combine(interactor.isSatelliteAllowed, airplaneModeRepository.isAirplaneMode) {
+ isSatelliteAllowed,
+ isAirplaneMode ->
+ isSatelliteAllowed && !isAirplaneMode
+ }
}
}
.stateIn(scope, SharingStarted.WhileSubscribed(), false)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/Clock.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/Clock.java
index a2d8d15..20d1fff 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/Clock.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/Clock.java
@@ -28,8 +28,6 @@
import android.icu.text.DateTimePatternGenerator;
import android.os.Bundle;
import android.os.Handler;
-import android.os.HandlerExecutor;
-import android.os.HandlerThread;
import android.os.Parcelable;
import android.os.SystemClock;
import android.os.UserHandle;
@@ -50,11 +48,11 @@
import com.android.settingslib.Utils;
import com.android.systemui.Dependency;
import com.android.systemui.FontSizeUtils;
+import com.android.systemui.res.R;
import com.android.systemui.broadcast.BroadcastDispatcher;
import com.android.systemui.demomode.DemoModeCommandReceiver;
import com.android.systemui.plugins.DarkIconDispatcher;
import com.android.systemui.plugins.DarkIconDispatcher.DarkReceiver;
-import com.android.systemui.res.R;
import com.android.systemui.settings.UserTracker;
import com.android.systemui.statusbar.CommandQueue;
import com.android.systemui.statusbar.phone.StatusBarIconController;
@@ -108,7 +106,6 @@
private final int mAmPmStyle;
private boolean mShowSeconds;
private Handler mSecondsHandler;
- private HandlerThread mHandlerThread;
// Fields to cache the width so the clock remains at an approximately constant width
private int mCharsAtCurrentWidth = -1;
@@ -149,8 +146,6 @@
}
mBroadcastDispatcher = Dependency.get(BroadcastDispatcher.class);
mUserTracker = Dependency.get(UserTracker.class);
- mHandlerThread = new HandlerThread("Clock");
- mHandlerThread.start();
setIncludeFontPadding(false);
}
@@ -210,8 +205,7 @@
Dependency.get(TunerService.class).addTunable(this, CLOCK_SECONDS,
StatusBarIconController.ICON_HIDE_LIST);
mCommandQueue.addCallback(this);
- mUserTracker.addCallback(mUserChangedCallback,
- new HandlerExecutor(mHandlerThread.getThreadHandler()));
+ mUserTracker.addCallback(mUserChangedCallback, mContext.getMainExecutor());
mCurrentUserId = mUserTracker.getUserId();
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/NextAlarmControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/NextAlarmControllerImpl.java
index a7440d6..b7d8ee3 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/NextAlarmControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/NextAlarmControllerImpl.java
@@ -21,8 +21,6 @@
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
-import android.os.HandlerExecutor;
-import android.os.HandlerThread;
import android.os.UserHandle;
import androidx.annotation.NonNull;
@@ -53,7 +51,6 @@
private final UserTracker mUserTracker;
private AlarmManager mAlarmManager;
private AlarmManager.AlarmClockInfo mNextAlarm;
- private HandlerThread mHandlerThread;
private final UserTracker.Callback mUserChangedCallback =
new UserTracker.Callback() {
@@ -78,10 +75,7 @@
IntentFilter filter = new IntentFilter();
filter.addAction(AlarmManager.ACTION_NEXT_ALARM_CLOCK_CHANGED);
broadcastDispatcher.registerReceiver(this, filter, null, UserHandle.ALL);
- mHandlerThread = new HandlerThread("NextAlarmControllerImpl");
- mHandlerThread.start();
- mUserTracker.addCallback(mUserChangedCallback,
- new HandlerExecutor(mHandlerThread.getThreadHandler()));
+ mUserTracker.addCallback(mUserChangedCallback, mainExecutor);
updateNextAlarm();
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SecurityControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SecurityControllerImpl.java
index 6a6efbc..9f4a906 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SecurityControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SecurityControllerImpl.java
@@ -157,7 +157,7 @@
// TODO: re-register network callback on user change.
mConnectivityManager.registerNetworkCallback(REQUEST, mNetworkCallback);
onUserSwitched(mUserTracker.getUserId());
- mUserTracker.addCallback(mUserChangedCallback, mBgExecutor);
+ mUserTracker.addCallback(mUserChangedCallback, mMainExecutor);
}
public void dump(PrintWriter pw, String[] args) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserInfoControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserInfoControllerImpl.java
index 0bc0e88..2ed9d15 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserInfoControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserInfoControllerImpl.java
@@ -36,9 +36,9 @@
import com.android.internal.util.UserIcons;
import com.android.settingslib.drawable.UserIconDrawable;
-import com.android.systemui.dagger.SysUISingleton;
-import com.android.systemui.dagger.qualifiers.Background;
import com.android.systemui.res.R;
+import com.android.systemui.dagger.SysUISingleton;
+import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.settings.UserTracker;
import java.util.ArrayList;
@@ -66,11 +66,11 @@
/**
*/
@Inject
- public UserInfoControllerImpl(Context context, @Background Executor bgExecutor,
+ public UserInfoControllerImpl(Context context, @Main Executor mainExecutor,
UserTracker userTracker) {
mContext = context;
mUserTracker = userTracker;
- mUserTracker.addCallback(mUserChangedCallback, bgExecutor);
+ mUserTracker.addCallback(mUserChangedCallback, mainExecutor);
IntentFilter profileFilter = new IntentFilter();
profileFilter.addAction(ContactsContract.Intents.ACTION_PROFILE_CHANGED);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ZenModeControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ZenModeControllerImpl.java
index f0b4930..df210b0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ZenModeControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ZenModeControllerImpl.java
@@ -29,7 +29,6 @@
import android.net.Uri;
import android.os.Handler;
import android.os.HandlerExecutor;
-import android.os.HandlerThread;
import android.os.Trace;
import android.os.UserHandle;
import android.os.UserManager;
@@ -82,7 +81,6 @@
private volatile int mZenMode;
private long mZenUpdateTime;
private NotificationManager.Policy mConsolidatedNotificationPolicy;
- private HandlerThread mHandlerThread;
private final UserTracker.Callback mUserChangedCallback =
new UserTracker.Callback() {
@@ -135,8 +133,6 @@
}
}
};
- mHandlerThread = new HandlerThread("ZenModeControllerImpl");
- mHandlerThread.start();
mNoMan = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
globalSettings.registerContentObserver(Global.ZEN_MODE, modeContentObserver);
updateZenMode(getModeSettingValueFromProvider());
@@ -147,8 +143,7 @@
mSetupObserver = new SetupObserver(handler);
mSetupObserver.register();
mUserManager = context.getSystemService(UserManager.class);
- mUserTracker.addCallback(mUserChangedCallback,
- new HandlerExecutor(mHandlerThread.getThreadHandler()));
+ mUserTracker.addCallback(mUserChangedCallback, new HandlerExecutor(handler));
// This registers the alarm broadcast receiver for the current user
mUserChangedCallback.onUserChanged(getCurrentUser(), context);
diff --git a/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java b/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java
index 77518db..2b9ad50 100644
--- a/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java
+++ b/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java
@@ -480,7 +480,7 @@
return;
}
- mUserTracker.addCallback(mUserTrackerCallback, mBgExecutor);
+ mUserTracker.addCallback(mUserTrackerCallback, mMainExecutor);
mDeviceProvisionedController.addCallback(mDeviceProvisionedListener);
diff --git a/packages/SystemUI/src/com/android/systemui/tuner/TunerServiceImpl.java b/packages/SystemUI/src/com/android/systemui/tuner/TunerServiceImpl.java
index f5b4d17..550a65c 100644
--- a/packages/SystemUI/src/com/android/systemui/tuner/TunerServiceImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/tuner/TunerServiceImpl.java
@@ -26,7 +26,6 @@
import android.net.Uri;
import android.os.Handler;
import android.os.HandlerExecutor;
-import android.os.HandlerThread;
import android.os.Looper;
import android.os.UserManager;
import android.provider.Settings;
@@ -39,11 +38,11 @@
import com.android.internal.util.ArrayUtils;
import com.android.systemui.DejankUtils;
+import com.android.systemui.res.R;
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.demomode.DemoModeController;
import com.android.systemui.qs.QSHost;
-import com.android.systemui.res.R;
import com.android.systemui.settings.UserTracker;
import com.android.systemui.statusbar.phone.StatusBarIconController;
import com.android.systemui.statusbar.phone.SystemUIDialog;
@@ -99,7 +98,6 @@
private UserTracker.Callback mCurrentUserTracker;
private UserTracker mUserTracker;
private final ComponentName mTunerComponent;
- private HandlerThread mHandlerThread;
/**
*/
@@ -119,8 +117,7 @@
mDemoModeController = demoModeController;
mUserTracker = userTracker;
mTunerComponent = new ComponentName(mContext, TunerActivity.class);
- mHandlerThread = new HandlerThread("TunerServiceImpl");
- mHandlerThread.start();
+
for (UserInfo user : UserManager.get(mContext).getUsers()) {
mCurrentUser = user.getUserHandle().getIdentifier();
if (getValue(TUNER_VERSION, 0) != CURRENT_TUNER_VERSION) {
@@ -138,7 +135,7 @@
}
};
mUserTracker.addCallback(mCurrentUserTracker,
- new HandlerExecutor(mHandlerThread.getThreadHandler()));
+ new HandlerExecutor(mainHandler));
}
@Override
diff --git a/packages/SystemUI/src/com/android/systemui/unfold/SysUIUnfoldModule.kt b/packages/SystemUI/src/com/android/systemui/unfold/SysUIUnfoldModule.kt
index 10fc83c..0016d95 100644
--- a/packages/SystemUI/src/com/android/systemui/unfold/SysUIUnfoldModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/unfold/SysUIUnfoldModule.kt
@@ -26,7 +26,6 @@
import com.android.systemui.unfold.util.UnfoldKeyguardVisibilityManager
import com.android.systemui.util.kotlin.getOrNull
import dagger.BindsInstance
-import dagger.Lazy
import dagger.Module
import dagger.Provides
import dagger.Subcomponent
@@ -57,7 +56,6 @@
rotationProvider: Optional<NaturalRotationUnfoldProgressProvider>,
@Named(UNFOLD_STATUS_BAR) scopedProvider: Optional<ScopedUnfoldTransitionProgressProvider>,
@UnfoldBg bgProvider: Optional<UnfoldTransitionProgressProvider>,
- unfoldLatencyTracker: Lazy<UnfoldLatencyTracker>,
factory: SysUIUnfoldComponent.Factory
): Optional<SysUIUnfoldComponent> {
val p1 = provider.getOrNull()
@@ -67,7 +65,7 @@
return if (p1 == null || p2 == null || p3 == null || p4 == null) {
Optional.empty()
} else {
- Optional.of(factory.create(p1, p2, p3, p4, unfoldLatencyTracker.get()))
+ Optional.of(factory.create(p1, p2, p3, p4))
}
}
}
@@ -82,8 +80,7 @@
@BindsInstance p1: UnfoldTransitionProgressProvider,
@BindsInstance p2: NaturalRotationUnfoldProgressProvider,
@BindsInstance p3: ScopedUnfoldTransitionProgressProvider,
- @BindsInstance @UnfoldBg p4: UnfoldTransitionProgressProvider,
- @BindsInstance p5: UnfoldLatencyTracker,
+ @BindsInstance @UnfoldBg p4: UnfoldTransitionProgressProvider
): SysUIUnfoldComponent
}
diff --git a/packages/SystemUI/src/com/android/systemui/unfold/UnfoldLatencyTracker.kt b/packages/SystemUI/src/com/android/systemui/unfold/UnfoldLatencyTracker.kt
index 8c66c2f..33fa9b8 100644
--- a/packages/SystemUI/src/com/android/systemui/unfold/UnfoldLatencyTracker.kt
+++ b/packages/SystemUI/src/com/android/systemui/unfold/UnfoldLatencyTracker.kt
@@ -22,7 +22,6 @@
import android.os.Trace
import android.util.Log
import com.android.internal.util.LatencyTracker
-import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.UiBackground
import com.android.systemui.keyguard.ScreenLifecycle
import com.android.systemui.unfold.UnfoldTransitionProgressProvider.TransitionProgressListener
@@ -42,7 +41,7 @@
* For now, the focus is on the time the inner display is visible, but in the future, it is easily
* possible to monitor the time to go from the inner screen to the outer.
*/
-@SysUISingleton
+@SysUIUnfoldScope
class UnfoldLatencyTracker
@Inject
constructor(
diff --git a/packages/SystemUI/src/com/android/systemui/unfold/UnfoldTransitionModule.kt b/packages/SystemUI/src/com/android/systemui/unfold/UnfoldTransitionModule.kt
index 8bef53c..9bd0e32 100644
--- a/packages/SystemUI/src/com/android/systemui/unfold/UnfoldTransitionModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/unfold/UnfoldTransitionModule.kt
@@ -177,17 +177,20 @@
@Module
interface Bindings {
- @Binds
- @IntoMap
- @ClassKey(UnfoldTraceLogger::class)
- fun bindUnfoldTraceLogger(impl: UnfoldTraceLogger): CoreStartable
-
@Binds fun bindRepository(impl: UnfoldTransitionRepositoryImpl): UnfoldTransitionRepository
@Binds fun bindInteractor(impl: UnfoldTransitionInteractorImpl): UnfoldTransitionInteractor
@Binds fun bindFoldStateRepository(impl: FoldStateRepositoryImpl): FoldStateRepository
}
+
+ @Module
+ interface Startables {
+ @Binds
+ @IntoMap
+ @ClassKey(UnfoldTraceLogger::class)
+ fun bindUnfoldTraceLogger(impl: UnfoldTraceLogger): CoreStartable
+ }
}
const val UNFOLD_STATUS_BAR = "unfold_status_bar"
diff --git a/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt b/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt
index 74e1339..37be1c6 100644
--- a/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt
@@ -17,6 +17,7 @@
package com.android.systemui.user.data.repository
+import android.annotation.SuppressLint
import android.content.Context
import android.content.pm.UserInfo
import android.os.UserHandle
@@ -190,7 +191,7 @@
}
}
- tracker.addCallback(callback, backgroundDispatcher.asExecutor())
+ tracker.addCallback(callback, mainDispatcher.asExecutor())
send(currentSelectionStatus)
awaitClose { tracker.removeCallback(callback) }
@@ -209,18 +210,15 @@
override val selectedUserInfo: Flow<UserInfo> = selectedUser.map { it.userInfo }
+ @SuppressLint("MissingPermission")
override fun refreshUsers() {
applicationScope.launch {
- val result = withContext(backgroundDispatcher) { manager.aliveUsers }
-
- if (result != null) {
- _userInfos.value =
- result
- // Users should be sorted by ascending creation time.
- .sortedBy { it.creationTime }
- // The guest user is always last, regardless of creation time.
- .sortedBy { it.isGuest }
- }
+ _userInfos.value =
+ withContext(backgroundDispatcher) { manager.aliveUsers }
+ // Users should be sorted by ascending creation time.
+ .sortedBy { it.creationTime }
+ // The guest user is always last, regardless of creation time.
+ .sortedBy { it.isGuest }
if (mainUserId == UserHandle.USER_NULL) {
val mainUser = withContext(backgroundDispatcher) { manager.mainUser }
diff --git a/packages/SystemUI/src/com/android/systemui/util/animation/data/repository/AnimationStatusRepository.kt b/packages/SystemUI/src/com/android/systemui/util/animation/data/repository/AnimationStatusRepository.kt
index adae782..31a8d86 100644
--- a/packages/SystemUI/src/com/android/systemui/util/animation/data/repository/AnimationStatusRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/util/animation/data/repository/AnimationStatusRepository.kt
@@ -26,7 +26,7 @@
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.withContext
+import kotlinx.coroutines.flow.flowOn
/** Utility class that could give information about if animation are enabled in the system */
interface AnimationStatusRepository {
@@ -45,24 +45,26 @@
* Emits true if animations are enabled in the system, after subscribing it immediately emits
* the current state
*/
- override fun areAnimationsEnabled(): Flow<Boolean> = conflatedCallbackFlow {
- val initialValue = withContext(backgroundDispatcher) { resolver.areAnimationsEnabled() }
- trySend(initialValue)
+ override fun areAnimationsEnabled(): Flow<Boolean> =
+ conflatedCallbackFlow {
+ val initialValue = resolver.areAnimationsEnabled()
+ trySend(initialValue)
- val observer =
- object : ContentObserver(backgroundHandler) {
- override fun onChange(selfChange: Boolean) {
- val updatedValue = resolver.areAnimationsEnabled()
- trySend(updatedValue)
- }
+ val observer =
+ object : ContentObserver(backgroundHandler) {
+ override fun onChange(selfChange: Boolean) {
+ val updatedValue = resolver.areAnimationsEnabled()
+ trySend(updatedValue)
+ }
+ }
+
+ resolver.registerContentObserver(
+ Settings.Global.getUriFor(Settings.Global.ANIMATOR_DURATION_SCALE),
+ /* notifyForDescendants= */ false,
+ observer
+ )
+
+ awaitClose { resolver.unregisterContentObserver(observer) }
}
-
- resolver.registerContentObserver(
- Settings.Global.getUriFor(Settings.Global.ANIMATOR_DURATION_SCALE),
- /* notifyForDescendants= */ false,
- observer
- )
-
- awaitClose { resolver.unregisterContentObserver(observer) }
- }
+ .flowOn(backgroundDispatcher)
}
diff --git a/packages/SystemUI/src/com/android/systemui/util/kotlin/CoroutinesModule.kt b/packages/SystemUI/src/com/android/systemui/util/kotlin/CoroutinesModule.kt
deleted file mode 100644
index cf6b0d9..0000000
--- a/packages/SystemUI/src/com/android/systemui/util/kotlin/CoroutinesModule.kt
+++ /dev/null
@@ -1,85 +0,0 @@
-package com.android.systemui.util.kotlin
-
-import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.dagger.qualifiers.Application
-import com.android.systemui.dagger.qualifiers.Background
-import com.android.systemui.dagger.qualifiers.Main
-import com.android.systemui.dagger.qualifiers.Tracing
-import com.android.app.tracing.coroutines.createCoroutineTracingContext
-import dagger.Module
-import dagger.Provides
-import kotlinx.coroutines.CoroutineDispatcher
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.plus
-import kotlin.coroutines.CoroutineContext
-
-/** Providers for various coroutines-related constructs. */
-@Module
-class CoroutinesModule {
- @Provides
- @SysUISingleton
- @Application
- fun applicationScope(
- @Main dispatcherContext: CoroutineContext,
- ): CoroutineScope = CoroutineScope(dispatcherContext)
-
- @Provides
- @SysUISingleton
- @Background
- fun bgApplicationScope(
- @Application applicationScope: CoroutineScope,
- @Background coroutineContext: CoroutineContext,
- ): CoroutineScope = applicationScope.plus(coroutineContext)
-
- @Provides
- @SysUISingleton
- @Main
- @Deprecated(
- "Use @Main CoroutineContext instead",
- ReplaceWith("mainCoroutineContext()", "kotlin.coroutines.CoroutineContext")
- )
- fun mainDispatcher(): CoroutineDispatcher = Dispatchers.Main.immediate
-
- @Provides
- @SysUISingleton
- @Main
- fun mainCoroutineContext(@Tracing tracingCoroutineContext: CoroutineContext): CoroutineContext {
- return Dispatchers.Main.immediate + tracingCoroutineContext
- }
-
- /**
- * Provide a [CoroutineDispatcher] backed by a thread pool containing at most X threads, where
- * X is the number of CPU cores available.
- *
- * Because there are multiple threads at play, there is no serialization order guarantee. You
- * should use a [kotlinx.coroutines.channels.Channel] for serialization if necessary.
- *
- * @see Dispatchers.Default
- */
- @Provides
- @SysUISingleton
- @Background
- @Deprecated(
- "Use @Background CoroutineContext instead",
- ReplaceWith("bgCoroutineContext()", "kotlin.coroutines.CoroutineContext")
- )
- fun bgDispatcher(): CoroutineDispatcher = Dispatchers.IO
-
-
- @Provides
- @Background
- @SysUISingleton
- fun bgCoroutineContext(@Tracing tracingCoroutineContext: CoroutineContext): CoroutineContext {
- return Dispatchers.IO + tracingCoroutineContext
- }
-
- @OptIn(ExperimentalCoroutinesApi::class)
- @Provides
- @Tracing
- @SysUISingleton
- fun tracingCoroutineContext(): CoroutineContext {
- return createCoroutineTracingContext()
- }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/util/kotlin/Flow.kt b/packages/SystemUI/src/com/android/systemui/util/kotlin/Flow.kt
index 8fe57e11..d47413f 100644
--- a/packages/SystemUI/src/com/android/systemui/util/kotlin/Flow.kt
+++ b/packages/SystemUI/src/com/android/systemui/util/kotlin/Flow.kt
@@ -20,21 +20,19 @@
import com.android.systemui.util.time.SystemClockImpl
import java.util.concurrent.atomic.AtomicReference
import kotlin.math.max
-import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.SharingStarted
-import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onStart
-import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
/**
@@ -106,6 +104,14 @@
/** Holds a [newValue] emitted from a [Flow], along with the [previousValue] emitted value. */
data class WithPrev<out S, out T : S>(val previousValue: S, val newValue: T)
+/** Emits a [Unit] only when the number of downstream subscribers of this flow increases. */
+fun <T> MutableSharedFlow<T>.onSubscriberAdded(): Flow<Unit> {
+ return subscriptionCount
+ .pairwise(initialValue = 0)
+ .filter { (previous, current) -> current > previous }
+ .map {}
+}
+
/**
* Returns a new [Flow] that combines the [Set] changes between each emission from [this] using
* [transform].
@@ -183,34 +189,6 @@
/**
* Returns a flow that mirrors the original flow, but delays values following emitted values for the
- * given [periodMs]. If the original flow emits more than one value during this period, only the
- * latest value is emitted.
- *
- * Example:
- * ```kotlin
- * flow {
- * emit(1) // t=0ms
- * delay(90)
- * emit(2) // t=90ms
- * delay(90)
- * emit(3) // t=180ms
- * delay(1010)
- * emit(4) // t=1190ms
- * delay(1010)
- * emit(5) // t=2200ms
- * }.throttle(1000)
- * ```
- *
- * produces the following emissions at the following times
- *
- * ```text
- * 1 (t=0ms), 3 (t=1000ms), 4 (t=2000ms), 5 (t=3000ms)
- * ```
- */
-fun <T> Flow<T>.throttle(periodMs: Long): Flow<T> = this.throttle(periodMs, SystemClockImpl())
-
-/**
- * Returns a flow that mirrors the original flow, but delays values following emitted values for the
* given [periodMs] as reported by the given [clock]. If the original flow emits more than one value
* during this period, only The latest value is emitted.
*
@@ -235,70 +213,37 @@
* 1 (t=0ms), 3 (t=1000ms), 4 (t=2000ms), 5 (t=3000ms)
* ```
*/
-fun <T> Flow<T>.throttle(periodMs: Long, clock: SystemClock): Flow<T> = channelFlow {
- coroutineScope {
- var previousEmitTimeMs = 0L
- var delayJob: Job? = null
- var sendJob: Job? = null
- val outerScope = this
+fun <T> Flow<T>.throttle(periodMs: Long, clock: SystemClock = SystemClockImpl()): Flow<T> =
+ channelFlow {
+ coroutineScope {
+ var previousEmitTimeMs = 0L
+ var delayJob: Job? = null
+ var sendJob: Job? = null
+ val outerScope = this
- collect {
- delayJob?.cancel()
- sendJob?.join()
- val currentTimeMs = clock.elapsedRealtime()
- val timeSinceLastEmit = currentTimeMs - previousEmitTimeMs
- val timeUntilNextEmit = max(0L, periodMs - timeSinceLastEmit)
- if (timeUntilNextEmit > 0L) {
- // We create delayJob to allow cancellation during the delay period
- delayJob = launch {
- delay(timeUntilNextEmit)
- sendJob =
- outerScope.launch(start = CoroutineStart.UNDISPATCHED) {
- send(it)
- previousEmitTimeMs = clock.elapsedRealtime()
- }
+ collect {
+ delayJob?.cancel()
+ sendJob?.join()
+ val currentTimeMs = clock.elapsedRealtime()
+ val timeSinceLastEmit = currentTimeMs - previousEmitTimeMs
+ val timeUntilNextEmit = max(0L, periodMs - timeSinceLastEmit)
+ if (timeUntilNextEmit > 0L) {
+ // We create delayJob to allow cancellation during the delay period
+ delayJob = launch {
+ delay(timeUntilNextEmit)
+ sendJob =
+ outerScope.launch(start = CoroutineStart.UNDISPATCHED) {
+ send(it)
+ previousEmitTimeMs = clock.elapsedRealtime()
+ }
+ }
+ } else {
+ send(it)
+ previousEmitTimeMs = currentTimeMs
}
- } else {
- send(it)
- previousEmitTimeMs = currentTimeMs
}
}
}
-}
-
-/**
- * Returns a [StateFlow] launched in the surrounding [CoroutineScope]. This [StateFlow] gets its
- * value by invoking [getValue] whenever an event is emitted from [changedSignals]. It will also
- * immediately invoke [getValue] to establish its initial value.
- */
-inline fun <T> CoroutineScope.stateFlow(
- changedSignals: Flow<*>,
- crossinline getValue: () -> T,
-): StateFlow<T> =
- changedSignals.map { getValue() }.stateIn(this, SharingStarted.Eagerly, getValue())
-
-inline fun <T1, T2, T3, T4, T5, T6, R> combine(
- flow: Flow<T1>,
- flow2: Flow<T2>,
- flow3: Flow<T3>,
- flow4: Flow<T4>,
- flow5: Flow<T5>,
- flow6: Flow<T6>,
- crossinline transform: suspend (T1, T2, T3, T4, T5, T6) -> R
-): Flow<R> {
- return kotlinx.coroutines.flow.combine(flow, flow2, flow3, flow4, flow5, flow6) { args: Array<*>
- ->
- @Suppress("UNCHECKED_CAST")
- transform(
- args[0] as T1,
- args[1] as T2,
- args[2] as T3,
- args[3] as T4,
- args[4] as T5,
- args[5] as T6
- )
- }
-}
inline fun <T1, T2, T3, T4, T5, T6, T7, R> combine(
flow: Flow<T1>,
diff --git a/packages/SystemUI/src/com/android/systemui/util/kotlin/GlobalCoroutinesModule.kt b/packages/SystemUI/src/com/android/systemui/util/kotlin/GlobalCoroutinesModule.kt
new file mode 100644
index 0000000..8ecf250
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/util/kotlin/GlobalCoroutinesModule.kt
@@ -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 com.android.systemui.util.kotlin
+
+import com.android.app.tracing.coroutines.createCoroutineTracingContext
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.dagger.qualifiers.Tracing
+import dagger.Module
+import dagger.Provides
+import javax.inject.Singleton
+import kotlin.coroutines.CoroutineContext
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+
+/** Providers for various application-wide coroutines-related constructs. */
+@Module
+class GlobalCoroutinesModule {
+ @Provides
+ @Singleton
+ @Application
+ fun applicationScope(
+ @Main dispatcherContext: CoroutineContext,
+ ): CoroutineScope = CoroutineScope(dispatcherContext)
+
+ @Provides
+ @Singleton
+ @Main
+ @Deprecated(
+ "Use @Main CoroutineContext instead",
+ ReplaceWith("mainCoroutineContext()", "kotlin.coroutines.CoroutineContext")
+ )
+ fun mainDispatcher(): CoroutineDispatcher = Dispatchers.Main.immediate
+
+ @Provides
+ @Singleton
+ @Main
+ fun mainCoroutineContext(@Tracing tracingCoroutineContext: CoroutineContext): CoroutineContext {
+ return Dispatchers.Main.immediate + tracingCoroutineContext
+ }
+
+ @OptIn(ExperimentalCoroutinesApi::class)
+ @Provides
+ @Tracing
+ @Singleton
+ fun tracingCoroutineContext(): CoroutineContext {
+ return createCoroutineTracingContext()
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/util/kotlin/SysUICoroutinesModule.kt b/packages/SystemUI/src/com/android/systemui/util/kotlin/SysUICoroutinesModule.kt
new file mode 100644
index 0000000..a13d85b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/util/kotlin/SysUICoroutinesModule.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.util.kotlin
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.dagger.qualifiers.Tracing
+import dagger.Module
+import dagger.Provides
+import kotlin.coroutines.CoroutineContext
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.plus
+
+/** Providers for various SystemIU specific coroutines-related constructs. */
+@Module
+class SysUICoroutinesModule {
+ @Provides
+ @SysUISingleton
+ @Background
+ fun bgApplicationScope(
+ @Application applicationScope: CoroutineScope,
+ @Background coroutineContext: CoroutineContext,
+ ): CoroutineScope = applicationScope.plus(coroutineContext)
+
+ /**
+ * Provide a [CoroutineDispatcher] backed by a thread pool containing at most X threads, where X
+ * is the number of CPU cores available.
+ *
+ * Because there are multiple threads at play, there is no serialization order guarantee. You
+ * should use a [kotlinx.coroutines.channels.Channel] for serialization if necessary.
+ *
+ * @see Dispatchers.Default
+ */
+ @Provides
+ @SysUISingleton
+ @Background
+ @Deprecated(
+ "Use @Background CoroutineContext instead",
+ ReplaceWith("bgCoroutineContext()", "kotlin.coroutines.CoroutineContext")
+ )
+ fun bgDispatcher(): CoroutineDispatcher = Dispatchers.IO
+
+ @Provides
+ @Background
+ @SysUISingleton
+ fun bgCoroutineContext(@Tracing tracingCoroutineContext: CoroutineContext): CoroutineContext {
+ return Dispatchers.IO + tracingCoroutineContext
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java b/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java
index 7c6ad23..e832506 100644
--- a/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java
+++ b/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java
@@ -32,8 +32,6 @@
import android.content.res.Configuration;
import android.graphics.Rect;
import android.inputmethodservice.InputMethodService;
-import android.os.HandlerExecutor;
-import android.os.HandlerThread;
import android.os.IBinder;
import android.util.Log;
import android.view.Display;
@@ -127,7 +125,6 @@
private final DisplayTracker mDisplayTracker;
private final NoteTaskInitializer mNoteTaskInitializer;
private final Executor mSysUiMainExecutor;
- private HandlerThread mHandlerThread;
// Listeners and callbacks. Note that we prefer member variable over anonymous class here to
// avoid the situation that some implementations, like KeyguardUpdateMonitor, use WeakReference
@@ -209,8 +206,6 @@
mDisplayTracker = displayTracker;
mNoteTaskInitializer = noteTaskInitializer;
mSysUiMainExecutor = sysUiMainExecutor;
- mHandlerThread = new HandlerThread("WMShell");
- mHandlerThread.start();
}
@Override
@@ -224,8 +219,7 @@
mKeyguardUpdateMonitor.registerCallback(mKeyguardUpdateMonitorCallback);
// Subscribe to user changes
- mUserTracker.addCallback(mUserChangedCallback,
- new HandlerExecutor(mHandlerThread.getThreadHandler()));
+ mUserTracker.addCallback(mUserChangedCallback, mContext.getMainExecutor());
mCommandQueue.addCallback(this);
mPipOptional.ifPresent(this::initPip);
@@ -353,10 +347,10 @@
desktopMode.addVisibleTasksListener(
new DesktopModeTaskRepository.VisibleTasksListener() {
@Override
- public void onVisibilityChanged(int displayId, boolean hasFreeformTasks) {
+ public void onTasksVisibilityChanged(int displayId, int visibleTasksCount) {
if (displayId == Display.DEFAULT_DISPLAY) {
mSysUiState.setFlag(SYSUI_STATE_FREEFORM_ACTIVE_IN_DESKTOP_MODE,
- hasFreeformTasks)
+ visibleTasksCount > 0)
.commitUpdate(mDisplayTracker.getDefaultDisplayId());
}
// TODO(b/278084491): update sysui state for changes on other displays
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/TestableWindowManager.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/TestableWindowManager.java
index 44770fa..b23dfdc 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/TestableWindowManager.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/TestableWindowManager.java
@@ -16,8 +16,10 @@
package com.android.systemui.accessibility;
+import android.annotation.NonNull;
import android.graphics.Rect;
import android.graphics.Region;
+import android.os.IBinder;
import android.view.Display;
import android.view.View;
import android.view.ViewGroup;
@@ -89,6 +91,11 @@
return mWindowManager.getMaximumWindowMetrics();
}
+ @Override
+ public @NonNull IBinder getDefaultToken() {
+ return mWindowManager.getDefaultToken();
+ }
+
public View getAttachedView() {
return mView;
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationAnimationControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationAnimationControllerTest.java
index d86d123..8299acb 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationAnimationControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationAnimationControllerTest.java
@@ -21,8 +21,10 @@
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyFloat;
+import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.atLeast;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
@@ -33,10 +35,16 @@
import android.annotation.Nullable;
import android.content.Context;
import android.graphics.Rect;
+import android.os.Binder;
import android.os.Handler;
import android.os.RemoteException;
+import android.platform.test.annotations.RequiresFlagsDisabled;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
import android.testing.AndroidTestingRunner;
import android.view.SurfaceControl;
+import android.view.SurfaceControlViewHost;
import android.view.View;
import android.view.WindowManager;
import android.view.WindowManagerGlobal;
@@ -46,6 +54,7 @@
import androidx.test.filters.LargeTest;
import com.android.internal.graphics.SfVsyncFrameCallbackProvider;
+import com.android.systemui.Flags;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.animation.AnimatorTestRule;
import com.android.systemui.model.SysUiState;
@@ -63,7 +72,10 @@
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
+import java.util.ArrayList;
+import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.Supplier;
@LargeTest
@RunWith(AndroidTestingRunner.class)
@@ -71,6 +83,8 @@
@Rule
public final AnimatorTestRule mAnimatorTestRule = new AnimatorTestRule();
+ @Rule
+ public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
private static final float DEFAULT_SCALE = 4.0f;
private static final float DEFAULT_CENTER_X = 400.0f;
private static final float DEFAULT_CENTER_Y = 500.0f;
@@ -107,6 +121,13 @@
private TestableWindowManager mWindowManager;
private ValueAnimator mValueAnimator;
+ // This list contains all SurfaceControlViewHosts created during a given test. If the
+ // magnification window is recreated during a test, the list will contain more than a single
+ // element.
+ private List<SurfaceControlViewHost> mSurfaceControlViewHosts = new ArrayList<>();
+ // The most recently created SurfaceControlViewHost.
+ private SurfaceControlViewHost mSurfaceControlViewHost;
+ private SurfaceControl.Transaction mTransaction;
@Before
public void setUp() throws Exception {
@@ -123,10 +144,27 @@
mValueAnimator = newValueAnimator();
mWindowMagnificationAnimationController = new WindowMagnificationAnimationController(
mContext, mValueAnimator);
- mController = new SpyWindowMagnificationController(mContext, mHandler,
+
+ Supplier<SurfaceControlViewHost> scvhSupplier = () -> {
+ mSurfaceControlViewHost = spy(new SurfaceControlViewHost(
+ mContext, mContext.getDisplay(), new Binder(), "WindowMagnification"));
+ mSurfaceControlViewHosts.add(mSurfaceControlViewHost);
+ return mSurfaceControlViewHost;
+ };
+
+ mTransaction = spy(new SurfaceControl.Transaction());
+ mController = new SpyWindowMagnificationController(
+ mContext,
+ mHandler,
mWindowMagnificationAnimationController,
- mSfVsyncFrameProvider, null, new SurfaceControl.Transaction(),
- mWindowMagnifierCallback, mSysUiState, mSecureSettings);
+ /* mirrorWindowControl= */ null,
+ mTransaction,
+ mWindowMagnifierCallback,
+ mSysUiState,
+ mSecureSettings,
+ scvhSupplier,
+ mSfVsyncFrameProvider);
+
mSpyController = mController.getSpyController();
}
@@ -235,8 +273,52 @@
verifyFinalSpec(DEFAULT_SCALE, DEFAULT_CENTER_X, DEFAULT_CENTER_Y);
}
+ @RequiresFlagsEnabled(Flags.FLAG_CREATE_WINDOWLESS_WINDOW_MAGNIFIER)
@Test
- public void enableWindowMagnificationWithScaleOne_enabled_AnimationAndInvokeCallback()
+ public void
+ enableWindowMagnificationScaleOne_enabledAndWindowlessFlagOn_AnimationAndCallbackTrue()
+ throws RemoteException {
+ enableWindowMagnificationWithoutAnimation();
+
+ // Wait for Rects updated.
+ waitForIdleSync();
+ View mirrorView = mSurfaceControlViewHost.getView();
+ final float targetScale = 1.0f;
+ // Move the magnifier to the top left corner, within the boundary
+ final float targetCenterX = mirrorView.getWidth() / 2.0f;
+ final float targetCenterY = mirrorView.getHeight() / 2.0f;
+
+ Mockito.reset(mSpyController);
+ getInstrumentation().runOnMainSync(() -> {
+ mWindowMagnificationAnimationController.enableWindowMagnification(targetScale,
+ targetCenterX, targetCenterY, mAnimationCallback);
+ mCurrentScale.set(mController.getScale());
+ mCurrentCenterX.set(mController.getCenterX());
+ mCurrentCenterY.set(mController.getCenterY());
+ advanceTimeBy(mWaitAnimationDuration);
+ });
+
+ verify(mSpyController, atLeast(2)).enableWindowMagnificationInternal(
+ mScaleCaptor.capture(),
+ mCenterXCaptor.capture(), mCenterYCaptor.capture(),
+ mOffsetXCaptor.capture(), mOffsetYCaptor.capture());
+ verifyStartValue(mScaleCaptor, mCurrentScale.get());
+ verifyStartValue(mCenterXCaptor, mCurrentCenterX.get());
+ verifyStartValue(mCenterYCaptor, mCurrentCenterY.get());
+ verifyStartValue(mOffsetXCaptor, 0f);
+ verifyStartValue(mOffsetYCaptor, 0f);
+
+ verifyFinalSpec(targetScale, targetCenterX, targetCenterY);
+
+ verify(mAnimationCallback).onResult(true);
+ assertEquals(WindowMagnificationAnimationController.STATE_ENABLED,
+ mWindowMagnificationAnimationController.getState());
+ }
+
+ @RequiresFlagsDisabled(Flags.FLAG_CREATE_WINDOWLESS_WINDOW_MAGNIFIER)
+ @Test
+ public void
+ enableWindowMagnificationScaleOne_enabledAndWindowlessFlagOff_AnimationAndCallbackTrue()
throws RemoteException {
enableWindowMagnificationWithoutAnimation();
@@ -475,8 +557,46 @@
verify(mAnimationCallback2).onResult(true);
}
+ @RequiresFlagsEnabled(Flags.FLAG_CREATE_WINDOWLESS_WINDOW_MAGNIFIER)
@Test
- public void enableWindowMagnificationWithOffset_expectedValues() {
+ public void enableWindowMagnificationWithOffset_windowlessFlagOn_expectedValues() {
+ final float offsetRatio = -0.1f;
+ final Rect windowBounds = new Rect(mWindowManager.getCurrentWindowMetrics().getBounds());
+
+ Mockito.reset(mSpyController);
+ getInstrumentation().runOnMainSync(() -> {
+ mWindowMagnificationAnimationController.enableWindowMagnification(DEFAULT_SCALE,
+ windowBounds.exactCenterX(), windowBounds.exactCenterY(),
+ offsetRatio, offsetRatio, mAnimationCallback);
+ advanceTimeBy(mWaitAnimationDuration);
+ });
+ // Wait for Rects update
+ waitForIdleSync();
+
+ final int mirrorSurfaceMargin = mContext.getResources().getDimensionPixelSize(
+ R.dimen.magnification_mirror_surface_margin);
+ final int defaultMagnificationWindowSize =
+ mController.getMagnificationWindowSizeFromIndex(
+ WindowMagnificationSettings.MagnificationSize.MEDIUM);
+ final int defaultMagnificationFrameSize =
+ defaultMagnificationWindowSize - 2 * mirrorSurfaceMargin;
+ final int expectedOffset = (int) (defaultMagnificationFrameSize / 2 * offsetRatio);
+
+ final float expectedX = (int) (windowBounds.exactCenterX() + expectedOffset
+ - defaultMagnificationWindowSize / 2);
+ final float expectedY = (int) (windowBounds.exactCenterY() + expectedOffset
+ - defaultMagnificationWindowSize / 2);
+
+ // This is called 4 times when (1) first creating WindowlessMirrorWindow (2) SurfaceView is
+ // created and we place the mirrored content as a child of the SurfaceView
+ // (3) the animation starts (4) the animation updates
+ verify(mTransaction, times(4))
+ .setPosition(any(SurfaceControl.class), eq(expectedX), eq(expectedY));
+ }
+
+ @RequiresFlagsDisabled(Flags.FLAG_CREATE_WINDOWLESS_WINDOW_MAGNIFIER)
+ @Test
+ public void enableWindowMagnificationWithOffset_windowlessFlagOff_expectedValues() {
final float offsetRatio = -0.1f;
final Rect windowBounds = new Rect(mWindowManager.getCurrentWindowMetrics().getBounds());
@@ -876,23 +996,28 @@
private static class SpyWindowMagnificationController extends WindowMagnificationController {
private WindowMagnificationController mSpyController;
- SpyWindowMagnificationController(Context context, Handler handler,
+ SpyWindowMagnificationController(Context context,
+ Handler handler,
WindowMagnificationAnimationController animationController,
- SfVsyncFrameCallbackProvider sfVsyncFrameProvider,
- MirrorWindowControl mirrorWindowControl, SurfaceControl.Transaction transaction,
- WindowMagnifierCallback callback, SysUiState sysUiState,
- SecureSettings secureSettings) {
+ MirrorWindowControl mirrorWindowControl,
+ SurfaceControl.Transaction transaction,
+ WindowMagnifierCallback callback,
+ SysUiState sysUiState,
+ SecureSettings secureSettings,
+ Supplier<SurfaceControlViewHost> scvhSupplier,
+ SfVsyncFrameCallbackProvider sfVsyncFrameProvider) {
super(
context,
handler,
animationController,
- sfVsyncFrameProvider,
mirrorWindowControl,
transaction,
callback,
sysUiState,
- WindowManagerGlobal::getWindowSession,
- secureSettings);
+ secureSettings,
+ scvhSupplier,
+ sfVsyncFrameProvider,
+ WindowManagerGlobal::getWindowSession);
mSpyController = Mockito.mock(WindowMagnificationController.class);
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationControllerTest.java
index 04aef82..2225ad6 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationControllerTest.java
@@ -68,6 +68,9 @@
import android.os.Handler;
import android.os.RemoteException;
import android.os.SystemClock;
+import android.platform.test.annotations.RequiresFlagsDisabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
import android.provider.Settings;
import android.testing.AndroidTestingRunner;
import android.testing.TestableLooper;
@@ -90,6 +93,7 @@
import androidx.test.filters.LargeTest;
import com.android.internal.graphics.SfVsyncFrameCallbackProvider;
+import com.android.systemui.Flags;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.animation.AnimatorTestRule;
import com.android.systemui.kosmos.KosmosJavaAdapter;
@@ -121,10 +125,13 @@
@LargeTest
@TestableLooper.RunWithLooper
@RunWith(AndroidTestingRunner.class)
+@RequiresFlagsDisabled(Flags.FLAG_CREATE_WINDOWLESS_WINDOW_MAGNIFIER)
public class WindowMagnificationControllerTest extends SysuiTestCase {
@Rule
public final AnimatorTestRule mAnimatorTestRule = new AnimatorTestRule();
+ @Rule
+ public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
private static final int LAYOUT_CHANGE_TIMEOUT_MS = 5000;
@Mock
@@ -216,13 +223,14 @@
mContext,
mHandler,
mWindowMagnificationAnimationController,
- mSfVsyncFrameProvider,
mMirrorWindowControl,
mTransaction,
mWindowMagnifierCallback,
mSysUiState,
- () -> mWindowSessionSpy,
- mSecureSettings);
+ mSecureSettings,
+ /* scvhSupplier= */ () -> null,
+ mSfVsyncFrameProvider,
+ /* globalWindowSessionSupplier= */ () -> mWindowSessionSpy);
verify(mMirrorWindowControl).setWindowDelegate(
any(MirrorWindowControl.MirrorWindowDelegate.class));
@@ -270,7 +278,7 @@
mInstrumentation.runOnMainSync(
() -> mWindowMagnificationController.enableWindowMagnification(Float.NaN, Float.NaN,
Float.NaN, /* magnificationFrameOffsetRatioX= */ 0,
- /* magnificationFrameOffsetRatioY= */ 0, null));
+ /* magnificationFrameOffsetRatioY= */ 0, null));
// Waits for the surface created
verify(mWindowMagnifierCallback, timeout(LAYOUT_CHANGE_TIMEOUT_MS)).onSourceBoundsChanged(
@@ -1415,7 +1423,7 @@
}
private MotionEvent obtainMotionEvent(long downTime, long eventTime, int action, float x,
- float y) {
+ float y) {
return mMotionEventHelper.obtainMotionEvent(downTime, eventTime, action, x, y);
}
@@ -1474,4 +1482,4 @@
});
}
}
-}
+}
\ No newline at end of file
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationControllerWindowlessMagnifierTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationControllerWindowlessMagnifierTest.java
new file mode 100644
index 0000000..66fb63b6c3
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationControllerWindowlessMagnifierTest.java
@@ -0,0 +1,1502 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS 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;
+
+import static android.content.pm.PackageManager.FEATURE_WINDOW_MAGNIFICATION;
+import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
+import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
+import static android.content.res.Configuration.ORIENTATION_UNDEFINED;
+import static android.view.MotionEvent.ACTION_DOWN;
+import static android.view.MotionEvent.ACTION_UP;
+import static android.view.WindowInsets.Type.systemGestures;
+import static android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
+
+import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_MAGNIFICATION_OVERLAP;
+
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.hasItems;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.AdditionalAnswers.returnsSecondArg;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyFloat;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.atLeast;
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.animation.ValueAnimator;
+import android.annotation.IdRes;
+import android.annotation.Nullable;
+import android.app.Instrumentation;
+import android.content.Context;
+import android.content.pm.ActivityInfo;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.graphics.Insets;
+import android.graphics.PointF;
+import android.graphics.Rect;
+import android.os.Binder;
+import android.os.Handler;
+import android.os.RemoteException;
+import android.os.SystemClock;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
+import android.provider.Settings;
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+import android.testing.TestableResources;
+import android.text.TextUtils;
+import android.util.Size;
+import android.view.AttachedSurfaceControl;
+import android.view.Display;
+import android.view.Gravity;
+import android.view.MotionEvent;
+import android.view.Surface;
+import android.view.SurfaceControl;
+import android.view.SurfaceControlViewHost;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewRootImpl;
+import android.view.WindowInsets;
+import android.view.WindowManager;
+import android.view.accessibility.AccessibilityNodeInfo;
+import android.view.accessibility.IRemoteMagnificationAnimationCallback;
+import android.widget.FrameLayout;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.LargeTest;
+
+import com.android.systemui.Flags;
+import com.android.systemui.SysuiTestCase;
+import com.android.systemui.animation.AnimatorTestRule;
+import com.android.systemui.kosmos.KosmosJavaAdapter;
+import com.android.systemui.model.SysUiState;
+import com.android.systemui.res.R;
+import com.android.systemui.settings.FakeDisplayTracker;
+import com.android.systemui.util.leak.ReferenceTestUtils;
+import com.android.systemui.util.settings.SecureSettings;
+import com.android.systemui.utils.os.FakeHandler;
+
+import com.google.common.util.concurrent.AtomicDouble;
+
+import org.junit.After;
+import org.junit.Assume;
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Supplier;
+
+@LargeTest
+@TestableLooper.RunWithLooper
+@RunWith(AndroidTestingRunner.class)
+@RequiresFlagsEnabled(Flags.FLAG_CREATE_WINDOWLESS_WINDOW_MAGNIFIER)
+public class WindowMagnificationControllerWindowlessMagnifierTest extends SysuiTestCase {
+
+ @Rule
+ public final AnimatorTestRule mAnimatorTestRule = new AnimatorTestRule();
+ @Rule
+ public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
+
+ private static final int LAYOUT_CHANGE_TIMEOUT_MS = 5000;
+ @Mock
+ private MirrorWindowControl mMirrorWindowControl;
+ @Mock
+ private WindowMagnifierCallback mWindowMagnifierCallback;
+ @Mock
+ IRemoteMagnificationAnimationCallback mAnimationCallback;
+ @Mock
+ IRemoteMagnificationAnimationCallback mAnimationCallback2;
+
+ private SurfaceControl.Transaction mTransaction;
+ @Mock
+ private SecureSettings mSecureSettings;
+
+ private long mWaitAnimationDuration;
+ private long mWaitBounceEffectDuration;
+
+ private Handler mHandler;
+ private TestableWindowManager mWindowManager;
+ private SysUiState mSysUiState;
+ private Resources mResources;
+ private WindowMagnificationAnimationController mWindowMagnificationAnimationController;
+ private WindowMagnificationController mWindowMagnificationController;
+ private Instrumentation mInstrumentation;
+ private final ValueAnimator mValueAnimator = ValueAnimator.ofFloat(0, 1.0f).setDuration(0);
+ private final FakeDisplayTracker mDisplayTracker = new FakeDisplayTracker(mContext);
+
+ private View mSpyView;
+ private View.OnTouchListener mTouchListener;
+
+ private MotionEventHelper mMotionEventHelper = new MotionEventHelper();
+
+ // This list contains all SurfaceControlViewHosts created during a given test. If the
+ // magnification window is recreated during a test, the list will contain more than a single
+ // element.
+ private List<SurfaceControlViewHost> mSurfaceControlViewHosts = new ArrayList<>();
+ // The most recently created SurfaceControlViewHost.
+ private SurfaceControlViewHost mSurfaceControlViewHost;
+ private KosmosJavaAdapter mKosmos;
+
+ /**
+ * return whether window magnification is supported for current test context.
+ */
+ private boolean isWindowModeSupported() {
+ return getContext().getPackageManager().hasSystemFeature(FEATURE_WINDOW_MAGNIFICATION);
+ }
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+ mKosmos = new KosmosJavaAdapter(this);
+ mContext = Mockito.spy(getContext());
+ mHandler = new FakeHandler(TestableLooper.get(this).getLooper());
+ mInstrumentation = InstrumentationRegistry.getInstrumentation();
+ final WindowManager wm = mContext.getSystemService(WindowManager.class);
+ mWindowManager = spy(new TestableWindowManager(wm));
+
+ mContext.addMockSystemService(Context.WINDOW_SERVICE, mWindowManager);
+ mSysUiState = new SysUiState(mDisplayTracker, mKosmos.getSceneContainerPlugin());
+ mSysUiState.addCallback(Mockito.mock(SysUiState.SysUiStateCallback.class));
+ when(mSecureSettings.getIntForUser(anyString(), anyInt(), anyInt())).then(
+ returnsSecondArg());
+ when(mSecureSettings.getFloatForUser(anyString(), anyFloat(), anyInt())).then(
+ returnsSecondArg());
+
+ mResources = getContext().getOrCreateTestableResources().getResources();
+ // prevent the config orientation from undefined, which may cause config.diff method
+ // neglecting the orientation update.
+ if (mResources.getConfiguration().orientation == ORIENTATION_UNDEFINED) {
+ mResources.getConfiguration().orientation = ORIENTATION_PORTRAIT;
+ }
+
+ // Using the animation duration in WindowMagnificationAnimationController for testing.
+ mWaitAnimationDuration = mResources.getInteger(
+ com.android.internal.R.integer.config_longAnimTime);
+ // Using the bounce effect duration in WindowMagnificationController for testing.
+ mWaitBounceEffectDuration = mResources.getInteger(
+ com.android.internal.R.integer.config_shortAnimTime);
+
+ mWindowMagnificationAnimationController = new WindowMagnificationAnimationController(
+ mContext, mValueAnimator);
+ Supplier<SurfaceControlViewHost> scvhSupplier = () -> {
+ mSurfaceControlViewHost = spy(new SurfaceControlViewHost(
+ mContext, mContext.getDisplay(), new Binder(), "WindowMagnification"));
+ ViewRootImpl viewRoot = mock(ViewRootImpl.class);
+ when(mSurfaceControlViewHost.getRootSurfaceControl()).thenReturn(viewRoot);
+ mSurfaceControlViewHosts.add(mSurfaceControlViewHost);
+ return mSurfaceControlViewHost;
+ };
+ mTransaction = spy(new SurfaceControl.Transaction());
+ mWindowMagnificationController =
+ new WindowMagnificationController(
+ mContext,
+ mHandler,
+ mWindowMagnificationAnimationController,
+ mMirrorWindowControl,
+ mTransaction,
+ mWindowMagnifierCallback,
+ mSysUiState,
+ mSecureSettings,
+ scvhSupplier,
+ /* sfVsyncFrameProvider= */ null,
+ /* globalWindowSessionSupplier= */ null);
+
+ verify(mMirrorWindowControl).setWindowDelegate(
+ any(MirrorWindowControl.MirrorWindowDelegate.class));
+ mSpyView = Mockito.spy(new View(mContext));
+ doAnswer((invocation) -> {
+ mTouchListener = invocation.getArgument(0);
+ return null;
+ }).when(mSpyView).setOnTouchListener(
+ any(View.OnTouchListener.class));
+
+ // skip test if window magnification is not supported to prevent fail results. (b/279820875)
+ Assume.assumeTrue(isWindowModeSupported());
+ }
+
+ @After
+ public void tearDown() {
+ mInstrumentation.runOnMainSync(
+ () -> mWindowMagnificationController.deleteWindowMagnification());
+ mValueAnimator.cancel();
+ }
+
+ @Test
+ public void initWindowMagnificationController_checkAllowDiagonalScrollingWithSecureSettings() {
+ verify(mSecureSettings).getIntForUser(
+ eq(Settings.Secure.ACCESSIBILITY_ALLOW_DIAGONAL_SCROLLING),
+ /* def */ eq(1), /* userHandle= */ anyInt());
+ assertTrue(mWindowMagnificationController.isDiagonalScrollingEnabled());
+ }
+
+ @Test
+ public void enableWindowMagnification_showControlAndNotifyBoundsChanged() {
+ mInstrumentation.runOnMainSync(() -> {
+ mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN, Float.NaN,
+ Float.NaN);
+ });
+
+ verify(mMirrorWindowControl).showControl();
+ verify(mWindowMagnifierCallback,
+ timeout(LAYOUT_CHANGE_TIMEOUT_MS).atLeastOnce()).onWindowMagnifierBoundsChanged(
+ eq(mContext.getDisplayId()), any(Rect.class));
+ }
+
+ @Test
+ public void enableWindowMagnification_notifySourceBoundsChanged() {
+ mInstrumentation.runOnMainSync(
+ () -> mWindowMagnificationController.enableWindowMagnification(Float.NaN, Float.NaN,
+ Float.NaN, /* magnificationFrameOffsetRatioX= */ 0,
+ /* magnificationFrameOffsetRatioY= */ 0, null));
+
+ // Waits for the surface created
+ verify(mWindowMagnifierCallback, timeout(LAYOUT_CHANGE_TIMEOUT_MS)).onSourceBoundsChanged(
+ (eq(mContext.getDisplayId())), any());
+ }
+
+ @Test
+ public void enableWindowMagnification_disabled_notifySourceBoundsChanged() {
+ enableWindowMagnification_notifySourceBoundsChanged();
+ mInstrumentation.runOnMainSync(
+ () -> mWindowMagnificationController.deleteWindowMagnification(null));
+ Mockito.reset(mWindowMagnifierCallback);
+
+ enableWindowMagnification_notifySourceBoundsChanged();
+ }
+
+ @Test
+ public void enableWindowMagnification_withAnimation_schedulesFrame() {
+ mInstrumentation.runOnMainSync(() -> {
+ mWindowMagnificationController.enableWindowMagnification(2.0f, 10,
+ 10, /* magnificationFrameOffsetRatioX= */ 0,
+ /* magnificationFrameOffsetRatioY= */ 0,
+ Mockito.mock(IRemoteMagnificationAnimationCallback.class));
+ });
+ advanceTimeBy(LAYOUT_CHANGE_TIMEOUT_MS);
+
+ verify(mTransaction, atLeastOnce()).setGeometry(any(), any(), any(),
+ eq(Surface.ROTATION_0));
+ }
+
+ @Test
+ public void moveWindowMagnifier_enabled_notifySourceBoundsChanged() {
+ mInstrumentation.runOnMainSync(() -> {
+ mWindowMagnificationController.enableWindowMagnification(Float.NaN, Float.NaN,
+ Float.NaN, 0, 0, null);
+ });
+
+ mInstrumentation.runOnMainSync(() -> {
+ mWindowMagnificationController.moveWindowMagnifier(10, 10);
+ });
+
+ final ArgumentCaptor<Rect> sourceBoundsCaptor = ArgumentCaptor.forClass(Rect.class);
+ verify(mWindowMagnifierCallback, atLeast(2)).onSourceBoundsChanged(
+ (eq(mContext.getDisplayId())), sourceBoundsCaptor.capture());
+ assertEquals(mWindowMagnificationController.getCenterX(),
+ sourceBoundsCaptor.getValue().exactCenterX(), 0);
+ assertEquals(mWindowMagnificationController.getCenterY(),
+ sourceBoundsCaptor.getValue().exactCenterY(), 0);
+ }
+
+ @Test
+ public void enableWindowMagnification_systemGestureExclusionRectsIsSet() {
+ mInstrumentation.runOnMainSync(() -> {
+ mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN, Float.NaN,
+ Float.NaN);
+ });
+ // Wait for Rects updated.
+ waitForIdleSync();
+
+ List<Rect> rects = mSurfaceControlViewHost.getView().getSystemGestureExclusionRects();
+ assertFalse(rects.isEmpty());
+ }
+
+ @Ignore("The default window size should be constrained after fixing b/288056772")
+ @Test
+ public void enableWindowMagnification_LargeScreen_windowSizeIsConstrained() {
+ final int screenSize = mWindowManager.getCurrentWindowMetrics().getBounds().width() * 10;
+ mWindowManager.setWindowBounds(new Rect(0, 0, screenSize, screenSize));
+
+ mInstrumentation.runOnMainSync(() -> {
+ mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN, Float.NaN,
+ Float.NaN);
+ });
+
+ final int halfScreenSize = screenSize / 2;
+ ViewGroup.LayoutParams params = mSurfaceControlViewHost.getView().getLayoutParams();
+ // The frame size should be the half of smaller value of window height/width unless it
+ //exceed the max frame size.
+ assertTrue(params.width < halfScreenSize);
+ assertTrue(params.height < halfScreenSize);
+ }
+
+ @Test
+ public void deleteWindowMagnification_destroyControlAndUnregisterComponentCallback() {
+ mInstrumentation.runOnMainSync(
+ () -> mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN,
+ Float.NaN,
+ Float.NaN));
+
+ mInstrumentation.runOnMainSync(
+ () -> mWindowMagnificationController.deleteWindowMagnification());
+
+ verify(mMirrorWindowControl).destroyControl();
+ verify(mContext).unregisterComponentCallbacks(mWindowMagnificationController);
+ }
+
+ @Test
+ public void deleteWindowMagnification_enableAtTheBottom_overlapFlagIsFalse() {
+ final WindowManager wm = mContext.getSystemService(WindowManager.class);
+ final Rect bounds = wm.getCurrentWindowMetrics().getBounds();
+ setSystemGestureInsets();
+
+ mInstrumentation.runOnMainSync(() -> {
+ mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN, Float.NaN,
+ bounds.bottom);
+ });
+ ReferenceTestUtils.waitForCondition(this::hasMagnificationOverlapFlag);
+
+ mInstrumentation.runOnMainSync(() -> {
+ mWindowMagnificationController.deleteWindowMagnification();
+ });
+
+ verify(mMirrorWindowControl).destroyControl();
+ assertFalse(hasMagnificationOverlapFlag());
+ }
+
+ @Test
+ public void deleteWindowMagnification_notifySourceBoundsChanged() {
+ mInstrumentation.runOnMainSync(
+ () -> mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN,
+ Float.NaN,
+ Float.NaN));
+
+ mInstrumentation.runOnMainSync(
+ () -> mWindowMagnificationController.deleteWindowMagnification());
+
+ // The first time is for notifying magnification enabled and the second time is for
+ // notifying magnification disabled.
+ verify(mWindowMagnifierCallback, times(2)).onSourceBoundsChanged(
+ (eq(mContext.getDisplayId())), any());
+ }
+
+ @Test
+ public void moveMagnifier_schedulesFrame() {
+ mInstrumentation.runOnMainSync(() -> {
+ mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN, Float.NaN,
+ Float.NaN);
+ });
+
+ waitForIdleSync();
+ mInstrumentation.runOnMainSync(
+ () -> mWindowMagnificationController.moveWindowMagnifier(100f, 100f));
+
+ verify(mTransaction, atLeastOnce()).setGeometry(any(), any(), any(),
+ eq(Surface.ROTATION_0));
+ }
+
+ @Test
+ public void moveWindowMagnifierToPositionWithAnimation_expectedValuesAndInvokeCallback()
+ throws RemoteException {
+ mInstrumentation.runOnMainSync(() -> {
+ mWindowMagnificationController.enableWindowMagnification(Float.NaN, Float.NaN,
+ Float.NaN, 0, 0, null);
+ });
+
+ final ArgumentCaptor<Rect> sourceBoundsCaptor = ArgumentCaptor.forClass(Rect.class);
+ verify(mWindowMagnifierCallback, timeout(LAYOUT_CHANGE_TIMEOUT_MS))
+ .onSourceBoundsChanged((eq(mContext.getDisplayId())), sourceBoundsCaptor.capture());
+ final float targetCenterX = sourceBoundsCaptor.getValue().exactCenterX() + 10;
+ final float targetCenterY = sourceBoundsCaptor.getValue().exactCenterY() + 10;
+
+ mInstrumentation.runOnMainSync(() -> {
+ mWindowMagnificationController.moveWindowMagnifierToPosition(
+ targetCenterX, targetCenterY, mAnimationCallback);
+ });
+ advanceTimeBy(mWaitAnimationDuration);
+
+ verify(mAnimationCallback, times(1)).onResult(eq(true));
+ verify(mAnimationCallback, never()).onResult(eq(false));
+ verify(mWindowMagnifierCallback, timeout(LAYOUT_CHANGE_TIMEOUT_MS))
+ .onSourceBoundsChanged((eq(mContext.getDisplayId())), sourceBoundsCaptor.capture());
+ assertEquals(mWindowMagnificationController.getCenterX(),
+ sourceBoundsCaptor.getValue().exactCenterX(), 0);
+ assertEquals(mWindowMagnificationController.getCenterY(),
+ sourceBoundsCaptor.getValue().exactCenterY(), 0);
+ assertEquals(mWindowMagnificationController.getCenterX(), targetCenterX, 0);
+ assertEquals(mWindowMagnificationController.getCenterY(), targetCenterY, 0);
+ }
+
+ @Test
+ public void moveWindowMagnifierToPositionMultipleTimes_expectedValuesAndInvokeCallback()
+ throws RemoteException {
+ mInstrumentation.runOnMainSync(() -> {
+ mWindowMagnificationController.enableWindowMagnification(Float.NaN, Float.NaN,
+ Float.NaN, 0, 0, null);
+ });
+
+ final ArgumentCaptor<Rect> sourceBoundsCaptor = ArgumentCaptor.forClass(Rect.class);
+ verify(mWindowMagnifierCallback, timeout(LAYOUT_CHANGE_TIMEOUT_MS))
+ .onSourceBoundsChanged((eq(mContext.getDisplayId())), sourceBoundsCaptor.capture());
+ final float centerX = sourceBoundsCaptor.getValue().exactCenterX();
+ final float centerY = sourceBoundsCaptor.getValue().exactCenterY();
+
+ mInstrumentation.runOnMainSync(() -> {
+ mWindowMagnificationController.moveWindowMagnifierToPosition(
+ centerX + 10, centerY + 10, mAnimationCallback);
+ mWindowMagnificationController.moveWindowMagnifierToPosition(
+ centerX + 20, centerY + 20, mAnimationCallback);
+ mWindowMagnificationController.moveWindowMagnifierToPosition(
+ centerX + 30, centerY + 30, mAnimationCallback);
+ mWindowMagnificationController.moveWindowMagnifierToPosition(
+ centerX + 40, centerY + 40, mAnimationCallback2);
+ });
+ advanceTimeBy(mWaitAnimationDuration);
+
+ // only the last one callback will return true
+ verify(mAnimationCallback2).onResult(eq(true));
+ // the others will return false
+ verify(mAnimationCallback, times(3)).onResult(eq(false));
+ verify(mWindowMagnifierCallback, timeout(LAYOUT_CHANGE_TIMEOUT_MS))
+ .onSourceBoundsChanged((eq(mContext.getDisplayId())), sourceBoundsCaptor.capture());
+ assertEquals(mWindowMagnificationController.getCenterX(),
+ sourceBoundsCaptor.getValue().exactCenterX(), 0);
+ assertEquals(mWindowMagnificationController.getCenterY(),
+ sourceBoundsCaptor.getValue().exactCenterY(), 0);
+ assertEquals(mWindowMagnificationController.getCenterX(), centerX + 40, 0);
+ assertEquals(mWindowMagnificationController.getCenterY(), centerY + 40, 0);
+ }
+
+ @Test
+ public void setScale_enabled_expectedValueAndUpdateStateDescription() {
+ mInstrumentation.runOnMainSync(
+ () -> mWindowMagnificationController.enableWindowMagnificationInternal(2.0f,
+ Float.NaN, Float.NaN));
+
+ mInstrumentation.runOnMainSync(() -> mWindowMagnificationController.setScale(3.0f));
+
+ assertEquals(3.0f, mWindowMagnificationController.getScale(), 0);
+ final View mirrorView = mSurfaceControlViewHost.getView();
+ assertNotNull(mirrorView);
+ assertThat(mirrorView.getStateDescription().toString(), containsString("300"));
+ }
+
+ @Test
+ public void onConfigurationChanged_disabled_withoutException() {
+ Display display = Mockito.spy(mContext.getDisplay());
+ when(display.getRotation()).thenReturn(Surface.ROTATION_90);
+ when(mContext.getDisplay()).thenReturn(display);
+
+ mInstrumentation.runOnMainSync(() -> {
+ mWindowMagnificationController.onConfigurationChanged(ActivityInfo.CONFIG_DENSITY);
+ mWindowMagnificationController.onConfigurationChanged(ActivityInfo.CONFIG_ORIENTATION);
+ mWindowMagnificationController.onConfigurationChanged(ActivityInfo.CONFIG_LOCALE);
+ mWindowMagnificationController.onConfigurationChanged(ActivityInfo.CONFIG_SCREEN_SIZE);
+ });
+ }
+
+ @Test
+ public void onOrientationChanged_enabled_updateDisplayRotationAndCenterStayAtSamePosition() {
+ final int newRotation = simulateRotateTheDevice();
+ final Rect windowBounds = new Rect(mWindowManager.getCurrentWindowMetrics().getBounds());
+ final float center = Math.min(windowBounds.exactCenterX(), windowBounds.exactCenterY());
+ final float displayWidth = windowBounds.width();
+ final PointF magnifiedCenter = new PointF(center, center + 5f);
+ mInstrumentation.runOnMainSync(() -> {
+ mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN,
+ magnifiedCenter.x, magnifiedCenter.y);
+ // Get the center again in case the center we set is out of screen.
+ magnifiedCenter.set(mWindowMagnificationController.getCenterX(),
+ mWindowMagnificationController.getCenterY());
+ });
+ // Rotate the window clockwise 90 degree.
+ windowBounds.set(windowBounds.top, windowBounds.left, windowBounds.bottom,
+ windowBounds.right);
+ mWindowManager.setWindowBounds(windowBounds);
+
+ mInstrumentation.runOnMainSync(() -> mWindowMagnificationController.onConfigurationChanged(
+ ActivityInfo.CONFIG_ORIENTATION));
+
+ assertEquals(newRotation, mWindowMagnificationController.mRotation);
+ final PointF expectedCenter = new PointF(magnifiedCenter.y,
+ displayWidth - magnifiedCenter.x);
+ final PointF actualCenter = new PointF(mWindowMagnificationController.getCenterX(),
+ mWindowMagnificationController.getCenterY());
+ assertEquals(expectedCenter, actualCenter);
+ }
+
+ @Test
+ public void onOrientationChanged_disabled_updateDisplayRotation() {
+ final Rect windowBounds = new Rect(mWindowManager.getCurrentWindowMetrics().getBounds());
+ // Rotate the window clockwise 90 degree.
+ windowBounds.set(windowBounds.top, windowBounds.left, windowBounds.bottom,
+ windowBounds.right);
+ mWindowManager.setWindowBounds(windowBounds);
+ final int newRotation = simulateRotateTheDevice();
+
+ mInstrumentation.runOnMainSync(() -> mWindowMagnificationController.onConfigurationChanged(
+ ActivityInfo.CONFIG_ORIENTATION));
+
+ assertEquals(newRotation, mWindowMagnificationController.mRotation);
+ }
+
+ @Test
+ public void onScreenSizeAndDensityChanged_enabledAtTheCenterOfScreen_keepSameWindowSizeRatio() {
+ // The default position is at the center of the screen.
+ final float expectedRatio = 0.5f;
+
+ mInstrumentation.runOnMainSync(() -> {
+ mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN, Float.NaN,
+ Float.NaN);
+ });
+
+ // Screen size and density change
+ mContext.getResources().getConfiguration().smallestScreenWidthDp =
+ mContext.getResources().getConfiguration().smallestScreenWidthDp * 2;
+ final Rect testWindowBounds = new Rect(
+ mWindowManager.getCurrentWindowMetrics().getBounds());
+ testWindowBounds.set(testWindowBounds.left, testWindowBounds.top,
+ testWindowBounds.right + 100, testWindowBounds.bottom + 100);
+ mWindowManager.setWindowBounds(testWindowBounds);
+
+ mInstrumentation.runOnMainSync(() -> {
+ mWindowMagnificationController.onConfigurationChanged(ActivityInfo.CONFIG_SCREEN_SIZE);
+ });
+
+ // The ratio of center to window size should be the same.
+ assertEquals(expectedRatio,
+ mWindowMagnificationController.getCenterX() / testWindowBounds.width(),
+ 0);
+ assertEquals(expectedRatio,
+ mWindowMagnificationController.getCenterY() / testWindowBounds.height(),
+ 0);
+ }
+
+ @Test
+ public void onScreenChangedToSavedDensity_enabled_restoreSavedMagnifierWindow() {
+ mContext.getResources().getConfiguration().smallestScreenWidthDp =
+ mContext.getResources().getConfiguration().smallestScreenWidthDp * 2;
+ int windowFrameSize = mResources.getDimensionPixelSize(
+ com.android.internal.R.dimen.accessibility_window_magnifier_min_size);
+ mWindowMagnificationController.mWindowMagnificationSizePrefs.saveSizeForCurrentDensity(
+ new Size(windowFrameSize, windowFrameSize));
+
+ mInstrumentation.runOnMainSync(() -> {
+ mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN, Float.NaN,
+ Float.NaN);
+ });
+
+ ViewGroup.LayoutParams params = mSurfaceControlViewHost.getView().getLayoutParams();
+ assertTrue(params.width == windowFrameSize);
+ assertTrue(params.height == windowFrameSize);
+ }
+
+ @Test
+ public void screenSizeIsChangedToLarge_enabled_defaultWindowSize() {
+ mInstrumentation.runOnMainSync(() -> {
+ mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN, Float.NaN,
+ Float.NaN);
+ });
+ final int screenSize = mWindowManager.getCurrentWindowMetrics().getBounds().width() * 10;
+ // Screen size and density change
+ mContext.getResources().getConfiguration().smallestScreenWidthDp =
+ mContext.getResources().getConfiguration().smallestScreenWidthDp * 2;
+ mWindowManager.setWindowBounds(new Rect(0, 0, screenSize, screenSize));
+
+ mInstrumentation.runOnMainSync(() -> {
+ mWindowMagnificationController.onConfigurationChanged(ActivityInfo.CONFIG_SCREEN_SIZE);
+ });
+
+ final int defaultWindowSize =
+ mWindowMagnificationController.getMagnificationWindowSizeFromIndex(
+ WindowMagnificationSettings.MagnificationSize.MEDIUM);
+ ViewGroup.LayoutParams params = mSurfaceControlViewHost.getView().getLayoutParams();
+
+ assertTrue(params.width == defaultWindowSize);
+ assertTrue(params.height == defaultWindowSize);
+ }
+
+ @Test
+ public void onDensityChanged_enabled_updateDimensionsAndResetWindowMagnification() {
+ mInstrumentation.runOnMainSync(() -> {
+ mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN, Float.NaN,
+ Float.NaN);
+ Mockito.reset(mWindowManager);
+ Mockito.reset(mMirrorWindowControl);
+ });
+
+ mInstrumentation.runOnMainSync(() -> {
+ mWindowMagnificationController.onConfigurationChanged(ActivityInfo.CONFIG_DENSITY);
+ });
+
+ verify(mResources, atLeastOnce()).getDimensionPixelSize(anyInt());
+ verify(mSurfaceControlViewHosts.get(0)).release();
+ verify(mMirrorWindowControl).destroyControl();
+ verify(mSurfaceControlViewHosts.get(1)).setView(any(), any());
+ verify(mMirrorWindowControl).showControl();
+ }
+
+ @Test
+ public void onDensityChanged_disabled_updateDimensions() {
+ mInstrumentation.runOnMainSync(() -> {
+ mWindowMagnificationController.onConfigurationChanged(ActivityInfo.CONFIG_DENSITY);
+ });
+
+ verify(mResources, atLeastOnce()).getDimensionPixelSize(anyInt());
+ }
+
+ @Test
+ public void initializeA11yNode_enabled_expectedValues() {
+ mInstrumentation.runOnMainSync(() -> {
+ mWindowMagnificationController.enableWindowMagnificationInternal(2.5f, Float.NaN,
+ Float.NaN);
+ });
+ final View mirrorView = mSurfaceControlViewHost.getView();
+ assertNotNull(mirrorView);
+ final AccessibilityNodeInfo nodeInfo = new AccessibilityNodeInfo();
+
+ mirrorView.onInitializeAccessibilityNodeInfo(nodeInfo);
+
+ assertNotNull(nodeInfo.getContentDescription());
+ assertThat(nodeInfo.getStateDescription().toString(), containsString("250"));
+ assertThat(nodeInfo.getActionList(),
+ hasItems(new AccessibilityAction(R.id.accessibility_action_zoom_in, null),
+ new AccessibilityAction(R.id.accessibility_action_zoom_out, null),
+ new AccessibilityAction(R.id.accessibility_action_move_right, null),
+ new AccessibilityAction(R.id.accessibility_action_move_left, null),
+ new AccessibilityAction(R.id.accessibility_action_move_down, null),
+ new AccessibilityAction(R.id.accessibility_action_move_up, null)));
+ }
+
+ @Test
+ public void performA11yActions_visible_expectedResults() {
+ final int displayId = mContext.getDisplayId();
+ mInstrumentation.runOnMainSync(() -> {
+ mWindowMagnificationController.enableWindowMagnificationInternal(1.5f, Float.NaN,
+ Float.NaN);
+ });
+
+ final View mirrorView = mSurfaceControlViewHost.getView();
+ assertTrue(
+ mirrorView.performAccessibilityAction(R.id.accessibility_action_zoom_out, null));
+ // Minimum scale is 1.0.
+ verify(mWindowMagnifierCallback).onPerformScaleAction(
+ eq(displayId), /* scale= */ eq(1.0f), /* updatePersistence= */ eq(true));
+
+ assertTrue(mirrorView.performAccessibilityAction(R.id.accessibility_action_zoom_in, null));
+ verify(mWindowMagnifierCallback).onPerformScaleAction(
+ eq(displayId), /* scale= */ eq(2.5f), /* updatePersistence= */ eq(true));
+
+ // TODO: Verify the final state when the mirror surface is visible.
+ assertTrue(mirrorView.performAccessibilityAction(R.id.accessibility_action_move_up, null));
+ assertTrue(
+ mirrorView.performAccessibilityAction(R.id.accessibility_action_move_down, null));
+ assertTrue(
+ mirrorView.performAccessibilityAction(R.id.accessibility_action_move_right, null));
+ assertTrue(
+ mirrorView.performAccessibilityAction(R.id.accessibility_action_move_left, null));
+ verify(mWindowMagnifierCallback, times(4)).onMove(eq(displayId));
+
+ assertTrue(mirrorView.performAccessibilityAction(
+ AccessibilityAction.ACTION_CLICK.getId(), null));
+ verify(mWindowMagnifierCallback).onClickSettingsButton(eq(displayId));
+ }
+
+ @Test
+ public void performA11yActions_visible_notifyAccessibilityActionPerformed() {
+ final int displayId = mContext.getDisplayId();
+ mInstrumentation.runOnMainSync(() -> {
+ mWindowMagnificationController.enableWindowMagnificationInternal(2.5f, Float.NaN,
+ Float.NaN);
+ });
+
+ final View mirrorView = mSurfaceControlViewHost.getView();
+ mirrorView.performAccessibilityAction(R.id.accessibility_action_move_up, null);
+
+ verify(mWindowMagnifierCallback).onAccessibilityActionPerformed(eq(displayId));
+ }
+
+ @Test
+ public void windowMagnifierEditMode_performA11yClickAction_exitEditMode() {
+ mInstrumentation.runOnMainSync(() -> {
+ mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN, Float.NaN,
+ Float.NaN);
+ mWindowMagnificationController.setEditMagnifierSizeMode(true);
+ });
+
+ View closeButton = getInternalView(R.id.close_button);
+ View bottomRightCorner = getInternalView(R.id.bottom_right_corner);
+ View bottomLeftCorner = getInternalView(R.id.bottom_left_corner);
+ View topRightCorner = getInternalView(R.id.top_right_corner);
+ View topLeftCorner = getInternalView(R.id.top_left_corner);
+
+ assertEquals(View.VISIBLE, closeButton.getVisibility());
+ assertEquals(View.VISIBLE, bottomRightCorner.getVisibility());
+ assertEquals(View.VISIBLE, bottomLeftCorner.getVisibility());
+ assertEquals(View.VISIBLE, topRightCorner.getVisibility());
+ assertEquals(View.VISIBLE, topLeftCorner.getVisibility());
+
+ final View mirrorView = mSurfaceControlViewHost.getView();
+ mInstrumentation.runOnMainSync(() ->
+ mirrorView.performAccessibilityAction(AccessibilityAction.ACTION_CLICK.getId(),
+ null));
+
+ assertEquals(View.GONE, closeButton.getVisibility());
+ assertEquals(View.GONE, bottomRightCorner.getVisibility());
+ assertEquals(View.GONE, bottomLeftCorner.getVisibility());
+ assertEquals(View.GONE, topRightCorner.getVisibility());
+ assertEquals(View.GONE, topLeftCorner.getVisibility());
+ }
+
+ @Test
+
+ public void windowWidthIsNotMax_performA11yActionIncreaseWidth_windowWidthIncreased() {
+ final Rect windowBounds = mWindowManager.getCurrentWindowMetrics().getBounds();
+ final int startingWidth = (int) (windowBounds.width() * 0.8);
+ final int startingHeight = (int) (windowBounds.height() * 0.8);
+ final float changeWindowSizeAmount = mContext.getResources().getFraction(
+ R.fraction.magnification_resize_window_size_amount,
+ /* base= */ 1,
+ /* pbase= */ 1);
+
+ mInstrumentation.runOnMainSync(() -> {
+ mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN, Float.NaN,
+ Float.NaN);
+ mWindowMagnificationController.setWindowSize(startingWidth, startingHeight);
+ mWindowMagnificationController.setEditMagnifierSizeMode(true);
+ });
+
+ final View mirrorView = mSurfaceControlViewHost.getView();
+ final AtomicInteger actualWindowHeight = new AtomicInteger();
+ final AtomicInteger actualWindowWidth = new AtomicInteger();
+
+ mInstrumentation.runOnMainSync(
+ () -> {
+ mirrorView.performAccessibilityAction(
+ R.id.accessibility_action_increase_window_width, null);
+ actualWindowHeight.set(
+ mSurfaceControlViewHost.getView().getLayoutParams().height);
+ actualWindowWidth.set(
+ mSurfaceControlViewHost.getView().getLayoutParams().width);
+ });
+
+ final int mirrorSurfaceMargin = mResources.getDimensionPixelSize(
+ R.dimen.magnification_mirror_surface_margin);
+ // Window width includes the magnifier frame and the margin. Increasing the window size
+ // will be increasing the amount of the frame size only.
+ int newWindowWidth =
+ (int) ((startingWidth - 2 * mirrorSurfaceMargin) * (1 + changeWindowSizeAmount))
+ + 2 * mirrorSurfaceMargin;
+ assertEquals(newWindowWidth, actualWindowWidth.get());
+ assertEquals(startingHeight, actualWindowHeight.get());
+ }
+
+ @Test
+ public void windowHeightIsNotMax_performA11yActionIncreaseHeight_windowHeightIncreased() {
+ final Rect windowBounds = mWindowManager.getCurrentWindowMetrics().getBounds();
+ final int startingWidth = (int) (windowBounds.width() * 0.8);
+ final int startingHeight = (int) (windowBounds.height() * 0.8);
+ final float changeWindowSizeAmount = mContext.getResources().getFraction(
+ R.fraction.magnification_resize_window_size_amount,
+ /* base= */ 1,
+ /* pbase= */ 1);
+
+ mInstrumentation.runOnMainSync(() -> {
+ mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN, Float.NaN,
+ Float.NaN);
+ mWindowMagnificationController.setWindowSize(startingWidth, startingHeight);
+ mWindowMagnificationController.setEditMagnifierSizeMode(true);
+ });
+
+ final View mirrorView = mSurfaceControlViewHost.getView();
+ final AtomicInteger actualWindowHeight = new AtomicInteger();
+ final AtomicInteger actualWindowWidth = new AtomicInteger();
+
+ mInstrumentation.runOnMainSync(
+ () -> {
+ mirrorView.performAccessibilityAction(
+ R.id.accessibility_action_increase_window_height, null);
+ actualWindowHeight.set(
+ mSurfaceControlViewHost.getView().getLayoutParams().height);
+ actualWindowWidth.set(
+ mSurfaceControlViewHost.getView().getLayoutParams().width);
+ });
+
+ final int mirrorSurfaceMargin = mResources.getDimensionPixelSize(
+ R.dimen.magnification_mirror_surface_margin);
+ // Window height includes the magnifier frame and the margin. Increasing the window size
+ // will be increasing the amount of the frame size only.
+ int newWindowHeight =
+ (int) ((startingHeight - 2 * mirrorSurfaceMargin) * (1 + changeWindowSizeAmount))
+ + 2 * mirrorSurfaceMargin;
+ assertEquals(startingWidth, actualWindowWidth.get());
+ assertEquals(newWindowHeight, actualWindowHeight.get());
+ }
+
+ @Test
+ public void windowWidthIsMax_noIncreaseWindowWidthA11yAction() {
+ final Rect windowBounds = mWindowManager.getCurrentWindowMetrics().getBounds();
+ final int startingWidth = windowBounds.width();
+ final int startingHeight = windowBounds.height();
+
+ mInstrumentation.runOnMainSync(() -> {
+ mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN, Float.NaN,
+ Float.NaN);
+ mWindowMagnificationController.setWindowSize(startingWidth, startingHeight);
+ mWindowMagnificationController.setEditMagnifierSizeMode(true);
+ });
+
+ final View mirrorView = mSurfaceControlViewHost.getView();
+ final AccessibilityNodeInfo accessibilityNodeInfo =
+ mirrorView.createAccessibilityNodeInfo();
+ assertFalse(accessibilityNodeInfo.getActionList().contains(
+ new AccessibilityAction(R.id.accessibility_action_increase_window_width, null)));
+ }
+
+ @Test
+ public void windowHeightIsMax_noIncreaseWindowHeightA11yAction() {
+ final Rect windowBounds = mWindowManager.getCurrentWindowMetrics().getBounds();
+ final int startingWidth = windowBounds.width();
+ final int startingHeight = windowBounds.height();
+
+ mInstrumentation.runOnMainSync(() -> {
+ mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN, Float.NaN,
+ Float.NaN);
+ mWindowMagnificationController.setWindowSize(startingWidth, startingHeight);
+ mWindowMagnificationController.setEditMagnifierSizeMode(true);
+ });
+
+ final View mirrorView = mSurfaceControlViewHost.getView();
+ final AccessibilityNodeInfo accessibilityNodeInfo =
+ mirrorView.createAccessibilityNodeInfo();
+ assertFalse(accessibilityNodeInfo.getActionList().contains(
+ new AccessibilityAction(R.id.accessibility_action_increase_window_height, null)));
+ }
+
+ @Test
+ public void windowWidthIsNotMin_performA11yActionDecreaseWidth_windowWidthDecreased() {
+ int mMinWindowSize = mResources.getDimensionPixelSize(
+ com.android.internal.R.dimen.accessibility_window_magnifier_min_size);
+ final int startingSize = (int) (mMinWindowSize * 1.1);
+ final float changeWindowSizeAmount = mContext.getResources().getFraction(
+ R.fraction.magnification_resize_window_size_amount,
+ /* base= */ 1,
+ /* pbase= */ 1);
+ mInstrumentation.runOnMainSync(() -> {
+ mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN, Float.NaN,
+ Float.NaN);
+ mWindowMagnificationController.setWindowSize(startingSize, startingSize);
+ mWindowMagnificationController.setEditMagnifierSizeMode(true);
+ });
+
+ final View mirrorView = mSurfaceControlViewHost.getView();
+ final AtomicInteger actualWindowHeight = new AtomicInteger();
+ final AtomicInteger actualWindowWidth = new AtomicInteger();
+
+ mInstrumentation.runOnMainSync(
+ () -> {
+ mirrorView.performAccessibilityAction(
+ R.id.accessibility_action_decrease_window_width, null);
+ actualWindowHeight.set(
+ mSurfaceControlViewHost.getView().getLayoutParams().height);
+ actualWindowWidth.set(
+ mSurfaceControlViewHost.getView().getLayoutParams().width);
+ });
+
+ final int mirrorSurfaceMargin = mResources.getDimensionPixelSize(
+ R.dimen.magnification_mirror_surface_margin);
+ // Window width includes the magnifier frame and the margin. Decreasing the window size
+ // will be decreasing the amount of the frame size only.
+ int newWindowWidth =
+ (int) ((startingSize - 2 * mirrorSurfaceMargin) * (1 - changeWindowSizeAmount))
+ + 2 * mirrorSurfaceMargin;
+ assertEquals(newWindowWidth, actualWindowWidth.get());
+ assertEquals(startingSize, actualWindowHeight.get());
+ }
+
+ @Test
+ public void windowHeightIsNotMin_performA11yActionDecreaseHeight_windowHeightDecreased() {
+ int mMinWindowSize = mResources.getDimensionPixelSize(
+ com.android.internal.R.dimen.accessibility_window_magnifier_min_size);
+ final int startingSize = (int) (mMinWindowSize * 1.1);
+ final float changeWindowSizeAmount = mContext.getResources().getFraction(
+ R.fraction.magnification_resize_window_size_amount,
+ /* base= */ 1,
+ /* pbase= */ 1);
+
+ mInstrumentation.runOnMainSync(() -> {
+ mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN, Float.NaN,
+ Float.NaN);
+ mWindowMagnificationController.setWindowSize(startingSize, startingSize);
+ mWindowMagnificationController.setEditMagnifierSizeMode(true);
+ });
+
+ final View mirrorView = mSurfaceControlViewHost.getView();
+ final AtomicInteger actualWindowHeight = new AtomicInteger();
+ final AtomicInteger actualWindowWidth = new AtomicInteger();
+
+ mInstrumentation.runOnMainSync(
+ () -> {
+ mirrorView.performAccessibilityAction(
+ R.id.accessibility_action_decrease_window_height, null);
+ actualWindowHeight.set(
+ mSurfaceControlViewHost.getView().getLayoutParams().height);
+ actualWindowWidth.set(
+ mSurfaceControlViewHost.getView().getLayoutParams().width);
+ });
+
+ final int mirrorSurfaceMargin = mResources.getDimensionPixelSize(
+ R.dimen.magnification_mirror_surface_margin);
+ // Window height includes the magnifier frame and the margin. Decreasing the window size
+ // will be decreasing the amount of the frame size only.
+ int newWindowHeight =
+ (int) ((startingSize - 2 * mirrorSurfaceMargin) * (1 - changeWindowSizeAmount))
+ + 2 * mirrorSurfaceMargin;
+ assertEquals(startingSize, actualWindowWidth.get());
+ assertEquals(newWindowHeight, actualWindowHeight.get());
+ }
+
+ @Test
+ public void windowWidthIsMin_noDecreaseWindowWidthA11yAction() {
+ int mMinWindowSize = mResources.getDimensionPixelSize(
+ com.android.internal.R.dimen.accessibility_window_magnifier_min_size);
+ final int startingSize = mMinWindowSize;
+
+ mInstrumentation.runOnMainSync(() -> {
+ mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN, Float.NaN,
+ Float.NaN);
+ mWindowMagnificationController.setWindowSize(startingSize, startingSize);
+ mWindowMagnificationController.setEditMagnifierSizeMode(true);
+ });
+
+ final View mirrorView = mSurfaceControlViewHost.getView();
+ final AccessibilityNodeInfo accessibilityNodeInfo =
+ mirrorView.createAccessibilityNodeInfo();
+ assertFalse(accessibilityNodeInfo.getActionList().contains(
+ new AccessibilityAction(R.id.accessibility_action_decrease_window_width, null)));
+ }
+
+ @Test
+ public void windowHeightIsMin_noDecreaseWindowHeightA11yAction() {
+ int mMinWindowSize = mResources.getDimensionPixelSize(
+ com.android.internal.R.dimen.accessibility_window_magnifier_min_size);
+ final int startingSize = mMinWindowSize;
+
+ mInstrumentation.runOnMainSync(() -> {
+ mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN, Float.NaN,
+ Float.NaN);
+ mWindowMagnificationController.setWindowSize(startingSize, startingSize);
+ mWindowMagnificationController.setEditMagnifierSizeMode(true);
+ });
+
+ final View mirrorView = mSurfaceControlViewHost.getView();
+ final AccessibilityNodeInfo accessibilityNodeInfo =
+ mirrorView.createAccessibilityNodeInfo();
+ assertFalse(accessibilityNodeInfo.getActionList().contains(
+ new AccessibilityAction(R.id.accessibility_action_decrease_window_height, null)));
+ }
+
+ @Test
+ public void enableWindowMagnification_hasA11yWindowTitle() {
+ mInstrumentation.runOnMainSync(() -> {
+ mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN, Float.NaN,
+ Float.NaN);
+ });
+
+ assertEquals(getContext().getResources().getString(
+ com.android.internal.R.string.android_system_label), getAccessibilityWindowTitle());
+ }
+
+ @Test
+ public void enableWindowMagnificationWithScaleLessThanOne_enabled_disabled() {
+ mInstrumentation.runOnMainSync(() -> {
+ mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN, Float.NaN,
+ Float.NaN);
+ });
+
+ mInstrumentation.runOnMainSync(() -> {
+ mWindowMagnificationController.enableWindowMagnificationInternal(0.9f, Float.NaN,
+ Float.NaN);
+ });
+
+ assertEquals(Float.NaN, mWindowMagnificationController.getScale(), 0);
+ }
+
+ @Test
+ public void enableWindowMagnification_rotationIsChanged_updateRotationValue() {
+ // the config orientation should not be undefined, since it would cause config.diff
+ // returning 0 and thus the orientation changed would not be detected
+ assertNotEquals(ORIENTATION_UNDEFINED, mResources.getConfiguration().orientation);
+
+ final Configuration config = mResources.getConfiguration();
+ config.orientation = config.orientation == ORIENTATION_LANDSCAPE ? ORIENTATION_PORTRAIT
+ : ORIENTATION_LANDSCAPE;
+ final int newRotation = simulateRotateTheDevice();
+
+ mInstrumentation.runOnMainSync(
+ () -> mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN,
+ Float.NaN, Float.NaN));
+
+ assertEquals(newRotation, mWindowMagnificationController.mRotation);
+ }
+
+ @Test
+ public void enableWindowMagnification_registerComponentCallback() {
+ mInstrumentation.runOnMainSync(
+ () -> mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN,
+ Float.NaN,
+ Float.NaN));
+
+ verify(mContext).registerComponentCallbacks(mWindowMagnificationController);
+ }
+
+ @Test
+ public void onLocaleChanged_enabled_updateA11yWindowTitle() {
+ final String newA11yWindowTitle = "new a11y window title";
+ mInstrumentation.runOnMainSync(() -> {
+ mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN, Float.NaN,
+ Float.NaN);
+ });
+ final TestableResources testableResources = getContext().getOrCreateTestableResources();
+ testableResources.addOverride(com.android.internal.R.string.android_system_label,
+ newA11yWindowTitle);
+
+ mInstrumentation.runOnMainSync(() -> {
+ mWindowMagnificationController.onConfigurationChanged(ActivityInfo.CONFIG_LOCALE);
+ });
+
+ assertTrue(TextUtils.equals(newA11yWindowTitle, getAccessibilityWindowTitle()));
+ }
+
+ @Ignore("it's flaky in presubmit but works in abtd, filter for now. b/305654925")
+ @Test
+ public void onSingleTap_enabled_scaleAnimates() {
+ mInstrumentation.runOnMainSync(() -> {
+ mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN, Float.NaN,
+ Float.NaN);
+ });
+
+ mInstrumentation.runOnMainSync(() -> {
+ mWindowMagnificationController.onSingleTap(mSpyView);
+ });
+
+ final View mirrorView = mSurfaceControlViewHost.getView();
+
+ final AtomicDouble maxScaleX = new AtomicDouble();
+ advanceTimeBy(mWaitBounceEffectDuration, /* runnableOnEachRefresh= */ () -> {
+ // For some reason the fancy way doesn't compile...
+ // maxScaleX.getAndAccumulate(mirrorView.getScaleX(), Math::max);
+ final double oldMax = maxScaleX.get();
+ final double newMax = Math.max(mirrorView.getScaleX(), oldMax);
+ assertTrue(maxScaleX.compareAndSet(oldMax, newMax));
+ });
+
+ assertTrue(maxScaleX.get() > 1.0);
+ }
+
+ @Test
+ public void moveWindowMagnificationToTheBottom_enabledWithGestureInset_overlapFlagIsTrue() {
+ final Rect bounds = mWindowManager.getCurrentWindowMetrics().getBounds();
+ setSystemGestureInsets();
+ mInstrumentation.runOnMainSync(() -> {
+ mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN, Float.NaN,
+ Float.NaN);
+ });
+
+ mInstrumentation.runOnMainSync(() -> {
+ mWindowMagnificationController.moveWindowMagnifier(0, bounds.height());
+ });
+
+ ReferenceTestUtils.waitForCondition(() -> hasMagnificationOverlapFlag());
+ }
+
+ @Test
+ public void moveWindowMagnificationToRightEdge_dragHandleMovesToLeftAndUpdatesTapExcludeRegion()
+ throws RemoteException {
+ final Rect bounds = mWindowManager.getCurrentWindowMetrics().getBounds();
+ setSystemGestureInsets();
+ mInstrumentation.runOnMainSync(
+ () -> {
+ mWindowMagnificationController.enableWindowMagnificationInternal(
+ Float.NaN, Float.NaN, Float.NaN);
+ });
+ // Wait for Region updated.
+ waitForIdleSync();
+
+ mInstrumentation.runOnMainSync(
+ () -> {
+ mWindowMagnificationController.moveWindowMagnifier(bounds.width(), 0);
+ });
+ // Wait for Region updated.
+ waitForIdleSync();
+
+ AttachedSurfaceControl viewRoot = mSurfaceControlViewHost.getRootSurfaceControl();
+ // Verifying two times in: (1) enable window magnification (2) reposition drag handle
+ verify(viewRoot, times(2)).setTouchableRegion(any());
+
+ View dragButton = getInternalView(R.id.drag_handle);
+ FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) dragButton.getLayoutParams();
+ assertEquals(Gravity.BOTTOM | Gravity.LEFT, params.gravity);
+ }
+
+ @Test
+ public void moveWindowMagnificationToLeftEdge_dragHandleMovesToRightAndUpdatesTapExcludeRegion()
+ throws RemoteException {
+ final Rect bounds = mWindowManager.getCurrentWindowMetrics().getBounds();
+ setSystemGestureInsets();
+ mInstrumentation.runOnMainSync(
+ () -> {
+ mWindowMagnificationController.enableWindowMagnificationInternal(
+ Float.NaN, Float.NaN, Float.NaN);
+ });
+ // Wait for Region updated.
+ waitForIdleSync();
+
+ mInstrumentation.runOnMainSync(
+ () -> {
+ mWindowMagnificationController.moveWindowMagnifier(-bounds.width(), 0);
+ });
+ // Wait for Region updated.
+ waitForIdleSync();
+
+ AttachedSurfaceControl viewRoot = mSurfaceControlViewHost.getRootSurfaceControl();
+ // Verifying one times in: (1) enable window magnification
+ verify(viewRoot).setTouchableRegion(any());
+
+ View dragButton = getInternalView(R.id.drag_handle);
+ FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) dragButton.getLayoutParams();
+ assertEquals(Gravity.BOTTOM | Gravity.RIGHT, params.gravity);
+ }
+
+ @Test
+ public void setMinimumWindowSize_enabled_expectedWindowSize() {
+ final int minimumWindowSize = mResources.getDimensionPixelSize(
+ com.android.internal.R.dimen.accessibility_window_magnifier_min_size);
+ final int expectedWindowHeight = minimumWindowSize;
+ final int expectedWindowWidth = minimumWindowSize;
+ mInstrumentation.runOnMainSync(
+ () -> mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN,
+ Float.NaN, Float.NaN));
+
+ final AtomicInteger actualWindowHeight = new AtomicInteger();
+ final AtomicInteger actualWindowWidth = new AtomicInteger();
+ mInstrumentation.runOnMainSync(() -> {
+ mWindowMagnificationController.setWindowSize(expectedWindowWidth, expectedWindowHeight);
+ actualWindowHeight.set(mSurfaceControlViewHost.getView().getLayoutParams().height);
+ actualWindowWidth.set(mSurfaceControlViewHost.getView().getLayoutParams().width);
+
+ });
+
+ assertEquals(expectedWindowHeight, actualWindowHeight.get());
+ assertEquals(expectedWindowWidth, actualWindowWidth.get());
+ }
+
+ @Test
+ public void setMinimumWindowSizeThenEnable_expectedWindowSize() {
+ final int minimumWindowSize = mResources.getDimensionPixelSize(
+ com.android.internal.R.dimen.accessibility_window_magnifier_min_size);
+ final int expectedWindowHeight = minimumWindowSize;
+ final int expectedWindowWidth = minimumWindowSize;
+
+ final AtomicInteger actualWindowHeight = new AtomicInteger();
+ final AtomicInteger actualWindowWidth = new AtomicInteger();
+ mInstrumentation.runOnMainSync(() -> {
+ mWindowMagnificationController.setWindowSize(expectedWindowWidth, expectedWindowHeight);
+ mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN,
+ Float.NaN, Float.NaN);
+ actualWindowHeight.set(mSurfaceControlViewHost.getView().getLayoutParams().height);
+ actualWindowWidth.set(mSurfaceControlViewHost.getView().getLayoutParams().width);
+ });
+
+ assertEquals(expectedWindowHeight, actualWindowHeight.get());
+ assertEquals(expectedWindowWidth, actualWindowWidth.get());
+ }
+
+ @Test
+ public void setWindowSizeLessThanMin_enabled_minimumWindowSize() {
+ final int minimumWindowSize = mResources.getDimensionPixelSize(
+ com.android.internal.R.dimen.accessibility_window_magnifier_min_size);
+ mInstrumentation.runOnMainSync(
+ () -> mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN,
+ Float.NaN, Float.NaN));
+
+ final AtomicInteger actualWindowHeight = new AtomicInteger();
+ final AtomicInteger actualWindowWidth = new AtomicInteger();
+ mInstrumentation.runOnMainSync(() -> {
+ mWindowMagnificationController.setWindowSize(minimumWindowSize - 10,
+ minimumWindowSize - 10);
+ actualWindowHeight.set(mSurfaceControlViewHost.getView().getLayoutParams().height);
+ actualWindowWidth.set(mSurfaceControlViewHost.getView().getLayoutParams().width);
+ });
+
+ assertEquals(minimumWindowSize, actualWindowHeight.get());
+ assertEquals(minimumWindowSize, actualWindowWidth.get());
+ }
+
+ @Test
+ public void setWindowSizeLargerThanScreenSize_enabled_windowSizeIsScreenSize() {
+ final Rect bounds = mWindowManager.getCurrentWindowMetrics().getBounds();
+ mInstrumentation.runOnMainSync(
+ () -> mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN,
+ Float.NaN, Float.NaN));
+
+ final AtomicInteger actualWindowHeight = new AtomicInteger();
+ final AtomicInteger actualWindowWidth = new AtomicInteger();
+ mInstrumentation.runOnMainSync(() -> {
+ mWindowMagnificationController.setWindowSize(bounds.width() + 10, bounds.height() + 10);
+ actualWindowHeight.set(mSurfaceControlViewHost.getView().getLayoutParams().height);
+ actualWindowWidth.set(mSurfaceControlViewHost.getView().getLayoutParams().width);
+ });
+
+ assertEquals(bounds.height(), actualWindowHeight.get());
+ assertEquals(bounds.width(), actualWindowWidth.get());
+ }
+
+ @Test
+ public void changeMagnificationSize_expectedWindowSize() {
+ final Rect bounds = mWindowManager.getCurrentWindowMetrics().getBounds();
+
+ final float magnificationScaleLarge = 2.5f;
+ final int initSize = Math.min(bounds.width(), bounds.height()) / 3;
+ final int magnificationSize = (int) (initSize * magnificationScaleLarge)
+ - (int) (initSize * magnificationScaleLarge) % 2;
+
+ final int expectedWindowHeight = magnificationSize;
+ final int expectedWindowWidth = magnificationSize;
+
+ mInstrumentation.runOnMainSync(
+ () ->
+ mWindowMagnificationController.enableWindowMagnificationInternal(
+ Float.NaN, Float.NaN, Float.NaN));
+
+ final AtomicInteger actualWindowHeight = new AtomicInteger();
+ final AtomicInteger actualWindowWidth = new AtomicInteger();
+ mInstrumentation.runOnMainSync(
+ () -> {
+ mWindowMagnificationController.changeMagnificationSize(
+ WindowMagnificationSettings.MagnificationSize.LARGE);
+ actualWindowHeight.set(
+ mSurfaceControlViewHost.getView().getLayoutParams().height);
+ actualWindowWidth.set(
+ mSurfaceControlViewHost.getView().getLayoutParams().width);
+ });
+
+ assertEquals(expectedWindowHeight, actualWindowHeight.get());
+ assertEquals(expectedWindowWidth, actualWindowWidth.get());
+ }
+
+ @Test
+ public void editModeOnDragCorner_resizesWindow() {
+ final Rect bounds = mWindowManager.getCurrentWindowMetrics().getBounds();
+
+ final int startingSize = (int) (bounds.width() / 2);
+
+ mInstrumentation.runOnMainSync(
+ () ->
+ mWindowMagnificationController.enableWindowMagnificationInternal(
+ Float.NaN, Float.NaN, Float.NaN));
+
+ final AtomicInteger actualWindowHeight = new AtomicInteger();
+ final AtomicInteger actualWindowWidth = new AtomicInteger();
+
+ mInstrumentation.runOnMainSync(
+ () -> {
+ mWindowMagnificationController.setWindowSize(startingSize, startingSize);
+ mWindowMagnificationController.setEditMagnifierSizeMode(true);
+ });
+
+ waitForIdleSync();
+
+ mInstrumentation.runOnMainSync(
+ () -> {
+ mWindowMagnificationController
+ .onDrag(getInternalView(R.id.bottom_right_corner), 2f, 1f);
+ actualWindowHeight.set(
+ mSurfaceControlViewHost.getView().getLayoutParams().height);
+ actualWindowWidth.set(
+ mSurfaceControlViewHost.getView().getLayoutParams().width);
+ });
+
+ assertEquals(startingSize + 1, actualWindowHeight.get());
+ assertEquals(startingSize + 2, actualWindowWidth.get());
+ }
+
+ @Test
+ public void editModeOnDragEdge_resizesWindowInOnlyOneDirection() {
+ final Rect bounds = mWindowManager.getCurrentWindowMetrics().getBounds();
+
+ final int startingSize = (int) (bounds.width() / 2f);
+
+ mInstrumentation.runOnMainSync(
+ () ->
+ mWindowMagnificationController.enableWindowMagnificationInternal(
+ Float.NaN, Float.NaN, Float.NaN));
+
+ final AtomicInteger actualWindowHeight = new AtomicInteger();
+ final AtomicInteger actualWindowWidth = new AtomicInteger();
+
+ mInstrumentation.runOnMainSync(
+ () -> {
+ mWindowMagnificationController.setWindowSize(startingSize, startingSize);
+ mWindowMagnificationController.setEditMagnifierSizeMode(true);
+ mWindowMagnificationController
+ .onDrag(getInternalView(R.id.bottom_handle), 2f, 1f);
+ actualWindowHeight.set(
+ mSurfaceControlViewHost.getView().getLayoutParams().height);
+ actualWindowWidth.set(
+ mSurfaceControlViewHost.getView().getLayoutParams().width);
+ });
+ assertEquals(startingSize + 1, actualWindowHeight.get());
+ assertEquals(startingSize, actualWindowWidth.get());
+ }
+
+ @Test
+ public void setWindowCenterOutOfScreen_enabled_magnificationCenterIsInsideTheScreen() {
+
+ final int minimumWindowSize = mResources.getDimensionPixelSize(
+ com.android.internal.R.dimen.accessibility_window_magnifier_min_size);
+ final Rect bounds = mWindowManager.getCurrentWindowMetrics().getBounds();
+ mInstrumentation.runOnMainSync(
+ () -> mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN,
+ Float.NaN, Float.NaN));
+
+ final AtomicInteger magnificationCenterX = new AtomicInteger();
+ final AtomicInteger magnificationCenterY = new AtomicInteger();
+ mInstrumentation.runOnMainSync(() -> {
+ mWindowMagnificationController.setWindowSizeAndCenter(minimumWindowSize,
+ minimumWindowSize, bounds.right, bounds.bottom);
+ magnificationCenterX.set((int) mWindowMagnificationController.getCenterX());
+ magnificationCenterY.set((int) mWindowMagnificationController.getCenterY());
+ });
+
+ assertTrue(magnificationCenterX.get() < bounds.right);
+ assertTrue(magnificationCenterY.get() < bounds.bottom);
+ }
+
+ @Test
+ public void performSingleTap_DragHandle() {
+ final Rect bounds = mWindowManager.getCurrentWindowMetrics().getBounds();
+ mInstrumentation.runOnMainSync(
+ () -> {
+ mWindowMagnificationController.enableWindowMagnificationInternal(
+ 1.5f, bounds.centerX(), bounds.centerY());
+ });
+ View dragButton = getInternalView(R.id.drag_handle);
+
+ // Perform a single-tap
+ final long downTime = SystemClock.uptimeMillis();
+ dragButton.dispatchTouchEvent(
+ obtainMotionEvent(downTime, 0, ACTION_DOWN, 100, 100));
+ dragButton.dispatchTouchEvent(
+ obtainMotionEvent(downTime, downTime, ACTION_UP, 100, 100));
+
+ verify(mSurfaceControlViewHost).setView(any(View.class), any());
+ }
+
+ private <T extends View> T getInternalView(@IdRes int idRes) {
+ View mirrorView = mSurfaceControlViewHost.getView();
+ T view = mirrorView.findViewById(idRes);
+ assertNotNull(view);
+ return view;
+ }
+
+ private MotionEvent obtainMotionEvent(long downTime, long eventTime, int action, float x,
+ float y) {
+ return mMotionEventHelper.obtainMotionEvent(downTime, eventTime, action, x, y);
+ }
+
+ private CharSequence getAccessibilityWindowTitle() {
+ final View mirrorView = mSurfaceControlViewHost.getView();
+ if (mirrorView == null) {
+ return null;
+ }
+ WindowManager.LayoutParams layoutParams =
+ (WindowManager.LayoutParams) mirrorView.getLayoutParams();
+ return layoutParams.accessibilityTitle;
+ }
+
+ private boolean hasMagnificationOverlapFlag() {
+ return (mSysUiState.getFlags() & SYSUI_STATE_MAGNIFICATION_OVERLAP) != 0;
+ }
+
+ private void setSystemGestureInsets() {
+ final WindowInsets testInsets = new WindowInsets.Builder()
+ .setInsets(systemGestures(), Insets.of(0, 0, 0, 10))
+ .build();
+ mWindowManager.setWindowInsets(testInsets);
+ }
+
+ private int updateMirrorSurfaceMarginDimension() {
+ return mContext.getResources().getDimensionPixelSize(
+ R.dimen.magnification_mirror_surface_margin);
+ }
+
+ @Surface.Rotation
+ private int simulateRotateTheDevice() {
+ final Display display = Mockito.spy(mContext.getDisplay());
+ final int currentRotation = display.getRotation();
+ final int newRotation = (currentRotation + 1) % 4;
+ when(display.getRotation()).thenReturn(newRotation);
+ when(mContext.getDisplay()).thenReturn(display);
+ return newRotation;
+ }
+
+ // advance time based on the device frame refresh rate
+ private void advanceTimeBy(long timeDelta) {
+ advanceTimeBy(timeDelta, /* runnableOnEachRefresh= */ null);
+ }
+
+ // advance time based on the device frame refresh rate, and trigger runnable on each refresh
+ private void advanceTimeBy(long timeDelta, @Nullable Runnable runnableOnEachRefresh) {
+ final float frameRate = mContext.getDisplay().getRefreshRate();
+ final int timeSlot = (int) (1000 / frameRate);
+ int round = (int) Math.ceil((double) timeDelta / timeSlot);
+ for (; round >= 0; round--) {
+ mInstrumentation.runOnMainSync(() -> {
+ mAnimatorTestRule.advanceTimeBy(timeSlot);
+ if (runnableOnEachRefresh != null) {
+ runnableOnEachRefresh.run();
+ }
+ });
+ }
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/LogContextInteractorImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/LogContextInteractorImplTest.kt
index b1e471a..fc34255 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/LogContextInteractorImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/LogContextInteractorImplTest.kt
@@ -1,3 +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 com.android.systemui.biometrics.domain.interactor
import android.hardware.biometrics.AuthenticateOptions
@@ -5,24 +21,19 @@
import android.hardware.biometrics.IBiometricContextListener.FoldState
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
-import com.android.systemui.biometrics.AuthController
import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.display.data.repository.DeviceStateRepository
+import com.android.systemui.display.data.repository.fakeDeviceStateRepository
import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
+import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractorFactory
import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.keyguard.shared.model.TransitionState
import com.android.systemui.keyguard.shared.model.TransitionStep
-import com.android.systemui.unfold.updates.FOLD_UPDATE_FINISH_CLOSED
-import com.android.systemui.unfold.updates.FOLD_UPDATE_FINISH_FULL_OPEN
-import com.android.systemui.unfold.updates.FOLD_UPDATE_FINISH_HALF_OPEN
-import com.android.systemui.unfold.updates.FOLD_UPDATE_START_CLOSING
-import com.android.systemui.unfold.updates.FOLD_UPDATE_START_OPENING
-import com.android.systemui.unfold.updates.FoldStateProvider
-import com.android.systemui.user.domain.interactor.SelectedUserInteractor
-import com.android.systemui.util.mockito.withArgCaptor
+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.TestScope
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Before
@@ -30,8 +41,6 @@
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
-import org.mockito.Mock
-import org.mockito.Mockito.verify
import org.mockito.junit.MockitoJUnit
@OptIn(ExperimentalCoroutinesApi::class)
@@ -41,31 +50,20 @@
@JvmField @Rule var mockitoRule = MockitoJUnit.rule()
- private val testScope = TestScope()
-
- @Mock private lateinit var foldProvider: FoldStateProvider
- @Mock private lateinit var authController: AuthController
- @Mock private lateinit var selectedUserInteractor: SelectedUserInteractor
-
- private lateinit var udfpsOverlayInteractor: UdfpsOverlayInteractor
- private lateinit var keyguardTransitionRepository: FakeKeyguardTransitionRepository
+ private val kosmos = testKosmos()
+ private val testScope = kosmos.testScope
+ private val deviceStateRepository = kosmos.fakeDeviceStateRepository
+ private val udfpsOverlayInteractor = kosmos.udfpsOverlayInteractor
+ private val keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository
private lateinit var interactor: LogContextInteractorImpl
@Before
fun setup() {
- keyguardTransitionRepository = FakeKeyguardTransitionRepository()
- udfpsOverlayInteractor =
- UdfpsOverlayInteractor(
- context,
- authController,
- selectedUserInteractor,
- testScope.backgroundScope,
- )
interactor =
LogContextInteractorImpl(
testScope.backgroundScope,
- foldProvider,
+ deviceStateRepository,
KeyguardTransitionInteractorFactory.create(
repository = keyguardTransitionRepository,
scope = testScope.backgroundScope,
@@ -189,33 +187,31 @@
@Test
fun foldStateChanges() =
testScope.runTest {
- val foldState = collectLastValue(interactor.foldState)
- runCurrent()
- val listener = foldProvider.captureListener()
+ val foldState by collectLastValue(interactor.foldState)
- listener.onFoldUpdate(FOLD_UPDATE_START_OPENING)
- assertThat(foldState()).isEqualTo(FoldState.UNKNOWN)
+ deviceStateRepository.emit(DeviceStateRepository.DeviceState.HALF_FOLDED)
+ assertThat(foldState).isEqualTo(FoldState.HALF_OPENED)
- listener.onFoldUpdate(FOLD_UPDATE_FINISH_HALF_OPEN)
- assertThat(foldState()).isEqualTo(FoldState.HALF_OPENED)
+ deviceStateRepository.emit(DeviceStateRepository.DeviceState.CONCURRENT_DISPLAY)
+ assertThat(foldState).isEqualTo(FoldState.FULLY_OPENED)
- listener.onFoldUpdate(FOLD_UPDATE_FINISH_FULL_OPEN)
- assertThat(foldState()).isEqualTo(FoldState.FULLY_OPENED)
+ deviceStateRepository.emit(DeviceStateRepository.DeviceState.UNFOLDED)
+ assertThat(foldState).isEqualTo(FoldState.FULLY_OPENED)
- listener.onFoldUpdate(FOLD_UPDATE_START_CLOSING)
- assertThat(foldState()).isEqualTo(FoldState.FULLY_OPENED)
+ deviceStateRepository.emit(DeviceStateRepository.DeviceState.FOLDED)
+ assertThat(foldState).isEqualTo(FoldState.FULLY_CLOSED)
- listener.onFoldUpdate(FOLD_UPDATE_FINISH_CLOSED)
- assertThat(foldState()).isEqualTo(FoldState.FULLY_CLOSED)
+ deviceStateRepository.emit(DeviceStateRepository.DeviceState.REAR_DISPLAY)
+ assertThat(foldState).isEqualTo(FoldState.FULLY_OPENED)
+
+ deviceStateRepository.emit(DeviceStateRepository.DeviceState.UNKNOWN)
+ assertThat(foldState).isEqualTo(FoldState.UNKNOWN)
}
@Test
fun contextSubscriberChanges() =
testScope.runTest {
- runCurrent()
- val foldListener = foldProvider.captureListener()
- foldListener.onFoldUpdate(FOLD_UPDATE_START_CLOSING)
- foldListener.onFoldUpdate(FOLD_UPDATE_FINISH_CLOSED)
+ deviceStateRepository.emit(DeviceStateRepository.DeviceState.FOLDED)
keyguardTransitionRepository.startTransitionTo(KeyguardState.AOD)
var folded: Int? = null
@@ -243,8 +239,7 @@
assertThat(displayState).isEqualTo(AuthenticateOptions.DISPLAY_STATE_AOD)
assertThat(ignoreTouches).isFalse()
- foldListener.onFoldUpdate(FOLD_UPDATE_START_OPENING)
- foldListener.onFoldUpdate(FOLD_UPDATE_FINISH_HALF_OPEN)
+ deviceStateRepository.emit(DeviceStateRepository.DeviceState.HALF_FOLDED)
keyguardTransitionRepository.startTransitionTo(KeyguardState.LOCKSCREEN)
runCurrent()
@@ -259,7 +254,7 @@
job.cancel()
// stale updates should be ignored
- foldListener.onFoldUpdate(FOLD_UPDATE_FINISH_FULL_OPEN)
+ deviceStateRepository.emit(DeviceStateRepository.DeviceState.UNFOLDED)
keyguardTransitionRepository.startTransitionTo(KeyguardState.AOD)
runCurrent()
@@ -270,8 +265,3 @@
private suspend fun FakeKeyguardTransitionRepository.startTransitionTo(newState: KeyguardState) =
sendTransitionStep(TransitionStep(to = newState, transitionState = TransitionState.STARTED))
-
-private fun FoldStateProvider.captureListener() =
- withArgCaptor<FoldStateProvider.FoldUpdatesListener> {
- verify(this@captureListener).addCallback(capture())
- }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractorTest.kt
index 9fe40d7..8b16da2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractorTest.kt
@@ -23,6 +23,7 @@
import com.android.systemui.keyguard.data.repository.KeyguardBlueprintRepository
import com.android.systemui.keyguard.ui.view.layout.blueprints.DefaultKeyguardBlueprint
import com.android.systemui.keyguard.ui.view.layout.blueprints.SplitShadeKeyguardBlueprint
+import com.android.systemui.keyguard.ui.view.layout.blueprints.transitions.IntraBlueprintTransitionType
import com.android.systemui.statusbar.policy.SplitShadeStateController
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.whenever
@@ -46,6 +47,10 @@
private lateinit var underTest: KeyguardBlueprintInteractor
private lateinit var testScope: TestScope
+ val refreshBluePrint: MutableSharedFlow<Unit> = MutableSharedFlow(extraBufferCapacity = 1)
+ val refreshBlueprintTransition: MutableSharedFlow<IntraBlueprintTransitionType> =
+ MutableSharedFlow(extraBufferCapacity = 1)
+
@Mock private lateinit var splitShadeStateController: SplitShadeStateController
@Mock private lateinit var keyguardBlueprintRepository: KeyguardBlueprintRepository
@@ -54,6 +59,9 @@
MockitoAnnotations.initMocks(this)
testScope = TestScope(StandardTestDispatcher())
whenever(keyguardBlueprintRepository.configurationChange).thenReturn(configurationFlow)
+ whenever(keyguardBlueprintRepository.refreshBluePrint).thenReturn(refreshBluePrint)
+ whenever(keyguardBlueprintRepository.refreshBlueprintTransition)
+ .thenReturn(refreshBlueprintTransition)
underTest =
KeyguardBlueprintInteractor(
@@ -105,4 +113,11 @@
underTest.transitionToBlueprint("abc")
verify(keyguardBlueprintRepository).applyBlueprint("abc")
}
+
+ @Test
+ fun testRefreshBlueprintWithTransition() {
+ underTest.refreshBlueprintWithTransition(IntraBlueprintTransitionType.DefaultTransition)
+ verify(keyguardBlueprintRepository)
+ .refreshBlueprintWithTransition(IntraBlueprintTransitionType.DefaultTransition)
+ }
}
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 e93ad0be3..2812718 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
@@ -1895,6 +1895,37 @@
coroutineContext.cancelChildren()
}
+ @Test
+ fun glanceableHubToDreaming() =
+ testScope.runTest {
+ // GIVEN a device that is not dreaming or dozing
+ keyguardRepository.setDreamingWithOverlay(false)
+ keyguardRepository.setDozeTransitionModel(
+ DozeTransitionModel(from = DozeStateModel.DOZE, to = DozeStateModel.FINISH)
+ )
+ runCurrent()
+
+ // GIVEN a prior transition has run to GLANCEABLE_HUB
+ runTransitionAndSetWakefulness(KeyguardState.LOCKSCREEN, KeyguardState.GLANCEABLE_HUB)
+
+ // WHEN the device begins to dream
+ keyguardRepository.setDreamingWithOverlay(true)
+ advanceUntilIdle()
+
+ val info =
+ withArgCaptor<TransitionInfo> {
+ verify(transitionRepository).startTransition(capture())
+ }
+ // THEN a transition to DREAMING should occur
+ assertThat(info.ownerName)
+ .isEqualTo(FromGlanceableHubTransitionInteractor::class.simpleName)
+ assertThat(info.from).isEqualTo(KeyguardState.GLANCEABLE_HUB)
+ assertThat(info.to).isEqualTo(KeyguardState.DREAMING)
+ assertThat(info.animator).isNotNull()
+
+ coroutineContext.cancelChildren()
+ }
+
private fun createKeyguardInteractor(): KeyguardInteractor {
return KeyguardInteractorFactory.create(
featureFlags = featureFlags,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSectionTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSectionTest.kt
index acb6ff0..2da74b0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSectionTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSectionTest.kt
@@ -165,7 +165,7 @@
val cs = ConstraintSet()
underTest.applyDefaultConstraints(cs)
- val expectedLargeClockTopMargin = LARGE_CLOCK_TOP - CLOCK_FADE_TRANSLATION_Y
+ val expectedLargeClockTopMargin = LARGE_CLOCK_TOP
assetLargeClockTop(cs, expectedLargeClockTopMargin)
val expectedSmallClockTopMargin = SMALL_CLOCK_TOP_SPLIT_SHADE
@@ -178,7 +178,7 @@
setSplitShade(false)
val cs = ConstraintSet()
underTest.applyDefaultConstraints(cs)
- val expectedLargeClockTopMargin = LARGE_CLOCK_TOP - CLOCK_FADE_TRANSLATION_Y
+ val expectedLargeClockTopMargin = LARGE_CLOCK_TOP
assetLargeClockTop(cs, expectedLargeClockTopMargin)
val expectedSmallClockTopMargin = SMALL_CLOCK_TOP_NON_SPLIT_SHADE
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/BouncerToGoneFlowsTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/BouncerToGoneFlowsTest.kt
index 75994da..ad2ae8b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/BouncerToGoneFlowsTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/BouncerToGoneFlowsTest.kt
@@ -19,7 +19,7 @@
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
-import com.android.systemui.bouncer.domain.interactor.primaryBouncerInteractor
+import com.android.systemui.bouncer.domain.interactor.mockPrimaryBouncerInteractor
import com.android.systemui.coroutines.collectValues
import com.android.systemui.flags.Flags
import com.android.systemui.flags.fakeFeatureFlagsClassic
@@ -65,7 +65,7 @@
private val keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository
private val shadeRepository = kosmos.shadeRepository
private val sysuiStatusBarStateController = kosmos.sysuiStatusBarStateController
- private val primaryBouncerInteractor = kosmos.primaryBouncerInteractor
+ private val primaryBouncerInteractor = kosmos.mockPrimaryBouncerInteractor
private val underTest = kosmos.bouncerToGoneFlows
@Before
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaHierarchyManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaHierarchyManagerTest.kt
index ba7927d..239bf65 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaHierarchyManagerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaHierarchyManagerTest.kt
@@ -532,6 +532,58 @@
}
@Test
+ fun testCommunalLocation_showsOverLockscreen() =
+ testScope.runTest {
+ // Device is on lock screen.
+ whenever(statusBarStateController.state).thenReturn(StatusBarState.KEYGUARD)
+
+ // UMO goes to communal even over the lock screen.
+ communalInteractor.onSceneChanged(CommunalSceneKey.Communal)
+ runCurrent()
+ verify(mediaCarouselController)
+ .onDesiredLocationChanged(
+ eq(MediaHierarchyManager.LOCATION_COMMUNAL_HUB),
+ nullable(),
+ eq(false),
+ anyLong(),
+ anyLong()
+ )
+ }
+
+ @Test
+ fun testCommunalLocation_showsUntilQsExpands() =
+ testScope.runTest {
+ // Device is on lock screen.
+ whenever(statusBarStateController.state).thenReturn(StatusBarState.KEYGUARD)
+
+ communalInteractor.onSceneChanged(CommunalSceneKey.Communal)
+ runCurrent()
+ verify(mediaCarouselController)
+ .onDesiredLocationChanged(
+ eq(MediaHierarchyManager.LOCATION_COMMUNAL_HUB),
+ nullable(),
+ eq(false),
+ anyLong(),
+ anyLong()
+ )
+ clearInvocations(mediaCarouselController)
+
+ // Start opening the shade.
+ mediaHierarchyManager.qsExpansion = 0.1f
+ runCurrent()
+
+ // UMO goes to the shade instead.
+ verify(mediaCarouselController)
+ .onDesiredLocationChanged(
+ eq(MediaHierarchyManager.LOCATION_QS),
+ any(MediaHostState::class.java),
+ eq(false),
+ anyLong(),
+ anyLong()
+ )
+ }
+
+ @Test
fun testQsExpandedChanged_noQqsMedia() {
// When we are looking at QQS with active media
whenever(statusBarStateController.state).thenReturn(StatusBarState.SHADE)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingServiceTest.java b/packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingServiceTest.java
index a2aed98..9ce77e5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingServiceTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingServiceTest.java
@@ -28,10 +28,10 @@
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
+import android.app.ActivityOptions.LaunchCookie;
import android.app.Notification;
import android.app.NotificationManager;
import android.content.Intent;
-import android.os.Binder;
import android.os.Handler;
import android.os.RemoteException;
import android.os.UserHandle;
@@ -146,7 +146,7 @@
@Test
public void testLogStartPartialRecording() {
- MediaProjectionCaptureTarget target = new MediaProjectionCaptureTarget(new Binder());
+ MediaProjectionCaptureTarget target = new MediaProjectionCaptureTarget(new LaunchCookie());
Intent startIntent = RecordingService.getStartIntent(mContext, 0, 0, false, target);
mRecordingService.onStartCommand(startIntent, 0, 0);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shared/system/RemoteTransitionTest.java b/packages/SystemUI/tests/src/com/android/systemui/shared/system/RemoteTransitionTest.java
index 190ee81..460892a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shared/system/RemoteTransitionTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shared/system/RemoteTransitionTest.java
@@ -49,7 +49,7 @@
import androidx.test.filters.SmallTest;
import com.android.systemui.SysuiTestCase;
-import com.android.wm.shell.util.TransitionUtil;
+import com.android.wm.shell.shared.TransitionUtil;
import org.junit.Before;
import org.junit.Test;
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/CommandQueueTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/CommandQueueTest.java
index 260bef8..5da3a56 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/CommandQueueTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/CommandQueueTest.java
@@ -25,6 +25,7 @@
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.verifyZeroInteractions;
import android.content.ComponentName;
import android.graphics.Rect;
@@ -32,11 +33,14 @@
import android.hardware.biometrics.PromptInfo;
import android.hardware.fingerprint.IUdfpsRefreshRateRequestCallback;
import android.os.Bundle;
+import android.platform.test.annotations.DisableFlags;
+import android.platform.test.annotations.EnableFlags;
import android.view.KeyEvent;
import android.view.WindowInsets;
import android.view.WindowInsets.Type.InsetsType;
import android.view.WindowInsetsController.Appearance;
import android.view.WindowInsetsController.Behavior;
+import android.view.accessibility.Flags;
import androidx.test.filters.SmallTest;
@@ -365,14 +369,50 @@
}
@Test
- public void testAddQsTile() {
+ @DisableFlags(Flags.FLAG_A11Y_QS_SHORTCUT)
+ public void addQsTile_withA11yQsShortcutFlagOff() {
ComponentName c = new ComponentName("testpkg", "testcls");
+
mCommandQueue.addQsTile(c);
waitForIdleSync();
+
verify(mCallbacks).addQsTile(eq(c));
}
@Test
+ @DisableFlags(Flags.FLAG_A11Y_QS_SHORTCUT)
+ public void addQsTileToFrontOrEnd_withA11yQsShortcutFlagOff_doNothing() {
+ ComponentName c = new ComponentName("testpkg", "testcls");
+
+ mCommandQueue.addQsTileToFrontOrEnd(c, true);
+ waitForIdleSync();
+
+ verifyZeroInteractions(mCallbacks);
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_A11Y_QS_SHORTCUT)
+ public void addQsTile_withA11yQsShortcutFlagOn() {
+ ComponentName c = new ComponentName("testpkg", "testcls");
+
+ mCommandQueue.addQsTile(c);
+ waitForIdleSync();
+
+ verify(mCallbacks).addQsTileToFrontOrEnd(eq(c), eq(false));
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_A11Y_QS_SHORTCUT)
+ public void addQsTileAtTheEnd_withA11yQsShortcutFlagOn() {
+ ComponentName c = new ComponentName("testpkg", "testcls");
+
+ mCommandQueue.addQsTileToFrontOrEnd(c, true);
+ waitForIdleSync();
+
+ verify(mCallbacks).addQsTileToFrontOrEnd(eq(c), eq(true));
+ }
+
+ @Test
public void testRemoveQsTile() {
ComponentName c = new ComponentName("testpkg", "testcls");
mCommandQueue.remQsTile(c);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacksTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacksTest.java
index 912c27d..ea4ae17 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacksTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacksTest.java
@@ -26,6 +26,7 @@
import android.app.ActivityManager;
import android.app.StatusBarManager;
+import android.content.ComponentName;
import android.os.PowerManager;
import android.os.UserHandle;
import android.os.Vibrator;
@@ -190,4 +191,31 @@
HapticFeedbackConstants.GESTURE_START
);
}
+
+ @Test
+ public void addQsTile_delegateCallToQsHost() {
+ ComponentName c = new ComponentName("testpkg", "testcls");
+
+ mSbcqCallbacks.addQsTile(c);
+
+ verify(mQSHost).addTile(c);
+ }
+
+ @Test
+ public void addQsTileToFrontOrEnd_toTheEnd_delegateCallToQsHost() {
+ ComponentName c = new ComponentName("testpkg", "testcls");
+
+ mSbcqCallbacks.addQsTileToFrontOrEnd(c, true);
+
+ verify(mQSHost).addTile(c, true);
+ }
+
+ @Test
+ public void addQsTileToFrontOrEnd_toTheFront_delegateCallToQsHost() {
+ ComponentName c = new ComponentName("testpkg", "testcls");
+
+ mSbcqCallbacks.addQsTileToFrontOrEnd(c, false);
+
+ verify(mQSHost).addTile(c, false);
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java
index 9c4984e..849a13b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java
@@ -41,6 +41,8 @@
import static java.util.Collections.emptySet;
+import static kotlinx.coroutines.flow.FlowKt.flowOf;
+
import android.app.ActivityManager;
import android.app.IWallpaperManager;
import android.app.WallpaperManager;
@@ -77,7 +79,6 @@
import com.android.internal.logging.testing.FakeMetricsLogger;
import com.android.internal.statusbar.IStatusBarService;
import com.android.keyguard.KeyguardUpdateMonitor;
-import com.android.keyguard.TestScopeProvider;
import com.android.keyguard.ViewMediatorCallback;
import com.android.systemui.InitController;
import com.android.systemui.SysuiTestCase;
@@ -92,6 +93,10 @@
import com.android.systemui.classifier.FalsingCollectorFake;
import com.android.systemui.classifier.FalsingManagerFake;
import com.android.systemui.colorextraction.SysuiColorExtractor;
+import com.android.systemui.communal.data.repository.CommunalRepository;
+import com.android.systemui.communal.domain.interactor.CommunalInteractor;
+import com.android.systemui.communal.shared.model.CommunalSceneKey;
+import com.android.systemui.communal.shared.model.ObservableCommunalTransitionState;
import com.android.systemui.demomode.DemoModeController;
import com.android.systemui.dump.DumpManager;
import com.android.systemui.flags.FakeFeatureFlags;
@@ -102,6 +107,7 @@
import com.android.systemui.keyguard.ScreenLifecycle;
import com.android.systemui.keyguard.WakefulnessLifecycle;
import com.android.systemui.keyguard.ui.viewmodel.LightRevealScrimViewModel;
+import com.android.systemui.kosmos.KosmosJavaAdapter;
import com.android.systemui.log.LogBuffer;
import com.android.systemui.navigationbar.NavigationBarController;
import com.android.systemui.notetask.NoteTaskController;
@@ -195,6 +201,8 @@
import javax.inject.Provider;
+import kotlinx.coroutines.test.TestScope;
+
@SmallTest
@RunWith(AndroidTestingRunner.class)
@RunWithLooper(setAsMainLooper = true)
@@ -203,11 +211,17 @@
private static final int FOLD_STATE_FOLDED = 0;
private static final int FOLD_STATE_UNFOLDED = 1;
+ private final KosmosJavaAdapter mKosmos = new KosmosJavaAdapter(this);
+
private CentralSurfacesImpl mCentralSurfaces;
private FakeMetricsLogger mMetricsLogger;
private PowerManager mPowerManager;
private VisualInterruptionDecisionProvider mVisualInterruptionDecisionProvider;
+
+ private final TestScope mTestScope = mKosmos.getTestScope();
+ private final CommunalInteractor mCommunalInteractor = mKosmos.getCommunalInteractor();
+ private final CommunalRepository mCommunalRepository = mKosmos.getCommunalRepository();
@Mock private NotificationsController mNotificationsController;
@Mock private LightBarController mLightBarController;
@Mock private StatusBarKeyguardViewManager mStatusBarKeyguardViewManager;
@@ -461,7 +475,7 @@
new DisplayMetrics(),
mMetricsLogger,
mShadeLogger,
- new JavaAdapter(TestScopeProvider.getTestScope()),
+ new JavaAdapter(mTestScope),
mUiBgExecutor,
mNotificationPanelViewController,
mNotificationMediaManager,
@@ -473,6 +487,7 @@
mScreenLifecycle,
mWakefulnessLifecycle,
mPowerInteractor,
+ mCommunalInteractor,
mStatusBarStateController,
Optional.of(mBubbles),
() -> mNoteTaskController,
@@ -821,6 +836,25 @@
}
@Test
+ public void testEnteringGlanceableHub_updatesScrim() {
+ // Transition to the glanceable hub.
+ mCommunalRepository.setTransitionState(flowOf(new ObservableCommunalTransitionState.Idle(
+ CommunalSceneKey.Communal.INSTANCE)));
+ mTestScope.getTestScheduler().runCurrent();
+
+ // ScrimState also transitions.
+ verify(mScrimController).transitionTo(ScrimState.GLANCEABLE_HUB);
+
+ // Transition away from the glanceable hub.
+ mCommunalRepository.setTransitionState(flowOf(new ObservableCommunalTransitionState.Idle(
+ CommunalSceneKey.Blank.INSTANCE)));
+ mTestScope.getTestScheduler().runCurrent();
+
+ // ScrimState goes back to UNLOCKED.
+ verify(mScrimController).transitionTo(eq(ScrimState.UNLOCKED), any());
+ }
+
+ @Test
public void testShowKeyguardImplementation_setsState() {
when(mLockscreenUserManager.getCurrentProfiles()).thenReturn(new SparseArray<>());
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java
index 423cc84..3bde6e3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java
@@ -51,6 +51,7 @@
import android.graphics.Color;
import android.testing.AndroidTestingRunner;
import android.testing.TestableLooper;
+import android.testing.ViewUtils;
import android.util.MathUtils;
import android.view.View;
@@ -59,13 +60,13 @@
import com.android.internal.colorextraction.ColorExtractor.GradientColors;
import com.android.keyguard.BouncerPanelExpansionCalculator;
import com.android.keyguard.KeyguardUpdateMonitor;
-import com.android.keyguard.TestScopeProvider;
import com.android.systemui.DejankUtils;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.animation.ShadeInterpolation;
import com.android.systemui.bouncer.shared.constants.KeyguardBouncerConstants;
import com.android.systemui.dock.DockManager;
import com.android.systemui.keyguard.KeyguardUnlockAnimationController;
+import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository;
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor;
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor;
import com.android.systemui.keyguard.shared.model.KeyguardState;
@@ -73,6 +74,7 @@
import com.android.systemui.keyguard.shared.model.TransitionStep;
import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerToGoneTransitionViewModel;
import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToGoneTransitionViewModel;
+import com.android.systemui.kosmos.KosmosJavaAdapter;
import com.android.systemui.scrim.ScrimView;
import com.android.systemui.shade.transition.LargeScreenShadeInterpolator;
import com.android.systemui.shade.transition.LinearLargeScreenShadeInterpolator;
@@ -103,7 +105,6 @@
import java.util.HashSet;
import java.util.Map;
-import kotlinx.coroutines.CoroutineDispatcher;
import kotlinx.coroutines.test.TestScope;
@RunWith(AndroidTestingRunner.class)
@@ -112,13 +113,14 @@
public class ScrimControllerTest extends SysuiTestCase {
@Rule public Expect mExpect = Expect.create();
+ private final KosmosJavaAdapter mKosmos = new KosmosJavaAdapter(this);
private final FakeConfigurationController mConfigurationController =
new FakeConfigurationController();
private final LargeScreenShadeInterpolator
mLinearLargeScreenShadeInterpolator = new LinearLargeScreenShadeInterpolator();
- private final TestScope mTestScope = TestScopeProvider.getTestScope();
+ private final TestScope mTestScope = mKosmos.getTestScope();
private final JavaAdapter mJavaAdapter = new JavaAdapter(mTestScope.getBackgroundScope());
private ScrimController mScrimController;
@@ -145,10 +147,12 @@
@Mock private PrimaryBouncerToGoneTransitionViewModel mPrimaryBouncerToGoneTransitionViewModel;
@Mock private AlternateBouncerToGoneTransitionViewModel
mAlternateBouncerToGoneTransitionViewModel;
- @Mock private KeyguardTransitionInteractor mKeyguardTransitionInteractor;
+ private final KeyguardTransitionInteractor mKeyguardTransitionInteractor =
+ mKosmos.getKeyguardTransitionInteractor();
+ private final FakeKeyguardTransitionRepository mKeyguardTransitionRepository =
+ mKosmos.getKeyguardTransitionRepository();
@Mock private KeyguardInteractor mKeyguardInteractor;
private final FakeWallpaperRepository mWallpaperRepository = new FakeWallpaperRepository();
- @Mock private CoroutineDispatcher mMainDispatcher;
@Mock private TypedArray mMockTypedArray;
// TODO(b/204991468): Use a real PanelExpansionStateManager object once this bug is fixed. (The
@@ -265,8 +269,6 @@
when(mDelayedWakeLockFactory.create(any(String.class))).thenReturn(mWakeLock);
when(mDockManager.isDocked()).thenReturn(false);
- when(mKeyguardTransitionInteractor.transition(any(), any()))
- .thenReturn(emptyFlow());
when(mPrimaryBouncerToGoneTransitionViewModel.getScrimAlpha())
.thenReturn(emptyFlow());
when(mAlternateBouncerToGoneTransitionViewModel.getScrimAlpha())
@@ -292,13 +294,16 @@
mKeyguardTransitionInteractor,
mKeyguardInteractor,
mWallpaperRepository,
- mMainDispatcher,
+ mKosmos.getTestDispatcher(),
mLinearLargeScreenShadeInterpolator);
mScrimController.start();
mScrimController.setScrimVisibleListener(visible -> mScrimVisibility = visible);
mScrimController.attachViews(mScrimBehind, mNotificationsScrim, mScrimInFront);
mScrimController.setAnimatorListener(mAnimatorListener);
+ // Attach behind scrim so flows that are collecting on it start running.
+ ViewUtils.attachView(mScrimBehind);
+
mScrimController.setHasBackdrop(false);
mWallpaperRepository.getWallpaperSupportsAmbientMode().setValue(false);
@@ -629,6 +634,164 @@
}
@Test
+ public void lockscreenToHubTransition_setsBehindScrimAlpha() {
+ // Start on lockscreen.
+ mScrimController.transitionTo(ScrimState.KEYGUARD);
+ finishAnimationsImmediately();
+
+ // Behind scrim starts at default alpha.
+ final float transitionProgress = 0f;
+ float expectedAlpha = ScrimState.KEYGUARD.getBehindAlpha();
+ mKeyguardTransitionRepository.sendTransitionStepJava(mKosmos.getTestScope(),
+ new TransitionStep(
+ KeyguardState.LOCKSCREEN,
+ KeyguardState.GLANCEABLE_HUB,
+ transitionProgress,
+ TransitionState.STARTED
+ ), true);
+ mTestScope.getTestScheduler().runCurrent();
+ assertThat(mScrimBehind.getViewAlpha()).isEqualTo(expectedAlpha);
+
+ // Scrim fades out as transition runs.
+ final float runningProgress = 0.2f;
+ expectedAlpha = (1 - runningProgress) * ScrimState.KEYGUARD.getBehindAlpha();
+ mKeyguardTransitionRepository.sendTransitionStepJava(mKosmos.getTestScope(),
+ new TransitionStep(
+ KeyguardState.LOCKSCREEN,
+ KeyguardState.GLANCEABLE_HUB,
+ runningProgress,
+ TransitionState.RUNNING
+ ), true);
+ mTestScope.getTestScheduler().runCurrent();
+ assertThat(mScrimBehind.getViewAlpha()).isEqualTo(expectedAlpha);
+
+ // Scrim invisible at end of transition.
+ final float finishedProgress = 1f;
+ expectedAlpha = 0f;
+ mKeyguardTransitionRepository.sendTransitionStepJava(mKosmos.getTestScope(),
+ new TransitionStep(
+ KeyguardState.LOCKSCREEN,
+ KeyguardState.GLANCEABLE_HUB,
+ finishedProgress,
+ TransitionState.FINISHED
+ ), true);
+ mTestScope.getTestScheduler().runCurrent();
+ assertThat(mScrimBehind.getViewAlpha()).isEqualTo(expectedAlpha);
+ }
+
+ @Test
+ public void hubToLockscreenTransition_setsViewAlpha() {
+ // Start on glanceable hub.
+ mScrimController.transitionTo(ScrimState.GLANCEABLE_HUB);
+ finishAnimationsImmediately();
+
+ // Behind scrim starts at 0 alpha.
+ final float transitionProgress = 0f;
+ float expectedAlpha = 0f;
+ mKeyguardTransitionRepository.sendTransitionStepJava(mKosmos.getTestScope(),
+ new TransitionStep(
+ KeyguardState.GLANCEABLE_HUB,
+ KeyguardState.LOCKSCREEN,
+ transitionProgress,
+ TransitionState.STARTED
+ ), true);
+ mTestScope.getTestScheduler().runCurrent();
+ assertThat(mScrimBehind.getViewAlpha()).isEqualTo(expectedAlpha);
+
+ // Scrim fades in as transition runs.
+ final float runningProgress = 0.2f;
+ expectedAlpha = runningProgress * ScrimState.KEYGUARD.getBehindAlpha();
+ mKeyguardTransitionRepository.sendTransitionStepJava(mKosmos.getTestScope(),
+ new TransitionStep(
+ KeyguardState.GLANCEABLE_HUB,
+ KeyguardState.LOCKSCREEN,
+ runningProgress,
+ TransitionState.RUNNING
+ ), true);
+ mTestScope.getTestScheduler().runCurrent();
+ assertThat(mScrimBehind.getViewAlpha()).isEqualTo(expectedAlpha);
+
+ // Scrim at default visibility at end of transition.
+ final float finishedProgress = 1f;
+ expectedAlpha = finishedProgress * ScrimState.KEYGUARD.getBehindAlpha();
+ mKeyguardTransitionRepository.sendTransitionStepJava(mKosmos.getTestScope(),
+ new TransitionStep(
+ KeyguardState.GLANCEABLE_HUB,
+ KeyguardState.LOCKSCREEN,
+ finishedProgress,
+ TransitionState.FINISHED
+ ), true);
+ mTestScope.getTestScheduler().runCurrent();
+ assertThat(mScrimBehind.getViewAlpha()).isEqualTo(expectedAlpha);
+ }
+
+ @Test
+ public void transitionToHub() {
+ mScrimController.setRawPanelExpansionFraction(0f);
+ mScrimController.setBouncerHiddenFraction(KeyguardBouncerConstants.EXPANSION_HIDDEN);
+ mScrimController.transitionTo(ScrimState.GLANCEABLE_HUB);
+ finishAnimationsImmediately();
+
+ // All scrims transparent on the hub.
+ assertScrimAlpha(Map.of(
+ mScrimInFront, TRANSPARENT,
+ mNotificationsScrim, TRANSPARENT,
+ mScrimBehind, TRANSPARENT));
+ }
+
+ @Test
+ public void openBouncerOnHub() {
+ mScrimController.transitionTo(ScrimState.GLANCEABLE_HUB);
+
+ // Open the bouncer.
+ mScrimController.setRawPanelExpansionFraction(0f);
+ mScrimController.setBouncerHiddenFraction(KeyguardBouncerConstants.EXPANSION_VISIBLE);
+ finishAnimationsImmediately();
+
+ // Only behind widget is visible.
+ assertScrimAlpha(Map.of(
+ mScrimInFront, TRANSPARENT,
+ mNotificationsScrim, TRANSPARENT,
+ mScrimBehind, OPAQUE));
+
+ // Bouncer is closed.
+ mScrimController.setBouncerHiddenFraction(KeyguardBouncerConstants.EXPANSION_HIDDEN);
+ mScrimController.transitionTo(ScrimState.GLANCEABLE_HUB);
+ finishAnimationsImmediately();
+
+ // All scrims are transparent.
+ assertScrimAlpha(Map.of(
+ mScrimInFront, TRANSPARENT,
+ mNotificationsScrim, TRANSPARENT,
+ mScrimBehind, TRANSPARENT));
+ }
+
+ @Test
+ public void openShadeOnHub() {
+ mScrimController.transitionTo(ScrimState.GLANCEABLE_HUB);
+
+ // Open the shade.
+ mScrimController.transitionTo(SHADE_LOCKED);
+ mScrimController.setQsPosition(1f, 0);
+ finishAnimationsImmediately();
+
+ // Shade scrims are visible.
+ assertScrimAlpha(Map.of(
+ mNotificationsScrim, OPAQUE,
+ mScrimInFront, TRANSPARENT,
+ mScrimBehind, OPAQUE));
+
+ mScrimController.transitionTo(ScrimState.GLANCEABLE_HUB);
+ finishAnimationsImmediately();
+
+ // All scrims are transparent.
+ assertScrimAlpha(Map.of(
+ mScrimInFront, TRANSPARENT,
+ mNotificationsScrim, TRANSPARENT,
+ mScrimBehind, TRANSPARENT));
+ }
+
+ @Test
public void onThemeChange_bouncerBehindTint_isUpdatedToSurfaceColor() {
assertEquals(BOUNCER.getBehindTint(), 0x112233);
mSurfaceColor = 0x223344;
@@ -1001,7 +1164,7 @@
mKeyguardTransitionInteractor,
mKeyguardInteractor,
mWallpaperRepository,
- mMainDispatcher,
+ mKosmos.getTestDispatcher(),
mLinearLargeScreenShadeInterpolator);
mScrimController.start();
mScrimController.setScrimVisibleListener(visible -> mScrimVisibility = visible);
@@ -1267,7 +1430,7 @@
ScrimState.UNINITIALIZED, ScrimState.KEYGUARD, BOUNCER,
ScrimState.DREAMING, ScrimState.BOUNCER_SCRIMMED, ScrimState.BRIGHTNESS_MIRROR,
ScrimState.UNLOCKED, SHADE_LOCKED, ScrimState.AUTH_SCRIMMED,
- ScrimState.AUTH_SCRIMMED_SHADE));
+ ScrimState.AUTH_SCRIMMED_SHADE, ScrimState.GLANCEABLE_HUB));
for (ScrimState state : ScrimState.values()) {
if (!lowPowerModeStates.contains(state) && !regularStates.contains(state)) {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/domain/interactor/DeviceBasedSatelliteInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/domain/interactor/DeviceBasedSatelliteInteractorTest.kt
index e010b86..d465b47 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/domain/interactor/DeviceBasedSatelliteInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/domain/interactor/DeviceBasedSatelliteInteractorTest.kt
@@ -16,6 +16,8 @@
package com.android.systemui.statusbar.pipeline.satellite.domain.interactor
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
import androidx.test.filters.SmallTest
import com.android.internal.telephony.flags.Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG
import com.android.systemui.SysuiTestCase
@@ -49,8 +51,6 @@
@Before
fun setUp() {
- mSetFlagsRule.enableFlags(FLAG_OEM_ENABLED_SATELLITE_FLAG)
-
underTest =
DeviceBasedSatelliteInteractor(
repo,
@@ -60,6 +60,7 @@
}
@Test
+ @EnableFlags(FLAG_OEM_ENABLED_SATELLITE_FLAG)
fun isSatelliteAllowed_falseWhenNotAllowed() =
testScope.runTest {
val latest by collectLastValue(underTest.isSatelliteAllowed)
@@ -72,6 +73,7 @@
}
@Test
+ @EnableFlags(FLAG_OEM_ENABLED_SATELLITE_FLAG)
fun isSatelliteAllowed_trueWhenAllowed() =
testScope.runTest {
val latest by collectLastValue(underTest.isSatelliteAllowed)
@@ -84,10 +86,10 @@
}
@Test
+ @DisableFlags(FLAG_OEM_ENABLED_SATELLITE_FLAG)
fun isSatelliteAllowed_offWhenFlagIsOff() =
testScope.runTest {
// GIVEN feature is disabled
- mSetFlagsRule.disableFlags(FLAG_OEM_ENABLED_SATELLITE_FLAG)
// Remake the interactor so the flag is read
underTest =
@@ -107,6 +109,7 @@
}
@Test
+ @EnableFlags(FLAG_OEM_ENABLED_SATELLITE_FLAG)
fun connectionState_matchesRepositoryValue() =
testScope.runTest {
val latest by collectLastValue(underTest.connectionState)
@@ -129,10 +132,10 @@
}
@Test
+ @DisableFlags(FLAG_OEM_ENABLED_SATELLITE_FLAG)
fun connectionState_offWhenFeatureIsDisabled() =
testScope.runTest {
// GIVEN the flag is disabled
- mSetFlagsRule.disableFlags(FLAG_OEM_ENABLED_SATELLITE_FLAG)
// Remake the interactor so the flag is read
underTest =
@@ -164,6 +167,7 @@
}
@Test
+ @EnableFlags(FLAG_OEM_ENABLED_SATELLITE_FLAG)
fun signalStrength_matchesRepo() =
testScope.runTest {
val latest by collectLastValue(underTest.signalStrength)
@@ -182,10 +186,10 @@
}
@Test
+ @DisableFlags(FLAG_OEM_ENABLED_SATELLITE_FLAG)
fun signalStrength_zeroWhenDisabled() =
testScope.runTest {
// GIVEN the flag is enabled
- mSetFlagsRule.disableFlags(FLAG_OEM_ENABLED_SATELLITE_FLAG)
// Remake the interactor so the flag is read
underTest =
@@ -212,6 +216,19 @@
}
@Test
+ @EnableFlags(FLAG_OEM_ENABLED_SATELLITE_FLAG)
+ fun areAllConnectionsOutOfService_noConnections_yes() =
+ testScope.runTest {
+ val latest by collectLastValue(underTest.areAllConnectionsOutOfService)
+
+ // GIVEN, 0 connections
+
+ // THEN the value is propagated to this interactor
+ assertThat(latest).isTrue()
+ }
+
+ @Test
+ @EnableFlags(FLAG_OEM_ENABLED_SATELLITE_FLAG)
fun areAllConnectionsOutOfService_twoConnectionsOos_yes() =
testScope.runTest {
val latest by collectLastValue(underTest.areAllConnectionsOutOfService)
@@ -229,6 +246,7 @@
}
@Test
+ @EnableFlags(FLAG_OEM_ENABLED_SATELLITE_FLAG)
fun areAllConnectionsOutOfService_oneConnectionOos_yes() =
testScope.runTest {
val latest by collectLastValue(underTest.areAllConnectionsOutOfService)
@@ -244,6 +262,7 @@
}
@Test
+ @EnableFlags(FLAG_OEM_ENABLED_SATELLITE_FLAG)
fun areAllConnectionsOutOfService_oneConnectionInService_no() =
testScope.runTest {
val latest by collectLastValue(underTest.areAllConnectionsOutOfService)
@@ -259,6 +278,7 @@
}
@Test
+ @EnableFlags(FLAG_OEM_ENABLED_SATELLITE_FLAG)
fun areAllConnectionsOutOfService_twoConnectionsOneInService_no() =
testScope.runTest {
val latest by collectLastValue(underTest.areAllConnectionsOutOfService)
@@ -276,6 +296,7 @@
}
@Test
+ @EnableFlags(FLAG_OEM_ENABLED_SATELLITE_FLAG)
fun areAllConnectionsOutOfService_twoConnectionsInService_no() =
testScope.runTest {
val latest by collectLastValue(underTest.areAllConnectionsOutOfService)
@@ -293,10 +314,10 @@
}
@Test
+ @DisableFlags(FLAG_OEM_ENABLED_SATELLITE_FLAG)
fun areAllConnectionsOutOfService_falseWhenFlagIsOff() =
testScope.runTest {
// GIVEN the flag is disabled
- mSetFlagsRule.disableFlags(FLAG_OEM_ENABLED_SATELLITE_FLAG)
// Remake the interactor so the flag is read
underTest =
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModelTest.kt
index 21c038a..f53fc46 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModelTest.kt
@@ -20,6 +20,7 @@
import com.android.systemui.SysuiTestCase
import com.android.systemui.common.shared.model.Icon
import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.statusbar.pipeline.airplane.data.repository.FakeAirplaneModeRepository
import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.FakeMobileIconsInteractor
import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy
import com.android.systemui.statusbar.pipeline.satellite.data.prod.FakeDeviceBasedSatelliteRepository
@@ -36,6 +37,7 @@
class DeviceBasedSatelliteViewModelTest : SysuiTestCase() {
private lateinit var underTest: DeviceBasedSatelliteViewModel
private lateinit var interactor: DeviceBasedSatelliteInteractor
+ private lateinit var airplaneModeRepository: FakeAirplaneModeRepository
private val repo = FakeDeviceBasedSatelliteRepository()
private val mobileIconsInteractor = FakeMobileIconsInteractor(FakeMobileMappingsProxy(), mock())
@@ -45,6 +47,7 @@
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
+ airplaneModeRepository = FakeAirplaneModeRepository()
interactor =
DeviceBasedSatelliteInteractor(
@@ -57,6 +60,7 @@
DeviceBasedSatelliteViewModel(
interactor,
testScope.backgroundScope,
+ airplaneModeRepository,
)
}
@@ -72,6 +76,9 @@
val i1 = mobileIconsInteractor.getMobileConnectionInteractorForSubId(1)
i1.isInService.value = false
+ // GIVEN apm is disabled
+ airplaneModeRepository.setIsAirplaneMode(false)
+
// THEN icon is null because we should not be showing it
assertThat(latest).isNull()
}
@@ -88,11 +95,33 @@
val i1 = mobileIconsInteractor.getMobileConnectionInteractorForSubId(1)
i1.isInService.value = true
+ // GIVEN apm is disabled
+ airplaneModeRepository.setIsAirplaneMode(false)
+
// THEN icon is null because we have service
assertThat(latest).isNull()
}
@Test
+ fun icon_nullWhenShouldNotShow_apmIsEnabled() =
+ testScope.runTest {
+ val latest by collectLastValue(underTest.icon)
+
+ // GIVEN satellite is allowed
+ repo.isSatelliteAllowedForCurrentLocation.value = true
+
+ // GIVEN all icons are OOS
+ val i1 = mobileIconsInteractor.getMobileConnectionInteractorForSubId(1)
+ i1.isInService.value = false
+
+ // GIVEN apm is enabled
+ airplaneModeRepository.setIsAirplaneMode(true)
+
+ // THEN icon is null because we should not be showing it
+ assertThat(latest).isNull()
+ }
+
+ @Test
fun icon_satelliteIsOff() =
testScope.runTest {
val latest by collectLastValue(underTest.icon)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/theme/ThemeOverlayControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/theme/ThemeOverlayControllerTest.java
index 457acd2..b58a41c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/theme/ThemeOverlayControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/theme/ThemeOverlayControllerTest.java
@@ -190,7 +190,7 @@
mWakefulnessLifecycle.dispatchFinishedWakingUp();
mThemeOverlayController.start();
- verify(mUserTracker).addCallback(mUserTrackerCallback.capture(), eq(mBgExecutor));
+ verify(mUserTracker).addCallback(mUserTrackerCallback.capture(), eq(mMainExecutor));
verify(mWallpaperManager).addOnColorsChangedListener(mColorsListener.capture(), eq(null),
eq(UserHandle.USER_ALL));
verify(mBroadcastDispatcher).registerReceiver(mBroadcastReceiver.capture(), any(),
diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplTest.kt
index abfff34..0669cb8 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplTest.kt
@@ -31,6 +31,7 @@
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.launchIn
@@ -46,6 +47,7 @@
import org.mockito.Mockito.`when` as whenever
import org.mockito.MockitoAnnotations
+@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@RunWith(JUnit4::class)
class UserRepositoryImplTest : SysuiTestCase() {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
index 7eba3f0..0a3c2d9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
@@ -2217,7 +2217,8 @@
FakeBubbleStateListener bubbleStateListener = new FakeBubbleStateListener();
mBubbleController.registerBubbleStateListener(bubbleStateListener);
- mBubbleController.expandStackAndSelectBubbleFromLauncher(mBubbleEntry.getKey(), 500, 1000);
+ mBubbleController.expandStackAndSelectBubbleFromLauncher(mBubbleEntry.getKey(),
+ new Rect(500, 1000, 600, 1100));
assertThat(mBubbleController.getLayerView().isExpanded()).isTrue();
diff --git a/packages/SystemUI/tests/utils/src/android/content/PackageManagerKosmos.kt b/packages/SystemUI/tests/utils/src/android/content/PackageManagerKosmos.kt
new file mode 100644
index 0000000..8901314
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/android/content/PackageManagerKosmos.kt
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.content
+
+import android.content.pm.PackageManager
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.util.mockito.mock
+
+val Kosmos.packageManager by Kosmos.Fixture { mock<PackageManager>() }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerInteractorKosmos.kt
index 06b6cda6..244ef8d 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerInteractorKosmos.kt
@@ -16,7 +16,40 @@
package com.android.systemui.bouncer.domain.interactor
+import android.content.applicationContext
+import com.android.keyguard.keyguardSecurityModel
+import com.android.keyguard.keyguardUpdateMonitor
+import com.android.systemui.bouncer.data.repository.keyguardBouncerRepository
+import com.android.systemui.bouncer.ui.BouncerView
+import com.android.systemui.classifier.falsingCollector
+import com.android.systemui.concurrency.fakeExecutor
+import com.android.systemui.deviceentry.domain.interactor.deviceEntryFaceAuthInteractor
+import com.android.systemui.keyguard.DismissCallbackRegistry
+import com.android.systemui.keyguard.data.repository.trustRepository
import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.statusbar.policy.KeyguardStateControllerImpl
+import com.android.systemui.user.domain.interactor.selectedUserInteractor
+import com.android.systemui.util.concurrency.mockExecutorHandler
import com.android.systemui.util.mockito.mock
-var Kosmos.primaryBouncerInteractor by Kosmos.Fixture { mock<PrimaryBouncerInteractor>() }
+var Kosmos.mockPrimaryBouncerInteractor by Kosmos.Fixture { mock<PrimaryBouncerInteractor>() }
+var Kosmos.primaryBouncerInteractor by
+ Kosmos.Fixture {
+ PrimaryBouncerInteractor(
+ repository = keyguardBouncerRepository,
+ primaryBouncerView = mock<BouncerView>(),
+ mainHandler = mockExecutorHandler(executor = fakeExecutor),
+ keyguardStateController = mock<KeyguardStateControllerImpl>(),
+ keyguardSecurityModel = keyguardSecurityModel,
+ primaryBouncerCallbackInteractor = mock<PrimaryBouncerCallbackInteractor>(),
+ falsingCollector = falsingCollector,
+ dismissCallbackRegistry = mock<DismissCallbackRegistry>(),
+ context = applicationContext,
+ keyguardUpdateMonitor = keyguardUpdateMonitor,
+ trustRepository = trustRepository,
+ applicationScope = applicationCoroutineScope,
+ selectedUserInteractor = selectedUserInteractor,
+ deviceEntryFaceAuthInteractor = deviceEntryFaceAuthInteractor,
+ )
+ }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelKosmos.kt
index d91c597..99dfe94 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelKosmos.kt
@@ -21,11 +21,13 @@
import com.android.systemui.bouncer.domain.interactor.bouncerActionButtonInteractor
import com.android.systemui.bouncer.domain.interactor.bouncerInteractor
import com.android.systemui.bouncer.domain.interactor.simBouncerInteractor
+import com.android.systemui.inputmethod.domain.interactor.inputMethodInteractor
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.Kosmos.Fixture
import com.android.systemui.kosmos.testDispatcher
import com.android.systemui.kosmos.testScope
import com.android.systemui.scene.shared.flag.sceneContainerFlags
+import com.android.systemui.user.domain.interactor.selectedUserInteractor
import com.android.systemui.user.ui.viewmodel.userSwitcherViewModel
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.time.systemClock
@@ -36,8 +38,10 @@
applicationScope = testScope.backgroundScope,
mainDispatcher = testDispatcher,
bouncerInteractor = bouncerInteractor,
+ inputMethodInteractor = inputMethodInteractor,
simBouncerInteractor = simBouncerInteractor,
authenticationInteractor = authenticationInteractor,
+ selectedUserInteractor = selectedUserInteractor,
flags = sceneContainerFlags,
selectedUser = userSwitcherViewModel.selectedUser,
users = userSwitcherViewModel.users,
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 bc7e7af..fab64e3 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalWidgetRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalWidgetRepository.kt
@@ -36,6 +36,14 @@
}
}
+ override fun deleteWidget(widgetId: Int) {
+ if (_communalWidgets.value.none { it.appWidgetId == widgetId }) {
+ return
+ }
+
+ _communalWidgets.value = _communalWidgets.value.filter { it.appWidgetId != widgetId }
+ }
+
private fun onConfigured(id: Int, providerInfo: AppWidgetProviderInfo, priority: Int) {
_communalWidgets.value += listOf(CommunalWidgetContentModel(id, providerInfo, priority))
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/panels/FakeSelectedComponentRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/controls/panels/FakeSelectedComponentRepository.kt
similarity index 93%
rename from packages/SystemUI/tests/src/com/android/systemui/controls/panels/FakeSelectedComponentRepository.kt
rename to packages/SystemUI/tests/utils/src/com/android/systemui/controls/panels/FakeSelectedComponentRepository.kt
index 002862e..a231212 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/controls/panels/FakeSelectedComponentRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/controls/panels/FakeSelectedComponentRepository.kt
@@ -17,6 +17,7 @@
package com.android.systemui.controls.panels
import android.os.UserHandle
+import com.android.systemui.kosmos.Kosmos
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
@@ -68,3 +69,6 @@
}
}
}
+
+val Kosmos.selectedComponentRepository by
+ Kosmos.Fixture<FakeSelectedComponentRepository> { FakeSelectedComponentRepository() }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/settings/FakeControlsSettingsRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/controls/settings/FakeControlsSettingsRepository.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/controls/settings/FakeControlsSettingsRepository.kt
rename to packages/SystemUI/tests/utils/src/com/android/systemui/controls/settings/FakeControlsSettingsRepository.kt
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractorKosmos.kt
index 0b1fb40..5575b05 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractorKosmos.kt
@@ -23,7 +23,7 @@
import com.android.keyguard.trustManager
import com.android.systemui.biometrics.data.repository.facePropertyRepository
import com.android.systemui.bouncer.domain.interactor.alternateBouncerInteractor
-import com.android.systemui.bouncer.domain.interactor.primaryBouncerInteractor
+import com.android.systemui.bouncer.domain.interactor.mockPrimaryBouncerInteractor
import com.android.systemui.deviceentry.data.repository.faceWakeUpTriggersConfig
import com.android.systemui.keyguard.data.repository.biometricSettingsRepository
import com.android.systemui.keyguard.data.repository.deviceEntryFaceAuthRepository
@@ -46,7 +46,7 @@
applicationScope = applicationCoroutineScope,
mainDispatcher = testDispatcher,
repository = deviceEntryFaceAuthRepository,
- primaryBouncerInteractor = { primaryBouncerInteractor },
+ primaryBouncerInteractor = { mockPrimaryBouncerInteractor },
alternateBouncerInteractor = alternateBouncerInteractor,
keyguardTransitionInteractor = keyguardTransitionInteractor,
faceAuthenticationLogger = faceAuthLogger,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/ui/binder/LiftToRunFaceAuthBinderKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/ui/binder/LiftToRunFaceAuthBinderKosmos.kt
new file mode 100644
index 0000000..2fead91
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/ui/binder/LiftToRunFaceAuthBinderKosmos.kt
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.deviceentry.ui.binder
+
+import android.content.packageManager
+import com.android.keyguard.keyguardUpdateMonitor
+import com.android.systemui.bouncer.domain.interactor.alternateBouncerInteractor
+import com.android.systemui.bouncer.domain.interactor.primaryBouncerInteractor
+import com.android.systemui.deviceentry.domain.interactor.deviceEntryFaceAuthInteractor
+import com.android.systemui.keyguard.domain.interactor.keyguardInteractor
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.power.domain.interactor.powerInteractor
+import com.android.systemui.util.sensors.asyncSensorManager
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+
+@ExperimentalCoroutinesApi
+val Kosmos.liftToRunFaceAuthBinder by
+ Kosmos.Fixture {
+ LiftToRunFaceAuthBinder(
+ scope = applicationCoroutineScope,
+ packageManager = packageManager,
+ asyncSensorManager = asyncSensorManager,
+ keyguardUpdateMonitor = keyguardUpdateMonitor,
+ keyguardInteractor = keyguardInteractor,
+ primaryBouncerInteractor = primaryBouncerInteractor,
+ alternateBouncerInteractor = alternateBouncerInteractor,
+ deviceEntryFaceAuthInteractor = deviceEntryFaceAuthInteractor,
+ powerInteractor = powerInteractor,
+ )
+ }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/DeviceStateRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/DeviceStateRepositoryKosmos.kt
new file mode 100644
index 0000000..5855060
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/DeviceStateRepositoryKosmos.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.display.data.repository
+
+import com.android.systemui.kosmos.Kosmos
+
+var Kosmos.deviceStateRepository: DeviceStateRepository by
+ Kosmos.Fixture { fakeDeviceStateRepository }
+val Kosmos.fakeDeviceStateRepository: FakeDeviceStateRepository by
+ Kosmos.Fixture { FakeDeviceStateRepository() }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/inputmethod/data/repository/FakeInputMethodRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/inputmethod/data/repository/FakeInputMethodRepository.kt
new file mode 100644
index 0000000..8e4461d
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/inputmethod/data/repository/FakeInputMethodRepository.kt
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS 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.inputmethod.data.repository
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.inputmethod.data.model.InputMethodModel
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.asFlow
+import kotlinx.coroutines.flow.flowOf
+
+@SysUISingleton
+class FakeInputMethodRepository : InputMethodRepository {
+
+ private var usersToEnabledInputMethods: MutableMap<Int, Flow<InputMethodModel>> = mutableMapOf()
+
+ var selectedInputMethodSubtypes = listOf<InputMethodModel.Subtype>()
+
+ /**
+ * The display ID on which the input method picker dialog was shown, or `null` if the dialog was
+ * not shown.
+ */
+ var inputMethodPickerShownDisplayId: Int? = null
+
+ fun setEnabledInputMethods(userId: Int, vararg enabledInputMethods: InputMethodModel) {
+ usersToEnabledInputMethods[userId] = enabledInputMethods.asFlow()
+ }
+
+ override suspend fun enabledInputMethods(
+ userId: Int,
+ fetchSubtypes: Boolean,
+ ): Flow<InputMethodModel> {
+ return usersToEnabledInputMethods[userId] ?: flowOf()
+ }
+
+ override suspend fun selectedInputMethodSubtypes(): List<InputMethodModel.Subtype> =
+ selectedInputMethodSubtypes
+
+ override suspend fun showInputMethodPicker(displayId: Int, showAuxiliarySubtypes: Boolean) {
+ inputMethodPickerShownDisplayId = displayId
+ }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/inputmethod/data/repository/InputMethodRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/inputmethod/data/repository/InputMethodRepositoryKosmos.kt
new file mode 100644
index 0000000..b71b9d8
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/inputmethod/data/repository/InputMethodRepositoryKosmos.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.inputmethod.data.repository
+
+import com.android.systemui.kosmos.Kosmos
+
+var Kosmos.inputMethodRepository: InputMethodRepository by
+ Kosmos.Fixture { fakeInputMethodRepository }
+val Kosmos.fakeInputMethodRepository by Kosmos.Fixture { FakeInputMethodRepository() }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/inputmethod/domain/interactor/InputMethodInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/inputmethod/domain/interactor/InputMethodInteractorKosmos.kt
new file mode 100644
index 0000000..da77575
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/inputmethod/domain/interactor/InputMethodInteractorKosmos.kt
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.inputmethod.domain.interactor
+
+import com.android.systemui.inputmethod.data.repository.inputMethodRepository
+import com.android.systemui.kosmos.Kosmos
+
+val Kosmos.inputMethodInteractor by
+ Kosmos.Fixture {
+ InputMethodInteractor(
+ repository = inputMethodRepository,
+ )
+ }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt
index 0c1dbfe..e20a0ab 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt
@@ -28,9 +28,12 @@
import java.util.UUID
import javax.inject.Inject
import junit.framework.Assert.fail
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Job
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow
+import kotlinx.coroutines.launch
import kotlinx.coroutines.test.TestCoroutineScheduler
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runCurrent
@@ -150,6 +153,15 @@
_transitions.emit(step)
}
+ /** Version of [sendTransitionStep] that's usable from Java tests. */
+ fun sendTransitionStepJava(
+ coroutineScope: CoroutineScope,
+ step: TransitionStep,
+ validateStep: Boolean = true
+ ): Job {
+ return coroutineScope.launch { sendTransitionStep(step, validateStep) }
+ }
+
suspend fun sendTransitionSteps(
steps: List<TransitionStep>,
testScope: TestScope,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/BouncerToGoneFlowsKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/BouncerToGoneFlowsKosmos.kt
index c71c1c3..ffa4133 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/BouncerToGoneFlowsKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/BouncerToGoneFlowsKosmos.kt
@@ -18,7 +18,7 @@
package com.android.systemui.keyguard.ui.viewmodel
-import com.android.systemui.bouncer.domain.interactor.primaryBouncerInteractor
+import com.android.systemui.bouncer.domain.interactor.mockPrimaryBouncerInteractor
import com.android.systemui.flags.featureFlagsClassic
import com.android.systemui.keyguard.ui.keyguardTransitionAnimationFlow
import com.android.systemui.kosmos.Kosmos
@@ -31,7 +31,7 @@
val Kosmos.bouncerToGoneFlows by Fixture {
BouncerToGoneFlows(
statusBarStateController = sysuiStatusBarStateController,
- primaryBouncerInteractor = primaryBouncerInteractor,
+ primaryBouncerInteractor = mockPrimaryBouncerInteractor,
keyguardDismissActionInteractor = mock(),
featureFlags = featureFlagsClassic,
shadeInteractor = shadeInteractor,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModelKosmos.kt
index ab28d0d6..4ecff73 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModelKosmos.kt
@@ -18,7 +18,7 @@
package com.android.systemui.keyguard.ui.viewmodel
-import com.android.systemui.bouncer.domain.interactor.primaryBouncerInteractor
+import com.android.systemui.bouncer.domain.interactor.mockPrimaryBouncerInteractor
import com.android.systemui.flags.featureFlagsClassic
import com.android.systemui.keyguard.ui.keyguardTransitionAnimationFlow
import com.android.systemui.kosmos.Kosmos
@@ -30,7 +30,7 @@
val Kosmos.primaryBouncerToGoneTransitionViewModel by Fixture {
PrimaryBouncerToGoneTransitionViewModel(
statusBarStateController = sysuiStatusBarStateController,
- primaryBouncerInteractor = primaryBouncerInteractor,
+ primaryBouncerInteractor = mockPrimaryBouncerInteractor,
keyguardDismissActionInteractor = mock(),
featureFlags = featureFlagsClassic,
bouncerToGoneFlows = bouncerToGoneFlows,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt
index 11f2938..083de10 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt
@@ -32,6 +32,8 @@
import com.android.systemui.flags.fakeFeatureFlagsClassic
import com.android.systemui.jank.interactionJankMonitor
import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
+import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
+import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
import com.android.systemui.model.sceneContainerPlugin
import com.android.systemui.plugins.statusbar.statusBarStateController
import com.android.systemui.power.data.repository.fakePowerRepository
@@ -61,6 +63,8 @@
val bouncerRepository by lazy { kosmos.bouncerRepository }
val communalRepository by lazy { kosmos.fakeCommunalRepository }
val keyguardRepository by lazy { kosmos.fakeKeyguardRepository }
+ val keyguardTransitionRepository by lazy { kosmos.fakeKeyguardTransitionRepository }
+ val keyguardTransitionInteractor by lazy { kosmos.keyguardTransitionInteractor }
val powerRepository by lazy { kosmos.fakePowerRepository }
val clock by lazy { kosmos.systemClock }
val mobileConnectionsRepository by lazy { kosmos.fakeMobileConnectionsRepository }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationStackAppearanceViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationStackAppearanceViewModelKosmos.kt
index f2f3a5a..d79633a 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationStackAppearanceViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationStackAppearanceViewModelKosmos.kt
@@ -18,6 +18,7 @@
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.Kosmos.Fixture
+import com.android.systemui.scene.domain.interactor.sceneInteractor
import com.android.systemui.shade.domain.interactor.shadeInteractor
import com.android.systemui.statusbar.notification.stack.domain.interactor.notificationStackAppearanceInteractor
@@ -25,5 +26,6 @@
NotificationStackAppearanceViewModel(
stackAppearanceInteractor = notificationStackAppearanceInteractor,
shadeInteractor = shadeInteractor,
+ sceneInteractor = sceneInteractor,
)
}
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 7c398cd..549a775 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
@@ -20,7 +20,9 @@
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.dreamingToLockscreenTransitionViewModel
import com.android.systemui.keyguard.ui.viewmodel.glanceableHubToLockscreenTransitionViewModel
+import com.android.systemui.keyguard.ui.viewmodel.lockscreenToDreamingTransitionViewModel
import com.android.systemui.keyguard.ui.viewmodel.lockscreenToGlanceableHubTransitionViewModel
import com.android.systemui.keyguard.ui.viewmodel.lockscreenToOccludedTransitionViewModel
import com.android.systemui.keyguard.ui.viewmodel.occludedToLockscreenTransitionViewModel
@@ -40,6 +42,8 @@
communalInteractor = communalInteractor,
occludedToLockscreenTransitionViewModel = occludedToLockscreenTransitionViewModel,
lockscreenToOccludedTransitionViewModel = lockscreenToOccludedTransitionViewModel,
+ dreamingToLockscreenTransitionViewModel = dreamingToLockscreenTransitionViewModel,
+ lockscreenToDreamingTransitionViewModel = lockscreenToDreamingTransitionViewModel,
glanceableHubToLockscreenTransitionViewModel = glanceableHubToLockscreenTransitionViewModel,
lockscreenToGlanceableHubTransitionViewModel = lockscreenToGlanceableHubTransitionViewModel,
aodBurnInViewModel = aodBurnInViewModel,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/util/sensors/AsyncSensorManagerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/util/sensors/AsyncSensorManagerKosmos.kt
new file mode 100644
index 0000000..117ae8c
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/util/sensors/AsyncSensorManagerKosmos.kt
@@ -0,0 +1,21 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.systemui.util.sensors
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.util.mockito.mock
+
+val Kosmos.asyncSensorManager by Kosmos.Fixture { mock<AsyncSensorManager>() }
diff --git a/packages/services/CameraExtensionsProxy/src/com/android/cameraextensions/CameraExtensionsProxyService.java b/packages/services/CameraExtensionsProxy/src/com/android/cameraextensions/CameraExtensionsProxyService.java
index 3aa9cc8..155c523 100644
--- a/packages/services/CameraExtensionsProxy/src/com/android/cameraextensions/CameraExtensionsProxyService.java
+++ b/packages/services/CameraExtensionsProxy/src/com/android/cameraextensions/CameraExtensionsProxyService.java
@@ -132,7 +132,7 @@
private static final String CAMERA_EXTENSION_VERSION_NAME =
"androidx.camera.extensions.impl.ExtensionVersionImpl";
- private static final String LATEST_VERSION = "1.4.0";
+ private static final String LATEST_VERSION = "1.5.0";
// No support for the init sequence
private static final String NON_INIT_VERSION_PREFIX = "1.0";
// Support advanced API and latency queries
@@ -1693,6 +1693,7 @@
private final Size mSize;
private final int mImageFormat;
private final int mDataspace;
+ private final long mUsage;
public OutputSurfaceImplStub(OutputSurface outputSurface) {
mSurface = outputSurface.surface;
@@ -1700,8 +1701,10 @@
mImageFormat = outputSurface.imageFormat;
if (mSurface != null) {
mDataspace = SurfaceUtils.getSurfaceDataspace(mSurface);
+ mUsage = SurfaceUtils.getSurfaceUsage(mSurface);
} else {
mDataspace = -1;
+ mUsage = 0;
}
}
@@ -1724,6 +1727,11 @@
public int getDataspace() {
return mDataspace;
}
+
+ @Override
+ public long getUsage() {
+ return mUsage;
+ }
}
private class PreviewExtenderImplStub extends IPreviewExtenderImpl.Stub implements
@@ -2471,6 +2479,11 @@
ret.size.height = imageReaderOutputConfig.getSize().getHeight();
ret.imageFormat = imageReaderOutputConfig.getImageFormat();
ret.capacity = imageReaderOutputConfig.getMaxImages();
+ if (EFV_SUPPORTED) {
+ ret.usage = imageReaderOutputConfig.getUsage();
+ } else {
+ ret.usage = 0;
+ }
} else if (output instanceof MultiResolutionImageReaderOutputConfigImpl) {
MultiResolutionImageReaderOutputConfigImpl multiResReaderConfig =
(MultiResolutionImageReaderOutputConfigImpl) output;
diff --git a/ravenwood/Android.bp b/ravenwood/Android.bp
index e013a3e..1ac69f6 100644
--- a/ravenwood/Android.bp
+++ b/ravenwood/Android.bp
@@ -30,6 +30,9 @@
"junit-src/**/*.java",
"junit-impl-src/**/*.java",
],
+ static_libs: [
+ "androidx.test.monitor-for-device",
+ ],
libs: [
"framework-minus-apex.ravenwood",
"junit",
@@ -61,3 +64,17 @@
"core-xml-for-host",
],
}
+
+java_host_for_device {
+ name: "androidx.test.monitor-for-device",
+ libs: [
+ "androidx.test.monitor-for-host",
+ ],
+}
+
+java_device_for_host {
+ name: "androidx.test.monitor-for-host",
+ libs: [
+ "androidx.test.monitor",
+ ],
+}
diff --git a/ravenwood/bulk_enable.py b/ravenwood/bulk_enable.py
new file mode 100644
index 0000000..36d398c
--- /dev/null
+++ b/ravenwood/bulk_enable.py
@@ -0,0 +1,75 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2024 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""
+Tool to bulk-enable tests that are now passing on Ravenwood.
+
+Currently only offers to include classes which are fully passing; ignores
+classes that have partial success.
+
+Typical usage:
+$ ENABLE_PROBE_IGNORED=1 atest MyTestsRavenwood
+$ cd /path/to/tests/root
+$ python bulk_enable.py /path/to/atest/output/host_log.txt
+"""
+
+import collections
+import os
+import re
+import subprocess
+import sys
+
+re_result = re.compile("I/ModuleListener.+?null-device-0 (.+?)#(.+?) ([A-Z_]+)(.*)$")
+
+ANNOTATION = "@android.platform.test.annotations.EnabledOnRavenwood"
+SED_ARG = "s/^((public )?class )/%s\\n\\1/g" % (ANNOTATION)
+
+STATE_PASSED = "PASSED"
+STATE_FAILURE = "FAILURE"
+STATE_ASSUMPTION_FAILURE = "ASSUMPTION_FAILURE"
+STATE_CANDIDATE = "CANDIDATE"
+
+stats_total = collections.defaultdict(int)
+stats_class = collections.defaultdict(lambda: collections.defaultdict(int))
+stats_method = collections.defaultdict()
+
+with open(sys.argv[1]) as f:
+ for line in f.readlines():
+ result = re_result.search(line)
+ if result:
+ clazz, method, state, msg = result.groups()
+ if state == STATE_FAILURE and "actually passed under Ravenwood" in msg:
+ state = STATE_CANDIDATE
+ stats_total[state] += 1
+ stats_class[clazz][state] += 1
+ stats_method[(clazz, method)] = state
+
+# Find classes who are fully "candidates" (would be entirely green if enabled)
+num_enabled = 0
+for clazz in stats_class.keys():
+ stats = stats_class[clazz]
+ if STATE_CANDIDATE in stats and len(stats) == 1:
+ num_enabled += stats[STATE_CANDIDATE]
+ print("Enabling fully-passing class", clazz)
+ clazz_match = re.compile("%s\.(kt|java)" % (clazz.split(".")[-1]))
+ for root, dirs, files in os.walk("."):
+ for f in files:
+ if clazz_match.match(f):
+ path = os.path.join(root, f)
+ subprocess.run(["sed", "-i", "-E", SED_ARG, path])
+
+print("Overall stats", stats_total)
+print("Candidates actually enabled", num_enabled)
diff --git a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java
index b3dbcde..3670459 100644
--- a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java
+++ b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java
@@ -16,17 +16,47 @@
package android.platform.test.ravenwood;
+import android.app.Instrumentation;
+import android.os.Bundle;
import android.os.HandlerThread;
import android.os.Looper;
+import android.util.Log;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.internal.os.RuntimeInit;
+
+import org.junit.runner.Description;
+
+import java.io.PrintStream;
+import java.util.Map;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
public class RavenwoodRuleImpl {
private static final String MAIN_THREAD_NAME = "RavenwoodMain";
+ /**
+ * When enabled, attempt to dump all thread stacks just before we hit the
+ * overall Tradefed timeout, to aid in debugging deadlocks.
+ */
+ private static final boolean ENABLE_TIMEOUT_STACKS = false;
+ private static final int TIMEOUT_MILLIS = 9_000;
+
+ private static final ScheduledExecutorService sTimeoutExecutor =
+ Executors.newScheduledThreadPool(1);
+
+ private static ScheduledFuture<?> sPendingTimeout;
+
public static boolean isOnRavenwood() {
return true;
}
public static void init(RavenwoodRule rule) {
+ RuntimeInit.redirectLogStreams();
+
android.os.Process.init$ravenwood(rule.mUid, rule.mPid);
android.os.Binder.init$ravenwood();
android.os.SystemProperties.init$ravenwood(
@@ -41,9 +71,22 @@
main.start();
Looper.setMainLooperForTest(main.getLooper());
}
+
+ InstrumentationRegistry.registerInstance(new Instrumentation(), Bundle.EMPTY);
+
+ if (ENABLE_TIMEOUT_STACKS) {
+ sPendingTimeout = sTimeoutExecutor.schedule(RavenwoodRuleImpl::dumpStacks,
+ TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
+ }
}
public static void reset(RavenwoodRule rule) {
+ if (ENABLE_TIMEOUT_STACKS) {
+ sPendingTimeout.cancel(false);
+ }
+
+ InstrumentationRegistry.registerInstance(null, Bundle.EMPTY);
+
if (rule.mProvideMainThread) {
Looper.getMainLooper().quit();
Looper.clearMainLooperForTest();
@@ -55,4 +98,26 @@
android.os.Binder.reset$ravenwood();
android.os.Process.reset$ravenwood();
}
+
+ public static void logTestRunner(String label, Description description) {
+ // This message string carefully matches the exact format emitted by on-device tests, to
+ // aid developers in debugging raw text logs
+ Log.e("TestRunner", label + ": " + description.getMethodName()
+ + "(" + description.getTestClass().getName() + ")");
+ }
+
+ private static void dumpStacks() {
+ final PrintStream out = System.err;
+ out.println("-----BEGIN ALL THREAD STACKS-----");
+ final Map<Thread, StackTraceElement[]> stacks = Thread.getAllStackTraces();
+ for (Map.Entry<Thread, StackTraceElement[]> stack : stacks.entrySet()) {
+ out.println();
+ Thread t = stack.getKey();
+ out.println(t.toString() + " ID=" + t.getId());
+ for (StackTraceElement e : stack.getValue()) {
+ out.println("\tat " + e);
+ }
+ }
+ out.println("-----END ALL THREAD STACKS-----");
+ }
}
diff --git a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodClassRule.java b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodClassRule.java
index 68b163e..8d76970 100644
--- a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodClassRule.java
+++ b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodClassRule.java
@@ -19,6 +19,7 @@
import static android.platform.test.ravenwood.RavenwoodRule.ENABLE_PROBE_IGNORED;
import static android.platform.test.ravenwood.RavenwoodRule.IS_ON_RAVENWOOD;
import static android.platform.test.ravenwood.RavenwoodRule.shouldEnableOnRavenwood;
+import static android.platform.test.ravenwood.RavenwoodRule.shouldStillIgnoreInProbeIgnoreMode;
import android.platform.test.annotations.DisabledOnRavenwood;
import android.platform.test.annotations.EnabledOnRavenwood;
@@ -45,6 +46,7 @@
}
if (ENABLE_PROBE_IGNORED) {
+ Assume.assumeFalse(shouldStillIgnoreInProbeIgnoreMode(description));
// Pass through to possible underlying RavenwoodRule for both environment
// configuration and handling method-level annotations
return base;
diff --git a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java
index 952ee0e..0285b38 100644
--- a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java
+++ b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java
@@ -27,7 +27,9 @@
import org.junit.runner.Description;
import org.junit.runners.model.Statement;
+import java.util.Objects;
import java.util.concurrent.atomic.AtomicInteger;
+import java.util.regex.Pattern;
/**
* {@code @Rule} that configures the Ravenwood test environment. This rule has no effect when
@@ -55,6 +57,43 @@
static final boolean ENABLE_PROBE_IGNORED = "1".equals(
System.getenv("RAVENWOOD_RUN_DISABLED_TESTS"));
+ /**
+ * When using ENABLE_PROBE_IGNORED, you may still want to skip certain tests,
+ * for example because the test would crash the JVM.
+ *
+ * This regex defines the tests that should still be disabled even if ENABLE_PROBE_IGNORED
+ * is set.
+ *
+ * Before running each test class and method, we check if this pattern can be found in
+ * the full test name (either [class full name], or [class full name] + "#" + [method name]),
+ * and if so, we skip it.
+ *
+ * For example, if you want to skip an entire test class, use:
+ * RAVENWOOD_REALLY_DISABLE='\.CustomTileDefaultsRepositoryTest$'
+ *
+ * For example, if you want to skip an entire test class, use:
+ * RAVENWOOD_REALLY_DISABLE='\.CustomTileDefaultsRepositoryTest#testSimple$'
+ *
+ * To ignore multiple classes, use (...|...), for example:
+ * RAVENWOOD_REALLY_DISABLE='\.(ClassA|ClassB)$'
+ *
+ * Because we use a regex-find, setting "." would disable all tests.
+ */
+ private static final Pattern REALLY_DISABLE_PATTERN = Pattern.compile(
+ Objects.requireNonNullElse(System.getenv("RAVENWOOD_REALLY_DISABLE"), ""));
+
+ private static final boolean ENABLE_REALLY_DISABLE_PATTERN =
+ !REALLY_DISABLE_PATTERN.pattern().isEmpty();
+
+ static {
+ if (ENABLE_PROBE_IGNORED) {
+ System.out.println("$RAVENWOOD_RUN_DISABLED_TESTS enabled: force running all tests");
+ if (ENABLE_REALLY_DISABLE_PATTERN) {
+ System.out.println("$RAVENWOOD_REALLY_DISABLE=" + REALLY_DISABLE_PATTERN.pattern());
+ }
+ }
+ }
+
private static final int SYSTEM_UID = 1000;
private static final int NOBODY_UID = 9999;
private static final int FIRST_APPLICATION_UID = 10000;
@@ -203,6 +242,21 @@
return true;
}
+ static boolean shouldStillIgnoreInProbeIgnoreMode(Description description) {
+ if (!ENABLE_REALLY_DISABLE_PATTERN) {
+ return false;
+ }
+
+ final var fullname = description.getTestClass().getName()
+ + (description.isTest() ? "#" + description.getMethodName() : "");
+
+ if (REALLY_DISABLE_PATTERN.matcher(fullname).find()) {
+ System.out.println("Still ignoring " + fullname);
+ return true;
+ }
+ return false;
+ }
+
@Override
public Statement apply(Statement base, Description description) {
// No special treatment when running outside Ravenwood; run tests as-is
@@ -226,9 +280,14 @@
public void evaluate() throws Throwable {
Assume.assumeTrue(shouldEnableOnRavenwood(description));
+ RavenwoodRuleImpl.logTestRunner("started", description);
RavenwoodRuleImpl.init(RavenwoodRule.this);
try {
base.evaluate();
+ RavenwoodRuleImpl.logTestRunner("finished", description);
+ } catch (Throwable t) {
+ RavenwoodRuleImpl.logTestRunner("failed", description);
+ throw t;
} finally {
RavenwoodRuleImpl.reset(RavenwoodRule.this);
}
@@ -245,6 +304,9 @@
return new Statement() {
@Override
public void evaluate() throws Throwable {
+ Assume.assumeFalse(shouldStillIgnoreInProbeIgnoreMode(description));
+
+ RavenwoodRuleImpl.logTestRunner("started", description);
RavenwoodRuleImpl.init(RavenwoodRule.this);
try {
base.evaluate();
@@ -254,6 +316,7 @@
Assume.assumeTrue(shouldEnableOnRavenwood(description));
throw t;
} finally {
+ RavenwoodRuleImpl.logTestRunner("finished", description);
RavenwoodRuleImpl.reset(RavenwoodRule.this);
}
diff --git a/ravenwood/junit-stub-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java b/ravenwood/junit-stub-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java
index d0c2e18..7d172f2 100644
--- a/ravenwood/junit-stub-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java
+++ b/ravenwood/junit-stub-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java
@@ -16,6 +16,8 @@
package android.platform.test.ravenwood;
+import org.junit.runner.Description;
+
public class RavenwoodRuleImpl {
public static boolean isOnRavenwood() {
return false;
@@ -28,4 +30,8 @@
public static void reset(RavenwoodRule rule) {
// No-op when running on a real device
}
+
+ public static void logTestRunner(String label, Description description) {
+ // No-op when running on a real device
+ }
}
diff --git a/ravenwood/ravenwood-annotation-allowed-classes.txt b/ravenwood/ravenwood-annotation-allowed-classes.txt
index eaf01a3..e49b64e 100644
--- a/ravenwood/ravenwood-annotation-allowed-classes.txt
+++ b/ravenwood/ravenwood-annotation-allowed-classes.txt
@@ -5,6 +5,7 @@
com.android.internal.logging.MetricsLogger
com.android.internal.logging.testing.FakeMetricsLogger
com.android.internal.logging.testing.UiEventLoggerFake
+com.android.internal.os.AndroidPrintStream
com.android.internal.os.BatteryStatsHistory
com.android.internal.os.BatteryStatsHistory$TraceDelegate
com.android.internal.os.BatteryStatsHistory$VarintParceler
@@ -16,6 +17,7 @@
com.android.internal.os.PowerProfile
com.android.internal.os.PowerStats
com.android.internal.os.PowerStats$Descriptor
+com.android.internal.os.RuntimeInit
com.android.internal.power.ModemPowerProfile
android.util.AtomicFile
@@ -72,6 +74,7 @@
android.os.ServiceSpecificException
android.os.SystemClock
android.os.SystemProperties
+android.os.TestLooperManager
android.os.ThreadLocalWorkSource
android.os.TimestampedValue
android.os.Trace
@@ -141,6 +144,8 @@
android.content.ContentProvider
+android.app.Instrumentation
+
android.metrics.LogMaker
android.view.Display$HdrCapabilities
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
index 57c0539..63784ba 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
@@ -16,6 +16,7 @@
package com.android.server.accessibility;
+import static android.Manifest.permission.INTERNAL_SYSTEM_WINDOW;
import static android.accessibilityservice.AccessibilityServiceInfo.FLAG_REQUEST_ACCESSIBILITY_BUTTON;
import static android.accessibilityservice.AccessibilityTrace.FLAGS_ACCESSIBILITY_MANAGER;
import static android.accessibilityservice.AccessibilityTrace.FLAGS_ACCESSIBILITY_MANAGER_CLIENT;
@@ -5724,6 +5725,21 @@
}
@Override
+ public void attachAccessibilityOverlayToDisplay_enforcePermission(
+ int displayId, SurfaceControl sc) {
+ mContext.enforceCallingPermission(
+ INTERNAL_SYSTEM_WINDOW, "attachAccessibilityOverlayToDisplay_enforcePermission");
+ mMainHandler.sendMessage(
+ obtainMessage(
+ AccessibilityManagerService::attachAccessibilityOverlayToDisplayInternal,
+ this,
+ -1,
+ displayId,
+ sc,
+ null));
+ }
+
+ @Override
public void attachAccessibilityOverlayToDisplay(
int interactionId,
int displayId,
@@ -5759,12 +5775,15 @@
t.close();
result = AccessibilityService.OVERLAY_RESULT_SUCCESS;
}
- // Send the result back to the service.
- try {
- callback.sendAttachOverlayResult(result, interactionId);
- } catch (RemoteException re) {
- Slog.e(LOG_TAG, "Exception while attaching overlay.", re);
- // the other side will time out
+
+ if (callback != null) {
+ // Send the result back to the service.
+ try {
+ callback.sendAttachOverlayResult(result, interactionId);
+ } catch (RemoteException re) {
+ Slog.e(LOG_TAG, "Exception while attaching overlay.", re);
+ // the other side will time out
+ }
}
}
}
diff --git a/services/backup/Android.bp b/services/backup/Android.bp
index d08a97e..2a85eb6 100644
--- a/services/backup/Android.bp
+++ b/services/backup/Android.bp
@@ -21,7 +21,6 @@
libs: ["services.core"],
static_libs: [
"app-compat-annotations",
- "backup_flags_lib",
],
lint: {
baseline_filename: "lint-baseline.xml",
@@ -33,8 +32,3 @@
package: "com.android.server.backup",
srcs: ["flags.aconfig"],
}
-
-java_aconfig_library {
- name: "backup_flags_lib",
- aconfig_declarations: "backup_flags",
-}
diff --git a/services/backup/flags.aconfig b/services/backup/flags.aconfig
index 1416c88..6a63b3a 100644
--- a/services/backup/flags.aconfig
+++ b/services/backup/flags.aconfig
@@ -24,4 +24,12 @@
description: "Enables the write buffer to pipes to be of maximum size."
bug: "265976737"
is_fixed_read_only: true
+}
+
+flag {
+ name: "enable_clear_pipe_after_restore_file"
+ namespace: "onboarding"
+ description: "Enables clearing the pipe buffer after restoring a single file to a BackupAgent."
+ bug: "320633449"
+ is_fixed_read_only: true
}
\ No newline at end of file
diff --git a/services/backup/java/com/android/server/backup/restore/PerformUnifiedRestoreTask.java b/services/backup/java/com/android/server/backup/restore/PerformUnifiedRestoreTask.java
index 0559708..d85dd87 100644
--- a/services/backup/java/com/android/server/backup/restore/PerformUnifiedRestoreTask.java
+++ b/services/backup/java/com/android/server/backup/restore/PerformUnifiedRestoreTask.java
@@ -732,6 +732,7 @@
BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY,
monitoringExtras);
EventLog.writeEvent(EventLogTags.RESTORE_TRANSPORT_FAILURE);
+ mStatus = BackupTransport.TRANSPORT_ERROR;
nextState = UnifiedRestoreState.FINAL;
} finally {
executeNextState(nextState);
diff --git a/services/core/Android.bp b/services/core/Android.bp
index 8e35b74..89896c3 100644
--- a/services/core/Android.bp
+++ b/services/core/Android.bp
@@ -211,7 +211,6 @@
"com_android_wm_shell_flags_lib",
"com.android.server.utils_aconfig-java",
"service-jobscheduler-deviceidle.flags-aconfig-java",
- "backup_flags_lib",
"policy_flags_lib",
"net_flags_lib",
"stats_flags_lib",
diff --git a/services/core/java/com/android/server/BinderCallsStatsService.java b/services/core/java/com/android/server/BinderCallsStatsService.java
index b87184a..416b36f 100644
--- a/services/core/java/com/android/server/BinderCallsStatsService.java
+++ b/services/core/java/com/android/server/BinderCallsStatsService.java
@@ -288,22 +288,23 @@
CachedDeviceState.Readonly.class);
mBinderCallsStats.setDeviceState(deviceState);
- BatteryStatsInternal batteryStatsInternal = getLocalService(
- BatteryStatsInternal.class);
- mBinderCallsStats.setCallStatsObserver(new BinderInternal.CallStatsObserver() {
- @Override
- public void noteCallStats(int workSourceUid, long incrementalCallCount,
- Collection<BinderCallsStats.CallStat> callStats) {
- batteryStatsInternal.noteBinderCallStats(workSourceUid,
- incrementalCallCount, callStats);
- }
+ if (!com.android.server.power.optimization.Flags.disableSystemServicePowerAttr()) {
+ BatteryStatsInternal batteryStatsInternal = getLocalService(
+ BatteryStatsInternal.class);
+ mBinderCallsStats.setCallStatsObserver(new BinderInternal.CallStatsObserver() {
+ @Override
+ public void noteCallStats(int workSourceUid, long incrementalCallCount,
+ Collection<BinderCallsStats.CallStat> callStats) {
+ batteryStatsInternal.noteBinderCallStats(workSourceUid,
+ incrementalCallCount, callStats);
+ }
- @Override
- public void noteBinderThreadNativeIds(int[] binderThreadNativeTids) {
- batteryStatsInternal.noteBinderThreadNativeIds(binderThreadNativeTids);
- }
- });
-
+ @Override
+ public void noteBinderThreadNativeIds(int[] binderThreadNativeTids) {
+ batteryStatsInternal.noteBinderThreadNativeIds(binderThreadNativeTids);
+ }
+ });
+ }
// It needs to be called before mService.systemReady to make sure the observer is
// initialized before installing it.
mWorkSourceProvider.systemReady(getContext());
diff --git a/services/core/java/com/android/server/IntentResolver.java b/services/core/java/com/android/server/IntentResolver.java
index a1e6b58f..81c9ee7 100644
--- a/services/core/java/com/android/server/IntentResolver.java
+++ b/services/core/java/com/android/server/IntentResolver.java
@@ -81,7 +81,7 @@
* Returns whether an intent matches the IntentFilter with a pre-resolved type.
*/
public static boolean intentMatchesFilter(
- IntentFilter filter, Intent intent, String resolvedType, boolean defaultOnly) {
+ IntentFilter filter, Intent intent, String resolvedType) {
final boolean debug = localLOGV
|| ((intent.getFlags() & Intent.FLAG_DEBUG_LOG_RESOLUTION) != 0);
@@ -97,10 +97,6 @@
int match = filter.match(intent.getAction(), resolvedType, intent.getScheme(),
intent.getData(), intent.getCategories(), TAG);
- if (match >= 0 && defaultOnly && !filter.hasCategory(Intent.CATEGORY_DEFAULT)) {
- match = IntentFilter.NO_MATCH_CATEGORY;
- }
-
if (match >= 0) {
if (debug) {
Slog.v(TAG, "Filter matched! match=0x" + Integer.toHexString(match));
diff --git a/services/core/java/com/android/server/SensitiveContentProtectionManagerService.java b/services/core/java/com/android/server/SensitiveContentProtectionManagerService.java
index c391642..f619ca3 100644
--- a/services/core/java/com/android/server/SensitiveContentProtectionManagerService.java
+++ b/services/core/java/com/android/server/SensitiveContentProtectionManagerService.java
@@ -27,6 +27,7 @@
import android.os.Handler;
import android.os.Looper;
import android.os.RemoteException;
+import android.os.Trace;
import android.os.UserHandle;
import android.service.notification.NotificationListenerService;
import android.service.notification.NotificationListenerService.RankingMap;
@@ -49,12 +50,12 @@
private static final String TAG = "SensitiveContentProtect";
private static final boolean DEBUG = false;
- @VisibleForTesting
- NotificationListener mNotificationListener;
+ @VisibleForTesting NotificationListener mNotificationListener;
private @Nullable MediaProjectionManager mProjectionManager;
private @Nullable WindowManagerInternal mWindowManager;
final Object mSensitiveContentProtectionLock = new Object();
+
@GuardedBy("mSensitiveContentProtectionLock")
private boolean mProjectionActive = false;
@@ -63,13 +64,24 @@
@Override
public void onStart(MediaProjectionInfo info) {
if (DEBUG) Log.d(TAG, "onStart projection: " + info);
- onProjectionStart();
+ Trace.beginSection(
+ "SensitiveContentProtectionManagerService.onProjectionStart");
+ try {
+ onProjectionStart();
+ } finally {
+ Trace.endSection();
+ }
}
@Override
public void onStop(MediaProjectionInfo info) {
if (DEBUG) Log.d(TAG, "onStop projection: " + info);
- onProjectionEnd();
+ Trace.beginSection("SensitiveContentProtectionManagerService.onProjectionStop");
+ try {
+ onProjectionEnd();
+ } finally {
+ Trace.endSection();
+ }
}
};
@@ -94,8 +106,7 @@
}
@VisibleForTesting
- void init(MediaProjectionManager projectionManager,
- WindowManagerInternal windowManager) {
+ void init(MediaProjectionManager projectionManager, WindowManagerInternal windowManager) {
if (DEBUG) Log.d(TAG, "init");
checkNotNull(projectionManager, "Failed to get valid MediaProjectionManager");
@@ -109,7 +120,8 @@
mProjectionManager.addCallback(mProjectionCallback, new Handler(Looper.getMainLooper()));
try {
- mNotificationListener.registerAsSystemService(getContext(),
+ mNotificationListener.registerAsSystemService(
+ getContext(),
new ComponentName(getContext(), NotificationListener.class),
UserHandle.USER_ALL);
} catch (RemoteException e) {
@@ -174,8 +186,8 @@
}
// notify windowmanager of any currently posted sensitive content notifications
- ArraySet<PackageInfo> packageInfos = getSensitivePackagesFromNotifications(
- notifications, rankingMap);
+ ArraySet<PackageInfo> packageInfos =
+ getSensitivePackagesFromNotifications(notifications, rankingMap);
mWindowManager.addBlockScreenCaptureForApps(packageInfos);
}
@@ -197,8 +209,8 @@
return sensitivePackages;
}
- private PackageInfo getSensitivePackageFromNotification(StatusBarNotification sbn,
- RankingMap rankingMap) {
+ private PackageInfo getSensitivePackageFromNotification(
+ StatusBarNotification sbn, RankingMap rankingMap) {
if (sbn == null) {
Log.w(TAG, "Unable to protect null notification");
return null;
@@ -220,38 +232,55 @@
@Override
public void onListenerConnected() {
super.onListenerConnected();
- // Projection started before notification listener was connected
- synchronized (mSensitiveContentProtectionLock) {
- if (mProjectionActive) {
- updateAppsThatShouldBlockScreenCapture();
+ Trace.beginSection("SensitiveContentProtectionManagerService.onListenerConnected");
+ try {
+ // Projection started before notification listener was connected
+ synchronized (mSensitiveContentProtectionLock) {
+ if (mProjectionActive) {
+ updateAppsThatShouldBlockScreenCapture();
+ }
}
+ } finally {
+ Trace.endSection();
}
}
@Override
public void onNotificationPosted(StatusBarNotification sbn, RankingMap rankingMap) {
super.onNotificationPosted(sbn, rankingMap);
- synchronized (mSensitiveContentProtectionLock) {
- if (!mProjectionActive) {
- return;
- }
+ Trace.beginSection("SensitiveContentProtectionManagerService.onNotificationPosted");
+ try {
+ synchronized (mSensitiveContentProtectionLock) {
+ if (!mProjectionActive) {
+ return;
+ }
- // notify windowmanager of any currently posted sensitive content notifications
- PackageInfo packageInfo = getSensitivePackageFromNotification(sbn, rankingMap);
+ // notify windowmanager of any currently posted sensitive content notifications
+ PackageInfo packageInfo = getSensitivePackageFromNotification(sbn, rankingMap);
- if (packageInfo != null) {
- mWindowManager.addBlockScreenCaptureForApps(new ArraySet(Set.of(packageInfo)));
+ if (packageInfo != null) {
+ mWindowManager.addBlockScreenCaptureForApps(
+ new ArraySet(Set.of(packageInfo)));
+ }
}
+ } finally {
+ Trace.endSection();
}
}
@Override
public void onNotificationRankingUpdate(RankingMap rankingMap) {
super.onNotificationRankingUpdate(rankingMap);
- synchronized (mSensitiveContentProtectionLock) {
- if (mProjectionActive) {
- updateAppsThatShouldBlockScreenCapture(rankingMap);
+ Trace.beginSection(
+ "SensitiveContentProtectionManagerService.onNotificationRankingUpdate");
+ try {
+ synchronized (mSensitiveContentProtectionLock) {
+ if (mProjectionActive) {
+ updateAppsThatShouldBlockScreenCapture(rankingMap);
+ }
}
+ } finally {
+ Trace.endSection();
}
}
}
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index 74902f7..a5531ae 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -135,6 +135,7 @@
import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_CONFIGURATION;
import static com.android.internal.util.FrameworkStatsLog.UNSAFE_INTENT_EVENT_REPORTED__EVENT_TYPE__INTERNAL_NON_EXPORTED_COMPONENT_MATCH;
import static com.android.internal.util.FrameworkStatsLog.UNSAFE_INTENT_EVENT_REPORTED__EVENT_TYPE__NEW_MUTABLE_IMPLICIT_PENDING_INTENT_RETRIEVED;
+import static com.android.sdksandbox.flags.Flags.sdkSandboxInstrumentationInfo;
import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_ALL;
import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_ALLOWLISTS;
import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_BACKGROUND_CHECK;
@@ -2100,7 +2101,8 @@
// Start watching app ops after we and the package manager are up and running.
mAppOpsService.startWatchingMode(AppOpsManager.OP_RUN_IN_BACKGROUND, null,
new IAppOpsCallback.Stub() {
- @Override public void opChanged(int op, int uid, String packageName) {
+ @Override public void opChanged(int op, int uid, String packageName,
+ String persistentDeviceId) {
if (op == AppOpsManager.OP_RUN_IN_BACKGROUND && packageName != null) {
if (getAppOpsManager().checkOpNoThrow(op, uid, packageName)
!= AppOpsManager.MODE_ALLOWED) {
@@ -2114,7 +2116,7 @@
mAppOpsService.startWatchingActive(cameraOp, new IAppOpsActiveCallback.Stub() {
@Override
public void opActiveChanged(int op, int uid, String packageName, String attributionTag,
- boolean active, @AttributionFlags int attributionFlags,
+ int virtualDeviceId, boolean active, @AttributionFlags int attributionFlags,
int attributionChainId) {
cameraActiveChanged(uid, active);
}
@@ -16141,10 +16143,22 @@
}
final ApplicationInfo sdkSandboxInfo;
+ final String processName;
try {
- sdkSandboxInfo =
- sandboxManagerLocal.getSdkSandboxApplicationInfoForInstrumentation(
- sdkSandboxClientAppInfo, isSdkInSandbox);
+ if (sdkSandboxInstrumentationInfo()) {
+ sdkSandboxInfo =
+ sandboxManagerLocal.getSdkSandboxApplicationInfoForInstrumentation(
+ sdkSandboxClientAppInfo, isSdkInSandbox);
+ processName = sdkSandboxInfo.processName;
+ } else {
+ final PackageManager pm = mContext.getPackageManager();
+ sdkSandboxInfo =
+ pm.getApplicationInfoAsUser(pm.getSdkSandboxPackageName(), 0, userId);
+ processName =
+ sandboxManagerLocal.getSdkSandboxProcessNameForInstrumentation(
+ sdkSandboxClientAppInfo);
+ sdkSandboxInfo.uid = Process.toSdkSandboxUid(sdkSandboxClientAppInfo.uid);
+ }
} catch (NameNotFoundException e) {
reportStartInstrumentationFailureLocked(
watcher, className, "Can't find SdkSandbox package");
@@ -16153,7 +16167,7 @@
ActiveInstrumentation activeInstr = new ActiveInstrumentation(this);
activeInstr.mClass = className;
- activeInstr.mTargetProcesses = new String[]{sdkSandboxInfo.processName};
+ activeInstr.mTargetProcesses = new String[]{processName};
activeInstr.mTargetInfo = sdkSandboxInfo;
activeInstr.mIsSdkInSandbox = isSdkInSandbox;
activeInstr.mProfileFile = profileFile;
@@ -16196,7 +16210,7 @@
ProcessRecord app = addAppLocked(
sdkSandboxInfo,
- sdkSandboxInfo.processName,
+ processName,
/* isolated= */ false,
/* isSdkSandbox= */ true,
sdkSandboxInfo.uid,
diff --git a/services/core/java/com/android/server/am/AppPermissionTracker.java b/services/core/java/com/android/server/am/AppPermissionTracker.java
index 18a9153..c641b35 100644
--- a/services/core/java/com/android/server/am/AppPermissionTracker.java
+++ b/services/core/java/com/android/server/am/AppPermissionTracker.java
@@ -393,7 +393,7 @@
private class MyAppOpsCallback extends IAppOpsCallback.Stub {
@Override
- public void opChanged(int op, int uid, String packageName) {
+ public void opChanged(int op, int uid, String packageName, String persistentDeviceId) {
mHandler.obtainMessage(MyHandler.MSG_APPOPS_CHANGED, op, uid, packageName)
.sendToTarget();
}
diff --git a/services/core/java/com/android/server/am/BatteryStatsService.java b/services/core/java/com/android/server/am/BatteryStatsService.java
index 3487ae3..4f46ecd 100644
--- a/services/core/java/com/android/server/am/BatteryStatsService.java
+++ b/services/core/java/com/android/server/am/BatteryStatsService.java
@@ -422,7 +422,9 @@
mStats.setExternalStatsSyncLocked(mWorker);
mStats.setRadioScanningTimeoutLocked(mContext.getResources().getInteger(
com.android.internal.R.integer.config_radioScanningTimeout) * 1000L);
- mStats.startTrackingSystemServerCpuTime();
+ if (!Flags.disableSystemServicePowerAttr()) {
+ mStats.startTrackingSystemServerCpuTime();
+ }
mAggregatedPowerStatsConfig = createAggregatedPowerStatsConfig();
mPowerStatsStore = new PowerStatsStore(systemDir, mHandler, mAggregatedPowerStatsConfig);
diff --git a/services/core/java/com/android/server/am/PendingIntentController.java b/services/core/java/com/android/server/am/PendingIntentController.java
index a20623c..5df9107 100644
--- a/services/core/java/com/android/server/am/PendingIntentController.java
+++ b/services/core/java/com/android/server/am/PendingIntentController.java
@@ -30,6 +30,7 @@
import android.app.AppGlobals;
import android.app.PendingIntent;
import android.app.PendingIntentStats;
+import android.app.compat.CompatChanges;
import android.content.IIntentSender;
import android.content.Intent;
import android.os.Binder;
@@ -136,6 +137,11 @@
+ "intent creator ("
+ packageName
+ ") because this option is meant for the pending intent sender");
+ if (CompatChanges.isChangeEnabled(PendingIntent.PENDING_INTENT_OPTIONS_CHECK,
+ callingUid)) {
+ throw new IllegalArgumentException("pendingIntentBackgroundActivityStartMode "
+ + "must not be set when creating a PendingIntent");
+ }
opts.setPendingIntentBackgroundActivityStartMode(
ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED);
}
diff --git a/services/core/java/com/android/server/am/PendingIntentRecord.java b/services/core/java/com/android/server/am/PendingIntentRecord.java
index 10d5fd3..95e130e 100644
--- a/services/core/java/com/android/server/am/PendingIntentRecord.java
+++ b/services/core/java/com/android/server/am/PendingIntentRecord.java
@@ -406,6 +406,9 @@
String resolvedType, IBinder allowlistToken, IIntentReceiver finishedReceiver,
String requiredPermission, IBinder resultTo, String resultWho, int requestCode,
int flagsMask, int flagsValues, Bundle options) {
+ final int callingUid = Binder.getCallingUid();
+ final int callingPid = Binder.getCallingPid();
+
if (intent != null) intent.setDefusable(true);
if (options != null) options.setDefusable(true);
@@ -458,6 +461,12 @@
+ key.packageName
+ ") because this option is meant for the pending intent "
+ "creator");
+ if (CompatChanges.isChangeEnabled(PendingIntent.PENDING_INTENT_OPTIONS_CHECK,
+ callingUid)) {
+ throw new IllegalArgumentException(
+ "pendingIntentCreatorBackgroundActivityStartMode "
+ + "must not be set when sending a PendingIntent");
+ }
opts.setPendingIntentCreatorBackgroundActivityStartMode(
ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED);
}
@@ -494,9 +503,6 @@
}
// We don't hold the controller lock beyond this point as we will be calling into AM and WM.
- final int callingUid = Binder.getCallingUid();
- final int callingPid = Binder.getCallingPid();
-
// Only system senders can declare a broadcast to be alarm-originated. We check
// this here rather than in the general case handling below to fail before the other
// invocation side effects such as allowlisting.
diff --git a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
index 7aafda5..1207616 100644
--- a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
+++ b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
@@ -162,6 +162,7 @@
"nfc",
"pdf_viewer",
"pixel_audio_android",
+ "pixel_biometrics_face",
"pixel_bluetooth",
"pixel_connectivity_gps",
"pixel_system_sw_video",
diff --git a/services/core/java/com/android/server/appop/AppOpsCheckingServiceImpl.java b/services/core/java/com/android/server/appop/AppOpsCheckingServiceImpl.java
index 0916967..9535db8 100644
--- a/services/core/java/com/android/server/appop/AppOpsCheckingServiceImpl.java
+++ b/services/core/java/com/android/server/appop/AppOpsCheckingServiceImpl.java
@@ -220,7 +220,7 @@
}
for (int i = 0; i < listenersCopy.size(); i++) {
- listenersCopy.get(i).onUidModeChanged(uid, op, mode);
+ listenersCopy.get(i).onUidModeChanged(uid, op, mode, persistentDeviceId);
}
return true;
diff --git a/services/core/java/com/android/server/appop/AppOpsCheckingServiceInterface.java b/services/core/java/com/android/server/appop/AppOpsCheckingServiceInterface.java
index f056f6b..8b4ea67 100644
--- a/services/core/java/com/android/server/appop/AppOpsCheckingServiceInterface.java
+++ b/services/core/java/com/android/server/appop/AppOpsCheckingServiceInterface.java
@@ -176,8 +176,9 @@
* @param uid The UID whose appop mode was changed.
* @param code The op code that was changed.
* @param mode The new mode.
+ * @param persistentDeviceId the device whose mode was changed
*/
- void onUidModeChanged(int uid, int code, int mode);
+ void onUidModeChanged(int uid, int code, int mode, String persistentDeviceId);
/**
* Invoked when a package's appop mode is changed.
diff --git a/services/core/java/com/android/server/appop/AppOpsService.java b/services/core/java/com/android/server/appop/AppOpsService.java
index 2ed217a..5c95d43 100644
--- a/services/core/java/com/android/server/appop/AppOpsService.java
+++ b/services/core/java/com/android/server/appop/AppOpsService.java
@@ -166,6 +166,7 @@
import com.android.server.LockGuard;
import com.android.server.SystemServerInitThreadPool;
import com.android.server.SystemServiceManager;
+import com.android.server.companion.virtual.VirtualDeviceManagerInternal;
import com.android.server.pm.PackageList;
import com.android.server.pm.PackageManagerLocal;
import com.android.server.pm.UserManagerInternal;
@@ -275,6 +276,10 @@
= new AppOpsManagerInternalImpl();
@Nullable private final DevicePolicyManagerInternal dpmi =
LocalServices.getService(DevicePolicyManagerInternal.class);
+ @Nullable private VirtualDeviceManagerInternal mVirtualDeviceManagerInternal;
+
+ /** Map of virtual device id -> persistent device id. */
+ private final SparseArray<String> mKnownDeviceIds = new SparseArray<>();
private final IPlatformCompat mPlatformCompat = IPlatformCompat.Stub.asInterface(
ServiceManager.getService(Context.PLATFORM_COMPAT_SERVICE));
@@ -595,45 +600,71 @@
final UidState uidState;
final @NonNull String packageName;
- /** attributionTag -> AttributedOp */
- final ArrayMap<String, AttributedOp> mAttributions = new ArrayMap<>(1);
+ /**
+ * Map to retrieve {@link AttributedOp} for a particular device and attribution tag.
+ *
+ * ArrayMap<Persistent Device Id, ArrayMap<Attribution Tag, AttributedOp>>
+ */
+ final ArrayMap<String, ArrayMap<String, AttributedOp>> mDeviceAttributedOps =
+ new ArrayMap<String, ArrayMap<String, AttributedOp>>(1);
Op(UidState uidState, String packageName, int op, int uid) {
this.op = op;
this.uid = uid;
this.uidState = uidState;
this.packageName = packageName;
+ // We keep an invariant that the persistent device will always have an entry in
+ // mDeviceAttributedOps.
+ mDeviceAttributedOps.put(PERSISTENT_DEVICE_ID_DEFAULT,
+ new ArrayMap<String, AttributedOp>());
}
void removeAttributionsWithNoTime() {
- for (int i = mAttributions.size() - 1; i >= 0; i--) {
- if (!mAttributions.valueAt(i).hasAnyTime()) {
- mAttributions.removeAt(i);
+ for (int deviceIndex = mDeviceAttributedOps.size() - 1; deviceIndex >= 0;
+ deviceIndex--) {
+ ArrayMap<String, AttributedOp> attributedOps = mDeviceAttributedOps.valueAt(
+ deviceIndex);
+ for (int tagIndex = attributedOps.size() - 1; tagIndex >= 0; tagIndex--) {
+ if (!attributedOps.valueAt(tagIndex).hasAnyTime()) {
+ attributedOps.removeAt(tagIndex);
+ }
+ }
+ if (!Objects.equals(PERSISTENT_DEVICE_ID_DEFAULT,
+ mDeviceAttributedOps.keyAt(deviceIndex)) && attributedOps.isEmpty()) {
+ mDeviceAttributedOps.removeAt(deviceIndex);
}
}
}
private @NonNull AttributedOp getOrCreateAttribution(@NonNull Op parent,
- @Nullable String attributionTag) {
- AttributedOp attributedOp;
+ @Nullable String attributionTag, String persistentDeviceId) {
+ ArrayMap<String, AttributedOp> attributedOps = mDeviceAttributedOps.get(
+ persistentDeviceId);
+ if (attributedOps == null) {
+ attributedOps = new ArrayMap<>();
+ mDeviceAttributedOps.put(persistentDeviceId, attributedOps);
+ }
+ AttributedOp attributedOp = attributedOps.get(attributionTag);
- attributedOp = mAttributions.get(attributionTag);
if (attributedOp == null) {
- attributedOp = new AttributedOp(AppOpsService.this, attributionTag, parent);
- mAttributions.put(attributionTag, attributedOp);
+ attributedOp = new AttributedOp(AppOpsService.this, attributionTag,
+ persistentDeviceId, parent);
+ attributedOps.put(attributionTag, attributedOp);
}
return attributedOp;
}
@NonNull OpEntry createEntryLocked() {
- final int numAttributions = mAttributions.size();
-
+ // TODO(b/308201969): Update this method when we introduce disk persistence of events
+ // for accesses on external devices.
+ final ArrayMap<String, AttributedOp> attributedOps = mDeviceAttributedOps.get(
+ PERSISTENT_DEVICE_ID_DEFAULT);
final ArrayMap<String, AppOpsManager.AttributedOpEntry> attributionEntries =
- new ArrayMap<>(numAttributions);
- for (int i = 0; i < numAttributions; i++) {
- attributionEntries.put(mAttributions.keyAt(i),
- mAttributions.valueAt(i).createAttributedOpEntryLocked());
+ new ArrayMap<>(attributedOps.size());
+ for (int i = 0; i < attributedOps.size(); i++) {
+ attributionEntries.put(attributedOps.keyAt(i),
+ attributedOps.valueAt(i).createAttributedOpEntryLocked());
}
return new OpEntry(
@@ -644,17 +675,15 @@
}
@NonNull OpEntry createSingleAttributionEntryLocked(@Nullable String attributionTag) {
- final int numAttributions = mAttributions.size();
-
+ // TODO(b/308201969): Update this method when we introduce disk persistence of events
+ // for accesses on external devices.
+ final ArrayMap<String, AttributedOp> attributedOps = mDeviceAttributedOps.get(
+ PERSISTENT_DEVICE_ID_DEFAULT);
final ArrayMap<String, AttributedOpEntry> attributionEntries = new ArrayMap<>(1);
- for (int i = 0; i < numAttributions; i++) {
- if (Objects.equals(mAttributions.keyAt(i), attributionTag)) {
- attributionEntries.put(mAttributions.keyAt(i),
- mAttributions.valueAt(i).createAttributedOpEntryLocked());
- break;
- }
+ if (attributedOps.get(attributionTag) != null) {
+ attributionEntries.put(attributionTag,
+ attributedOps.get(attributionTag).createAttributedOpEntryLocked());
}
-
return new OpEntry(
op,
mAppOpsCheckingService.getPackageMode(
@@ -663,13 +692,15 @@
}
boolean isRunning() {
- final int numAttributions = mAttributions.size();
- for (int i = 0; i < numAttributions; i++) {
- if (mAttributions.valueAt(i).isRunning()) {
- return true;
+ for (int deviceIndex = 0; deviceIndex < mDeviceAttributedOps.size(); deviceIndex++) {
+ ArrayMap<String, AttributedOp> attributedOps = mDeviceAttributedOps.valueAt(
+ deviceIndex);
+ for (int tagIndex = 0; tagIndex < attributedOps.size(); tagIndex++) {
+ if (attributedOps.valueAt(tagIndex).isRunning()) {
+ return true;
+ }
}
}
-
return false;
}
}
@@ -738,7 +769,15 @@
@Override
public void onOpModeChanged(int op, int uid, String packageName) throws RemoteException {
- mCallback.opChanged(op, uid, packageName);
+ throw new IllegalStateException(
+ "unimplemented onOpModeChanged method called for op: " + op + " uid: " + uid
+ + " packageName: " + packageName);
+ }
+
+ @Override
+ public void onOpModeChanged(int op, int uid, String packageName, String persistentDeviceId)
+ throws RemoteException {
+ mCallback.opChanged(op, uid, packageName, persistentDeviceId);
}
}
@@ -910,6 +949,7 @@
public AppOpsService(File recentAccessesFile, File storageFile, Handler handler,
Context context) {
mContext = context;
+ mKnownDeviceIds.put(Context.DEVICE_ID_DEFAULT, PERSISTENT_DEVICE_ID_DEFAULT);
for (int switchedCode = 0; switchedCode < _NUM_OP; switchedCode++) {
int switchCode = AppOpsManager.opToSwitch(switchedCode);
@@ -927,10 +967,11 @@
mAppOpsCheckingService.addAppOpsModeChangedListener(
new AppOpsCheckingServiceInterface.AppOpsModeChangedListener() {
@Override
- public void onUidModeChanged(int uid, int code, int mode) {
+ public void onUidModeChanged(int uid, int code, int mode,
+ String persistentDeviceId) {
mHandler.sendMessage(PooledLambda.obtainMessage(
AppOpsService::notifyOpChangedForAllPkgsInUid, AppOpsService.this,
- code, uid, false));
+ code, uid, false, persistentDeviceId));
}
@Override
@@ -941,8 +982,9 @@
packageName, code, mode, userId));
}
});
+ // Only notify default device as other devices are unaffected by restriction changes.
mAppOpsRestrictions = new AppOpsRestrictionsImpl(context, handler,
- code -> notifyWatchersOfChange(code, UID_ANY));
+ code -> notifyWatchersOnDefaultDevice(code, UID_ANY));
LockGuard.installLock(this, LockGuard.INDEX_APP_OPS);
mStorageFile = new AtomicFile(storageFile, "appops_legacy");
@@ -1043,25 +1085,28 @@
int numOps = ops.size();
for (int opNum = 0; opNum < numOps; opNum++) {
Op op = ops.valueAt(opNum);
+ for (int deviceIndex = op.mDeviceAttributedOps.size() - 1; deviceIndex >= 0;
+ deviceIndex--) {
+ ArrayMap<String, AttributedOp> attributedOps =
+ op.mDeviceAttributedOps.valueAt(deviceIndex);
+ for (int tagIndex = attributedOps.size() - 1; tagIndex >= 0;
+ tagIndex--) {
+ String tag = attributedOps.keyAt(tagIndex);
+ if (attributionTags.contains(tag)) {
+ // attribution still exist after upgrade
+ continue;
+ }
- int numAttributions = op.mAttributions.size();
- for (int attributionNum = numAttributions - 1; attributionNum >= 0;
- attributionNum--) {
- String attributionTag = op.mAttributions.keyAt(attributionNum);
+ String newAttributionTag = dstAttributionTags.get(tag);
- if (attributionTags.contains(attributionTag)) {
- // attribution still exist after upgrade
- continue;
+ AttributedOp newAttributedOp = op.getOrCreateAttribution(op,
+ newAttributionTag,
+ op.mDeviceAttributedOps.keyAt(deviceIndex));
+ newAttributedOp.add(attributedOps.get(tag));
+ attributedOps.remove(tag);
+
+ scheduleFastWriteLocked();
}
-
- String newAttributionTag = dstAttributionTags.get(attributionTag);
-
- AttributedOp newAttributedOp = op.getOrCreateAttribution(op,
- newAttributionTag);
- newAttributedOp.add(op.mAttributions.valueAt(attributionNum));
- op.mAttributions.removeAt(attributionNum);
-
- scheduleFastWriteLocked();
}
}
}
@@ -1070,6 +1115,8 @@
};
public void systemReady() {
+ mVirtualDeviceManagerInternal = LocalServices.getService(
+ VirtualDeviceManagerInternal.class);
mAppOpsCheckingService.systemReady();
initializeUidStates();
@@ -1144,7 +1191,17 @@
final String changedPkg = changedPkgs[i];
// We trust packagemanager to insert matching uid and packageNames in the
// extras
- notifyOpChanged(onModeChangedListeners, code, changedUid, changedPkg);
+ Set<String> devices;
+ if (mVirtualDeviceManagerInternal != null) {
+ devices = mVirtualDeviceManagerInternal.getAllPersistentDeviceIds();
+ } else {
+ devices = new ArraySet<>();
+ devices.add(PERSISTENT_DEVICE_ID_DEFAULT);
+ }
+ for (String device: devices) {
+ notifyOpChanged(onModeChangedListeners, code, changedUid, changedPkg,
+ device);
+ }
}
}
}
@@ -1295,17 +1352,19 @@
final int numOps = removedOps.size();
for (int opNum = 0; opNum < numOps; opNum++) {
final Op op = removedOps.valueAt(opNum);
+ for (int deviceIndex = 0; deviceIndex < op.mDeviceAttributedOps.size();
+ deviceIndex++) {
+ ArrayMap<String, AttributedOp> attributedOps =
+ op.mDeviceAttributedOps.valueAt(deviceIndex);
+ for (int tagIndex = 0; tagIndex < attributedOps.size(); tagIndex++) {
+ AttributedOp attributedOp = attributedOps.valueAt(tagIndex);
- final int numAttributions = op.mAttributions.size();
- for (int attributionNum = 0; attributionNum < numAttributions;
- attributionNum++) {
- AttributedOp attributedOp = op.mAttributions.valueAt(attributionNum);
-
- while (attributedOp.isRunning()) {
- attributedOp.finished(attributedOp.mInProgressEvents.keyAt(0));
- }
- while (attributedOp.isPaused()) {
- attributedOp.finished(attributedOp.mPausedInProgressEvents.keyAt(0));
+ while (attributedOp.isRunning()) {
+ attributedOp.finished(attributedOp.mInProgressEvents.keyAt(0));
+ }
+ while (attributedOp.isPaused()) {
+ attributedOp.finished(attributedOp.mPausedInProgressEvents.keyAt(0));
+ }
}
}
}
@@ -1380,7 +1439,7 @@
== AppOpsManager.MODE_FOREGROUND) {
mHandler.sendMessage(PooledLambda.obtainMessage(
AppOpsService::notifyOpChangedForAllPkgsInUid,
- this, code, uidState.uid, true));
+ this, code, uidState.uid, true, PERSISTENT_DEVICE_ID_DEFAULT));
} else if (!uidState.pkgOps.isEmpty()) {
final ArraySet<OnOpModeChangedListener> listenerSet =
mOpModeWatchers.get(code);
@@ -1405,7 +1464,8 @@
mHandler.sendMessage(PooledLambda.obtainMessage(
AppOpsService::notifyOpChanged,
this, listenerSet.valueAt(cbi), code, uidState.uid,
- uidState.pkgOps.keyAt(pkgi)));
+ uidState.pkgOps.keyAt(pkgi),
+ PERSISTENT_DEVICE_ID_DEFAULT));
}
}
}
@@ -1422,14 +1482,15 @@
int numOps = ops.size();
for (int opNum = 0; opNum < numOps; opNum++) {
Op op = ops.valueAt(opNum);
-
- int numAttributions = op.mAttributions.size();
- for (int attributionNum = 0; attributionNum < numAttributions;
- attributionNum++) {
- AttributedOp attributedOp = op.mAttributions.valueAt(
- attributionNum);
-
- attributedOp.onUidStateChanged(state);
+ for (int deviceIndex = 0; deviceIndex < op.mDeviceAttributedOps.size();
+ deviceIndex++) {
+ ArrayMap<String, AttributedOp> attributedOps =
+ op.mDeviceAttributedOps.valueAt(deviceIndex);
+ for (int tagIndex = 0; tagIndex < attributedOps.size();
+ tagIndex++) {
+ AttributedOp attributedOp = attributedOps.valueAt(tagIndex);
+ attributedOp.onUidStateChanged(state);
+ }
}
}
}
@@ -1774,7 +1835,7 @@
private void pruneOpLocked(Op op, int uid, String packageName) {
op.removeAttributionsWithNoTime();
- if (op.mAttributions.isEmpty()) {
+ if (op.mDeviceAttributedOps.isEmpty()) {
Ops ops = getOpsLocked(uid, packageName, null, false, null, /* edit */ false);
if (ops != null) {
ops.remove(op.op);
@@ -1870,8 +1931,10 @@
uidState.uid, PERSISTENT_DEVICE_ID_DEFAULT, code, mode)) {
return;
}
+ // TODO(b/266164193): Ensure this behavior is device-aware after uid op mode for runtime
+ // permissions is deprecated.
if (mode != MODE_ERRORED && mode != previousMode) {
- updateStartedOpModeForUidLocked(code, mode == MODE_IGNORED, uid);
+ updateStartedOpModeForUidForDefaultDeviceLocked(code, mode == MODE_IGNORED, uid);
}
}
@@ -1884,8 +1947,10 @@
* @param code The op that changed
* @param uid The uid the op was changed for
* @param onlyForeground Only notify watchers that watch for foreground changes
+ * @param persistentDeviceId device the op was changed for
*/
- private void notifyOpChangedForAllPkgsInUid(int code, int uid, boolean onlyForeground) {
+ private void notifyOpChangedForAllPkgsInUid(int code, int uid, boolean onlyForeground,
+ String persistentDeviceId) {
String[] uidPackageNames = getPackagesForUid(uid);
ArrayMap<OnOpModeChangedListener, ArraySet<String>> callbackSpecs = null;
synchronized (this) {
@@ -1951,17 +2016,17 @@
final OnOpModeChangedListener callback = callbackSpecs.keyAt(i);
final ArraySet<String> reportedPackageNames = callbackSpecs.valueAt(i);
if (reportedPackageNames == null) {
- mHandler.sendMessage(PooledLambda.obtainMessage(
- AppOpsService::notifyOpChanged,
- this, callback, code, uid, (String) null));
+ mHandler.sendMessage(
+ PooledLambda.obtainMessage(AppOpsService::notifyOpChanged, this,
+ callback, code, uid, (String) null, persistentDeviceId));
} else {
final int reportedPackageCount = reportedPackageNames.size();
for (int j = 0; j < reportedPackageCount; j++) {
final String reportedPackageName = reportedPackageNames.valueAt(j);
- mHandler.sendMessage(PooledLambda.obtainMessage(
- AppOpsService::notifyOpChanged,
- this, callback, code, uid, reportedPackageName));
+ mHandler.sendMessage(
+ PooledLambda.obtainMessage(AppOpsService::notifyOpChanged, this,
+ callback, code, uid, reportedPackageName, persistentDeviceId));
}
}
}
@@ -1999,13 +2064,17 @@
}
scheduleFastWriteLocked();
if (mode != MODE_ERRORED) {
- updateStartedOpModeForUidLocked(code, mode == MODE_IGNORED, uid);
+ // Notify on PERSISTENT_DEVICE_ID_DEFAULT only as only uid modes are device-aware,
+ // not package modes.
+ updateStartedOpModeForUidForDefaultDeviceLocked(code, mode == MODE_IGNORED, uid);
}
}
if (repCbs != null && uid != -1) {
+ // Notify on PERSISTENT_DEVICE_ID_DEFAULT only as only uid modes are device-aware, not
+ // package modes.
mHandler.sendMessage(PooledLambda.obtainMessage(AppOpsService::notifyOpChanged, this,
- repCbs, code, uid, packageName));
+ repCbs, code, uid, packageName, PERSISTENT_DEVICE_ID_DEFAULT));
}
}
@@ -2161,15 +2230,15 @@
}
private void notifyOpChanged(ArraySet<OnOpModeChangedListener> callbacks, int code,
- int uid, String packageName) {
+ int uid, String packageName, String persistentDeviceId) {
for (int i = 0; i < callbacks.size(); i++) {
final OnOpModeChangedListener callback = callbacks.valueAt(i);
- notifyOpChanged(callback, code, uid, packageName);
+ notifyOpChanged(callback, code, uid, packageName, persistentDeviceId);
}
}
private void notifyOpChanged(OnOpModeChangedListener onModeChangedListener, int code,
- int uid, String packageName) {
+ int uid, String packageName, String persistentDeviceId) {
Objects.requireNonNull(onModeChangedListener);
if (uid != UID_ANY && onModeChangedListener.getWatchingUid() >= 0
@@ -2197,7 +2266,8 @@
onModeChangedListener.getCallingUid())) {
continue;
}
- onModeChangedListener.onOpModeChanged(switchedCode, uid, packageName);
+ onModeChangedListener.onOpModeChanged(switchedCode, uid, packageName,
+ persistentDeviceId);
} catch (RemoteException e) {
/* ignore */
} finally {
@@ -2289,7 +2359,8 @@
boolean changed = false;
for (int i = mUidStates.size() - 1; i >= 0; i--) {
UidState uidState = mUidStates.valueAt(i);
- // TODO(b/299330771): Check non default modes for all devices.
+ // TODO(b/266164193): Ensure this behavior is device-aware after uid op mode for
+ // runtime permissions is deprecated.
SparseIntArray opModes =
mAppOpsCheckingService.getNonDefaultUidModes(
uidState.uid, PERSISTENT_DEVICE_ID_DEFAULT);
@@ -2301,7 +2372,6 @@
int previousMode = opModes.valueAt(j);
int newMode = isUidOpGrantedByRole(uidState.uid, code) ? MODE_ALLOWED :
AppOpsManager.opToDefaultMode(code);
- // TODO(b/299330771): Set mode for all necessary devices.
mAppOpsCheckingService.setUidMode(
uidState.uid,
PERSISTENT_DEVICE_ID_DEFAULT,
@@ -2375,7 +2445,7 @@
allChanges = addChange(allChanges, curOp.op, uid, packageName,
previousMode);
curOp.removeAttributionsWithNoTime();
- if (curOp.mAttributions.isEmpty()) {
+ if (curOp.mDeviceAttributedOps.isEmpty()) {
pkgOps.removeAt(j);
}
}
@@ -2399,9 +2469,18 @@
ArrayList<ChangeRec> reports = ent.getValue();
for (int i=0; i<reports.size(); i++) {
ChangeRec rep = reports.get(i);
- mHandler.sendMessage(PooledLambda.obtainMessage(
- AppOpsService::notifyOpChanged,
- this, cb, rep.op, rep.uid, rep.pkg));
+ Set<String> devices;
+ if (mVirtualDeviceManagerInternal != null) {
+ devices = mVirtualDeviceManagerInternal.getAllPersistentDeviceIds();
+ } else {
+ devices = new ArraySet<>();
+ devices.add(PERSISTENT_DEVICE_ID_DEFAULT);
+ }
+ for (String device: devices) {
+ mHandler.sendMessage(PooledLambda.obtainMessage(
+ AppOpsService::notifyOpChanged,
+ this, cb, rep.op, rep.uid, rep.pkg, device));
+ }
}
}
}
@@ -2595,6 +2674,12 @@
private int checkOperationImpl(int code, int uid, String packageName,
@Nullable String attributionTag, int virtualDeviceId, boolean raw) {
verifyIncomingOp(code);
+ if (!isValidVirtualDeviceId(virtualDeviceId)) {
+ Slog.w(TAG,
+ "checkOperationImpl returned MODE_IGNORED as virtualDeviceId " + virtualDeviceId
+ + " is invalid");
+ return AppOpsManager.MODE_IGNORED;
+ }
if (!isIncomingPackageValid(packageName, UserHandle.getUserId(uid))) {
return AppOpsManager.opToDefaultMode(code);
}
@@ -2603,7 +2688,8 @@
if (resolvedPackageName == null) {
return AppOpsManager.MODE_IGNORED;
}
- return checkOperationUnchecked(code, uid, resolvedPackageName, attributionTag, raw);
+ return checkOperationUnchecked(code, uid, resolvedPackageName, attributionTag,
+ virtualDeviceId, raw);
}
/**
@@ -2617,7 +2703,7 @@
* @return The mode of the op
*/
private @Mode int checkOperationUnchecked(int code, int uid, @NonNull String packageName,
- @Nullable String attributionTag, boolean raw) {
+ @Nullable String attributionTag, int virtualDeviceId, boolean raw) {
PackageVerificationResult pvr;
try {
pvr = verifyAndGetBypass(uid, packageName, null);
@@ -2630,19 +2716,19 @@
return AppOpsManager.MODE_IGNORED;
}
synchronized (this) {
- if (isOpRestrictedLocked(uid, code, packageName, attributionTag, pvr.bypass, true)) {
+ if (isOpRestrictedLocked(uid, code, packageName, attributionTag, virtualDeviceId,
+ pvr.bypass, true)) {
return AppOpsManager.MODE_IGNORED;
}
code = AppOpsManager.opToSwitch(code);
UidState uidState = getUidStateLocked(uid, false);
- // TODO(b/299330771): Check mode for the relevant device.
if (uidState != null
&& mAppOpsCheckingService.getUidMode(
- uidState.uid, PERSISTENT_DEVICE_ID_DEFAULT, code)
+ uidState.uid, getPersistentId(virtualDeviceId), code)
!= AppOpsManager.opToDefaultMode(code)) {
final int rawMode =
mAppOpsCheckingService.getUidMode(
- uidState.uid, PERSISTENT_DEVICE_ID_DEFAULT, code);
+ uidState.uid, getPersistentId(virtualDeviceId), code);
return raw ? rawMode : uidState.evalMode(code, rawMode);
}
Op op = getOpLocked(code, uid, packageName, null, false, pvr.bypass, /* edit */ false);
@@ -2683,8 +2769,9 @@
mAudioRestrictionManager.setZenModeAudioRestriction(
code, usage, uid, mode, exceptionPackages);
+ // Only notify default device as other devices are unaffected by restriction changes.
mHandler.sendMessage(PooledLambda.obtainMessage(
- AppOpsService::notifyWatchersOfChange, this, code, UID_ANY));
+ AppOpsService::notifyWatchersOnDefaultDevice, this, code, UID_ANY));
}
@@ -2694,11 +2781,12 @@
mAudioRestrictionManager.setCameraAudioRestriction(mode);
+ // Only notify default device as other devices are unaffected by restriction changes.
mHandler.sendMessage(PooledLambda.obtainMessage(
- AppOpsService::notifyWatchersOfChange, this,
+ AppOpsService::notifyWatchersOnDefaultDevice, this,
AppOpsManager.OP_PLAY_AUDIO, UID_ANY));
mHandler.sendMessage(PooledLambda.obtainMessage(
- AppOpsService::notifyWatchersOfChange, this,
+ AppOpsService::notifyWatchersOnDefaultDevice, this,
AppOpsManager.OP_VIBRATE, UID_ANY));
}
@@ -2761,11 +2849,18 @@
final String proxyPackageName = attributionSource.getPackageName();
final String proxyAttributionTag = attributionSource.getAttributionTag();
final int proxiedUid = attributionSource.getNextUid();
+ final int proxyVirtualDeviceId = attributionSource.getDeviceId();
final String proxiedPackageName = attributionSource.getNextPackageName();
final String proxiedAttributionTag = attributionSource.getNextAttributionTag();
verifyIncomingProxyUid(attributionSource);
verifyIncomingOp(code);
+ if (!isValidVirtualDeviceId(proxyVirtualDeviceId)) {
+ Slog.w(TAG, "noteProxyOperationImpl returned MODE_IGNORED as virtualDeviceId "
+ + proxyVirtualDeviceId + " is invalid");
+ return new SyncNotedAppOp(AppOpsManager.MODE_IGNORED, code, proxiedAttributionTag,
+ proxiedPackageName);
+ }
if (!isIncomingPackageValid(proxiedPackageName, UserHandle.getUserId(proxiedUid))
|| !isIncomingPackageValid(proxyPackageName, UserHandle.getUserId(proxyUid))) {
return new SyncNotedAppOp(AppOpsManager.MODE_ERRORED, code, proxiedAttributionTag,
@@ -2792,8 +2887,9 @@
: AppOpsManager.OP_FLAG_UNTRUSTED_PROXY;
final SyncNotedAppOp proxyReturn = noteOperationUnchecked(code, proxyUid,
- resolveProxyPackageName, proxyAttributionTag, Process.INVALID_UID, null, null,
- proxyFlags, !isProxyTrusted, "proxy " + message, shouldCollectMessage);
+ resolveProxyPackageName, proxyAttributionTag, proxyVirtualDeviceId,
+ Process.INVALID_UID, null, null, proxyFlags, !isProxyTrusted,
+ "proxy " + message, shouldCollectMessage);
if (proxyReturn.getOpMode() != AppOpsManager.MODE_ALLOWED) {
return new SyncNotedAppOp(proxyReturn.getOpMode(), code, proxiedAttributionTag,
proxiedPackageName);
@@ -2810,8 +2906,9 @@
final int proxiedFlags = isProxyTrusted ? AppOpsManager.OP_FLAG_TRUSTED_PROXIED
: AppOpsManager.OP_FLAG_UNTRUSTED_PROXIED;
return noteOperationUnchecked(code, proxiedUid, resolveProxiedPackageName,
- proxiedAttributionTag, proxyUid, resolveProxyPackageName, proxyAttributionTag,
- proxiedFlags, shouldCollectAsyncNotedOp, message, shouldCollectMessage);
+ proxiedAttributionTag, proxyVirtualDeviceId, proxyUid, resolveProxyPackageName,
+ proxyAttributionTag, proxiedFlags, shouldCollectAsyncNotedOp, message,
+ shouldCollectMessage);
}
@Override
@@ -2838,6 +2935,13 @@
boolean shouldCollectMessage) {
verifyIncomingUid(uid);
verifyIncomingOp(code);
+ if (!isValidVirtualDeviceId(virtualDeviceId)) {
+ Slog.w(TAG,
+ "checkOperationImpl returned MODE_IGNORED as virtualDeviceId " + virtualDeviceId
+ + " is invalid");
+ return new SyncNotedAppOp(AppOpsManager.MODE_IGNORED, code, attributionTag,
+ packageName);
+ }
if (!isIncomingPackageValid(packageName, UserHandle.getUserId(uid))) {
// TODO(b/302609140): Remove extra logging after this issue is diagnosed.
if (code == OP_BLUETOOTH_CONNECT) {
@@ -2854,15 +2958,16 @@
packageName);
}
return noteOperationUnchecked(code, uid, resolvedPackageName, attributionTag,
- Process.INVALID_UID, null, null, AppOpsManager.OP_FLAG_SELF,
- shouldCollectAsyncNotedOp, message, shouldCollectMessage);
+ virtualDeviceId, Process.INVALID_UID, null, null,
+ AppOpsManager.OP_FLAG_SELF, shouldCollectAsyncNotedOp, message,
+ shouldCollectMessage);
}
private SyncNotedAppOp noteOperationUnchecked(int code, int uid, @NonNull String packageName,
- @Nullable String attributionTag, int proxyUid, String proxyPackageName,
- @Nullable String proxyAttributionTag, @OpFlags int flags,
- boolean shouldCollectAsyncNotedOp, @Nullable String message,
- boolean shouldCollectMessage) {
+ @Nullable String attributionTag, int virtualDeviceId, int proxyUid,
+ String proxyPackageName, @Nullable String proxyAttributionTag, @OpFlags int flags,
+ boolean shouldCollectAsyncNotedOp, @Nullable String message,
+ boolean shouldCollectMessage) {
PackageVerificationResult pvr;
try {
pvr = verifyAndGetBypass(uid, packageName, attributionTag, proxyPackageName);
@@ -2891,8 +2996,8 @@
final Ops ops = getOpsLocked(uid, packageName, attributionTag,
pvr.isAttributionTagValid, pvr.bypass, /* edit */ true);
if (ops == null) {
- scheduleOpNotedIfNeededLocked(code, uid, packageName, attributionTag, flags,
- AppOpsManager.MODE_IGNORED);
+ scheduleOpNotedIfNeededLocked(code, uid, packageName, attributionTag,
+ virtualDeviceId, flags, AppOpsManager.MODE_IGNORED);
if (DEBUG) Slog.d(TAG, "noteOperation: no op for code " + code + " uid " + uid
+ " package " + packageName + "flags: " +
AppOpsManager.flagsToString(flags));
@@ -2911,7 +3016,8 @@
packageName);
}
final Op op = getOpLocked(ops, code, uid, true);
- final AttributedOp attributedOp = op.getOrCreateAttribution(op, attributionTag);
+ final AttributedOp attributedOp = op.getOrCreateAttribution(op, attributionTag,
+ getPersistentId(virtualDeviceId));
if (attributedOp.isRunning()) {
Slog.w(TAG, "Noting op not finished: uid " + uid + " pkg " + packageName + " code "
+ code + " startTime of in progress event="
@@ -2920,33 +3026,33 @@
final int switchCode = AppOpsManager.opToSwitch(code);
final UidState uidState = ops.uidState;
- if (isOpRestrictedLocked(uid, code, packageName, attributionTag, pvr.bypass, false)) {
+ if (isOpRestrictedLocked(uid, code, packageName, attributionTag, virtualDeviceId,
+ pvr.bypass, false)) {
attributedOp.rejected(uidState.getState(), flags);
- scheduleOpNotedIfNeededLocked(code, uid, packageName, attributionTag, flags,
- AppOpsManager.MODE_IGNORED);
+ scheduleOpNotedIfNeededLocked(code, uid, packageName, attributionTag,
+ virtualDeviceId, flags, AppOpsManager.MODE_IGNORED);
return new SyncNotedAppOp(AppOpsManager.MODE_IGNORED, code, attributionTag,
packageName);
}
- // TODO(b/299330771): Check mode for the relevant device.
// If there is a non-default per UID policy (we set UID op mode only if
// non-default) it takes over, otherwise use the per package policy.
if (mAppOpsCheckingService.getUidMode(
- uidState.uid, PERSISTENT_DEVICE_ID_DEFAULT, switchCode)
+ uidState.uid, getPersistentId(virtualDeviceId), switchCode)
!= AppOpsManager.opToDefaultMode(switchCode)) {
final int uidMode =
uidState.evalMode(
code,
mAppOpsCheckingService.getUidMode(
uidState.uid,
- PERSISTENT_DEVICE_ID_DEFAULT,
+ getPersistentId(virtualDeviceId),
switchCode));
if (uidMode != AppOpsManager.MODE_ALLOWED) {
if (DEBUG) Slog.d(TAG, "noteOperation: uid reject #" + uidMode + " for code "
+ switchCode + " (" + code + ") uid " + uid + " package "
+ packageName + " flags: " + AppOpsManager.flagsToString(flags));
attributedOp.rejected(uidState.getState(), flags);
- scheduleOpNotedIfNeededLocked(code, uid, packageName, attributionTag, flags,
- uidMode);
+ scheduleOpNotedIfNeededLocked(code, uid, packageName, attributionTag,
+ virtualDeviceId, flags, uidMode);
// TODO(b/302609140): Remove extra logging after this issue is diagnosed.
if (code == OP_BLUETOOTH_CONNECT && uidMode == MODE_ERRORED) {
Slog.e(TAG, "noting OP_BLUETOOTH_CONNECT returned MODE_ERRORED as"
@@ -2969,8 +3075,8 @@
+ switchCode + " (" + code + ") uid " + uid + " package "
+ packageName + " flags: " + AppOpsManager.flagsToString(flags));
attributedOp.rejected(uidState.getState(), flags);
- scheduleOpNotedIfNeededLocked(code, uid, packageName, attributionTag, flags,
- mode);
+ scheduleOpNotedIfNeededLocked(code, uid, packageName, attributionTag,
+ virtualDeviceId, flags, mode);
// TODO(b/302609140): Remove extra logging after this issue is diagnosed.
if (code == OP_BLUETOOTH_CONNECT && mode == MODE_ERRORED) {
Slog.e(TAG, "noting OP_BLUETOOTH_CONNECT returned MODE_ERRORED as"
@@ -2986,11 +3092,10 @@
: "." + attributionTag) + " flags: "
+ AppOpsManager.flagsToString(flags));
}
- scheduleOpNotedIfNeededLocked(code, uid, packageName, attributionTag, flags,
- AppOpsManager.MODE_ALLOWED);
+ scheduleOpNotedIfNeededLocked(code, uid, packageName, attributionTag,
+ virtualDeviceId, flags, AppOpsManager.MODE_ALLOWED);
attributedOp.accessed(proxyUid, proxyPackageName, proxyAttributionTag,
- uidState.getState(),
- flags);
+ uidState.getState(), flags);
if (shouldCollectAsyncNotedOp) {
collectAsyncNotedOp(uid, packageName, code, attributionTag, flags, message,
@@ -3314,6 +3419,13 @@
int attributionChainId) {
verifyIncomingUid(uid);
verifyIncomingOp(code);
+ if (!isValidVirtualDeviceId(virtualDeviceId)) {
+ Slog.w(TAG,
+ "startOperationImpl returned MODE_IGNORED as virtualDeviceId " + virtualDeviceId
+ + " is invalid");
+ return new SyncNotedAppOp(AppOpsManager.MODE_IGNORED, code, attributionTag,
+ packageName);
+ }
if (!isIncomingPackageValid(packageName, UserHandle.getUserId(uid))) {
return new SyncNotedAppOp(AppOpsManager.MODE_ERRORED, code, attributionTag,
packageName);
@@ -3349,9 +3461,9 @@
}
return startOperationUnchecked(clientId, code, uid, packageName, attributionTag,
- Process.INVALID_UID, null, null, OP_FLAG_SELF, startIfModeDefault,
- shouldCollectAsyncNotedOp, message, shouldCollectMessage, attributionFlags,
- attributionChainId);
+ virtualDeviceId, Process.INVALID_UID, null, null, OP_FLAG_SELF,
+ startIfModeDefault, shouldCollectAsyncNotedOp, message, shouldCollectMessage,
+ attributionFlags, attributionChainId);
}
/** @deprecated Use {@link #startProxyOperationWithState} instead. */
@@ -3390,11 +3502,18 @@
final String proxyPackageName = attributionSource.getPackageName();
final String proxyAttributionTag = attributionSource.getAttributionTag();
final int proxiedUid = attributionSource.getNextUid();
+ final int proxyVirtualDeviceId = attributionSource.getDeviceId();
final String proxiedPackageName = attributionSource.getNextPackageName();
final String proxiedAttributionTag = attributionSource.getNextAttributionTag();
verifyIncomingProxyUid(attributionSource);
verifyIncomingOp(code);
+ if (!isValidVirtualDeviceId(proxyVirtualDeviceId)) {
+ Slog.w(TAG, "startProxyOperationImpl returned MODE_IGNORED as virtualDeviceId "
+ + proxyVirtualDeviceId + " is invalid");
+ return new SyncNotedAppOp(AppOpsManager.MODE_IGNORED, code, proxiedAttributionTag,
+ proxiedPackageName);
+ }
if (!isIncomingPackageValid(proxyPackageName, UserHandle.getUserId(proxyUid))
|| !isIncomingPackageValid(proxiedPackageName, UserHandle.getUserId(proxiedUid))) {
return new SyncNotedAppOp(AppOpsManager.MODE_ERRORED, code, proxiedAttributionTag,
@@ -3435,7 +3554,8 @@
// Test if the proxied operation will succeed before starting the proxy operation
final SyncNotedAppOp testProxiedOp = startOperationDryRun(code,
proxiedUid, resolvedProxiedPackageName, proxiedAttributionTag,
- resolvedProxyPackageName, proxiedFlags, startIfModeDefault);
+ proxyVirtualDeviceId, resolvedProxyPackageName, proxiedFlags,
+ startIfModeDefault);
if (!shouldStartForMode(testProxiedOp.getOpMode(), startIfModeDefault)) {
return testProxiedOp;
@@ -3445,8 +3565,9 @@
: AppOpsManager.OP_FLAG_UNTRUSTED_PROXY;
final SyncNotedAppOp proxyAppOp = startOperationUnchecked(clientId, code, proxyUid,
- resolvedProxyPackageName, proxyAttributionTag, Process.INVALID_UID, null, null,
- proxyFlags, startIfModeDefault, !isProxyTrusted, "proxy " + message,
+ resolvedProxyPackageName, proxyAttributionTag, proxyVirtualDeviceId,
+ Process.INVALID_UID, null, null, proxyFlags,
+ startIfModeDefault, !isProxyTrusted, "proxy " + message,
shouldCollectMessage, proxyAttributionFlags, attributionChainId);
if (!shouldStartForMode(proxyAppOp.getOpMode(), startIfModeDefault)) {
return proxyAppOp;
@@ -3454,9 +3575,9 @@
}
return startOperationUnchecked(clientId, code, proxiedUid, resolvedProxiedPackageName,
- proxiedAttributionTag, proxyUid, resolvedProxyPackageName, proxyAttributionTag,
- proxiedFlags, startIfModeDefault, shouldCollectAsyncNotedOp, message,
- shouldCollectMessage, proxiedAttributionFlags, attributionChainId);
+ proxiedAttributionTag, proxyVirtualDeviceId, proxyUid, resolvedProxyPackageName,
+ proxyAttributionTag, proxiedFlags, startIfModeDefault, shouldCollectAsyncNotedOp,
+ message, shouldCollectMessage, proxiedAttributionFlags, attributionChainId);
}
private boolean shouldStartForMode(int mode, boolean startIfModeDefault) {
@@ -3464,11 +3585,11 @@
}
private SyncNotedAppOp startOperationUnchecked(IBinder clientId, int code, int uid,
- @NonNull String packageName, @Nullable String attributionTag, int proxyUid,
- String proxyPackageName, @Nullable String proxyAttributionTag, @OpFlags int flags,
- boolean startIfModeDefault, boolean shouldCollectAsyncNotedOp, @Nullable String message,
- boolean shouldCollectMessage, @AttributionFlags int attributionFlags,
- int attributionChainId) {
+ @NonNull String packageName, @Nullable String attributionTag, int virtualDeviceId,
+ int proxyUid, String proxyPackageName, @Nullable String proxyAttributionTag,
+ @OpFlags int flags, boolean startIfModeDefault, boolean shouldCollectAsyncNotedOp,
+ @Nullable String message, boolean shouldCollectMessage,
+ @AttributionFlags int attributionFlags, int attributionChainId) {
PackageVerificationResult pvr;
try {
pvr = verifyAndGetBypass(uid, packageName, attributionTag, proxyPackageName);
@@ -3492,8 +3613,8 @@
pvr.isAttributionTagValid, pvr.bypass, /* edit */ true);
if (ops == null) {
scheduleOpStartedIfNeededLocked(code, uid, packageName, attributionTag,
- flags, AppOpsManager.MODE_IGNORED, startType, attributionFlags,
- attributionChainId);
+ virtualDeviceId, flags, AppOpsManager.MODE_IGNORED, startType,
+ attributionFlags, attributionChainId);
if (DEBUG) Slog.d(TAG, "startOperation: no op for code " + code + " uid " + uid
+ " package " + packageName + " flags: "
+ AppOpsManager.flagsToString(flags));
@@ -3501,23 +3622,23 @@
packageName);
}
final Op op = getOpLocked(ops, code, uid, true);
- final AttributedOp attributedOp = op.getOrCreateAttribution(op, attributionTag);
+ final AttributedOp attributedOp = op.getOrCreateAttribution(op, attributionTag,
+ getPersistentId(virtualDeviceId));
final UidState uidState = ops.uidState;
- isRestricted = isOpRestrictedLocked(uid, code, packageName, attributionTag, pvr.bypass,
- false);
+ isRestricted = isOpRestrictedLocked(uid, code, packageName, attributionTag,
+ virtualDeviceId, pvr.bypass, false);
final int switchCode = AppOpsManager.opToSwitch(code);
- // TODO(b/299330771): Check mode for the relevant device.
// If there is a non-default per UID policy (we set UID op mode only if
// non-default) it takes over, otherwise use the per package policy.
if (mAppOpsCheckingService.getUidMode(
- uidState.uid, PERSISTENT_DEVICE_ID_DEFAULT, switchCode)
+ uidState.uid, getPersistentId(virtualDeviceId), switchCode)
!= AppOpsManager.opToDefaultMode(switchCode)) {
final int uidMode =
uidState.evalMode(
code,
mAppOpsCheckingService.getUidMode(
uidState.uid,
- PERSISTENT_DEVICE_ID_DEFAULT,
+ getPersistentId(virtualDeviceId),
switchCode));
if (!shouldStartForMode(uidMode, startIfModeDefault)) {
if (DEBUG) {
@@ -3527,7 +3648,8 @@
}
attributedOp.rejected(uidState.getState(), flags);
scheduleOpStartedIfNeededLocked(code, uid, packageName, attributionTag,
- flags, uidMode, startType, attributionFlags, attributionChainId);
+ virtualDeviceId, flags, uidMode, startType, attributionFlags,
+ attributionChainId);
return new SyncNotedAppOp(uidMode, code, attributionTag, packageName);
}
} else {
@@ -3547,7 +3669,8 @@
+ packageName + " flags: " + AppOpsManager.flagsToString(flags));
attributedOp.rejected(uidState.getState(), flags);
scheduleOpStartedIfNeededLocked(code, uid, packageName, attributionTag,
- flags, mode, startType, attributionFlags, attributionChainId);
+ virtualDeviceId, flags, mode, startType, attributionFlags,
+ attributionChainId);
return new SyncNotedAppOp(mode, code, attributionTag, packageName);
}
}
@@ -3557,19 +3680,19 @@
try {
if (isRestricted) {
attributedOp.createPaused(clientId, proxyUid, proxyPackageName,
- proxyAttributionTag, uidState.getState(), flags,
+ proxyAttributionTag, virtualDeviceId, uidState.getState(), flags,
attributionFlags, attributionChainId);
} else {
attributedOp.started(clientId, proxyUid, proxyPackageName,
- proxyAttributionTag, uidState.getState(), flags,
+ proxyAttributionTag, virtualDeviceId, uidState.getState(), flags,
attributionFlags, attributionChainId);
startType = START_TYPE_STARTED;
}
} catch (RemoteException e) {
throw new RuntimeException(e);
}
- scheduleOpStartedIfNeededLocked(code, uid, packageName, attributionTag, flags,
- isRestricted ? MODE_IGNORED : MODE_ALLOWED, startType, attributionFlags,
+ scheduleOpStartedIfNeededLocked(code, uid, packageName, attributionTag, virtualDeviceId,
+ flags, isRestricted ? MODE_IGNORED : MODE_ALLOWED, startType, attributionFlags,
attributionChainId);
}
@@ -3590,7 +3713,7 @@
* the proxied app can successfully start the operation.
*/
private SyncNotedAppOp startOperationDryRun(int code, int uid,
- @NonNull String packageName, @Nullable String attributionTag,
+ @NonNull String packageName, @Nullable String attributionTag, int virtualDeviceId,
String proxyPackageName, @OpFlags int flags,
boolean startIfModeDefault) {
PackageVerificationResult pvr;
@@ -3624,21 +3747,20 @@
}
final Op op = getOpLocked(ops, code, uid, true);
final UidState uidState = ops.uidState;
- isRestricted = isOpRestrictedLocked(uid, code, packageName, attributionTag, pvr.bypass,
- false);
+ isRestricted = isOpRestrictedLocked(uid, code, packageName, attributionTag,
+ virtualDeviceId, pvr.bypass, false);
final int switchCode = AppOpsManager.opToSwitch(code);
- // TODO(b/299330771): Check mode for the relevant device.
// If there is a non-default mode per UID policy (we set UID op mode only if
// non-default) it takes over, otherwise use the per package policy.
if (mAppOpsCheckingService.getUidMode(
- uidState.uid, PERSISTENT_DEVICE_ID_DEFAULT, switchCode)
+ uidState.uid, getPersistentId(virtualDeviceId), switchCode)
!= AppOpsManager.opToDefaultMode(switchCode)) {
final int uidMode =
uidState.evalMode(
code,
mAppOpsCheckingService.getUidMode(
uidState.uid,
- PERSISTENT_DEVICE_ID_DEFAULT,
+ getPersistentId(virtualDeviceId),
switchCode));
if (!shouldStartForMode(uidMode, startIfModeDefault)) {
if (DEBUG) {
@@ -3697,6 +3819,11 @@
String attributionTag, int virtualDeviceId) {
verifyIncomingUid(uid);
verifyIncomingOp(code);
+ if (!isValidVirtualDeviceId(virtualDeviceId)) {
+ Slog.w(TAG, "finishOperationImpl was a no-op as virtualDeviceId " + virtualDeviceId
+ + " is invalid");
+ return;
+ }
if (!isIncomingPackageValid(packageName, UserHandle.getUserId(uid))) {
return;
}
@@ -3706,7 +3833,8 @@
return;
}
- finishOperationUnchecked(clientId, code, uid, resolvedPackageName, attributionTag);
+ finishOperationUnchecked(clientId, code, uid, resolvedPackageName, attributionTag,
+ virtualDeviceId);
}
/** @deprecated Use {@link #finishProxyOperationWithState} instead. */
@@ -3731,6 +3859,7 @@
final String proxyPackageName = attributionSource.getPackageName();
final String proxyAttributionTag = attributionSource.getAttributionTag();
final int proxiedUid = attributionSource.getNextUid();
+ final int proxyVirtualDeviceId = attributionSource.getDeviceId();
final String proxiedPackageName = attributionSource.getNextPackageName();
final String proxiedAttributionTag = attributionSource.getNextAttributionTag();
@@ -3739,6 +3868,11 @@
verifyIncomingProxyUid(attributionSource);
verifyIncomingOp(code);
+ if (!isValidVirtualDeviceId(proxyVirtualDeviceId)) {
+ Slog.w(TAG, "finishProxyOperationImpl was a no-op as virtualDeviceId "
+ + proxyVirtualDeviceId + " is invalid");
+ return null;
+ }
if (!isIncomingPackageValid(proxyPackageName, UserHandle.getUserId(proxyUid))
|| !isIncomingPackageValid(proxiedPackageName, UserHandle.getUserId(proxiedUid))) {
return null;
@@ -3752,7 +3886,7 @@
if (!skipProxyOperation) {
finishOperationUnchecked(clientId, code, proxyUid, resolvedProxyPackageName,
- proxyAttributionTag);
+ proxyAttributionTag, proxyVirtualDeviceId);
}
String resolvedProxiedPackageName = AppOpsManager.resolvePackageName(proxiedUid,
@@ -3762,13 +3896,13 @@
}
finishOperationUnchecked(clientId, code, proxiedUid, resolvedProxiedPackageName,
- proxiedAttributionTag);
+ proxiedAttributionTag, proxyVirtualDeviceId);
return null;
}
private void finishOperationUnchecked(IBinder clientId, int code, int uid, String packageName,
- String attributionTag) {
+ String attributionTag, int virtualDeviceId) {
PackageVerificationResult pvr;
try {
pvr = verifyAndGetBypass(uid, packageName, attributionTag);
@@ -3788,7 +3922,9 @@
+ attributionTag + ") op=" + AppOpsManager.opToName(code));
return;
}
- final AttributedOp attributedOp = op.mAttributions.get(attributionTag);
+ final AttributedOp attributedOp =
+ op.mDeviceAttributedOps.getOrDefault(getPersistentId(virtualDeviceId),
+ new ArrayMap<>()).get(attributionTag);
if (attributedOp == null) {
Slog.e(TAG, "Attribution not found: uid=" + uid + " pkg=" + packageName + "("
+ attributionTag + ") op=" + AppOpsManager.opToName(code));
@@ -3805,8 +3941,8 @@
}
void scheduleOpActiveChangedIfNeededLocked(int code, int uid, @NonNull
- String packageName, @Nullable String attributionTag, boolean active, @AttributionFlags
- int attributionFlags, int attributionChainId) {
+ String packageName, @Nullable String attributionTag, int virtualDeviceId,
+ boolean active, @AttributionFlags int attributionFlags, int attributionChainId) {
ArraySet<ActiveCallback> dispatchedCallbacks = null;
final int callbackListCount = mActiveWatchers.size();
for (int i = 0; i < callbackListCount; i++) {
@@ -3827,13 +3963,14 @@
}
mHandler.sendMessage(PooledLambda.obtainMessage(
AppOpsService::notifyOpActiveChanged,
- this, dispatchedCallbacks, code, uid, packageName, attributionTag, active,
- attributionFlags, attributionChainId));
+ this, dispatchedCallbacks, code, uid, packageName, attributionTag,
+ virtualDeviceId, active, attributionFlags, attributionChainId));
}
private void notifyOpActiveChanged(ArraySet<ActiveCallback> callbacks,
int code, int uid, @NonNull String packageName, @Nullable String attributionTag,
- boolean active, @AttributionFlags int attributionFlags, int attributionChainId) {
+ int virtualDeviceId, boolean active, @AttributionFlags int attributionFlags,
+ int attributionChainId) {
// There are features watching for mode changes such as window manager
// and location manager which are in our process. The callbacks in these
// features may require permissions our remote caller does not have.
@@ -3847,7 +3984,7 @@
continue;
}
callback.mCallback.opActiveChanged(code, uid, packageName, attributionTag,
- active, attributionFlags, attributionChainId);
+ virtualDeviceId, active, attributionFlags, attributionChainId);
} catch (RemoteException e) {
/* do nothing */
}
@@ -3858,7 +3995,7 @@
}
void scheduleOpStartedIfNeededLocked(int code, int uid, String pkgName,
- String attributionTag, @OpFlags int flags, @Mode int result,
+ String attributionTag, int virtualDeviceId, @OpFlags int flags, @Mode int result,
@AppOpsManager.OnOpStartedListener.StartedType int startedType,
@AttributionFlags int attributionFlags, int attributionChainId) {
ArraySet<StartedCallback> dispatchedCallbacks = null;
@@ -3885,13 +4022,14 @@
mHandler.sendMessage(PooledLambda.obtainMessage(
AppOpsService::notifyOpStarted,
- this, dispatchedCallbacks, code, uid, pkgName, attributionTag, flags,
- result, startedType, attributionFlags, attributionChainId));
+ this, dispatchedCallbacks, code, uid, pkgName, attributionTag, virtualDeviceId,
+ flags, result, startedType, attributionFlags, attributionChainId));
}
private void notifyOpStarted(ArraySet<StartedCallback> callbacks,
- int code, int uid, String packageName, String attributionTag, @OpFlags int flags,
- @Mode int result, @AppOpsManager.OnOpStartedListener.StartedType int startedType,
+ int code, int uid, String packageName, String attributionTag, int virtualDeviceId,
+ @OpFlags int flags, @Mode int result,
+ @AppOpsManager.OnOpStartedListener.StartedType int startedType,
@AttributionFlags int attributionFlags, int attributionChainId) {
final long identity = Binder.clearCallingIdentity();
try {
@@ -3902,8 +4040,9 @@
if (shouldIgnoreCallback(code, callback.mCallingPid, callback.mCallingUid)) {
continue;
}
- callback.mCallback.opStarted(code, uid, packageName, attributionTag, flags,
- result, startedType, attributionFlags, attributionChainId);
+ callback.mCallback.opStarted(code, uid, packageName, attributionTag,
+ virtualDeviceId, flags, result, startedType, attributionFlags,
+ attributionChainId);
} catch (RemoteException e) {
/* do nothing */
}
@@ -3914,7 +4053,7 @@
}
private void scheduleOpNotedIfNeededLocked(int code, int uid, String packageName,
- String attributionTag, @OpFlags int flags, @Mode int result) {
+ String attributionTag, int virtualDeviceId, @OpFlags int flags, @Mode int result) {
ArraySet<NotedCallback> dispatchedCallbacks = null;
final int callbackListCount = mNotedWatchers.size();
for (int i = 0; i < callbackListCount; i++) {
@@ -3935,13 +4074,13 @@
}
mHandler.sendMessage(PooledLambda.obtainMessage(
AppOpsService::notifyOpChecked,
- this, dispatchedCallbacks, code, uid, packageName, attributionTag, flags,
- result));
+ this, dispatchedCallbacks, code, uid, packageName, attributionTag,
+ virtualDeviceId, flags, result));
}
private void notifyOpChecked(ArraySet<NotedCallback> callbacks,
- int code, int uid, String packageName, String attributionTag, @OpFlags int flags,
- @Mode int result) {
+ int code, int uid, String packageName, String attributionTag, int virtualDeviceId,
+ @OpFlags int flags, @Mode int result) {
// There are features watching for checks in our process. The callbacks in
// these features may require permissions our remote caller does not have.
final long identity = Binder.clearCallingIdentity();
@@ -3953,8 +4092,8 @@
if (shouldIgnoreCallback(code, callback.mCallingPid, callback.mCallingUid)) {
continue;
}
- callback.mCallback.opNoted(code, uid, packageName, attributionTag, flags,
- result);
+ callback.mCallback.opNoted(code, uid, packageName, attributionTag,
+ virtualDeviceId, flags, result);
} catch (RemoteException e) {
/* do nothing */
}
@@ -4028,6 +4167,22 @@
watcherPid, watcherUid) != PackageManager.PERMISSION_GRANTED;
}
+ private boolean isValidVirtualDeviceId(int virtualDeviceId) {
+ if (virtualDeviceId == Context.DEVICE_ID_DEFAULT) {
+ return true;
+ }
+ if (mVirtualDeviceManagerInternal == null) {
+ return true;
+ }
+ if (mVirtualDeviceManagerInternal.isValidVirtualDeviceId(virtualDeviceId)) {
+ mKnownDeviceIds.put(virtualDeviceId,
+ mVirtualDeviceManagerInternal.getPersistentIdForDevice(virtualDeviceId));
+ return true;
+ }
+
+ return false;
+ }
+
private void verifyIncomingOp(int op) {
if (op >= 0 && op < AppOpsManager._NUM_OP) {
// Enforce manage appops permission if it's a restricted read op.
@@ -4488,7 +4643,12 @@
}
private boolean isOpRestrictedLocked(int uid, int code, String packageName,
- String attributionTag, @Nullable RestrictionBypass appBypass, boolean isCheckOp) {
+ String attributionTag, int virtualDeviceId, @Nullable RestrictionBypass appBypass,
+ boolean isCheckOp) {
+ // Restrictions only apply to the default device.
+ if (virtualDeviceId != Context.DEVICE_ID_DEFAULT) {
+ return false;
+ }
int restrictionSetCount = mOpGlobalRestrictions.size();
for (int i = 0; i < restrictionSetCount; i++) {
@@ -4662,7 +4822,10 @@
private void readAttributionOp(TypedXmlPullParser parser, @NonNull Op parent,
@Nullable String attribution)
throws NumberFormatException, IOException, XmlPullParserException {
- final AttributedOp attributedOp = parent.getOrCreateAttribution(parent, attribution);
+ // TODO(b/308201969): Update this method when we introduce disk persistence of events
+ // for accesses on external devices.
+ final AttributedOp attributedOp =
+ parent.getOrCreateAttribution(parent, attribution, PERSISTENT_DEVICE_ID_DEFAULT);
final long key = parser.getAttributeLong(null, "n");
final int uidState = extractUidStateFromKey(key);
@@ -5369,18 +5532,23 @@
private void dumpStatesLocked(@NonNull PrintWriter pw, @Nullable String filterAttributionTag,
@HistoricalOpsRequestFilter int filter, long nowElapsed, @NonNull Op op, long now,
@NonNull SimpleDateFormat sdf, @NonNull Date date, @NonNull String prefix) {
- final int numAttributions = op.mAttributions.size();
+ // TODO(b/299330771): Dump data for all devices.
+ ArrayMap<String, AttributedOp> defaultDeviceAttributedOps = op.mDeviceAttributedOps.get(
+ PERSISTENT_DEVICE_ID_DEFAULT);
+
+ final int numAttributions = defaultDeviceAttributedOps.size();
for (int i = 0; i < numAttributions; i++) {
if ((filter & FILTER_BY_ATTRIBUTION_TAG) != 0 && !Objects.equals(
- op.mAttributions.keyAt(i), filterAttributionTag)) {
+ defaultDeviceAttributedOps.keyAt(i), filterAttributionTag)) {
continue;
}
- pw.print(prefix + op.mAttributions.keyAt(i) + "=[\n");
- dumpStatesLocked(pw, nowElapsed, op, op.mAttributions.keyAt(i), now, sdf, date,
- prefix + " ");
+ pw.print(prefix + defaultDeviceAttributedOps.keyAt(i) + "=[\n");
+ dumpStatesLocked(pw, nowElapsed, op, defaultDeviceAttributedOps.keyAt(i), now, sdf,
+ date, prefix + " ");
pw.print(prefix + "]\n");
}
+
}
private void dumpStatesLocked(@NonNull PrintWriter pw, long nowElapsed, @NonNull Op op,
@@ -5462,8 +5630,11 @@
pw.println();
}
}
+ // TODO(b/299330771): Dump running starts for all devices.
+ final AttributedOp attributedOp =
+ op.mDeviceAttributedOps.getOrDefault(PERSISTENT_DEVICE_ID_DEFAULT,
+ new ArrayMap<>()).get(attributionTag);
- final AttributedOp attributedOp = op.mAttributions.get(attributionTag);
if (attributedOp.isRunning()) {
long earliestElapsedTime = Long.MAX_VALUE;
long maxNumStarts = 0;
@@ -5828,7 +5999,7 @@
}
for (int i=0; i<mUidStates.size(); i++) {
UidState uidState = mUidStates.valueAt(i);
- // TODO(b/299330771): Get modes for all devices.
+ // TODO(b/299330771): Dump modes for all devices.
final SparseIntArray opModes =
mAppOpsCheckingService.getNonDefaultUidModes(
uidState.uid, PERSISTENT_DEVICE_ID_DEFAULT);
@@ -6043,11 +6214,13 @@
if (restrictionState.setRestriction(code, restricted, excludedPackageTags,
userHandle)) {
+ // Notify on PERSISTENT_DEVICE_ID_DEFAULT only as only the default device is
+ // affected by restrictions.
mHandler.sendMessage(PooledLambda.obtainMessage(
- AppOpsService::notifyWatchersOfChange, this, code, UID_ANY));
+ AppOpsService::notifyWatchersOnDefaultDevice, this, code, UID_ANY));
mHandler.sendMessage(PooledLambda.obtainMessage(
- AppOpsService::updateStartedOpModeForUser, this, code, restricted,
- userHandle));
+ AppOpsService::updateStartedOpModeForUserForDefaultDevice, this, code,
+ restricted, userHandle));
}
if (restrictionState.isDefault()) {
@@ -6057,7 +6230,8 @@
}
}
- private void updateStartedOpModeForUser(int code, boolean restricted, int userId) {
+ private void updateStartedOpModeForUserForDefaultDevice(int code, boolean restricted,
+ int userId) {
synchronized (AppOpsService.this) {
int numUids = mUidStates.size();
for (int uidNum = 0; uidNum < numUids; uidNum++) {
@@ -6065,12 +6239,13 @@
if (userId != UserHandle.USER_ALL && UserHandle.getUserId(uid) != userId) {
continue;
}
- updateStartedOpModeForUidLocked(code, restricted, uid);
+ updateStartedOpModeForUidForDefaultDeviceLocked(code, restricted, uid);
}
}
}
- private void updateStartedOpModeForUidLocked(int code, boolean restricted, int uid) {
+ private void updateStartedOpModeForUidForDefaultDeviceLocked(int code, boolean restricted,
+ int uid) {
UidState uidState = mUidStates.get(uid);
if (uidState == null) {
return;
@@ -6089,9 +6264,11 @@
if (mode != MODE_ALLOWED && mode != MODE_FOREGROUND) {
continue;
}
- int numAttrTags = op.mAttributions.size();
- for (int attrNum = 0; attrNum < numAttrTags; attrNum++) {
- AttributedOp attrOp = op.mAttributions.valueAt(attrNum);
+ ArrayMap<String, AttributedOp> defaultDeviceAttributedOps = op.mDeviceAttributedOps.get(
+ PERSISTENT_DEVICE_ID_DEFAULT);
+ for (int tagIndex = 0; tagIndex < defaultDeviceAttributedOps.size();
+ tagIndex++) {
+ AttributedOp attrOp = defaultDeviceAttributedOps.valueAt(tagIndex);
if (restricted && attrOp.isRunning()) {
attrOp.pause();
} else if (attrOp.isPaused()) {
@@ -6101,7 +6278,7 @@
}
}
- private void notifyWatchersOfChange(int code, int uid) {
+ private void notifyWatchersOnDefaultDevice(int code, int uid) {
final ArraySet<OnOpModeChangedListener> modeChangedListenerSet;
synchronized (this) {
modeChangedListenerSet = mOpModeWatchers.get(code);
@@ -6109,8 +6286,7 @@
return;
}
}
-
- notifyOpChanged(modeChangedListenerSet, code, uid, null);
+ notifyOpChanged(modeChangedListenerSet, code, uid, null, PERSISTENT_DEVICE_ID_DEFAULT);
}
@Override
@@ -6582,6 +6758,25 @@
return packageNames;
}
+ @NonNull private String getPersistentId(int virtualDeviceId) {
+ if (virtualDeviceId == Context.DEVICE_ID_DEFAULT) {
+ return PERSISTENT_DEVICE_ID_DEFAULT;
+ }
+ if (mVirtualDeviceManagerInternal == null) {
+ return PERSISTENT_DEVICE_ID_DEFAULT;
+ }
+ String persistentId =
+ mVirtualDeviceManagerInternal.getPersistentIdForDevice(virtualDeviceId);
+ if (persistentId == null) {
+ persistentId = mKnownDeviceIds.get(virtualDeviceId);
+ }
+ if (persistentId != null) {
+ return persistentId;
+ }
+ throw new IllegalStateException(
+ "Requested persistentId for invalid virtualDeviceId: " + virtualDeviceId);
+ }
+
private final class ClientUserRestrictionState implements DeathRecipient {
private final IBinder token;
@@ -6713,12 +6908,14 @@
}
if (restrictionState.setRestriction(code, restricted)) {
+ // Notify on PERSISTENT_DEVICE_ID_DEFAULT only as only the default device is
+ // affected by restrictions.
mHandler.sendMessage(PooledLambda.obtainMessage(
- AppOpsService::notifyWatchersOfChange, AppOpsService.this, code,
- UID_ANY));
+ AppOpsService::notifyWatchersOnDefaultDevice, AppOpsService.this,
+ code, UID_ANY));
mHandler.sendMessage(PooledLambda.obtainMessage(
- AppOpsService::updateStartedOpModeForUser, AppOpsService.this,
- code, restricted, UserHandle.USER_ALL));
+ AppOpsService::updateStartedOpModeForUserForDefaultDevice,
+ AppOpsService.this, code, restricted, UserHandle.USER_ALL));
}
if (restrictionState.isDefault()) {
diff --git a/services/core/java/com/android/server/appop/AttributedOp.java b/services/core/java/com/android/server/appop/AttributedOp.java
index 0ded75a..94baf88 100644
--- a/services/core/java/com/android/server/appop/AttributedOp.java
+++ b/services/core/java/com/android/server/appop/AttributedOp.java
@@ -42,6 +42,7 @@
final class AttributedOp {
private final @NonNull AppOpsService mAppOpsService;
public final @Nullable String tag;
+ public final @NonNull String persistentDeviceId;
public final @NonNull AppOpsService.Op parent;
/**
@@ -81,9 +82,10 @@
@Nullable ArrayMap<IBinder, InProgressStartOpEvent> mPausedInProgressEvents;
AttributedOp(@NonNull AppOpsService appOpsService, @Nullable String tag,
- @NonNull AppOpsService.Op parent) {
+ @NonNull String persistentDeviceId, @NonNull AppOpsService.Op parent) {
mAppOpsService = appOpsService;
this.tag = tag;
+ this.persistentDeviceId = persistentDeviceId;
this.parent = parent;
}
@@ -196,23 +198,26 @@
*/
public void started(@NonNull IBinder clientId, int proxyUid,
@Nullable String proxyPackageName, @Nullable String proxyAttributionTag,
- @AppOpsManager.UidState int uidState, @AppOpsManager.OpFlags int flags,
- @AppOpsManager.AttributionFlags
- int attributionFlags, int attributionChainId) throws RemoteException {
+ int proxyVirtualDeviceId, @AppOpsManager.UidState int uidState,
+ @AppOpsManager.OpFlags int flags, @AppOpsManager.AttributionFlags int attributionFlags,
+ int attributionChainId) throws RemoteException {
startedOrPaused(clientId, proxyUid, proxyPackageName,
- proxyAttributionTag, uidState, flags, /* triggeredByUidStateChange */ false,
- /* isStarted */ true, attributionFlags, attributionChainId);
+ proxyAttributionTag, proxyVirtualDeviceId, uidState, flags,
+ /* triggeredByUidStateChange */ false, /* isStarted */ true, attributionFlags,
+ attributionChainId);
}
@SuppressWarnings("GuardedBy") // Lock is held on mAppOpsService
private void startedOrPaused(@NonNull IBinder clientId, int proxyUid,
@Nullable String proxyPackageName, @Nullable String proxyAttributionTag,
- @AppOpsManager.UidState int uidState, @AppOpsManager.OpFlags int flags,
- boolean triggeredByUidStateChange, boolean isStarted, @AppOpsManager.AttributionFlags
- int attributionFlags, int attributionChainId) throws RemoteException {
+ int proxyVirtualDeviceId, @AppOpsManager.UidState int uidState,
+ @AppOpsManager.OpFlags int flags, boolean triggeredByUidStateChange,
+ boolean isStarted, @AppOpsManager.AttributionFlags int attributionFlags,
+ int attributionChainId) throws RemoteException {
if (!triggeredByUidStateChange && !parent.isRunning() && isStarted) {
mAppOpsService.scheduleOpActiveChangedIfNeededLocked(parent.op, parent.uid,
- parent.packageName, tag, true, attributionFlags, attributionChainId);
+ parent.packageName, tag, proxyVirtualDeviceId, true, attributionFlags,
+ attributionChainId);
}
if (isStarted && mInProgressEvents == null) {
@@ -227,7 +232,7 @@
InProgressStartOpEvent event = events.get(clientId);
if (event == null) {
event = mAppOpsService.mInProgressStartOpEventPool.acquire(startTime,
- SystemClock.elapsedRealtime(), clientId, tag,
+ SystemClock.elapsedRealtime(), clientId, tag, proxyVirtualDeviceId,
PooledLambda.obtainRunnable(AppOpsService::onClientDeath, this, clientId),
proxyUid, proxyPackageName, proxyAttributionTag, uidState, flags,
attributionFlags, attributionChainId);
@@ -320,8 +325,8 @@
// TODO ntmyren: Also callback for single attribution tag activity changes
if (!triggeredByUidStateChange && !parent.isRunning()) {
mAppOpsService.scheduleOpActiveChangedIfNeededLocked(parent.op,
- parent.uid, parent.packageName, tag, false,
- event.getAttributionFlags(), event.getAttributionChainId());
+ parent.uid, parent.packageName, tag, event.getVirtualDeviceId(),
+ false, event.getAttributionFlags(), event.getAttributionChainId());
}
}
}
@@ -362,11 +367,13 @@
*/
public void createPaused(@NonNull IBinder clientId, int proxyUid,
@Nullable String proxyPackageName, @Nullable String proxyAttributionTag,
- @AppOpsManager.UidState int uidState, @AppOpsManager.OpFlags int flags,
- @AppOpsManager.AttributionFlags
- int attributionFlags, int attributionChainId) throws RemoteException {
+ int proxyVirtualDeviceId, @AppOpsManager.UidState int uidState,
+ @AppOpsManager.OpFlags int flags,
+ @AppOpsManager.AttributionFlags int attributionFlags,
+ int attributionChainId) throws RemoteException {
startedOrPaused(clientId, proxyUid, proxyPackageName, proxyAttributionTag,
- uidState, flags, false, false, attributionFlags, attributionChainId);
+ proxyVirtualDeviceId, uidState, flags, false, false,
+ attributionFlags, attributionChainId);
}
/**
@@ -387,7 +394,7 @@
finishOrPause(event.getClientId(), false, true);
mAppOpsService.scheduleOpActiveChangedIfNeededLocked(parent.op, parent.uid,
- parent.packageName, tag, false,
+ parent.packageName, tag, event.getVirtualDeviceId(), false,
event.getAttributionFlags(), event.getAttributionChainId());
}
mInProgressEvents = null;
@@ -419,14 +426,15 @@
event.getAttributionFlags(), event.getAttributionChainId());
if (shouldSendActive) {
mAppOpsService.scheduleOpActiveChangedIfNeededLocked(parent.op, parent.uid,
- parent.packageName, tag, true, event.getAttributionFlags(),
- event.getAttributionChainId());
+ parent.packageName, tag, event.getVirtualDeviceId(), true,
+ event.getAttributionFlags(), event.getAttributionChainId());
}
// Note: this always sends MODE_ALLOWED, even if the mode is FOREGROUND
// TODO ntmyren: figure out how to get the real mode.
mAppOpsService.scheduleOpStartedIfNeededLocked(parent.op, parent.uid,
- parent.packageName, tag, event.getFlags(), MODE_ALLOWED, START_TYPE_RESUMED,
- event.getAttributionFlags(), event.getAttributionChainId());
+ parent.packageName, tag, event.getVirtualDeviceId(), event.getFlags(),
+ MODE_ALLOWED, START_TYPE_RESUMED, event.getAttributionFlags(),
+ event.getAttributionChainId());
}
mPausedInProgressEvents = null;
}
@@ -488,13 +496,15 @@
// previously removed unfinished start counts back
if (proxy != null) {
startedOrPaused(event.getClientId(), proxy.getUid(),
- proxy.getPackageName(), proxy.getAttributionTag(), newState,
- event.getFlags(), true, isRunning,
+ proxy.getPackageName(), proxy.getAttributionTag(),
+ event.getVirtualDeviceId(), newState, event.getFlags(),
+ true, isRunning,
event.getAttributionFlags(), event.getAttributionChainId());
} else {
startedOrPaused(event.getClientId(), Process.INVALID_UID, null, null,
- newState, event.getFlags(), true, isRunning,
- event.getAttributionFlags(), event.getAttributionChainId());
+ event.getVirtualDeviceId(), newState, event.getFlags(), true,
+ isRunning, event.getAttributionFlags(),
+ event.getAttributionChainId());
}
events = isRunning ? mInProgressEvents : mPausedInProgressEvents;
@@ -508,7 +518,7 @@
"Cannot switch to new uidState " + newState);
}
mAppOpsService.scheduleOpActiveChangedIfNeededLocked(parent.op,
- parent.uid, parent.packageName, tag, false,
+ parent.uid, parent.packageName, tag, event.getVirtualDeviceId(), false,
eventAttributionFlags, eventAttributionChainId);
}
}
@@ -644,6 +654,9 @@
/** Id of the client that started the event */
private @NonNull IBinder mClientId;
+ /** virtual device id */
+ private int mVirtualDeviceId;
+
/** The attribution tag for this operation */
private @Nullable String mAttributionTag;
@@ -685,7 +698,7 @@
* @throws RemoteException If the client is dying
*/
InProgressStartOpEvent(long startTime, long startElapsedTime,
- @NonNull IBinder clientId, @Nullable String attributionTag,
+ @NonNull IBinder clientId, int virtualDeviceId, @Nullable String attributionTag,
@NonNull Runnable onDeath, @AppOpsManager.UidState int uidState,
@Nullable AppOpsManager.OpEventProxyInfo proxy, @AppOpsManager.OpFlags int flags,
@AppOpsManager.AttributionFlags int attributionFlags, int attributionChainId)
@@ -693,6 +706,7 @@
mStartTime = startTime;
mStartElapsedTime = startElapsedTime;
mClientId = clientId;
+ mVirtualDeviceId = virtualDeviceId;
mAttributionTag = attributionTag;
mOnDeath = onDeath;
mUidState = uidState;
@@ -737,7 +751,7 @@
* @throws RemoteException If the client is dying
*/
public void reinit(long startTime, long startElapsedTime, @NonNull IBinder clientId,
- @Nullable String attributionTag, @NonNull Runnable onDeath,
+ @Nullable String attributionTag, int virtualDeviceId, @NonNull Runnable onDeath,
@AppOpsManager.UidState int uidState, @AppOpsManager.OpFlags int flags,
@Nullable AppOpsManager.OpEventProxyInfo proxy,
@AppOpsManager.AttributionFlags int attributionFlags,
@@ -749,6 +763,7 @@
mClientId = clientId;
mAttributionTag = attributionTag;
mOnDeath = onDeath;
+ mVirtualDeviceId = virtualDeviceId;
mUidState = uidState;
mFlags = flags;
@@ -802,6 +817,11 @@
return mAttributionChainId;
}
+ /** @return virtual device id for the access */
+ public int getVirtualDeviceId() {
+ return mVirtualDeviceId;
+ }
+
public void setStartTime(long startTime) {
mStartTime = startTime;
}
@@ -824,11 +844,11 @@
}
InProgressStartOpEvent acquire(long startTime, long elapsedTime, @NonNull IBinder clientId,
- @Nullable String attributionTag, @NonNull Runnable onDeath, int proxyUid,
- @Nullable String proxyPackageName, @Nullable String proxyAttributionTag,
- @AppOpsManager.UidState int uidState, @AppOpsManager.OpFlags int flags,
- @AppOpsManager.AttributionFlags
- int attributionFlags, int attributionChainId) throws RemoteException {
+ @Nullable String attributionTag, int virtualDeviceId, @NonNull Runnable onDeath,
+ int proxyUid, @Nullable String proxyPackageName,
+ @Nullable String proxyAttributionTag, @AppOpsManager.UidState int uidState,
+ @AppOpsManager.OpFlags int flags, @AppOpsManager.AttributionFlags
+ int attributionFlags, int attributionChainId) throws RemoteException {
InProgressStartOpEvent recycled = acquire();
@@ -839,14 +859,15 @@
}
if (recycled != null) {
- recycled.reinit(startTime, elapsedTime, clientId, attributionTag, onDeath,
- uidState, flags, proxyInfo, attributionFlags, attributionChainId,
+ recycled.reinit(startTime, elapsedTime, clientId, attributionTag, virtualDeviceId,
+ onDeath, uidState, flags, proxyInfo, attributionFlags, attributionChainId,
mOpEventProxyInfoPool);
return recycled;
}
- return new InProgressStartOpEvent(startTime, elapsedTime, clientId, attributionTag,
- onDeath, uidState, proxyInfo, flags, attributionFlags, attributionChainId);
+ return new InProgressStartOpEvent(startTime, elapsedTime, clientId, virtualDeviceId,
+ attributionTag, onDeath, uidState, proxyInfo, flags, attributionFlags,
+ attributionChainId);
}
}
diff --git a/services/core/java/com/android/server/appop/OnOpModeChangedListener.java b/services/core/java/com/android/server/appop/OnOpModeChangedListener.java
index 1d1a9e7..f5f34c1 100644
--- a/services/core/java/com/android/server/appop/OnOpModeChangedListener.java
+++ b/services/core/java/com/android/server/appop/OnOpModeChangedListener.java
@@ -16,8 +16,11 @@
package com.android.server.appop;
+import android.companion.virtual.VirtualDeviceManager;
import android.os.RemoteException;
+import java.util.Objects;
+
/**
* Listener for mode changes, encapsulates methods that should be triggered in the event of a mode
* change.
@@ -95,6 +98,20 @@
throws RemoteException;
/**
+ * Method that should be triggered when the app-op's mode is changed.
+ * @param op app-op whose mode-change is being listened to.
+ * @param uid user-is associated with the app-op.
+ * @param packageName package name associated with the app-op.
+ * @param persistentDeviceId device associated with the app-op.
+ */
+ public void onOpModeChanged(int op, int uid, String packageName, String persistentDeviceId)
+ throws RemoteException {
+ if (Objects.equals(persistentDeviceId, VirtualDeviceManager.PERSISTENT_DEVICE_ID_DEFAULT)) {
+ onOpModeChanged(op, uid, packageName);
+ }
+ }
+
+ /**
* Return human readable string representing the listener.
*/
public abstract String toString();
diff --git a/services/core/java/com/android/server/biometrics/sensors/AcquisitionClient.java b/services/core/java/com/android/server/biometrics/sensors/AcquisitionClient.java
index aa6a0f1..fbd32a6 100644
--- a/services/core/java/com/android/server/biometrics/sensors/AcquisitionClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/AcquisitionClient.java
@@ -112,10 +112,8 @@
getLogger().logOnError(getContext(), getOperationContext(),
errorCode, vendorCode, getTargetUserId());
try {
- if (getListener() != null) {
- mShouldSendErrorToClient = false;
- getListener().onError(getSensorId(), getCookie(), errorCode, vendorCode);
- }
+ mShouldSendErrorToClient = false;
+ getListener().onError(getSensorId(), getCookie(), errorCode, vendorCode);
} catch (RemoteException e) {
Slog.w(TAG, "Failed to invoke sendError", e);
}
@@ -147,9 +145,7 @@
final int errorCode = BiometricConstants.BIOMETRIC_ERROR_CANCELED;
try {
- if (getListener() != null) {
- getListener().onError(getSensorId(), getCookie(), errorCode, 0 /* vendorCode */);
- }
+ getListener().onError(getSensorId(), getCookie(), errorCode, 0 /* vendorCode */);
} catch (RemoteException e) {
Slog.w(TAG, "Failed to invoke sendError", e);
}
@@ -181,7 +177,7 @@
}
try {
- if (getListener() != null && shouldSend) {
+ if (shouldSend) {
getListener().onAcquired(getSensorId(), acquiredInfo, vendorCode);
}
} catch (RemoteException e) {
diff --git a/services/core/java/com/android/server/biometrics/sensors/AuthenticationClient.java b/services/core/java/com/android/server/biometrics/sensors/AuthenticationClient.java
index f9568ea..506b456 100644
--- a/services/core/java/com/android/server/biometrics/sensors/AuthenticationClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/AuthenticationClient.java
@@ -265,17 +265,13 @@
Slog.d(TAG, "Skipping addAuthToken");
}
try {
- if (listener != null) {
- if (!mIsRestricted) {
- listener.onAuthenticationSucceeded(getSensorId(), identifier, byteToken,
- getTargetUserId(), mIsStrongBiometric);
- } else {
- listener.onAuthenticationSucceeded(getSensorId(), null /* identifier */,
- byteToken,
- getTargetUserId(), mIsStrongBiometric);
- }
+ if (!mIsRestricted) {
+ listener.onAuthenticationSucceeded(getSensorId(), identifier, byteToken,
+ getTargetUserId(), mIsStrongBiometric);
} else {
- Slog.e(TAG, "Received successful auth, but client was not listening");
+ listener.onAuthenticationSucceeded(getSensorId(), null /* identifier */,
+ byteToken,
+ getTargetUserId(), mIsStrongBiometric);
}
} catch (RemoteException e) {
Slog.e(TAG, "Unable to notify listener", e);
@@ -301,11 +297,7 @@
}
try {
- if (listener != null) {
- listener.onAuthenticationFailed(getSensorId());
- } else {
- Slog.e(TAG, "Received failed auth, but client was not listening");
- }
+ listener.onAuthenticationFailed(getSensorId());
} catch (RemoteException e) {
Slog.e(TAG, "Unable to notify listener", e);
mCallback.onClientFinished(this, false);
diff --git a/services/core/java/com/android/server/biometrics/sensors/BaseClientMonitor.java b/services/core/java/com/android/server/biometrics/sensors/BaseClientMonitor.java
index 0216e49..a408852 100644
--- a/services/core/java/com/android/server/biometrics/sensors/BaseClientMonitor.java
+++ b/services/core/java/com/android/server/biometrics/sensors/BaseClientMonitor.java
@@ -22,6 +22,7 @@
import android.annotation.Nullable;
import android.content.Context;
import android.hardware.biometrics.BiometricConstants;
+import android.hardware.biometrics.IBiometricSensorReceiver;
import android.os.IBinder;
import android.os.RemoteException;
import android.util.Slog;
@@ -55,7 +56,7 @@
@Nullable private IBinder mToken;
private long mRequestId;
- @Nullable private ClientMonitorCallbackConverter mListener;
+ @NonNull private ClientMonitorCallbackConverter mListener;
// Currently only used for authentication client. The cookie generated by BiometricService
// is never 0.
private final int mCookie;
@@ -95,7 +96,8 @@
mContext = context;
mToken = token;
mRequestId = -1;
- mListener = listener;
+ mListener = listener == null ? new ClientMonitorCallbackConverter(
+ new IBiometricSensorReceiver.Default()) : listener;
mTargetUserId = userId;
mOwner = owner;
mCookie = cookie;
@@ -199,7 +201,7 @@
}
mToken = null;
if (clearListener) {
- mListener = null;
+ mListener = new ClientMonitorCallbackConverter(new IBiometricSensorReceiver.Default());
}
}
@@ -233,8 +235,8 @@
return mOwner;
}
- @Nullable
- public final ClientMonitorCallbackConverter getListener() {
+ @NonNull
+ protected ClientMonitorCallbackConverter getListener() {
return mListener;
}
@@ -312,9 +314,7 @@
final int errorCode = BiometricConstants.BIOMETRIC_ERROR_CANCELED;
try {
ClientMonitorCallbackConverter listener = getListener();
- if (listener != null) {
- listener.onError(getSensorId(), getCookie(), errorCode, 0 /* vendorCode */);
- }
+ listener.onError(getSensorId(), getCookie(), errorCode, 0 /* vendorCode */);
} catch (RemoteException e) {
Slog.w(TAG, "Failed to invoke sendError", e);
}
diff --git a/services/core/java/com/android/server/biometrics/sensors/EnrollClient.java b/services/core/java/com/android/server/biometrics/sensors/EnrollClient.java
index 2c4d30b..8e7004d 100644
--- a/services/core/java/com/android/server/biometrics/sensors/EnrollClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/EnrollClient.java
@@ -82,9 +82,7 @@
final ClientMonitorCallbackConverter listener = getListener();
try {
- if (listener != null) {
- listener.onEnrollResult(identifier, remaining);
- }
+ listener.onEnrollResult(identifier, remaining);
} catch (RemoteException e) {
Slog.e(TAG, "Remote exception", e);
}
diff --git a/services/core/java/com/android/server/biometrics/sensors/RemovalClient.java b/services/core/java/com/android/server/biometrics/sensors/RemovalClient.java
index 45ffa23..d2ef278 100644
--- a/services/core/java/com/android/server/biometrics/sensors/RemovalClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/RemovalClient.java
@@ -74,13 +74,9 @@
if (identifier == null) {
Slog.e(TAG, "identifier was null, skipping onRemove()");
try {
- if (getListener() != null) {
- getListener().onError(getSensorId(), getCookie(),
- BiometricConstants.BIOMETRIC_ERROR_UNABLE_TO_REMOVE,
- 0 /* vendorCode */);
- } else {
- Slog.e(TAG, "Error, listener was null, not sending onError callback");
- }
+ getListener().onError(getSensorId(), getCookie(),
+ BiometricConstants.BIOMETRIC_ERROR_UNABLE_TO_REMOVE,
+ 0 /* vendorCode */);
} catch (RemoteException e) {
Slog.w(TAG, "Failed to send error to client for onRemoved", e);
}
@@ -93,9 +89,7 @@
identifier.getBiometricId());
try {
- if (getListener() != null) {
- getListener().onRemoved(identifier, remaining);
- }
+ getListener().onRemoved(identifier, remaining);
} catch (RemoteException e) {
Slog.w(TAG, "Failed to notify Removed:", e);
}
diff --git a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceAuthenticationClient.java b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceAuthenticationClient.java
index f35de93..415d294 100644
--- a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceAuthenticationClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceAuthenticationClient.java
@@ -337,7 +337,7 @@
onAcquiredInternal(acquireInfo, vendorCode, false /* shouldSend */);
final boolean shouldSend = shouldSendAcquiredMessage(acquireInfo, vendorCode);
- if (shouldSend && getListener() != null) {
+ if (shouldSend) {
try {
getListener().onAuthenticationFrame(frame);
} catch (RemoteException e) {
diff --git a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceEnrollClient.java b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceEnrollClient.java
index f5c4529..5f370f2 100644
--- a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceEnrollClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceEnrollClient.java
@@ -152,7 +152,7 @@
onAcquiredInternal(acquireInfo, vendorCode, false /* shouldSend */);
final boolean shouldSend = shouldSendAcquiredMessage(acquireInfo, vendorCode);
- if (shouldSend && getListener() != null) {
+ if (shouldSend) {
try {
getListener().onEnrollmentFrame(frame);
} catch (RemoteException e) {
diff --git a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceGenerateChallengeClient.java b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceGenerateChallengeClient.java
index e404bd2..cf45eb8 100644
--- a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceGenerateChallengeClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceGenerateChallengeClient.java
@@ -58,11 +58,6 @@
void onChallengeGenerated(int sensorId, int userId, long challenge) {
try {
final ClientMonitorCallbackConverter listener = getListener();
- if (listener == null) {
- Slog.e(TAG, "Listener is null in onChallengeGenerated");
- mCallback.onClientFinished(this, false /* success */);
- return;
- }
listener.onChallengeGenerated(sensorId, userId, challenge);
mCallback.onClientFinished(this, true /* success */);
} catch (RemoteException e) {
diff --git a/services/core/java/com/android/server/biometrics/sensors/face/hidl/FaceGetFeatureClient.java b/services/core/java/com/android/server/biometrics/sensors/face/hidl/FaceGetFeatureClient.java
index 9812536..47aaeec 100644
--- a/services/core/java/com/android/server/biometrics/sensors/face/hidl/FaceGetFeatureClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/face/hidl/FaceGetFeatureClient.java
@@ -61,9 +61,7 @@
@Override
public void unableToStart() {
try {
- if (getListener() != null) {
- getListener().onFeatureGet(false /* success */, new int[0], new boolean[0]);
- }
+ getListener().onFeatureGet(false /* success */, new int[0], new boolean[0]);
} catch (RemoteException e) {
Slog.e(TAG, "Unable to send error", e);
}
@@ -85,9 +83,7 @@
featureState[0] = result.value;
mValue = result.value;
- if (getListener() != null) {
- getListener().onFeatureGet(result.status == Status.OK, features, featureState);
- }
+ getListener().onFeatureGet(result.status == Status.OK, features, featureState);
mCallback.onClientFinished(this, true /* success */);
} catch (RemoteException e) {
Slog.e(TAG, "Unable to getFeature", e);
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java
index 6912961..e0fd44b 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java
@@ -450,9 +450,7 @@
pc.major);
}
- if (getListener() != null) {
- getListener().onUdfpsPointerDown(getSensorId());
- }
+ getListener().onUdfpsPointerDown(getSensorId());
} catch (RemoteException e) {
Slog.e(TAG, "Remote exception", e);
}
@@ -471,9 +469,7 @@
session.getSession().onPointerUp(pc.pointerId);
}
- if (getListener() != null) {
- getListener().onUdfpsPointerUp(getSensorId());
- }
+ getListener().onUdfpsPointerUp(getSensorId());
} catch (RemoteException e) {
Slog.e(TAG, "Remote exception", e);
}
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintDetectClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintDetectClient.java
index a7fb774..cb220b9e 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintDetectClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintDetectClient.java
@@ -175,11 +175,7 @@
vibrateSuccess();
try {
- if (getListener() != null) {
- getListener().onDetected(getSensorId(), getTargetUserId(), mIsStrongBiometric);
- } else {
- Slog.e(TAG, "Listener is null!");
- }
+ getListener().onDetected(getSensorId(), getTargetUserId(), mIsStrongBiometric);
mCallback.onClientFinished(this, true /* success */);
} catch (RemoteException e) {
Slog.e(TAG, "Remote exception when sending onDetected", e);
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintEnrollClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintEnrollClient.java
index 3fb9223..225bd59 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintEnrollClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintEnrollClient.java
@@ -305,9 +305,7 @@
pc.major);
}
- if (getListener() != null) {
- getListener().onUdfpsPointerDown(getSensorId());
- }
+ getListener().onUdfpsPointerDown(getSensorId());
} catch (RemoteException e) {
Slog.e(TAG, "Unable to send pointer down", e);
}
@@ -325,9 +323,7 @@
session.getSession().onPointerUp(pc.pointerId);
}
- if (getListener() != null) {
- getListener().onUdfpsPointerUp(getSensorId());
- }
+ getListener().onUdfpsPointerUp(getSensorId());
} catch (RemoteException e) {
Slog.e(TAG, "Unable to send pointer up", e);
}
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintGenerateChallengeClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintGenerateChallengeClient.java
index ce693ff..d2f36ce 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintGenerateChallengeClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintGenerateChallengeClient.java
@@ -59,11 +59,6 @@
void onChallengeGenerated(int sensorId, int userId, long challenge) {
try {
final ClientMonitorCallbackConverter listener = getListener();
- if (listener == null) {
- Slog.e(TAG, "Listener is null in onChallengeGenerated");
- mCallback.onClientFinished(this, false /* success */);
- return;
- }
listener.onChallengeGenerated(sensorId, userId, challenge);
mCallback.onClientFinished(this, true /* success */);
} catch (RemoteException e) {
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/Fingerprint21UdfpsMock.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/Fingerprint21UdfpsMock.java
index 9232e11..f857946 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/Fingerprint21UdfpsMock.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/Fingerprint21UdfpsMock.java
@@ -28,6 +28,7 @@
import android.hardware.fingerprint.FingerprintManager.AuthenticationResult;
import android.hardware.fingerprint.FingerprintSensorProperties;
import android.hardware.fingerprint.FingerprintSensorPropertiesInternal;
+import android.hardware.fingerprint.IFingerprintServiceReceiver;
import android.hardware.fingerprint.IUdfpsOverlayController;
import android.os.Handler;
import android.os.IBinder;
@@ -367,7 +368,8 @@
final IBinder token = client.getToken();
final long operationId = authClient.getOperationId();
final int cookie = client.getCookie();
- final ClientMonitorCallbackConverter listener = client.getListener();
+ final ClientMonitorCallbackConverter listener = new ClientMonitorCallbackConverter(
+ new IFingerprintServiceReceiver.Default());
final boolean restricted = authClient.isRestricted();
final int statsClient = client.getLogger().getStatsClient();
final boolean isKeyguard = authClient.isKeyguard();
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintAuthenticationClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintAuthenticationClient.java
index 7a329e9..60c532c 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintAuthenticationClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintAuthenticationClient.java
@@ -279,12 +279,10 @@
mALSProbeCallback.getProbe().enable();
UdfpsHelper.onFingerDown(getFreshDaemon(), (int) pc.x, (int) pc.y, pc.minor, pc.major);
- if (getListener() != null) {
- try {
- getListener().onUdfpsPointerDown(getSensorId());
- } catch (RemoteException e) {
- Slog.e(TAG, "Remote exception", e);
- }
+ try {
+ getListener().onUdfpsPointerDown(getSensorId());
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Remote exception", e);
}
}
@@ -295,12 +293,10 @@
mALSProbeCallback.getProbe().disable();
UdfpsHelper.onFingerUp(getFreshDaemon());
- if (getListener() != null) {
- try {
- getListener().onUdfpsPointerUp(getSensorId());
- } catch (RemoteException e) {
- Slog.e(TAG, "Remote exception", e);
- }
+ try {
+ getListener().onUdfpsPointerUp(getSensorId());
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Remote exception", e);
}
}
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintDetectClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintDetectClient.java
index 6e029d2..50e48fe 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintDetectClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintDetectClient.java
@@ -153,12 +153,10 @@
final PerformanceTracker pm = PerformanceTracker.getInstanceForSensorId(getSensorId());
pm.incrementAuthForUser(getTargetUserId(), authenticated);
- if (getListener() != null) {
- try {
- getListener().onDetected(getSensorId(), getTargetUserId(), mIsStrongBiometric);
- } catch (RemoteException e) {
- Slog.e(TAG, "Remote exception when sending onDetected", e);
- }
+ try {
+ getListener().onDetected(getSensorId(), getTargetUserId(), mIsStrongBiometric);
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Remote exception when sending onDetected", e);
}
}
diff --git a/services/core/java/com/android/server/content/SyncManager.java b/services/core/java/com/android/server/content/SyncManager.java
index df179a9..5b23364c 100644
--- a/services/core/java/com/android/server/content/SyncManager.java
+++ b/services/core/java/com/android/server/content/SyncManager.java
@@ -138,6 +138,7 @@
import java.util.HashSet;
import java.util.List;
import java.util.Map;
+import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.Random;
import java.util.Set;
@@ -3870,8 +3871,12 @@
final SyncStorageEngine.EndPoint info = syncOperation.target;
if (activeSyncContext.mIsLinkedToDeath) {
- activeSyncContext.mSyncAdapter.asBinder().unlinkToDeath(activeSyncContext, 0);
- activeSyncContext.mIsLinkedToDeath = false;
+ try {
+ activeSyncContext.mSyncAdapter.asBinder().unlinkToDeath(activeSyncContext, 0);
+ activeSyncContext.mIsLinkedToDeath = false;
+ } catch (NoSuchElementException e) {
+ Slog.wtf(TAG, "Failed to unlink active sync adapter to death", e);
+ }
}
final long elapsedTime = SystemClock.elapsedRealtime() - activeSyncContext.mStartTime;
String historyMessage;
diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java
index 245fcea..93addcd 100644
--- a/services/core/java/com/android/server/display/DisplayManagerService.java
+++ b/services/core/java/com/android/server/display/DisplayManagerService.java
@@ -1656,14 +1656,16 @@
ContentRecordingSession session = null;
try {
if (projection != null) {
- IBinder launchCookie = projection.getLaunchCookie();
- if (launchCookie == null) {
+ IBinder taskWindowContainerToken = projection.getLaunchCookie() == null ? null
+ : projection.getLaunchCookie().binder;
+ if (taskWindowContainerToken == null) {
// Record a particular display.
session = ContentRecordingSession.createDisplaySession(
virtualDisplayConfig.getDisplayIdToMirror());
} else {
// Record a single task indicated by the launch cookie.
- session = ContentRecordingSession.createTaskSession(launchCookie);
+ session = ContentRecordingSession.createTaskSession(
+ taskWindowContainerToken);
}
}
} catch (RemoteException e) {
diff --git a/services/core/java/com/android/server/display/mode/DisplayModeDirector.java b/services/core/java/com/android/server/display/mode/DisplayModeDirector.java
index 82461fa..8b4e1ff 100644
--- a/services/core/java/com/android/server/display/mode/DisplayModeDirector.java
+++ b/services/core/java/com/android/server/display/mode/DisplayModeDirector.java
@@ -394,6 +394,8 @@
if (mDisplayObserver.isExternalDisplayLocked(displayId)) {
primarySummary.maxRenderFrameRate = Math.max(baseMode.getRefreshRate(),
primarySummary.maxRenderFrameRate);
+ appRequestSummary.maxRenderFrameRate = Math.max(baseMode.getRefreshRate(),
+ appRequestSummary.maxRenderFrameRate);
}
return new DesiredDisplayModeSpecs(baseMode.getModeId(),
diff --git a/services/core/java/com/android/server/graphics/fonts/UpdatableFontDir.java b/services/core/java/com/android/server/graphics/fonts/UpdatableFontDir.java
index 0f40ca0..1715254 100644
--- a/services/core/java/com/android/server/graphics/fonts/UpdatableFontDir.java
+++ b/services/core/java/com/android/server/graphics/fonts/UpdatableFontDir.java
@@ -16,8 +16,6 @@
package com.android.server.graphics.fonts;
-import static android.graphics.fonts.FontFamily.Builder.VARIABLE_FONT_FAMILY_TYPE_NONE;
-
import static com.android.server.graphics.fonts.FontManagerService.SystemFontException;
import android.annotation.NonNull;
@@ -580,11 +578,11 @@
return null;
}
resolvedFonts.add(new FontConfig.Font(info.mFile, null, info.getPostScriptName(),
- font.getFontStyle(), font.getIndex(), font.getFontVariationSettings(), null));
+ font.getFontStyle(), font.getIndex(), font.getFontVariationSettings(),
+ null /* family name */, FontConfig.Font.VAR_TYPE_AXES_NONE));
}
FontConfig.FontFamily family = new FontConfig.FontFamily(resolvedFonts,
- LocaleList.getEmptyLocaleList(), FontConfig.FontFamily.VARIANT_DEFAULT,
- VARIABLE_FONT_FAMILY_TYPE_NONE);
+ LocaleList.getEmptyLocaleList(), FontConfig.FontFamily.VARIANT_DEFAULT);
return new FontConfig.NamedFamilyList(Collections.singletonList(family),
fontFamily.getName());
}
diff --git a/services/core/java/com/android/server/hdmi/HdmiCecLocalDevicePlayback.java b/services/core/java/com/android/server/hdmi/HdmiCecLocalDevicePlayback.java
index ba4d320..731c78e 100644
--- a/services/core/java/com/android/server/hdmi/HdmiCecLocalDevicePlayback.java
+++ b/services/core/java/com/android/server/hdmi/HdmiCecLocalDevicePlayback.java
@@ -514,6 +514,18 @@
protected int handleRoutingInformation(HdmiCecMessage message) {
assertRunOnServiceThread();
int physicalAddress = HdmiUtils.twoBytesToInt(message.getParams());
+ HdmiDeviceInfo sourceDevice = mService.getHdmiCecNetwork()
+ .getCecDeviceInfo(message.getSource());
+ // Ignore <Routing Information> messages pointing to the same physical address as the
+ // message sender. In this case, we shouldn't consider the sender to be the active source.
+ // See more b/321771821#comment7.
+ if (sourceDevice != null
+ && sourceDevice.getLogicalAddress() != Constants.ADDR_TV
+ && sourceDevice.getPhysicalAddress() == physicalAddress) {
+ Slog.d(TAG, "<Routing Information> is ignored, it is pointing to the same physical"
+ + " address as the message sender");
+ return Constants.HANDLED;
+ }
handleRoutingChangeAndInformation(physicalAddress, message);
return Constants.HANDLED;
}
diff --git a/services/core/java/com/android/server/hdmi/OneTouchPlayAction.java b/services/core/java/com/android/server/hdmi/OneTouchPlayAction.java
index 30a4f31..3c3cfe6 100644
--- a/services/core/java/com/android/server/hdmi/OneTouchPlayAction.java
+++ b/services/core/java/com/android/server/hdmi/OneTouchPlayAction.java
@@ -45,6 +45,13 @@
@VisibleForTesting
static final int STATE_WAITING_FOR_REPORT_POWER_STATUS = 1;
+ // State in which the action is delayed. If the action starts and
+ // {@link PowerManager#isInteractive} returns false, it could indicate the beginning of a
+ // standby process. In this scenario, the action will be removed when
+ // {@link HdmiCecLocalDeviceSource#disableDevice} is called, therefore we delay the action.
+ @VisibleForTesting
+ static final int STATE_CHECK_STANDBY_PROCESS_STARTED = 2;
+
// The maximum number of times we send <Give Device Power Status> before we give up.
// We wait up to RESPONSE_TIMEOUT_MS * LOOP_COUNTER_MAX = 20 seconds.
private static final int LOOP_COUNTER_MAX = 10;
@@ -87,6 +94,22 @@
boolean start() {
// Because only source device can create this action, it's safe to cast.
mSource = source();
+
+ if (!mSource.mService.getPowerManager().isInteractive()) {
+ Slog.d(TAG, "PowerManager is not interactive. Delay the action to check if standby"
+ + " started!");
+ mState = STATE_CHECK_STANDBY_PROCESS_STARTED;
+ addTimer(mState, HdmiConfig.TIMEOUT_MS);
+ } else {
+ startAction();
+ }
+
+ return true;
+ }
+
+ private void startAction() {
+ Slog.i(TAG, "Start action.");
+
sendCommand(HdmiCecMessageBuilder.buildTextViewOn(getSourceAddress(), mTargetAddress));
boolean is20TargetOnBefore = mIsCec20 && getTargetDevicePowerStatus(mSource, mTargetAddress,
@@ -116,12 +139,11 @@
maySendActiveSource();
}
finishWithCallback(HdmiControlManager.RESULT_SUCCESS);
- return true;
+ return;
}
}
mState = STATE_WAITING_FOR_REPORT_POWER_STATUS;
addTimer(mState, HdmiConfig.TIMEOUT_MS);
- return true;
}
private void setAndBroadcastActiveSource() {
@@ -174,14 +196,22 @@
if (mState != state) {
return;
}
- if (state == STATE_WAITING_FOR_REPORT_POWER_STATUS) {
- if (mPowerStatusCounter++ < LOOP_COUNTER_MAX) {
- queryDevicePowerStatus();
- addTimer(mState, HdmiConfig.TIMEOUT_MS);
- } else {
- // Couldn't wake up the TV for whatever reason. Report failure.
- finishWithCallback(HdmiControlManager.RESULT_TIMEOUT);
- }
+ switch (state) {
+ case STATE_WAITING_FOR_REPORT_POWER_STATUS:
+ if (mPowerStatusCounter++ < LOOP_COUNTER_MAX) {
+ queryDevicePowerStatus();
+ addTimer(mState, HdmiConfig.TIMEOUT_MS);
+ } else {
+ // Couldn't wake up the TV for whatever reason. Report failure.
+ finishWithCallback(HdmiControlManager.RESULT_TIMEOUT);
+ }
+ return;
+ case STATE_CHECK_STANDBY_PROCESS_STARTED:
+ Slog.d(TAG, "Action was not removed, start the action.");
+ startAction();
+ return;
+ default:
+ return;
}
}
diff --git a/services/core/java/com/android/server/input/InputSettingsObserver.java b/services/core/java/com/android/server/input/InputSettingsObserver.java
index 165dfe4..5ffc380 100644
--- a/services/core/java/com/android/server/input/InputSettingsObserver.java
+++ b/services/core/java/com/android/server/input/InputSettingsObserver.java
@@ -64,6 +64,8 @@
(reason) -> updateTouchpadNaturalScrollingEnabled()),
Map.entry(Settings.System.getUriFor(Settings.System.TOUCHPAD_TAP_TO_CLICK),
(reason) -> updateTouchpadTapToClickEnabled()),
+ Map.entry(Settings.System.getUriFor(Settings.System.TOUCHPAD_TAP_DRAGGING),
+ (reason) -> updateTouchpadTapDraggingEnabled()),
Map.entry(Settings.System.getUriFor(Settings.System.TOUCHPAD_RIGHT_CLICK_ZONE),
(reason) -> updateTouchpadRightClickZoneEnabled()),
Map.entry(Settings.System.getUriFor(Settings.System.SHOW_TOUCHES),
@@ -158,6 +160,10 @@
mNative.setTouchpadTapToClickEnabled(InputSettings.useTouchpadTapToClick(mContext));
}
+ private void updateTouchpadTapDraggingEnabled() {
+ mNative.setTouchpadTapDraggingEnabled(InputSettings.useTouchpadTapDragging(mContext));
+ }
+
private void updateTouchpadRightClickZoneEnabled() {
mNative.setTouchpadRightClickZoneEnabled(InputSettings.useTouchpadRightClickZone(mContext));
}
diff --git a/services/core/java/com/android/server/input/NativeInputManagerService.java b/services/core/java/com/android/server/input/NativeInputManagerService.java
index bc82078..e5f3484 100644
--- a/services/core/java/com/android/server/input/NativeInputManagerService.java
+++ b/services/core/java/com/android/server/input/NativeInputManagerService.java
@@ -129,6 +129,8 @@
void setTouchpadTapToClickEnabled(boolean enabled);
+ void setTouchpadTapDraggingEnabled(boolean enabled);
+
void setTouchpadRightClickZoneEnabled(boolean enabled);
void setShowTouches(boolean enabled);
@@ -377,6 +379,9 @@
public native void setTouchpadTapToClickEnabled(boolean enabled);
@Override
+ public native void setTouchpadTapDraggingEnabled(boolean enabled);
+
+ @Override
public native void setTouchpadRightClickZoneEnabled(boolean enabled);
@Override
diff --git a/services/core/java/com/android/server/inputmethod/ClientController.java b/services/core/java/com/android/server/inputmethod/ClientController.java
index ece236a..86f4db9 100644
--- a/services/core/java/com/android/server/inputmethod/ClientController.java
+++ b/services/core/java/com/android/server/inputmethod/ClientController.java
@@ -17,6 +17,7 @@
package com.android.server.inputmethod;
import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.content.pm.PackageManagerInternal;
import android.os.IBinder;
import android.os.RemoteException;
@@ -29,6 +30,7 @@
import java.util.ArrayList;
import java.util.List;
+import java.util.function.Consumer;
/**
* Store and manage {@link InputMethodManagerService} clients. This class was designed to be a
@@ -62,7 +64,7 @@
// TODO(b/314150112): Make this field private when breaking the cycle with IMMS.
@GuardedBy("ImfLock.class")
- final ArrayMap<IBinder, ClientState> mClients = new ArrayMap<>();
+ private final ArrayMap<IBinder, ClientState> mClients = new ArrayMap<>();
@GuardedBy("ImfLock.class")
private final List<ClientControllerCallback> mCallbacks = new ArrayList<>();
@@ -145,6 +147,19 @@
}
@GuardedBy("ImfLock.class")
+ @Nullable
+ ClientState getClient(IBinder binder) {
+ return mClients.get(binder);
+ }
+
+ @GuardedBy("ImfLock.class")
+ void forAllClients(Consumer<ClientState> consumer) {
+ for (int i = 0; i < mClients.size(); i++) {
+ consumer.accept(mClients.valueAt(i));
+ }
+ }
+
+ @GuardedBy("ImfLock.class")
boolean verifyClientAndPackageMatch(
@NonNull IInputMethodClient client, @NonNull String packageName) {
final ClientState cs = mClients.get(client.asBinder());
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
index 4767ebd..f031b7b 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
@@ -205,6 +205,7 @@
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Consumer;
import java.util.function.IntConsumer;
/**
@@ -270,7 +271,6 @@
@NonNull
private final String[] mNonPreemptibleInputMethods;
- // TODO(b/314150112): Move this to ClientController.
@UserIdInt
private int mLastSwitchUserId;
@@ -1819,10 +1819,8 @@
}
mLastSwitchUserId = newUserId;
-
if (mIsInteractive && clientToBeReset != null) {
- final ClientState cs =
- mClientController.mClients.get(clientToBeReset.asBinder());
+ final ClientState cs = mClientController.getClient(clientToBeReset.asBinder());
if (cs == null) {
// The client is already gone.
return;
@@ -2165,26 +2163,25 @@
/**
* Hide the IME if the removed user is the current user.
*/
+ @GuardedBy("ImfLock.class")
private void onClientRemoved(ClientState client) {
- synchronized (ImfLock.class) {
- clearClientSessionLocked(client);
- clearClientSessionForAccessibilityLocked(client);
- if (mCurClient == client) {
- hideCurrentInputLocked(mCurFocusedWindow, null /* statsToken */, 0 /* flags */,
- null /* resultReceiver */, SoftInputShowHideReason.HIDE_REMOVE_CLIENT);
- if (mBoundToMethod) {
- mBoundToMethod = false;
- IInputMethodInvoker curMethod = getCurMethodLocked();
- if (curMethod != null) {
- // When we unbind input, we are unbinding the client, so we always
- // unbind ime and a11y together.
- curMethod.unbindInput();
- AccessibilityManagerInternal.get().unbindInput();
- }
+ clearClientSessionLocked(client);
+ clearClientSessionForAccessibilityLocked(client);
+ if (mCurClient == client) {
+ hideCurrentInputLocked(mCurFocusedWindow, null /* statsToken */, 0 /* flags */,
+ null /* resultReceiver */, SoftInputShowHideReason.HIDE_REMOVE_CLIENT);
+ if (mBoundToMethod) {
+ mBoundToMethod = false;
+ IInputMethodInvoker curMethod = getCurMethodLocked();
+ if (curMethod != null) {
+ // When we unbind input, we are unbinding the client, so we always
+ // unbind ime and a11y together.
+ curMethod.unbindInput();
+ AccessibilityManagerInternal.get().unbindInput();
}
- mBoundToAccessibility = false;
- mCurClient = null;
}
+ mBoundToAccessibility = false;
+ mCurClient = null;
if (mCurFocusedWindowClient == client) {
mCurFocusedWindowClient = null;
mCurFocusedWindowEditorInfo = null;
@@ -2192,7 +2189,6 @@
}
}
- // TODO(b/314150112): Move this to ClientController.
@GuardedBy("ImfLock.class")
void unbindCurrentClientLocked(@UnbindReason int unbindClientReason) {
if (mCurClient != null) {
@@ -2883,11 +2879,16 @@
@GuardedBy("ImfLock.class")
void clearClientSessionsLocked() {
if (getCurMethodLocked() != null) {
- final int numClients = mClientController.mClients.size();
- for (int i = 0; i < numClients; ++i) {
- clearClientSessionLocked(mClientController.mClients.valueAt(i));
- clearClientSessionForAccessibilityLocked(mClientController.mClients.valueAt(i));
- }
+ // TODO(b/322816970): Replace this with lambda.
+ mClientController.forAllClients(new Consumer<ClientState>() {
+
+ @GuardedBy("ImfLock.class")
+ @Override
+ public void accept(ClientState c) {
+ clearClientSessionLocked(c);
+ clearClientSessionForAccessibilityLocked(c);
+ }
+ });
finishSessionLocked(mEnabledSession);
for (int i = 0; i < mEnabledAccessibilitySessions.size(); i++) {
@@ -3732,9 +3733,9 @@
return InputBindResult.INVALID_USER;
}
- final ClientState cs = mClientController.mClients.get(client.asBinder());
+ final ClientState cs = mClientController.getClient(client.asBinder());
if (cs == null) {
- throw new IllegalArgumentException("unknown client " + client.asBinder());
+ throw new IllegalArgumentException("Unknown client " + client.asBinder());
}
final int imeClientFocus = mWindowManagerInternal.hasInputMethodClientFocus(
@@ -3906,8 +3907,7 @@
// We need to check if this is the current client with
// focus in the window manager, to allow this call to
// be made before input is started in it.
- final ClientState cs =
- mClientController.mClients.get(client.asBinder());
+ final ClientState cs = mClientController.getClient(client.asBinder());
if (cs == null) {
ImeTracker.forLogging().onFailed(statsToken, ImeTracker.PHASE_SERVER_CLIENT_KNOWN);
throw new IllegalArgumentException("unknown client " + client.asBinder());
@@ -4518,16 +4518,17 @@
@Override
public void startImeTrace() {
super.startImeTrace_enforcePermission();
-
ImeTracing.getInstance().startTrace(null /* printwriter */);
- ArrayMap<IBinder, ClientState> clients;
synchronized (ImfLock.class) {
- clients = new ArrayMap<>(mClientController.mClients);
- }
- for (ClientState state : clients.values()) {
- if (state != null) {
- state.mClient.setImeTraceEnabled(true /* enabled */);
- }
+ // TODO(b/322816970): Replace this with lambda.
+ mClientController.forAllClients(new Consumer<ClientState>() {
+
+ @GuardedBy("ImfLock.class")
+ @Override
+ public void accept(ClientState c) {
+ c.mClient.setImeTraceEnabled(true /* enabled */);
+ }
+ });
}
}
@@ -4538,14 +4539,16 @@
super.stopImeTrace_enforcePermission();
ImeTracing.getInstance().stopTrace(null /* printwriter */);
- ArrayMap<IBinder, ClientState> clients;
synchronized (ImfLock.class) {
- clients = new ArrayMap<>(mClientController.mClients);
- }
- for (ClientState state : clients.values()) {
- if (state != null) {
- state.mClient.setImeTraceEnabled(false /* enabled */);
- }
+ // TODO(b/322816970): Replace this with lambda.
+ mClientController.forAllClients(new Consumer<ClientState>() {
+
+ @GuardedBy("ImfLock.class")
+ @Override
+ public void accept(ClientState c) {
+ c.mClient.setImeTraceEnabled(false /* enabled */);
+ }
+ });
}
}
@@ -5779,11 +5782,15 @@
// We only have sessions when we bound to an input method. Remove this session
// from all clients.
if (getCurMethodLocked() != null) {
- final int numClients = mClientController.mClients.size();
- for (int i = 0; i < numClients; ++i) {
- clearClientSessionForAccessibilityLocked(
- mClientController.mClients.valueAt(i), accessibilityConnectionId);
- }
+ // TODO(b/322816970): Replace this with lambda.
+ mClientController.forAllClients(new Consumer<ClientState>() {
+
+ @GuardedBy("ImfLock.class")
+ @Override
+ public void accept(ClientState c) {
+ clearClientSessionForAccessibilityLocked(c, accessibilityConnectionId);
+ }
+ });
AccessibilitySessionState session = mEnabledAccessibilitySessions.get(
accessibilityConnectionId);
if (session != null) {
@@ -5967,19 +5974,26 @@
p.println(" InputMethod #" + i + ":");
info.dump(p, " ");
}
+ // Dump ClientController#mClients
p.println(" ClientStates:");
- // TODO(b/314150112): move client related dump info to ClientController#dump
- final int numClients = mClientController.mClients.size();
- for (int i = 0; i < numClients; ++i) {
- final ClientState ci = mClientController.mClients.valueAt(i);
- p.println(" " + ci + ":");
- p.println(" client=" + ci.mClient);
- p.println(" fallbackInputConnection=" + ci.mFallbackInputConnection);
- p.println(" sessionRequested=" + ci.mSessionRequested);
- p.println(" sessionRequestedForAccessibility="
- + ci.mSessionRequestedForAccessibility);
- p.println(" curSession=" + ci.mCurSession);
- }
+ // TODO(b/322816970): Replace this with lambda.
+ mClientController.forAllClients(new Consumer<ClientState>() {
+
+ @GuardedBy("ImfLock.class")
+ @Override
+ public void accept(ClientState c) {
+ p.println(" " + c + ":");
+ p.println(" client=" + c.mClient);
+ p.println(" fallbackInputConnection="
+ + c.mFallbackInputConnection);
+ p.println(" sessionRequested="
+ + c.mSessionRequested);
+ p.println(
+ " sessionRequestedForAccessibility="
+ + c.mSessionRequestedForAccessibility);
+ p.println(" curSession=" + c.mCurSession);
+ }
+ });
p.println(" mCurMethodId=" + getSelectedMethodIdLocked());
client = mCurClient;
p.println(" mCurClient=" + client + " mCurSeq=" + getSequenceNumberLocked());
@@ -6583,14 +6597,16 @@
}
}
boolean isImeTraceEnabled = ImeTracing.getInstance().isEnabled();
- ArrayMap<IBinder, ClientState> clients;
synchronized (ImfLock.class) {
- clients = new ArrayMap<>(mClientController.mClients);
- }
- for (ClientState state : clients.values()) {
- if (state != null) {
- state.mClient.setImeTraceEnabled(isImeTraceEnabled);
- }
+ // TODO(b/322816970): Replace this with lambda.
+ mClientController.forAllClients(new Consumer<ClientState>() {
+
+ @GuardedBy("ImfLock.class")
+ @Override
+ public void accept(ClientState c) {
+ c.mClient.setImeTraceEnabled(isImeTraceEnabled);
+ }
+ });
}
return ShellCommandResult.SUCCESS;
}
diff --git a/services/core/java/com/android/server/locksettings/SyntheticPasswordManager.java b/services/core/java/com/android/server/locksettings/SyntheticPasswordManager.java
index cc205d4..cc58f38 100644
--- a/services/core/java/com/android/server/locksettings/SyntheticPasswordManager.java
+++ b/services/core/java/com/android/server/locksettings/SyntheticPasswordManager.java
@@ -1541,8 +1541,14 @@
*/
public @NonNull AuthenticationResult unlockTokenBasedProtector(
IGateKeeperService gatekeeper, long protectorId, byte[] token, int userId) {
- SyntheticPasswordBlob blob = SyntheticPasswordBlob.fromBytes(loadState(SP_BLOB_NAME,
- protectorId, userId));
+ byte[] data = loadState(SP_BLOB_NAME, protectorId, userId);
+ if (data == null) {
+ AuthenticationResult result = new AuthenticationResult();
+ result.gkResponse = VerifyCredentialResponse.ERROR;
+ Slogf.w(TAG, "spblob not found for protector %016x, user %d", protectorId, userId);
+ return result;
+ }
+ SyntheticPasswordBlob blob = SyntheticPasswordBlob.fromBytes(data);
return unlockTokenBasedProtectorInternal(gatekeeper, protectorId, blob.mProtectorType,
token, userId);
}
diff --git a/services/core/java/com/android/server/media/MediaSessionService.java b/services/core/java/com/android/server/media/MediaSessionService.java
index 1d516e2..7fabdf2 100644
--- a/services/core/java/com/android/server/media/MediaSessionService.java
+++ b/services/core/java/com/android/server/media/MediaSessionService.java
@@ -34,6 +34,8 @@
import android.app.KeyguardManager;
import android.app.NotificationManager;
import android.app.PendingIntent;
+import android.app.usage.UsageStatsManager;
+import android.app.usage.UsageStatsManagerInternal;
import android.content.ActivityNotFoundException;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
@@ -68,6 +70,7 @@
import android.os.HandlerThread;
import android.os.IBinder;
import android.os.Message;
+import android.os.PersistableBundle;
import android.os.PowerExemptionManager;
import android.os.PowerManager;
import android.os.Process;
@@ -100,7 +103,9 @@
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.HashMap;
+import java.util.HashSet;
import java.util.List;
+import java.util.Set;
/**
* System implementation of MediaSessionManager
@@ -145,6 +150,8 @@
private AudioManager mAudioManager;
private boolean mHasFeatureLeanback;
private ActivityManagerInternal mActivityManagerInternal;
+ private UsageStatsManagerInternal mUsageStatsManagerInternal;
+ private final Set<Integer> mUserEngagingSessions = new HashSet<>();
// The FullUserRecord of the current users. (i.e. The foreground user that isn't a profile)
// It's always not null after the MediaSessionService is started.
@@ -230,6 +237,7 @@
mContext.registerReceiver(mNotificationListenerEnabledChangedReceiver, filter);
mActivityManagerInternal = LocalServices.getService(ActivityManagerInternal.class);
+ mUsageStatsManagerInternal = LocalServices.getService(UsageStatsManagerInternal.class);
}
@Override
@@ -582,12 +590,43 @@
if (allowRunningInForeground) {
mActivityManagerInternal.startForegroundServiceDelegate(
foregroundServiceDelegationOptions, /* connection= */ null);
+ reportMediaInteractionEvent(record, /* userEngaged= */ true);
} else {
mActivityManagerInternal.stopForegroundServiceDelegate(
foregroundServiceDelegationOptions);
+ reportMediaInteractionEvent(record, /* userEngaged= */ false);
}
}
+ private void reportMediaInteractionEvent(MediaSessionRecordImpl record, boolean userEngaged) {
+ if (!android.app.usage.Flags.userInteractionTypeApi()) {
+ return;
+ }
+
+ String packageName = record.getPackageName();
+ int sessionUid = record.getUid();
+ String actionToLog = null;
+ if (userEngaged) {
+ if (!mUserEngagingSessions.contains(sessionUid)) {
+ actionToLog = "start";
+ }
+ mUserEngagingSessions.add(sessionUid);
+ } else {
+ if (mUserEngagingSessions.contains(sessionUid)) {
+ actionToLog = "stop";
+ }
+ mUserEngagingSessions.remove(sessionUid);
+ }
+
+ if (actionToLog != null) {
+ PersistableBundle extras = new PersistableBundle();
+ extras.putString(UsageStatsManager.EXTRA_EVENT_CATEGORY, "android.media");
+ extras.putString(UsageStatsManager.EXTRA_EVENT_ACTION, actionToLog);
+ mUsageStatsManagerInternal.reportUserInteractionEvent(
+ packageName, record.getUserId(), extras);
+ }
+ }
+
void tempAllowlistTargetPkgIfPossible(int targetUid, String targetPackage,
int callingPid, int callingUid, String callingPackage, String reason) {
final long token = Binder.clearCallingIdentity();
diff --git a/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java b/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java
index 978f468..bbb19e3 100644
--- a/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java
+++ b/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java
@@ -35,6 +35,7 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.ActivityManagerInternal;
+import android.app.ActivityOptions.LaunchCookie;
import android.app.AppOpsManager;
import android.app.IProcessObserver;
import android.app.compat.CompatChanges;
@@ -548,8 +549,11 @@
DEFAULT_DISPLAY));
break;
case RECORD_CONTENT_TASK:
- setReviewedConsentSessionLocked(ContentRecordingSession.createTaskSession(
- mProjectionGrant.getLaunchCookie()));
+ IBinder taskWindowContainerToken =
+ mProjectionGrant.getLaunchCookie() == null ? null
+ : mProjectionGrant.getLaunchCookie().binder;
+ setReviewedConsentSessionLocked(
+ ContentRecordingSession.createTaskSession(taskWindowContainerToken));
break;
}
}
@@ -973,7 +977,7 @@
private IBinder mToken;
private IBinder.DeathRecipient mDeathEater;
private boolean mRestoreSystemAlertWindow;
- private IBinder mLaunchCookie = null;
+ private LaunchCookie mLaunchCookie = null;
// Values for tracking token validity.
// Timeout value to compare creation time against.
@@ -1186,14 +1190,14 @@
@android.annotation.EnforcePermission(android.Manifest.permission.MANAGE_MEDIA_PROJECTION)
@Override // Binder call
- public void setLaunchCookie(IBinder launchCookie) {
+ public void setLaunchCookie(LaunchCookie launchCookie) {
setLaunchCookie_enforcePermission();
mLaunchCookie = launchCookie;
}
@android.annotation.EnforcePermission(android.Manifest.permission.MANAGE_MEDIA_PROJECTION)
@Override // Binder call
- public IBinder getLaunchCookie() {
+ public LaunchCookie getLaunchCookie() {
getLaunchCookie_enforcePermission();
return mLaunchCookie;
}
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index 425659e..4da2cc9 100755
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -10869,6 +10869,7 @@
ArrayList<Notification.Action> smartActions = record.getSystemGeneratedSmartActions();
ArrayList<CharSequence> smartReplies = record.getSmartReplies();
if (redactSensitiveNotificationsFromUntrustedListeners()
+ && info != null
&& !mListeners.isUidTrusted(info.uid)
&& mListeners.hasSensitiveContent(record)) {
smartActions = null;
diff --git a/services/core/java/com/android/server/notification/ZenModeHelper.java b/services/core/java/com/android/server/notification/ZenModeHelper.java
index 923be56d..41ff415 100644
--- a/services/core/java/com/android/server/notification/ZenModeHelper.java
+++ b/services/core/java/com/android/server/notification/ZenModeHelper.java
@@ -210,6 +210,9 @@
mDefaultConfig = readDefaultConfig(mContext.getResources());
updateDefaultAutomaticRuleNames();
+ if (Flags.modesApi()) {
+ updateDefaultAutomaticRulePolicies();
+ }
mConfig = mDefaultConfig.copy();
synchronized (mConfigsArrayLock) {
mConfigs.put(UserHandle.USER_SYSTEM, mConfig);
@@ -840,9 +843,13 @@
&& !PACKAGE_ANDROID.equals(ruleToRemove.pkg)) {
String deletedKey = ZenModeConfig.deletedRuleKey(ruleToRemove);
if (deletedKey != null) {
- ruleToRemove.deletionInstant = Instant.now(mClock);
+ ZenRule deletedRule = ruleToRemove.copy();
+ deletedRule.deletionInstant = Instant.now(mClock);
+ // If the rule is restored it shouldn't be active (or snoozed).
+ deletedRule.snoozing = false;
+ deletedRule.condition = null;
// Overwrites a previously-deleted rule with the same conditionId, but that's okay.
- config.deletedRules.put(deletedKey, ruleToRemove);
+ config.deletedRules.put(deletedKey, deletedRule);
}
}
}
@@ -1962,6 +1969,21 @@
}
}
+ // Updates the policies in the default automatic rules (provided via default XML config) to
+ // be fully filled in default values.
+ private void updateDefaultAutomaticRulePolicies() {
+ if (!Flags.modesApi()) {
+ // Should be checked before calling, but just in case.
+ return;
+ }
+ ZenPolicy defaultPolicy = mDefaultConfig.toZenPolicy();
+ for (ZenRule rule : mDefaultConfig.automaticRules.values()) {
+ if (ZenModeConfig.DEFAULT_RULE_IDS.contains(rule.id) && rule.zenPolicy == null) {
+ rule.zenPolicy = defaultPolicy.copy();
+ }
+ }
+ }
+
@VisibleForTesting
protected void applyRestrictions() {
final boolean zenOn = mZenMode != Global.ZEN_MODE_OFF;
diff --git a/services/core/java/com/android/server/pm/AppsFilterBase.java b/services/core/java/com/android/server/pm/AppsFilterBase.java
index a5bc2c3..98b7c96 100644
--- a/services/core/java/com/android/server/pm/AppsFilterBase.java
+++ b/services/core/java/com/android/server/pm/AppsFilterBase.java
@@ -24,6 +24,7 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.content.pm.Flags;
import android.content.pm.SigningDetails;
import android.os.Binder;
import android.os.Handler;
@@ -318,6 +319,11 @@
existingSettings.untrackedStorage());
}
+ private static boolean isQueryableBySdkSandbox(int callingUid, int targetUid) {
+ return Flags.allowSdkSandboxQueryIntentActivities()
+ && targetUid == Process.getAppUidForSdkSandboxUid(callingUid);
+ }
+
/**
* See
* {@link AppsFilterSnapshot#shouldFilterApplication(PackageDataSnapshot, int, Object,
@@ -338,9 +344,11 @@
} else if (Process.isSdkSandboxUid(callingAppId)) {
final int targetAppId = targetPkgSetting.getAppId();
final int targetUid = UserHandle.getUid(userId, targetAppId);
- // we only allow sdk sandbox processes access to forcequeryable packages
+ // we only allow sdk sandbox processes access to forcequeryable packages or
+ // if the target app is the sandbox's client app
return !isForceQueryable(targetPkgSetting.getAppId())
- && !isImplicitlyQueryable(callingUid, targetUid);
+ && !isImplicitlyQueryable(callingUid, targetUid)
+ && !isQueryableBySdkSandbox(callingUid, targetUid);
}
// use cache
if (mCacheReady && mCacheEnabled) {
diff --git a/services/core/java/com/android/server/pm/ComputerEngine.java b/services/core/java/com/android/server/pm/ComputerEngine.java
index 0555d90..0be8e6e 100644
--- a/services/core/java/com/android/server/pm/ComputerEngine.java
+++ b/services/core/java/com/android/server/pm/ComputerEngine.java
@@ -586,7 +586,7 @@
list.add(ri);
PackageManagerServiceUtils.applyEnforceIntentFilterMatching(
mInjector.getCompatibility(), mComponentResolver,
- list, false, intent, resolvedType, flags, filterCallingUid);
+ list, false, intent, resolvedType, filterCallingUid);
}
}
} else {
@@ -616,7 +616,7 @@
// We also have to ensure all components match the original intent
PackageManagerServiceUtils.applyEnforceIntentFilterMatching(
mInjector.getCompatibility(), mComponentResolver,
- list, false, originalIntent, resolvedType, flags, filterCallingUid);
+ list, false, originalIntent, resolvedType, filterCallingUid);
}
return skipPostResolution ? list : applyPostResolutionFilter(
@@ -700,7 +700,7 @@
list.add(ri);
PackageManagerServiceUtils.applyEnforceIntentFilterMatching(
mInjector.getCompatibility(), mComponentResolver,
- list, false, intent, resolvedType, flags, callingUid);
+ list, false, intent, resolvedType, callingUid);
}
}
} else {
@@ -712,7 +712,7 @@
// We also have to ensure all components match the original intent
PackageManagerServiceUtils.applyEnforceIntentFilterMatching(
mInjector.getCompatibility(), mComponentResolver,
- list, false, originalIntent, resolvedType, flags, callingUid);
+ list, false, originalIntent, resolvedType, callingUid);
}
return list;
diff --git a/services/core/java/com/android/server/pm/DexOptHelper.java b/services/core/java/com/android/server/pm/DexOptHelper.java
index a10bae9..3abf3a5 100644
--- a/services/core/java/com/android/server/pm/DexOptHelper.java
+++ b/services/core/java/com/android/server/pm/DexOptHelper.java
@@ -16,7 +16,10 @@
package com.android.server.pm;
+import static android.content.pm.PackageManager.INSTALL_REASON_DEVICE_RESTORE;
+import static android.content.pm.PackageManager.INSTALL_REASON_DEVICE_SETUP;
import static android.os.Trace.TRACE_TAG_DALVIK;
+import static android.os.incremental.IncrementalManager.isIncrementalPath;
import static com.android.server.LocalManagerRegistry.ManagerNotFoundException;
import static com.android.server.pm.ApexManager.ActiveApexInfo;
@@ -27,6 +30,8 @@
import static com.android.server.pm.PackageManagerService.REASON_BOOT_AFTER_OTA;
import static com.android.server.pm.PackageManagerService.REASON_CMDLINE;
import static com.android.server.pm.PackageManagerService.REASON_FIRST_BOOT;
+import static com.android.server.pm.PackageManagerService.SCAN_AS_APEX;
+import static com.android.server.pm.PackageManagerService.SCAN_AS_INSTANT_APP;
import static com.android.server.pm.PackageManagerService.STUB_SUFFIX;
import static com.android.server.pm.PackageManagerService.TAG;
import static com.android.server.pm.PackageManagerServiceCompilerMapping.getCompilerFilterForReason;
@@ -45,6 +50,8 @@
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
+import android.content.pm.Flags;
+import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.content.pm.SharedLibraryInfo;
import android.content.pm.dex.ArtManager;
@@ -54,6 +61,7 @@
import android.os.SystemProperties;
import android.os.Trace;
import android.os.UserHandle;
+import android.provider.Settings.Global;
import android.text.TextUtils;
import android.util.ArraySet;
import android.util.Log;
@@ -1091,4 +1099,73 @@
+ " has unsupported status " + status);
}
}
+
+ /**
+ * Returns DexoptOptions by the given InstallRequest.
+ */
+ static DexoptOptions getDexoptOptionsByInstallRequest(InstallRequest installRequest,
+ DexManager dexManager) {
+ final PackageSetting ps = installRequest.getScannedPackageSetting();
+ final String packageName = ps.getPackageName();
+ final boolean isBackupOrRestore =
+ installRequest.getInstallReason() == INSTALL_REASON_DEVICE_RESTORE
+ || installRequest.getInstallReason() == INSTALL_REASON_DEVICE_SETUP;
+ final int dexoptFlags = DexoptOptions.DEXOPT_BOOT_COMPLETE
+ | DexoptOptions.DEXOPT_CHECK_FOR_PROFILES_UPDATES
+ | DexoptOptions.DEXOPT_INSTALL_WITH_DEX_METADATA_FILE
+ | (isBackupOrRestore ? DexoptOptions.DEXOPT_FOR_RESTORE : 0);
+ // Compute the compilation reason from the installation scenario.
+ final int compilationReason =
+ dexManager.getCompilationReasonForInstallScenario(
+ installRequest.getInstallScenario());
+ return new DexoptOptions(packageName, compilationReason, dexoptFlags);
+ }
+
+ /**
+ * Use ArtService to perform dexopt by the given InstallRequest.
+ */
+ static DexoptResult dexoptPackageUsingArtService(InstallRequest installRequest,
+ DexoptOptions dexoptOptions) {
+ final PackageSetting ps = installRequest.getScannedPackageSetting();
+ final String packageName = ps.getPackageName();
+
+ PackageManagerLocal packageManagerLocal =
+ LocalManagerRegistry.getManager(PackageManagerLocal.class);
+ try (PackageManagerLocal.FilteredSnapshot snapshot =
+ packageManagerLocal.withFilteredSnapshot()) {
+ boolean ignoreDexoptProfile =
+ (installRequest.getInstallFlags()
+ & PackageManager.INSTALL_IGNORE_DEXOPT_PROFILE)
+ != 0;
+ /*@DexoptFlags*/ int extraFlags =
+ ignoreDexoptProfile && Flags.useArtServiceV2()
+ ? ArtFlags.FLAG_IGNORE_PROFILE
+ : 0;
+ DexoptParams params = dexoptOptions.convertToDexoptParams(extraFlags);
+ DexoptResult dexOptResult = getArtManagerLocal().dexoptPackage(
+ snapshot, packageName, params);
+
+ return dexOptResult;
+ }
+ }
+
+ /**
+ * Returns whether to perform dexopt by the given InstallRequest.
+ */
+ static boolean shouldPerformDexopt(InstallRequest installRequest, DexoptOptions dexoptOptions,
+ Context context) {
+ final boolean isApex = ((installRequest.getScanFlags() & SCAN_AS_APEX) != 0);
+ final boolean instantApp = ((installRequest.getScanFlags() & SCAN_AS_INSTANT_APP) != 0);
+ final PackageSetting ps = installRequest.getScannedPackageSetting();
+ final AndroidPackage pkg = ps.getPkg();
+ final boolean onIncremental = isIncrementalPath(ps.getPathString());
+
+ return (!instantApp || Global.getInt(context.getContentResolver(),
+ Global.INSTANT_APP_DEXOPT_ENABLED, 0) != 0)
+ && pkg != null
+ && !pkg.isDebuggable()
+ && (!onIncremental)
+ && dexoptOptions.isCompilationEnabled()
+ && !isApex;
+ }
}
diff --git a/services/core/java/com/android/server/pm/InstallPackageHelper.java b/services/core/java/com/android/server/pm/InstallPackageHelper.java
index e70c5ea..28f3d59 100644
--- a/services/core/java/com/android/server/pm/InstallPackageHelper.java
+++ b/services/core/java/com/android/server/pm/InstallPackageHelper.java
@@ -32,8 +32,6 @@
import static android.content.pm.PackageManager.INSTALL_FAILED_TEST_ONLY;
import static android.content.pm.PackageManager.INSTALL_FAILED_UID_CHANGED;
import static android.content.pm.PackageManager.INSTALL_FAILED_UPDATE_INCOMPATIBLE;
-import static android.content.pm.PackageManager.INSTALL_REASON_DEVICE_RESTORE;
-import static android.content.pm.PackageManager.INSTALL_REASON_DEVICE_SETUP;
import static android.content.pm.PackageManager.INSTALL_STAGED;
import static android.content.pm.PackageManager.INSTALL_SUCCEEDED;
import static android.content.pm.PackageManager.UNINSTALL_REASON_UNKNOWN;
@@ -170,10 +168,7 @@
import com.android.internal.util.ArrayUtils;
import com.android.internal.util.CollectionUtils;
import com.android.server.EventLogTags;
-import com.android.server.LocalManagerRegistry;
import com.android.server.SystemConfig;
-import com.android.server.art.model.ArtFlags;
-import com.android.server.art.model.DexoptParams;
import com.android.server.art.model.DexoptResult;
import com.android.server.criticalevents.CriticalEventLog;
import com.android.server.pm.Installer.LegacyDexoptDisabledException;
@@ -800,10 +795,27 @@
"restoreAndPostInstall userId=" + userId + " package=" + request.getPkg());
}
- // A restore should be requested at this point if (a) the install
- // succeeded, (b) the operation is not an update.
+ PackageSetting packageSetting = null;
+
final boolean update = request.isUpdate();
- boolean doRestore = !update && request.getPkg() != null;
+ boolean doRestore = false;
+ if (request.getPkg() != null && !request.isArchived()) {
+ // A restore should be requested at this point:
+ // if the install succeeded and it's not an archived install
+ if (!update) {
+ // AND the operation is not an update,
+ doRestore = true;
+ } else {
+ // OR the package has never been restored.
+ String packageName = request.getPkg().getPackageName();
+ synchronized (mPm.mLock) {
+ packageSetting = mPm.mSettings.getPackageLPr(packageName);
+ if (packageSetting != null && packageSetting.isPendingRestore()) {
+ doRestore = true;
+ }
+ }
+ }
+ }
// Set up the post-install work request bookkeeping. This will be used
// and cleaned up by the post-install event handling regardless of whether
@@ -833,7 +845,13 @@
doRestore = performRollbackManagerRestore(userId, token, request);
}
- if (!doRestore) {
+ if (doRestore) {
+ if (packageSetting != null) {
+ synchronized (mPm.mLock) {
+ packageSetting.setPendingRestore(false);
+ }
+ }
+ } else {
// No restore possible, or the Backup Manager was mysteriously not
// available -- just fire the post-install work request directly.
if (DEBUG_INSTALL) Log.v(TAG, "No restore - queue post-install for " + token);
@@ -2464,8 +2482,6 @@
final ArraySet<IncrementalStorage> incrementalStorages = new ArraySet<>();
for (ReconciledPackage reconciledPkg : reconciledPackages) {
final InstallRequest installRequest = reconciledPkg.mInstallRequest;
- final boolean instantApp = ((installRequest.getScanFlags() & SCAN_AS_INSTANT_APP) != 0);
- final boolean isApex = ((installRequest.getScanFlags() & SCAN_AS_APEX) != 0);
final PackageSetting ps = installRequest.getScannedPackageSetting();
final String packageName = ps.getPackageName();
final String codePath = ps.getPathString();
@@ -2507,28 +2523,14 @@
}
}
- // Compute the compilation reason from the installation scenario.
- final int compilationReason =
- mDexManager.getCompilationReasonForInstallScenario(
- installRequest.getInstallScenario());
-
// Construct the DexoptOptions early to see if we should skip running dexopt.
//
// Do not run PackageDexOptimizer through the local performDexOpt
// method because `pkg` may not be in `mPackages` yet.
//
// Also, don't fail application installs if the dexopt step fails.
- final boolean isBackupOrRestore =
- installRequest.getInstallReason() == INSTALL_REASON_DEVICE_RESTORE
- || installRequest.getInstallReason() == INSTALL_REASON_DEVICE_SETUP;
-
- final int dexoptFlags = DexoptOptions.DEXOPT_BOOT_COMPLETE
- | DexoptOptions.DEXOPT_CHECK_FOR_PROFILES_UPDATES
- | DexoptOptions.DEXOPT_INSTALL_WITH_DEX_METADATA_FILE
- | (isBackupOrRestore ? DexoptOptions.DEXOPT_FOR_RESTORE : 0);
- DexoptOptions dexoptOptions =
- new DexoptOptions(packageName, compilationReason, dexoptFlags);
-
+ DexoptOptions dexoptOptions = DexOptHelper.getDexoptOptionsByInstallRequest(
+ installRequest, mDexManager);
// Check whether we need to dexopt the app.
//
// NOTE: it is IMPORTANT to call dexopt:
@@ -2555,16 +2557,9 @@
//
// TODO(b/174695087): instantApp and onIncremental should be removed and their install
// path moved to SCENARIO_FAST.
- final boolean performDexopt =
- (!instantApp || android.provider.Settings.Global.getInt(
- mContext.getContentResolver(),
- android.provider.Settings.Global.INSTANT_APP_DEXOPT_ENABLED, 0) != 0)
- && pkg != null
- && !pkg.isDebuggable()
- && (!onIncremental)
- && dexoptOptions.isCompilationEnabled()
- && !isApex;
+ final boolean performDexopt = DexOptHelper.shouldPerformDexopt(installRequest,
+ dexoptOptions, mContext);
if (performDexopt) {
Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "dexopt");
@@ -2579,23 +2574,9 @@
realPkgSetting.getPkgState().setUpdatedSystemApp(isUpdatedSystemApp);
if (useArtService()) {
- PackageManagerLocal packageManagerLocal =
- LocalManagerRegistry.getManager(PackageManagerLocal.class);
- try (PackageManagerLocal.FilteredSnapshot snapshot =
- packageManagerLocal.withFilteredSnapshot()) {
- boolean ignoreDexoptProfile =
- (installRequest.getInstallFlags()
- & PackageManager.INSTALL_IGNORE_DEXOPT_PROFILE)
- != 0;
- /*@DexoptFlags*/ int extraFlags =
- ignoreDexoptProfile && Flags.useArtServiceV2()
- ? ArtFlags.FLAG_IGNORE_PROFILE
- : 0;
- DexoptParams params = dexoptOptions.convertToDexoptParams(extraFlags);
- DexoptResult dexOptResult = DexOptHelper.getArtManagerLocal().dexoptPackage(
- snapshot, packageName, params);
- installRequest.onDexoptFinished(dexOptResult);
- }
+ DexoptResult dexOptResult = DexOptHelper.dexoptPackageUsingArtService(
+ installRequest, dexoptOptions);
+ installRequest.onDexoptFinished(dexOptResult);
} else {
try {
mPackageDexOptimizer.performDexOpt(pkg, realPkgSetting,
@@ -2919,7 +2900,7 @@
mPm.scheduleDeferredNoKillPostDelete(args);
if (Flags.improveInstallDontKill()) {
synchronized (mPm.mInstallLock) {
- PackageManagerServiceUtils.linkSplitsToOldDirs(mPm.mInstaller,
+ PackageManagerServiceUtils.linkFilesToOldDirs(mPm.mInstaller,
packageName, pkgSetting.getPath(), pkgSetting.getOldPaths());
}
}
diff --git a/services/core/java/com/android/server/pm/LauncherAppsService.java b/services/core/java/com/android/server/pm/LauncherAppsService.java
index b5346a3..43328fc 100644
--- a/services/core/java/com/android/server/pm/LauncherAppsService.java
+++ b/services/core/java/com/android/server/pm/LauncherAppsService.java
@@ -18,6 +18,10 @@
import static android.Manifest.permission.READ_FRAME_BUFFER;
import static android.app.ActivityOptions.KEY_SPLASH_SCREEN_THEME;
+import static android.app.AppOpsManager.MODE_ALLOWED;
+import static android.app.AppOpsManager.MODE_IGNORED;
+import static android.app.AppOpsManager.OP_ARCHIVE_ICON_OVERLAY;
+import static android.app.AppOpsManager.OP_UNARCHIVAL_CONFIRMATION;
import static android.app.ComponentOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED;
import static android.app.ComponentOptions.MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED;
import static android.app.PendingIntent.FLAG_IMMUTABLE;
@@ -43,6 +47,7 @@
import android.app.ActivityManagerInternal;
import android.app.ActivityOptions;
import android.app.AppGlobals;
+import android.app.AppOpsManager;
import android.app.IApplicationThread;
import android.app.PendingIntent;
import android.app.admin.DevicePolicyCache;
@@ -213,6 +218,7 @@
private final ActivityTaskManagerInternal mActivityTaskManagerInternal;
private final ShortcutServiceInternal mShortcutServiceInternal;
private final PackageManagerInternal mPackageManagerInternal;
+ private final AppOpsManager mAppOpsManager;
private final PackageCallbackList<IOnAppsChangedListener> mListeners
= new PackageCallbackList<IOnAppsChangedListener>();
private final DevicePolicyManager mDpm;
@@ -253,6 +259,7 @@
LocalServices.getService(ShortcutServiceInternal.class));
mPackageManagerInternal = Objects.requireNonNull(
LocalServices.getService(PackageManagerInternal.class));
+ mAppOpsManager = mContext.getSystemService(AppOpsManager.class);
mShortcutServiceInternal.addListener(mPackageMonitor);
mShortcutChangeHandler = new ShortcutChangeHandler(mUserManagerInternal);
mShortcutServiceInternal.addShortcutChangeCallback(mShortcutChangeHandler);
@@ -1719,7 +1726,8 @@
.scheme("android-app")
.authority(callingPackage)
.build())
- .setPackage(installerPackageName);
+ .setPackage(installerPackageName)
+ .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
}
@Override
@@ -1997,6 +2005,23 @@
}
}
+ @Override
+ public void setArchiveCompatibilityOptions(boolean enableIconOverlay,
+ boolean enableUnarchivalConfirmation) {
+ int callingUid = Binder.getCallingUid();
+ Binder.withCleanCallingIdentity(
+ () -> {
+ mAppOpsManager.setUidMode(
+ OP_ARCHIVE_ICON_OVERLAY,
+ callingUid,
+ enableIconOverlay ? MODE_ALLOWED : MODE_IGNORED);
+ mAppOpsManager.setUidMode(
+ OP_UNARCHIVAL_CONFIRMATION,
+ callingUid,
+ enableUnarchivalConfirmation ? MODE_ALLOWED : MODE_IGNORED);
+ });
+ }
+
/** Checks if user is a profile of or same as listeningUser.
* and the user is enabled. */
private boolean isEnabledProfileOf(UserHandle listeningUser, UserHandle user,
diff --git a/services/core/java/com/android/server/pm/PackageArchiver.java b/services/core/java/com/android/server/pm/PackageArchiver.java
index 32f5646..474b590 100644
--- a/services/core/java/com/android/server/pm/PackageArchiver.java
+++ b/services/core/java/com/android/server/pm/PackageArchiver.java
@@ -20,6 +20,7 @@
import static android.app.ActivityManager.START_CLASS_NOT_FOUND;
import static android.app.ActivityManager.START_PERMISSION_DENIED;
import static android.app.ActivityManager.START_SUCCESS;
+import static android.app.AppOpsManager.MODE_ALLOWED;
import static android.app.AppOpsManager.MODE_IGNORED;
import static android.app.ComponentOptions.MODE_BACKGROUND_ACTIVITY_START_DENIED;
import static android.content.pm.ArchivedActivityInfo.bytesFromBitmap;
@@ -275,11 +276,12 @@
Slog.i(TAG, TextUtils.formatSimple("Unarchival is starting for: %s", packageName));
try {
- // TODO(b/311709794) Make showUnarchivalConfirmation dependent on the compat options.
requestUnarchive(packageName, callerPackageName,
getOrCreateLauncherListener(userId, packageName),
UserHandle.of(userId),
- false /* showUnarchivalConfirmation= */);
+ getAppOpsManager().checkOp(
+ AppOpsManager.OP_UNARCHIVAL_CONFIRMATION, callingUid, callerPackageName)
+ == MODE_ALLOWED);
} catch (Throwable t) {
Slog.e(TAG, TextUtils.formatSimple(
"Unexpected error occurred while unarchiving package %s: %s.", packageName,
@@ -796,7 +798,8 @@
* <p> The icon is returned without any treatment/overlay. In the rare case the app had multiple
* launcher activities, only one of the icons is returned arbitrarily.
*/
- public Bitmap getArchivedAppIcon(@NonNull String packageName, @NonNull UserHandle user) {
+ public Bitmap getArchivedAppIcon(@NonNull String packageName, @NonNull UserHandle user,
+ String callingPackageName) {
Objects.requireNonNull(packageName);
Objects.requireNonNull(user);
@@ -819,7 +822,13 @@
// TODO(b/298452477) Handle monochrome icons.
// In the rare case the archived app defined more than two launcher activities, we choose
// the first one arbitrarily.
- return includeCloudOverlay(decodeIcon(archiveState.getActivityInfos().get(0)));
+ Bitmap icon = decodeIcon(archiveState.getActivityInfos().get(0));
+ if (getAppOpsManager().checkOp(
+ AppOpsManager.OP_ARCHIVE_ICON_OVERLAY, callingUid, callingPackageName)
+ == MODE_ALLOWED) {
+ icon = includeCloudOverlay(icon);
+ }
+ return icon;
}
/**
diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java
index 135bd4f..b705e84 100644
--- a/services/core/java/com/android/server/pm/PackageManagerService.java
+++ b/services/core/java/com/android/server/pm/PackageManagerService.java
@@ -598,6 +598,8 @@
static final int DEFAULT_FILE_ACCESS_MODE = 0644;
+ static final int DEFAULT_NATIVE_LIBRARY_FILE_ACCESS_MODE = 0755;
+
final Handler mHandler;
final Handler mBackgroundHandler;
@@ -1550,6 +1552,8 @@
}
pkgSetting
.setPkg(null)
+ // This package was installed as archived. Need to mark it for later restore.
+ .setPendingRestore(true)
.modifyUserState(userId)
.setInstalled(false)
.setArchiveState(archiveState);
@@ -6414,8 +6418,10 @@
}
@Override
- public Bitmap getArchivedAppIcon(@NonNull String packageName, @NonNull UserHandle user) {
- return mInstallerService.mPackageArchiver.getArchivedAppIcon(packageName, user);
+ public Bitmap getArchivedAppIcon(@NonNull String packageName, @NonNull UserHandle user,
+ @NonNull String callingPackageName) {
+ return mInstallerService.mPackageArchiver.getArchivedAppIcon(packageName, user,
+ callingPackageName);
}
@Override
@@ -7807,7 +7813,8 @@
mResolveActivity.launchMode = ActivityInfo.LAUNCH_MULTIPLE;
mResolveActivity.flags = ActivityInfo.FLAG_EXCLUDE_FROM_RECENTS
| ActivityInfo.FLAG_FINISH_ON_CLOSE_SYSTEM_DIALOGS
- | ActivityInfo.FLAG_CAN_DISPLAY_ON_REMOTE_DEVICES;
+ | ActivityInfo.FLAG_CAN_DISPLAY_ON_REMOTE_DEVICES
+ | ActivityInfo.FLAG_HARDWARE_ACCELERATED;
mResolveActivity.theme = 0;
mResolveActivity.exported = true;
mResolveActivity.enabled = true;
@@ -7841,7 +7848,8 @@
mResolveActivity.documentLaunchMode = ActivityInfo.DOCUMENT_LAUNCH_NEVER;
mResolveActivity.flags = ActivityInfo.FLAG_EXCLUDE_FROM_RECENTS
| ActivityInfo.FLAG_RELINQUISH_TASK_IDENTITY
- | ActivityInfo.FLAG_CAN_DISPLAY_ON_REMOTE_DEVICES;
+ | ActivityInfo.FLAG_CAN_DISPLAY_ON_REMOTE_DEVICES
+ | ActivityInfo.FLAG_HARDWARE_ACCELERATED;
mResolveActivity.theme = R.style.Theme_Material_Dialog_Alert;
mResolveActivity.exported = true;
mResolveActivity.enabled = true;
diff --git a/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java b/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java
index 8531692..4f9ed03 100644
--- a/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java
+++ b/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java
@@ -23,6 +23,7 @@
import static android.system.OsConstants.O_CREAT;
import static android.system.OsConstants.O_RDWR;
+import static com.android.internal.content.NativeLibraryHelper.LIB64_DIR_NAME;
import static com.android.internal.content.NativeLibraryHelper.LIB_DIR_NAME;
import static com.android.internal.util.FrameworkStatsLog.UNSAFE_INTENT_EVENT_REPORTED__EVENT_TYPE__EXPLICIT_INTENT_FILTER_UNMATCH;
import static com.android.server.LocalManagerRegistry.ManagerNotFoundException;
@@ -31,6 +32,7 @@
import static com.android.server.pm.PackageManagerService.DEBUG_INTENT_MATCHING;
import static com.android.server.pm.PackageManagerService.DEBUG_PREFERRED;
import static com.android.server.pm.PackageManagerService.DEFAULT_FILE_ACCESS_MODE;
+import static com.android.server.pm.PackageManagerService.DEFAULT_NATIVE_LIBRARY_FILE_ACCESS_MODE;
import static com.android.server.pm.PackageManagerService.RANDOM_CODEPATH_PREFIX;
import static com.android.server.pm.PackageManagerService.RANDOM_DIR_PREFIX;
import static com.android.server.pm.PackageManagerService.SHELL_PACKAGE_NAME;
@@ -44,7 +46,7 @@
import android.annotation.UserIdInt;
import android.app.ActivityManager;
import android.compat.annotation.ChangeId;
-import android.compat.annotation.Disabled;
+import android.compat.annotation.EnabledAfter;
import android.compat.annotation.Overridable;
import android.content.Context;
import android.content.Intent;
@@ -198,7 +200,7 @@
*/
@Overridable
@ChangeId
- @Disabled /* Enforcement reverted in T: b/274147456 */
+ @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
private static final long ENFORCE_INTENTS_TO_MATCH_INTENT_FILTERS = 161252188;
/**
@@ -1192,8 +1194,7 @@
public static void applyEnforceIntentFilterMatching(
PlatformCompat compat, ComponentResolverApi resolver,
List<ResolveInfo> resolveInfos, boolean isReceiver,
- Intent intent, String resolvedType, @PackageManager.ResolveInfoFlagsBits long flags,
- int filterCallingUid) {
+ Intent intent, String resolvedType, int filterCallingUid) {
if (DISABLE_ENFORCE_INTENTS_TO_MATCH_INTENT_FILTERS.get()) return;
// Do not enforce filter matching when the caller is system or root
@@ -1203,10 +1204,9 @@
? new LogPrinter(Log.VERBOSE, TAG, Log.LOG_ID_SYSTEM)
: null;
- final boolean defaultOnly = (flags & PackageManager.MATCH_DEFAULT_ONLY) != 0;
-
- final boolean enforce = compat.isChangeEnabledByUidInternal(
- ENFORCE_INTENTS_TO_MATCH_INTENT_FILTERS, filterCallingUid);
+ final boolean enforce = android.security.Flags.enforceIntentFilterMatch()
+ && compat.isChangeEnabledByUidInternal(
+ ENFORCE_INTENTS_TO_MATCH_INTENT_FILTERS, filterCallingUid);
for (int i = resolveInfos.size() - 1; i >= 0; --i) {
final ComponentInfo info = resolveInfos.get(i).getComponentInfo();
@@ -1237,8 +1237,7 @@
boolean match = false;
for (int j = 0, size = comp.getIntents().size(); j < size; ++j) {
IntentFilter intentFilter = comp.getIntents().get(j).getIntentFilter();
- if (IntentResolver.intentMatchesFilter(
- intentFilter, intent, resolvedType, defaultOnly)) {
+ if (IntentResolver.intentMatchesFilter(intentFilter, intent, resolvedType)) {
match = true;
break;
}
@@ -1557,7 +1556,7 @@
return initiatingPackageName == null || SHELL_PACKAGE_NAME.equals(initiatingPackageName);
}
- public static void linkSplitsToOldDirs(@NonNull Installer installer,
+ public static void linkFilesToOldDirs(@NonNull Installer installer,
@NonNull String packageName,
@NonNull File newPath,
@Nullable Set<File> oldPaths) {
@@ -1572,56 +1571,108 @@
if (filesInNewPath == null || filesInNewPath.length == 0) {
return;
}
- final List<String> splitApkNames = new ArrayList<String>();
- for (int i = 0; i < filesInNewPath.length; i++) {
- if (!filesInNewPath[i].isDirectory() && filesInNewPath[i].toString().endsWith(".apk")) {
- splitApkNames.add(filesInNewPath[i].getName());
+ final List<File> splitApks = new ArrayList<>();
+ for (File file : filesInNewPath) {
+ if (!file.isDirectory() && file.toString().endsWith(".apk")) {
+ splitApks.add(file);
}
}
- final int numSplits = splitApkNames.size();
- if (numSplits == 0) {
+ if (splitApks.isEmpty()) {
return;
}
+ final File[] splitApkNames = splitApks.toArray(new File[0]);
for (File oldPath : oldPaths) {
if (!oldPath.exists()) {
continue;
}
- for (int i = 0; i < numSplits; i++) {
- final String splitApkName = splitApkNames.get(i);
- final File linkedSplit = new File(oldPath, splitApkName);
- if (linkedSplit.exists()) {
- if (DEBUG) {
- Slog.d(PackageManagerService.TAG, "Skipping existing linked split <"
- + linkedSplit + ">");
- }
- continue;
- }
- final File sourceSplit = new File(newPath, splitApkName);
- try {
- installer.linkFile(packageName, splitApkName,
- newPath.getAbsolutePath(), oldPath.getAbsolutePath());
- if (DEBUG) {
- Slog.d(PackageManagerService.TAG, "Linked <"
- + sourceSplit + "> to <" + linkedSplit + ">");
- }
- } catch (Installer.InstallerException e) {
- Slog.w(PackageManagerService.TAG, "Failed to link split <"
- + sourceSplit + " > to <" + linkedSplit + ">", e);
- continue;
- }
- try {
- Os.chmod(linkedSplit.getAbsolutePath(), DEFAULT_FILE_ACCESS_MODE);
- } catch (ErrnoException e) {
- Slog.w(PackageManagerService.TAG, "Failed to set mode for linked split <"
- + linkedSplit + ">", e);
- continue;
- }
- if (!SELinux.restorecon(linkedSplit)) {
- Slog.w(PackageManagerService.TAG, "Failed to restorecon for linked split <"
- + linkedSplit + ">");
- }
+ linkFilesAndSetModes(installer, packageName, newPath, oldPath, splitApkNames,
+ DEFAULT_FILE_ACCESS_MODE);
+ linkNativeLibraries(installer, packageName, newPath, oldPath, LIB_DIR_NAME);
+ linkNativeLibraries(installer, packageName, newPath, oldPath, LIB64_DIR_NAME);
+ }
+
+ }
+
+ private static void linkNativeLibraries(@NonNull Installer installer,
+ @NonNull String packageName,
+ @NonNull File sourcePath, @NonNull File targetPath,
+ @NonNull String libDirName) {
+ final File sourceLibDir = new File(sourcePath, libDirName);
+ if (!sourceLibDir.exists()) {
+ return;
+ }
+ final File targetLibDir = new File(targetPath, libDirName);
+ if (!targetLibDir.exists()) {
+ try {
+ NativeLibraryHelper.createNativeLibrarySubdir(targetLibDir);
+ } catch (IOException e) {
+ Slog.w(PackageManagerService.TAG, "Failed to create native library dir at <"
+ + targetLibDir + ">", e);
+ return;
}
}
- //TODO(b/291212866): support native libs
+ final File[] archs = sourceLibDir.listFiles();
+ if (archs == null) {
+ return;
+ }
+ for (File arch : archs) {
+ final File targetArchDir = new File(targetLibDir, arch.getName());
+ if (!targetArchDir.exists()) {
+ try {
+ NativeLibraryHelper.createNativeLibrarySubdir(targetArchDir);
+ } catch (IOException e) {
+ Slog.w(PackageManagerService.TAG, "Failed to create native library subdir at <"
+ + targetArchDir + ">", e);
+ continue;
+ }
+ }
+ final File sourceArchDir = new File(sourceLibDir, arch.getName());
+ final File[] files = sourceArchDir.listFiles();
+ if (files == null || files.length == 0) {
+ continue;
+ }
+ linkFilesAndSetModes(installer, packageName, sourceArchDir, targetArchDir, files,
+ DEFAULT_NATIVE_LIBRARY_FILE_ACCESS_MODE);
+ }
+ }
+
+ // Link the files with specified names from under the sourcePath to be under the targetPath
+ private static void linkFilesAndSetModes(@NonNull Installer installer, String packageName,
+ @NonNull File sourcePath, @NonNull File targetPath, @NonNull File[] files, int mode) {
+ for (File file : files) {
+ final String fileName = file.getName();
+ final File sourceFile = new File(sourcePath, fileName);
+ final File targetFile = new File(targetPath, fileName);
+ if (targetFile.exists()) {
+ if (DEBUG) {
+ Slog.d(PackageManagerService.TAG, "Skipping existing linked file <"
+ + targetFile + ">");
+ }
+ continue;
+ }
+ try {
+ installer.linkFile(packageName, fileName,
+ sourcePath.getAbsolutePath(), targetPath.getAbsolutePath());
+ if (DEBUG) {
+ Slog.d(PackageManagerService.TAG, "Linked <"
+ + sourceFile + "> to <" + targetFile + ">");
+ }
+ } catch (Installer.InstallerException e) {
+ Slog.w(PackageManagerService.TAG, "Failed to link native library <"
+ + sourceFile + "> to <" + targetFile + ">", e);
+ continue;
+ }
+ try {
+ Os.chmod(targetFile.getAbsolutePath(), mode);
+ } catch (ErrnoException e) {
+ Slog.w(PackageManagerService.TAG, "Failed to set mode for linked file <"
+ + targetFile + ">", e);
+ continue;
+ }
+ if (!SELinux.restorecon(targetFile)) {
+ Slog.w(PackageManagerService.TAG, "Failed to restorecon for linked file <"
+ + targetFile + ">");
+ }
+ }
}
}
diff --git a/services/core/java/com/android/server/pm/PackageSetting.java b/services/core/java/com/android/server/pm/PackageSetting.java
index 74c482b..f474d32 100644
--- a/services/core/java/com/android/server/pm/PackageSetting.java
+++ b/services/core/java/com/android/server/pm/PackageSetting.java
@@ -94,7 +94,8 @@
INSTALL_PERMISSION_FIXED,
UPDATE_AVAILABLE,
FORCE_QUERYABLE_OVERRIDE,
- SCANNED_AS_STOPPED_SYSTEM_APP
+ SCANNED_AS_STOPPED_SYSTEM_APP,
+ PENDING_RESTORE,
})
public @interface Flags {
}
@@ -102,6 +103,7 @@
private static final int UPDATE_AVAILABLE = 1 << 1;
private static final int FORCE_QUERYABLE_OVERRIDE = 1 << 2;
private static final int SCANNED_AS_STOPPED_SYSTEM_APP = 1 << 3;
+ private static final int PENDING_RESTORE = 1 << 4;
}
private int mBooleans;
@@ -543,6 +545,20 @@
return mSharedUserAppId > 0;
}
+ /**
+ * @see PackageState#isPendingRestore()
+ */
+ public PackageSetting setPendingRestore(boolean value) {
+ setBoolean(Booleans.PENDING_RESTORE, value);
+ onChanged();
+ return this;
+ }
+
+ @Override
+ public boolean isPendingRestore() {
+ return getBoolean(Booleans.PENDING_RESTORE);
+ }
+
@Override
public String toString() {
return "PackageSetting{"
diff --git a/services/core/java/com/android/server/pm/ResolveIntentHelper.java b/services/core/java/com/android/server/pm/ResolveIntentHelper.java
index 203e1de..b664e39 100644
--- a/services/core/java/com/android/server/pm/ResolveIntentHelper.java
+++ b/services/core/java/com/android/server/pm/ResolveIntentHelper.java
@@ -459,7 +459,7 @@
list.add(ri);
PackageManagerServiceUtils.applyEnforceIntentFilterMatching(
mPlatformCompat, componentResolver, list, true, intent,
- resolvedType, flags, filterCallingUid);
+ resolvedType, filterCallingUid);
}
}
} else {
@@ -485,7 +485,7 @@
// We also have to ensure all components match the original intent
PackageManagerServiceUtils.applyEnforceIntentFilterMatching(
mPlatformCompat, componentResolver,
- list, true, originalIntent, resolvedType, flags, filterCallingUid);
+ list, true, originalIntent, resolvedType, filterCallingUid);
}
return computer.applyPostResolutionFilter(list, instantAppPkgName, false, queryingUid,
diff --git a/services/core/java/com/android/server/pm/Settings.java b/services/core/java/com/android/server/pm/Settings.java
index 7cf1d33..c7ee649 100644
--- a/services/core/java/com/android/server/pm/Settings.java
+++ b/services/core/java/com/android/server/pm/Settings.java
@@ -4537,6 +4537,8 @@
t.traceBegin("createNewUser-" + userHandle);
Installer.Batch batch = new Installer.Batch();
final boolean skipPackageAllowList = userTypeInstallablePackages == null;
+ // Use the same timestamp for all system apps that are to be installed on the new user
+ final long currentTimeMillis = System.currentTimeMillis();
synchronized (mLock) {
final int size = mPackages.size();
for (int i = 0; i < size; i++) {
@@ -4552,6 +4554,9 @@
ps.getPackageName()));
// Only system apps are initially installed.
ps.setInstalled(shouldReallyInstall, userHandle);
+ if (Flags.fixSystemAppsFirstInstallTime() && shouldReallyInstall) {
+ ps.setFirstInstallTime(currentTimeMillis, userHandle);
+ }
// Non-Apex system apps, that are not included in the allowlist in
// initialNonStoppedSystemPackages, should be marked as stopped by default.
@@ -4878,7 +4883,7 @@
@NeverCompile // Avoid size overhead of debugging code.
void dumpPackageLPr(PrintWriter pw, String prefix, String checkinTag,
- ArraySet<String> permissionNames, PackageSetting ps,
+ ArraySet<String> permissionNames, @NonNull PackageSetting ps,
LegacyPermissionState permissionsState, SimpleDateFormat sdf, Date date,
List<UserInfo> users, boolean dumpAll, boolean dumpAllComponents) {
AndroidPackage pkg = ps.getPkg();
@@ -5017,6 +5022,10 @@
pw.print(prefix); pw.print(" privateFlags="); printFlags(pw,
privateFlags, PRIVATE_FLAG_DUMP_SPEC); pw.println();
}
+ if (ps.isPendingRestore()) {
+ pw.print(prefix); pw.print(" pendingRestore=true");
+ pw.println();
+ }
if (!pkg.isUpdatableSystem()) {
pw.print(prefix); pw.print(" updatableSystem=false");
pw.println();
@@ -5227,6 +5236,9 @@
pw.print(prefix); pw.print(" privatePkgFlags="); printFlags(pw, ps.getPrivateFlags(),
PRIVATE_FLAG_DUMP_SPEC);
pw.println();
+ if (ps.isPendingRestore()) {
+ pw.print(prefix); pw.println(" pendingRestore=true");
+ }
pw.print(prefix); pw.print(" apexModuleName="); pw.println(ps.getApexModuleName());
if (pkg != null && pkg.getOverlayTarget() != null) {
diff --git a/services/core/java/com/android/server/pm/pkg/PackageState.java b/services/core/java/com/android/server/pm/pkg/PackageState.java
index a7ae4eb..e0ee199 100644
--- a/services/core/java/com/android/server/pm/pkg/PackageState.java
+++ b/services/core/java/com/android/server/pm/pkg/PackageState.java
@@ -265,6 +265,14 @@
*/
boolean hasSharedUser();
+
+ /**
+ * Whether this app needs to be restore during next install/update.
+ * E.g. if an app was installed as archived and never had a chance to restore its data.
+ * @hide
+ */
+ boolean isPendingRestore();
+
/**
* Retrieves the shared user app ID. Note that the actual shared user data is not available here
* and must be queried separately.
diff --git a/services/core/java/com/android/server/pm/verify/domain/OWNERS b/services/core/java/com/android/server/pm/verify/domain/OWNERS
index c669112..b451fe4 100644
--- a/services/core/java/com/android/server/pm/verify/domain/OWNERS
+++ b/services/core/java/com/android/server/pm/verify/domain/OWNERS
@@ -1,5 +1,4 @@
# Bug component: 36137
+include /PACKAGE_MANAGER_OWNERS
-chiuwinson@google.com
-patb@google.com
-toddke@google.com
\ No newline at end of file
+wloh@google.com
\ No newline at end of file
diff --git a/services/core/java/com/android/server/policy/PermissionPolicyService.java b/services/core/java/com/android/server/policy/PermissionPolicyService.java
index 61348b5..f15646f 100644
--- a/services/core/java/com/android/server/policy/PermissionPolicyService.java
+++ b/services/core/java/com/android/server/policy/PermissionPolicyService.java
@@ -43,6 +43,7 @@
import android.app.KeyguardManager;
import android.app.TaskInfo;
import android.app.compat.CompatChanges;
+import android.companion.virtual.VirtualDeviceManager;
import android.compat.annotation.ChangeId;
import android.compat.annotation.EnabledAfter;
import android.content.BroadcastReceiver;
@@ -234,7 +235,12 @@
this::synchronizeUidPermissionsAndAppOpsAsync);
mAppOpsCallback = new IAppOpsCallback.Stub() {
- public void opChanged(int op, int uid, @Nullable String packageName) {
+ public void opChanged(int op, int uid, @Nullable String packageName,
+ String persistentDeviceId) {
+ if (Objects.equals(persistentDeviceId,
+ VirtualDeviceManager.PERSISTENT_DEVICE_ID_DEFAULT)) {
+ return;
+ }
if (packageName != null) {
synchronizeUidPermissionsAndAppOpsAsync(uid);
}
diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java
index 1fdcc64..51790b8 100644
--- a/services/core/java/com/android/server/policy/PhoneWindowManager.java
+++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java
@@ -75,6 +75,7 @@
import static android.view.WindowManagerGlobal.ADD_PERMISSION_DENIED;
import static android.view.contentprotection.flags.Flags.createAccessibilityOverlayAppOpEnabled;
+import static com.android.hardware.input.Flags.emojiAndScreenshotKeycodesAvailable;
import static com.android.internal.config.sysui.SystemUiDeviceConfigFlags.SCREENSHOT_KEYCHORD_DELAY;
import static com.android.server.policy.WindowManagerPolicy.WindowManagerFuncs.CAMERA_LENS_COVERED;
import static com.android.server.policy.WindowManagerPolicy.WindowManagerFuncs.CAMERA_LENS_COVER_ABSENT;
@@ -3779,6 +3780,11 @@
sendSystemKeyToStatusBarAsync(event);
return true;
}
+ case KeyEvent.KEYCODE_SCREENSHOT:
+ if (emojiAndScreenshotKeycodesAvailable() && down && repeatCount == 0) {
+ interceptScreenshotChord(SCREENSHOT_KEY_OTHER, 0 /*pressDelay*/);
+ }
+ return true;
}
if (isValidGlobalKey(keyCode)
&& mGlobalKeyManager.handleGlobalKey(mContext, keyCode, event)) {
@@ -5022,6 +5028,12 @@
case KeyEvent.KEYCODE_MACRO_4:
result &= ~ACTION_PASS_TO_USER;
break;
+ case KeyEvent.KEYCODE_EMOJI_PICKER:
+ if (!emojiAndScreenshotKeycodesAvailable()) {
+ // Don't allow EMOJI_PICKER key to be dispatched until flag is released.
+ result &= ~ACTION_PASS_TO_USER;
+ }
+ break;
}
if (useHapticFeedback) {
diff --git a/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java b/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java
index 09b19e6..25e749f 100644
--- a/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java
+++ b/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java
@@ -138,6 +138,7 @@
import com.android.modules.utils.TypedXmlPullParser;
import com.android.modules.utils.TypedXmlSerializer;
import com.android.net.module.util.NetworkCapabilitiesUtils;
+import com.android.server.power.optimization.Flags;
import com.android.server.power.stats.SystemServerCpuThreadReader.SystemServiceCpuThreadTimes;
import libcore.util.EmptyArray;
@@ -185,7 +186,8 @@
// TODO: remove "tcp" from network methods, since we measure total stats.
// Current on-disk Parcel version. Must be updated when the format of the parcelable changes
- public static final int VERSION = 214;
+ public static final int VERSION =
+ !Flags.disableSystemServicePowerAttr() ? 214 : 215;
// The maximum number of names wakelocks we will keep track of
// per uid; once the limit is reached, we batch the remaining wakelocks
@@ -1753,7 +1755,9 @@
mCpuUidActiveTimeReader = new KernelCpuUidActiveTimeReader(true, mClock);
mCpuUidClusterTimeReader = new KernelCpuUidClusterTimeReader(true, mClock);
mKernelWakelockReader = new KernelWakelockReader();
- mSystemServerCpuThreadReader = SystemServerCpuThreadReader.create();
+ if (!Flags.disableSystemServicePowerAttr()) {
+ mSystemServerCpuThreadReader = SystemServerCpuThreadReader.create();
+ }
mKernelMemoryBandwidthStats = new KernelMemoryBandwidthStats();
mTmpRailStats = new RailStats();
}
@@ -11459,7 +11463,7 @@
@Override
public BatteryStatsHistoryIterator iterateBatteryStatsHistory(long startTimeMs,
long endTimeMs) {
- return mHistory.copy().iterate(startTimeMs, endTimeMs);
+ return mHistory.iterate(startTimeMs, endTimeMs);
}
@Override
@@ -11702,7 +11706,9 @@
EnergyConsumerStats.resetIfNotNull(mGlobalEnergyConsumerStats);
- resetIfNotNull(mBinderThreadCpuTimesUs, false, elapsedRealtimeUs);
+ if (!Flags.disableSystemServicePowerAttr()) {
+ resetIfNotNull(mBinderThreadCpuTimesUs, false, elapsedRealtimeUs);
+ }
mNumAllUidCpuTimeReads = 0;
mNumUidsRemoved = 0;
@@ -13676,7 +13682,9 @@
mKernelCpuSpeedReaders[i].readDelta();
}
}
- mSystemServerCpuThreadReader.readDelta();
+ if (!Flags.disableSystemServicePowerAttr()) {
+ mSystemServerCpuThreadReader.readDelta();
+ }
return;
}
@@ -15696,23 +15704,25 @@
}
}
- updateSystemServiceCallStats();
- if (mBinderThreadCpuTimesUs != null) {
- pw.println("Per UID System server binder time in ms:");
- long[] systemServiceTimeAtCpuSpeeds = getSystemServiceTimeAtCpuSpeeds();
- for (int i = 0; i < size; i++) {
- int u = mUidStats.keyAt(i);
- Uid uid = mUidStats.get(u);
- double proportionalSystemServiceUsage = uid.getProportionalSystemServiceUsage();
- long timeUs = 0;
- for (int j = systemServiceTimeAtCpuSpeeds.length - 1; j >= 0; j--) {
- timeUs += systemServiceTimeAtCpuSpeeds[j] * proportionalSystemServiceUsage;
- }
+ if (!Flags.disableSystemServicePowerAttr()) {
+ updateSystemServiceCallStats();
+ if (mBinderThreadCpuTimesUs != null) {
+ pw.println("Per UID System server binder time in ms:");
+ long[] systemServiceTimeAtCpuSpeeds = getSystemServiceTimeAtCpuSpeeds();
+ for (int i = 0; i < size; i++) {
+ int u = mUidStats.keyAt(i);
+ Uid uid = mUidStats.get(u);
+ double proportionalSystemServiceUsage = uid.getProportionalSystemServiceUsage();
+ long timeUs = 0;
+ for (int j = systemServiceTimeAtCpuSpeeds.length - 1; j >= 0; j--) {
+ timeUs += systemServiceTimeAtCpuSpeeds[j] * proportionalSystemServiceUsage;
+ }
- pw.print(" ");
- pw.print(u);
- pw.print(": ");
- pw.println(timeUs / 1000);
+ pw.print(" ");
+ pw.print(u);
+ pw.print(": ");
+ pw.println(timeUs / 1000);
+ }
}
}
}
@@ -16428,8 +16438,10 @@
}
}
- mBinderThreadCpuTimesUs =
- LongSamplingCounterArray.readSummaryFromParcelLocked(in, mOnBatteryTimeBase);
+ if (!Flags.disableSystemServicePowerAttr()) {
+ mBinderThreadCpuTimesUs =
+ LongSamplingCounterArray.readSummaryFromParcelLocked(in, mOnBatteryTimeBase);
+ }
}
/**
@@ -16973,7 +16985,9 @@
}
}
- LongSamplingCounterArray.writeSummaryToParcelLocked(out, mBinderThreadCpuTimesUs);
+ if (!Flags.disableSystemServicePowerAttr()) {
+ LongSamplingCounterArray.writeSummaryToParcelLocked(out, mBinderThreadCpuTimesUs);
+ }
}
@GuardedBy("this")
@@ -16985,7 +16999,9 @@
// if we had originally pulled a time before the RTC was set.
getStartClockTime();
- updateSystemServiceCallStats();
+ if (!Flags.disableSystemServicePowerAttr()) {
+ updateSystemServiceCallStats();
+ }
}
@GuardedBy("this")
diff --git a/services/core/java/com/android/server/power/stats/BatteryUsageStatsProvider.java b/services/core/java/com/android/server/power/stats/BatteryUsageStatsProvider.java
index c3221e4..30b80ae 100644
--- a/services/core/java/com/android/server/power/stats/BatteryUsageStatsProvider.java
+++ b/services/core/java/com/android/server/power/stats/BatteryUsageStatsProvider.java
@@ -96,11 +96,13 @@
mPowerCalculators.add(new CustomEnergyConsumerPowerCalculator(mPowerProfile));
mPowerCalculators.add(new UserPowerCalculator());
- // It is important that SystemServicePowerCalculator be applied last,
- // because it re-attributes some of the power estimated by the other
- // calculators.
- mPowerCalculators.add(
- new SystemServicePowerCalculator(mCpuScalingPolicies, mPowerProfile));
+ if (!com.android.server.power.optimization.Flags.disableSystemServicePowerAttr()) {
+ // It is important that SystemServicePowerCalculator be applied last,
+ // because it re-attributes some of the power estimated by the other
+ // calculators.
+ mPowerCalculators.add(
+ new SystemServicePowerCalculator(mCpuScalingPolicies, mPowerProfile));
+ }
}
}
return mPowerCalculators;
diff --git a/services/core/java/com/android/server/power/stats/PowerStatsAggregator.java b/services/core/java/com/android/server/power/stats/PowerStatsAggregator.java
index cd3db36..ba4c127 100644
--- a/services/core/java/com/android/server/power/stats/PowerStatsAggregator.java
+++ b/services/core/java/com/android/server/power/stats/PowerStatsAggregator.java
@@ -74,8 +74,7 @@
boolean clockUpdateAdded = false;
long baseTime = startTimeMs > 0 ? startTimeMs : UNINITIALIZED;
long lastTime = 0;
- try (BatteryStatsHistoryIterator iterator =
- mHistory.copy().iterate(startTimeMs, endTimeMs)) {
+ try (BatteryStatsHistoryIterator iterator = mHistory.iterate(startTimeMs, endTimeMs)) {
while (iterator.hasNext()) {
BatteryStats.HistoryItem item = iterator.next();
diff --git a/services/core/java/com/android/server/power/stats/flags.aconfig b/services/core/java/com/android/server/power/stats/flags.aconfig
index 0f13571..6546646 100644
--- a/services/core/java/com/android/server/power/stats/flags.aconfig
+++ b/services/core/java/com/android/server/power/stats/flags.aconfig
@@ -13,3 +13,11 @@
description: "Feature flag for streamlined battery stats"
bug: "285646152"
}
+
+flag {
+ name: "disable_system_service_power_attr"
+ namespace: "backstage_power"
+ description: "Deprecation of system service power re-attribution"
+ bug: "311793616"
+ is_fixed_read_only: true
+}
diff --git a/services/core/java/com/android/server/servicewatcher/CurrentUserServiceSupplier.java b/services/core/java/com/android/server/servicewatcher/CurrentUserServiceSupplier.java
index 6677e7e..1526230 100644
--- a/services/core/java/com/android/server/servicewatcher/CurrentUserServiceSupplier.java
+++ b/services/core/java/com/android/server/servicewatcher/CurrentUserServiceSupplier.java
@@ -36,10 +36,12 @@
import android.content.pm.ResolveInfo;
import android.content.pm.ServiceInfo;
import android.content.res.Resources;
+import android.location.flags.Flags;
import android.os.Bundle;
import android.os.Process;
import android.os.UserHandle;
import android.util.Log;
+import android.util.TypedValue;
import com.android.internal.util.Preconditions;
import com.android.server.FgThread;
@@ -70,6 +72,10 @@
private static final String EXTRA_SERVICE_VERSION = "serviceVersion";
private static final String EXTRA_SERVICE_IS_MULTIUSER = "serviceIsMultiuser";
+ // a package value that will never match against any package (we can't use null since this will
+ // match against any package).
+ private static final String NO_MATCH_PACKAGE = "";
+
private static final Comparator<BoundServiceInfo> sBoundServiceInfoComparator = (o1, o2) -> {
if (o1 == o2) {
return 0;
@@ -196,7 +202,19 @@
Resources resources = context.getResources();
boolean enableOverlay = resources.getBoolean(enableOverlayResId);
if (!enableOverlay) {
- return resources.getString(nonOverlayPackageResId);
+ if (Flags.fixServiceWatcher()) {
+ // we don't use getText() or similar because it won't return null values
+ TypedValue out = new TypedValue();
+ resources.getValue(nonOverlayPackageResId, out, true);
+ CharSequence explicitPackage = out.coerceToString();
+ if (explicitPackage == null) {
+ return NO_MATCH_PACKAGE;
+ } else {
+ return explicitPackage.toString();
+ }
+ } else {
+ return resources.getString(nonOverlayPackageResId);
+ }
} else {
return null;
}
@@ -233,6 +251,10 @@
@Override
public boolean hasMatchingService() {
+ if (Flags.fixServiceWatcher() && NO_MATCH_PACKAGE.equals(mIntent.getPackage())) {
+ return false;
+ }
+
int intentQueryFlags = MATCH_DIRECT_BOOT_AWARE | MATCH_DIRECT_BOOT_UNAWARE;
if (mMatchSystemAppsOnly) {
intentQueryFlags |= MATCH_SYSTEM_ONLY;
@@ -268,6 +290,10 @@
@Override
public BoundServiceInfo getServiceInfo() {
+ if (Flags.fixServiceWatcher() && NO_MATCH_PACKAGE.equals(mIntent.getPackage())) {
+ return null;
+ }
+
BoundServiceInfo bestServiceInfo = null;
// only allow services in the correct direct boot state to match
diff --git a/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java b/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java
index 285bcc3..0ffd002 100644
--- a/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java
+++ b/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java
@@ -2058,7 +2058,8 @@
}
private void registerCpuCyclesPerThreadGroupCluster() {
- if (KernelCpuBpfTracking.isSupported()) {
+ if (KernelCpuBpfTracking.isSupported()
+ && !com.android.server.power.optimization.Flags.disableSystemServicePowerAttr()) {
int tagId = FrameworkStatsLog.CPU_CYCLES_PER_THREAD_GROUP_CLUSTER;
PullAtomMetadata metadata = new PullAtomMetadata.Builder()
.setAdditiveFields(new int[]{3, 4})
@@ -2073,6 +2074,10 @@
}
int pullCpuCyclesPerThreadGroupCluster(int atomTag, List<StatsEvent> pulledData) {
+ if (com.android.server.power.optimization.Flags.disableSystemServicePowerAttr()) {
+ return StatsManager.PULL_SKIP;
+ }
+
SystemServiceCpuThreadTimes times = LocalServices.getService(BatteryStatsInternal.class)
.getSystemServiceCpuThreadTimes();
if (times == null) {
diff --git a/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java b/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java
index b271a03..a4c6959 100644
--- a/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java
+++ b/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java
@@ -18,6 +18,7 @@
import android.annotation.Nullable;
import android.app.ITransientNotificationCallback;
+import android.content.ComponentName;
import android.hardware.fingerprint.IUdfpsRefreshRateRequestCallback;
import android.os.Bundle;
import android.os.IBinder;
@@ -241,4 +242,17 @@
* @see com.android.internal.statusbar.IStatusBar#showMediaOutputSwitcher
*/
void showMediaOutputSwitcher(String packageName);
+
+ /**
+ * Add a tile to the Quick Settings Panel
+ * @param tile the ComponentName of the {@link android.service.quicksettings.TileService}
+ * @param end if true, the tile will be added at the end. If false, at the beginning.
+ */
+ void addQsTileToFrontOrEnd(ComponentName tile, boolean end);
+
+ /**
+ * Remove the tile from the Quick Settings Panel
+ * @param tile the ComponentName of the {@link android.service.quicksettings.TileService}
+ */
+ void removeQsTile(ComponentName tile);
}
diff --git a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
index b21721a..4955358 100644
--- a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
+++ b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
@@ -89,6 +89,7 @@
import android.view.WindowInsets.Type.InsetsType;
import android.view.WindowInsetsController.Appearance;
import android.view.WindowInsetsController.Behavior;
+import android.view.accessibility.Flags;
import com.android.internal.R;
import com.android.internal.annotations.GuardedBy;
@@ -834,6 +835,20 @@
}
}
}
+
+ @Override
+ public void addQsTileToFrontOrEnd(ComponentName tile, boolean end) {
+ if (Flags.a11yQsShortcut()) {
+ StatusBarManagerService.this.addQsTileToFrontOrEnd(tile, end);
+ }
+ }
+
+ @Override
+ public void removeQsTile(ComponentName tile) {
+ if (Flags.a11yQsShortcut()) {
+ StatusBarManagerService.this.remTile(tile);
+ }
+ }
};
private final GlobalActionsProvider mGlobalActionsProvider = new GlobalActionsProvider() {
@@ -934,11 +949,26 @@
}
public void addTile(ComponentName component) {
+ if (Flags.a11yQsShortcut()) {
+ addQsTileToFrontOrEnd(component, false);
+ } else {
+ enforceStatusBarOrShell();
+
+ if (mBar != null) {
+ try {
+ mBar.addQsTile(component);
+ } catch (RemoteException ex) {
+ }
+ }
+ }
+ }
+
+ private void addQsTileToFrontOrEnd(ComponentName tile, boolean end) {
enforceStatusBarOrShell();
if (mBar != null) {
try {
- mBar.addQsTile(component);
+ mBar.addQsTileToFrontOrEnd(tile, end);
} catch (RemoteException ex) {
}
}
diff --git a/services/core/java/com/android/server/trust/TrustAgentWrapper.java b/services/core/java/com/android/server/trust/TrustAgentWrapper.java
index 3abebf8..d102054 100644
--- a/services/core/java/com/android/server/trust/TrustAgentWrapper.java
+++ b/services/core/java/com/android/server/trust/TrustAgentWrapper.java
@@ -443,6 +443,8 @@
mPendingSuccessfulUnlock = false;
}
+ // It's okay to use the "Inner" version of isDeviceLocked since they differ only for
+ // profiles, which cannot be switched to and thus don't support trust agents anyway.
if (mTrustManagerService.isDeviceLockedInner(mUserId)) {
onDeviceLocked();
} else {
diff --git a/services/core/java/com/android/server/trust/TrustManagerService.java b/services/core/java/com/android/server/trust/TrustManagerService.java
index e5a8a6d..2b05993 100644
--- a/services/core/java/com/android/server/trust/TrustManagerService.java
+++ b/services/core/java/com/android/server/trust/TrustManagerService.java
@@ -83,7 +83,6 @@
import com.android.internal.util.DumpUtils;
import com.android.internal.widget.LockPatternUtils;
import com.android.server.SystemService;
-import com.android.server.companion.virtual.VirtualDeviceManagerInternal;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
@@ -160,7 +159,6 @@
private final ActivityManager mActivityManager;
private FingerprintManager mFingerprintManager;
private FaceManager mFaceManager;
- private VirtualDeviceManagerInternal mVirtualDeviceManager;
private enum TrustState {
// UNTRUSTED means that TrustManagerService is currently *not* giving permission for the
@@ -190,25 +188,30 @@
new SparseArray<>();
/**
- * Stores the locked state for users on the device. There are three different type of users
+ * Stores the locked state for users on the device. There are several different types of users
* which are handled slightly differently:
* <ul>
- * <li> Users with real keyguard
+ * <li> Users with real keyguard:
* These are users who can be switched to ({@link UserInfo#supportsSwitchToByUser()}). Their
* locked state is derived by a combination of user secure state, keyguard state, trust agent
* decision and biometric authentication result. These are updated via
* {@link #refreshDeviceLockedForUser(int)} and result stored in {@link #mDeviceLockedForUser}.
- * <li> Managed profiles with unified challenge
- * Managed profile with unified challenge always shares the same locked state as their parent,
+ * <li> Profiles with unified challenge:
+ * Profiles with a unified challenge always share the same locked state as their parent,
* so their locked state is not recorded in {@link #mDeviceLockedForUser}. Instead,
* {@link ITrustManager#isDeviceLocked(int)} always resolves their parent user handle and
* queries its locked state instead.
- * <li> Managed profiles with separate challenge
- * Locked state for profile with separate challenge is determined by other parts of the
- * framework (mostly PowerManager) and pushed to TrustManagerService via
- * {@link ITrustManager#setDeviceLockedForUser(int, boolean)}. Although in a corner case when
- * the profile has a separate but empty challenge, setting its {@link #mDeviceLockedForUser} to
- * {@code false} is actually done by {@link #refreshDeviceLockedForUser(int)}.
+ * <li> Profiles without unified challenge:
+ * The locked state for profiles that do not have a unified challenge (e.g. they have a
+ * separate challenge from their parent, or they have no parent at all) is determined by other
+ * parts of the framework (mostly PowerManager) and pushed to TrustManagerService via
+ * {@link ITrustManager#setDeviceLockedForUser(int, boolean)}.
+ * However, in the case where such a profile has an empty challenge, setting its
+ * {@link #mDeviceLockedForUser} to {@code false} is actually done by
+ * {@link #refreshDeviceLockedForUser(int)}.
+ * (This serves as a corner case for managed profiles with a separate but empty challenge. It
+ * is always currently the case for Communal profiles, for which having a non-empty challenge
+ * is not currently supported.)
* </ul>
* TODO: Rename {@link ITrustManager#setDeviceLockedForUser(int, boolean)} to
* {@code setDeviceLockedForProfile} to better reflect its purpose. Unifying
@@ -796,7 +799,7 @@
/**
* Update the user's locked state. Only applicable to users with a real keyguard
- * ({@link UserInfo#supportsSwitchToByUser}) and unsecured managed profiles.
+ * ({@link UserInfo#supportsSwitchToByUser}) and unsecured profiles.
*
* If this is called due to an unlock operation set unlockedUser to prevent the lock from
* being prematurely reset for that user while keyguard is still in the process of going away.
@@ -828,7 +831,11 @@
boolean secure = mLockPatternUtils.isSecure(id);
if (!info.supportsSwitchToByUser()) {
- if (info.isManagedProfile() && !secure) {
+ if (info.isProfile() && !secure
+ && !mLockPatternUtils.isProfileWithUnifiedChallenge(id)) {
+ // Unsecured profiles need to be explicitly set to false.
+ // However, Unified challenge profiles officially shouldn't have a presence in
+ // mDeviceLockedForUser at all, since that's not how they're tracked.
setDeviceLockedForUser(id, false);
}
continue;
@@ -1855,6 +1862,7 @@
}
}
+ /** If the userId has a parent, returns that parent's userId. Otherwise userId is returned. */
private int resolveProfileParent(int userId) {
final long identity = Binder.clearCallingIdentity();
try {
diff --git a/services/core/java/com/android/server/tv/TvInputManagerService.java b/services/core/java/com/android/server/tv/TvInputManagerService.java
index e434df7..089a886 100644
--- a/services/core/java/com/android/server/tv/TvInputManagerService.java
+++ b/services/core/java/com/android/server/tv/TvInputManagerService.java
@@ -2791,6 +2791,29 @@
}
@Override
+ public void notifyTvAdSessionData(
+ IBinder sessionToken, String type, Bundle data, int userId) {
+ final int callingUid = Binder.getCallingUid();
+ final int callingPid = Binder.getCallingPid();
+ final int resolvedUserId = resolveCallingUserId(callingPid, callingUid,
+ userId, "notifyTvAdSessionData");
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ synchronized (mLock) {
+ try {
+ SessionState sessionState = getSessionStateLocked(sessionToken, callingUid,
+ resolvedUserId);
+ getSessionLocked(sessionState).notifyTvAdSessionData(type, data);
+ } catch (RemoteException | SessionNotFoundException e) {
+ Slog.e(TAG, "error in notifyTvAdSessionData", e);
+ }
+ }
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
+
+ @Override
public int getClientPid(String sessionId) {
ensureTunerResourceAccessPermission();
final long identity = Binder.clearCallingIdentity();
@@ -4322,6 +4345,23 @@
}
}
}
+
+ @Override
+ public void onTvInputSessionData(String type, Bundle data) {
+ synchronized (mLock) {
+ if (DEBUG) {
+ Slog.d(TAG, "onTvInputSessionData()");
+ }
+ if (mSessionState.session == null || mSessionState.client == null) {
+ return;
+ }
+ try {
+ mSessionState.client.onTvInputSessionData(type, data, mSessionState.seq);
+ } catch (RemoteException e) {
+ Slog.e(TAG, "error in onTvInputSessionData", e);
+ }
+ }
+ }
}
@VisibleForTesting
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 b6d0ca1..ffce50e 100644
--- a/services/core/java/com/android/server/tv/interactive/TvInteractiveAppManagerService.java
+++ b/services/core/java/com/android/server/tv/interactive/TvInteractiveAppManagerService.java
@@ -937,6 +937,41 @@
}
@Override
+ public void sendAppLinkCommand(String serviceId, Bundle command, int userId) {
+ final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(),
+ Binder.getCallingUid(), userId, "sendAppLinkCommand");
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ synchronized (mLock) {
+ UserState userState = getOrCreateUserStateLocked(resolvedUserId);
+ TvAdServiceState adServiceState = userState.mAdServiceMap.get(serviceId);
+ if (adServiceState == null) {
+ Slogf.e(TAG, "failed to sendAppLinkCommand - unknown service id "
+ + serviceId);
+ return;
+ }
+ ComponentName componentName = adServiceState.mInfo.getComponent();
+ AdServiceState serviceState = userState.mAdServiceStateMap.get(componentName);
+ if (serviceState == null) {
+ serviceState = new AdServiceState(componentName, serviceId, resolvedUserId);
+ serviceState.addPendingAppLinkCommand(command);
+ userState.mAdServiceStateMap.put(componentName, serviceState);
+ updateAdServiceConnectionLocked(componentName, resolvedUserId);
+ } else if (serviceState.mService != null) {
+ serviceState.mService.sendAppLinkCommand(command);
+ } else {
+ serviceState.addPendingAppLinkCommand(command);
+ updateAdServiceConnectionLocked(componentName, resolvedUserId);
+ }
+ }
+ } catch (RemoteException e) {
+ Slogf.e(TAG, "error in sendAppLinkCommand", e);
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
+
+ @Override
public void createSession(final ITvAdClient client, final String serviceId, String type,
int seq, int userId) {
final int callingUid = Binder.getCallingUid();
@@ -1320,6 +1355,32 @@
}
@Override
+ public void notifyTvInputSessionData(
+ IBinder sessionToken, String type, Bundle data, int userId) {
+ if (DEBUG) {
+ Slogf.d(TAG, "notifyTvInputSessionData(type=%d)", type);
+ }
+ final int callingUid = Binder.getCallingUid();
+ final int callingPid = Binder.getCallingPid();
+ final int resolvedUserId = resolveCallingUserId(callingPid, callingUid, userId,
+ "notifyTvInputSessionData");
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ synchronized (mLock) {
+ try {
+ AdSessionState sessionState =
+ getAdSessionStateLocked(sessionToken, callingUid, resolvedUserId);
+ getAdSessionLocked(sessionState).notifyTvInputSessionData(type, data);
+ } catch (RemoteException | SessionNotFoundException e) {
+ Slogf.e(TAG, "error in notifyTvInputSessionData", e);
+ }
+ }
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
+
+ @Override
public void registerCallback(final ITvAdManagerCallback callback, int userId) {
int callingPid = Binder.getCallingPid();
int callingUid = Binder.getCallingUid();
@@ -4152,6 +4213,25 @@
}
@Override
+ public void onRequestSigning2(String id, String algorithm, String host,
+ int port, byte[] data) {
+ synchronized (mLock) {
+ if (DEBUG) {
+ Slogf.d(TAG, "onRequestSigning");
+ }
+ if (mSessionState.mSession == null || mSessionState.mClient == null) {
+ return;
+ }
+ try {
+ mSessionState.mClient.onRequestSigning2(
+ id, algorithm, host, port, data, mSessionState.mSeq);
+ } catch (RemoteException e) {
+ Slogf.e(TAG, "error in onRequestSigning", e);
+ }
+ }
+ }
+
+ @Override
public void onRequestCertificate(String host, int port) {
synchronized (mLock) {
if (DEBUG) {
@@ -4339,6 +4419,110 @@
}
}
+ @Override
+ public void onRequestCurrentVideoBounds() {
+ synchronized (mLock) {
+ if (DEBUG) {
+ Slogf.d(TAG, "onRequestCurrentVideoBounds");
+ }
+ if (mSessionState.mSession == null || mSessionState.mClient == null) {
+ return;
+ }
+ try {
+ mSessionState.mClient.onRequestCurrentVideoBounds(mSessionState.mSeq);
+ } catch (RemoteException e) {
+ Slogf.e(TAG, "error in onRequestCurrentVideoBounds", e);
+ }
+ }
+ }
+
+ @Override
+ public void onRequestCurrentChannelUri() {
+ synchronized (mLock) {
+ if (DEBUG) {
+ Slogf.d(TAG, "onRequestCurrentChannelUri");
+ }
+ if (mSessionState.mSession == null || mSessionState.mClient == null) {
+ return;
+ }
+ try {
+ mSessionState.mClient.onRequestCurrentChannelUri(mSessionState.mSeq);
+ } catch (RemoteException e) {
+ Slogf.e(TAG, "error in onRequestCurrentChannelUri", e);
+ }
+ }
+ }
+
+ @Override
+ public void onRequestTrackInfoList() {
+ synchronized (mLock) {
+ if (DEBUG) {
+ Slogf.d(TAG, "onRequestTrackInfoList");
+ }
+ if (mSessionState.mSession == null || mSessionState.mClient == null) {
+ return;
+ }
+ try {
+ mSessionState.mClient.onRequestTrackInfoList(mSessionState.mSeq);
+ } catch (RemoteException e) {
+ Slogf.e(TAG, "error in onRequestTrackInfoList", e);
+ }
+ }
+ }
+
+ @Override
+ public void onRequestCurrentTvInputId() {
+ synchronized (mLock) {
+ if (DEBUG) {
+ Slogf.d(TAG, "onRequestCurrentTvInputId");
+ }
+ if (mSessionState.mSession == null || mSessionState.mClient == null) {
+ return;
+ }
+ try {
+ mSessionState.mClient.onRequestCurrentTvInputId(mSessionState.mSeq);
+ } catch (RemoteException e) {
+ Slogf.e(TAG, "error in onRequestCurrentTvInputId", e);
+ }
+ }
+ }
+
+
+ @Override
+ public void onRequestSigning(String id, String algorithm, String alias, byte[] data) {
+ synchronized (mLock) {
+ if (DEBUG) {
+ Slogf.d(TAG, "onRequestSigning");
+ }
+ if (mSessionState.mSession == null || mSessionState.mClient == null) {
+ return;
+ }
+ try {
+ mSessionState.mClient.onRequestSigning(
+ id, algorithm, alias, data, mSessionState.mSeq);
+ } catch (RemoteException e) {
+ Slogf.e(TAG, "error in onRequestSigning", e);
+ }
+ }
+ }
+
+ @Override
+ public void onTvAdSessionData(String type, Bundle data) {
+ synchronized (mLock) {
+ if (DEBUG) {
+ Slogf.d(TAG, "onTvAdSessionData");
+ }
+ if (mSessionState.mSession == null || mSessionState.mClient == null) {
+ return;
+ }
+ try {
+ mSessionState.mClient.onTvAdSessionData(type, data, mSessionState.mSeq);
+ } catch (RemoteException e) {
+ Slogf.e(TAG, "error in onTvAdSessionData", e);
+ }
+ }
+ }
+
@GuardedBy("mLock")
private boolean addAdSessionTokenToClientStateLocked(ITvAdSession session) {
try {
diff --git a/services/core/java/com/android/server/vcn/VcnContext.java b/services/core/java/com/android/server/vcn/VcnContext.java
index ed04e5f..1383708 100644
--- a/services/core/java/com/android/server/vcn/VcnContext.java
+++ b/services/core/java/com/android/server/vcn/VcnContext.java
@@ -34,7 +34,7 @@
@NonNull private final Looper mLooper;
@NonNull private final VcnNetworkProvider mVcnNetworkProvider;
@NonNull private final FeatureFlags mFeatureFlags;
- @NonNull private final com.android.net.flags.FeatureFlags mCoreNetFeatureFlags;
+ @NonNull private final android.net.platform.flags.FeatureFlags mCoreNetFeatureFlags;
private final boolean mIsInTestMode;
public VcnContext(
@@ -49,7 +49,7 @@
// Auto-generated class
mFeatureFlags = new FeatureFlagsImpl();
- mCoreNetFeatureFlags = new com.android.net.flags.FeatureFlagsImpl();
+ mCoreNetFeatureFlags = new android.net.platform.flags.FeatureFlagsImpl();
}
@NonNull
diff --git a/services/core/java/com/android/server/vibrator/VibrationScaler.java b/services/core/java/com/android/server/vibrator/VibrationScaler.java
index 0a7872f..c9805c7 100644
--- a/services/core/java/com/android/server/vibrator/VibrationScaler.java
+++ b/services/core/java/com/android/server/vibrator/VibrationScaler.java
@@ -17,10 +17,10 @@
package com.android.server.vibrator;
import android.annotation.NonNull;
-import android.annotation.Nullable;
import android.content.Context;
import android.hardware.vibrator.V1_0.EffectStrength;
import android.os.IExternalVibratorService;
+import android.os.VibrationAttributes;
import android.os.VibrationEffect;
import android.os.Vibrator;
import android.os.vibrator.Flags;
@@ -57,8 +57,7 @@
private final SparseArray<ScaleLevel> mScaleLevels;
private final VibrationSettings mSettingsController;
private final int mDefaultVibrationAmplitude;
-
- private SparseArray<Float> mAdaptiveHapticsScales;
+ private final SparseArray<Float> mAdaptiveHapticsScales = new SparseArray<>();
VibrationScaler(Context context, VibrationSettings settingsController) {
mSettingsController = settingsController;
@@ -147,7 +146,7 @@
// If adaptive haptics scaling is available for this usage, apply it to the segment.
if (Flags.adaptiveHapticsEnabled()
- && mAdaptiveHapticsScales != null && mAdaptiveHapticsScales.size() > 0
+ && mAdaptiveHapticsScales.size() > 0
&& mAdaptiveHapticsScales.contains(usageHint)) {
float adaptiveScale = mAdaptiveHapticsScales.get(usageHint);
segment = segment.scale(adaptiveScale);
@@ -187,13 +186,35 @@
}
/**
- * Updates the adaptive haptics scales.
- * @param scales the new vibration scales to apply.
+ * Updates the adaptive haptics scales list by adding or modifying the scale for this usage.
+ *
+ * @param usageHint one of VibrationAttributes.USAGE_*.
+ * @param scale The scaling factor that should be applied to vibrations of this usage.
*
* @hide
*/
- public void updateAdaptiveHapticsScales(@Nullable SparseArray<Float> scales) {
- mAdaptiveHapticsScales = scales;
+ public void updateAdaptiveHapticsScale(@VibrationAttributes.Usage int usageHint, float scale) {
+ mAdaptiveHapticsScales.put(usageHint, scale);
+ }
+
+ /**
+ * Removes the usage from the cached adaptive haptics scales list.
+ *
+ * @param usageHint one of VibrationAttributes.USAGE_*.
+ *
+ * @hide
+ */
+ public void removeAdaptiveHapticsScale(@VibrationAttributes.Usage int usageHint) {
+ mAdaptiveHapticsScales.remove(usageHint);
+ }
+
+ /**
+ * Removes all cached adaptive haptics scales.
+ *
+ * @hide
+ */
+ public void clearAdaptiveHapticsScales() {
+ mAdaptiveHapticsScales.clear();
}
/** Mapping of Vibrator.VIBRATION_INTENSITY_* values to {@link EffectStrength}. */
diff --git a/services/core/java/com/android/server/vibrator/VibrationSettings.java b/services/core/java/com/android/server/vibrator/VibrationSettings.java
index 839c207..fab0430 100644
--- a/services/core/java/com/android/server/vibrator/VibrationSettings.java
+++ b/services/core/java/com/android/server/vibrator/VibrationSettings.java
@@ -376,6 +376,25 @@
}
/**
+ * Returns the duration, in milliseconds, that the vibrator control service will wait for new
+ * vibration params.
+ * @return The request vibration params timeout in milliseconds.
+ * @hide
+ */
+ public int getRequestVibrationParamsTimeoutMs() {
+ return mVibrationConfig.getRequestVibrationParamsTimeoutMs();
+ }
+
+ /**
+ * The list of usages that should request vibration params before they are played. These
+ * usages don't have strong latency requirements, e.g. ringtone and notification, and can be
+ * slightly delayed.
+ */
+ public int[] getRequestVibrationParamsForUsages() {
+ return mVibrationConfig.getRequestVibrationParamsForUsages();
+ }
+
+ /**
* Return a {@link VibrationEffect} that should be played if the device do not support given
* {@code effectId}.
*
diff --git a/services/core/java/com/android/server/vibrator/VibrationStepConductor.java b/services/core/java/com/android/server/vibrator/VibrationStepConductor.java
index 624da80..9cf942e 100644
--- a/services/core/java/com/android/server/vibrator/VibrationStepConductor.java
+++ b/services/core/java/com/android/server/vibrator/VibrationStepConductor.java
@@ -21,7 +21,9 @@
import android.os.Build;
import android.os.CombinedVibration;
import android.os.IBinder;
+import android.os.VibrationAttributes;
import android.os.VibrationEffect;
+import android.os.vibrator.Flags;
import android.os.vibrator.PrebakedSegment;
import android.os.vibrator.PrimitiveSegment;
import android.os.vibrator.RampSegment;
@@ -38,6 +40,8 @@
import java.util.List;
import java.util.PriorityQueue;
import java.util.Queue;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.TimeUnit;
/**
* Creates and manages a queue of steps for performing a VibrationEffect, as well as coordinating
@@ -65,15 +69,18 @@
public final VibrationThread.VibratorManagerHooks vibratorManagerHooks;
private final DeviceAdapter mDeviceAdapter;
+ private final VibrationScaler mVibrationScaler;
// Not guarded by lock because it's mostly used to read immutable fields by this conductor.
// This is only modified here at the prepareToStart method which always runs at the vibration
// thread, to update the adapted effect and report start time.
private final HalVibration mVibration;
-
private final PriorityQueue<Step> mNextSteps = new PriorityQueue<>();
private final Queue<Step> mPendingOnVibratorCompleteSteps = new LinkedList<>();
+ @Nullable
+ private final CompletableFuture<Void> mRequestVibrationParamsFuture;
+
// Signalling fields.
// Note that vibrator callback signals may happen inside vibrator HAL calls made by the
// VibrationThread, or on an external executor, so this lock should not be held for anything
@@ -97,10 +104,14 @@
VibrationStepConductor(HalVibration vib, VibrationSettings vibrationSettings,
DeviceAdapter deviceAdapter,
+ VibrationScaler vibrationScaler,
+ CompletableFuture<Void> requestVibrationParamsFuture,
VibrationThread.VibratorManagerHooks vibratorManagerHooks) {
this.mVibration = vib;
this.vibrationSettings = vibrationSettings;
this.mDeviceAdapter = deviceAdapter;
+ mVibrationScaler = vibrationScaler;
+ mRequestVibrationParamsFuture = requestVibrationParamsFuture;
this.vibratorManagerHooks = vibratorManagerHooks;
this.mSignalVibratorsComplete =
new IntArray(mDeviceAdapter.getAvailableVibratorIds().length);
@@ -143,7 +154,15 @@
if (Build.IS_DEBUGGABLE) {
expectIsVibrationThread(true);
}
- // Scaling happened before the effect was dispatched to this conductor (or to input devices)
+
+ if (!mVibration.callerInfo.attrs.isFlagSet(
+ VibrationAttributes.FLAG_BYPASS_USER_VIBRATION_INTENSITY_SCALE)) {
+ if (Flags.adaptiveHapticsEnabled()) {
+ waitForVibrationParamsIfRequired();
+ }
+ mVibration.scaleEffects(mVibrationScaler::scale);
+ }
+
mVibration.adaptToDevice(mDeviceAdapter);
CombinedVibration.Sequential sequentialEffect = toSequential(mVibration.getEffectToPlay());
mPendingVibrateSteps++;
@@ -361,6 +380,9 @@
+ mSignalCancelImmediate);
}
}
+ if (mRequestVibrationParamsFuture != null) {
+ mRequestVibrationParamsFuture.cancel(/* mayInterruptIfRunning= */true);
+ }
mLock.notify();
}
}
@@ -420,6 +442,30 @@
}
}
+ /**
+ * Blocks until the vibration params future is complete.
+ *
+ * This should be called by the VibrationThread and may be interrupted by calling
+ * `notifyCancelled` from outside it.
+ */
+ private void waitForVibrationParamsIfRequired() {
+ if (Build.IS_DEBUGGABLE) {
+ expectIsVibrationThread(true);
+ }
+
+ if (mRequestVibrationParamsFuture == null) {
+ return;
+ }
+
+ try {
+ mRequestVibrationParamsFuture.orTimeout(
+ vibrationSettings.getRequestVibrationParamsTimeoutMs(),
+ TimeUnit.MILLISECONDS).get();
+ } catch (Throwable e) {
+ Slog.w(TAG, "Failed to retrieve vibration params.", e);
+ }
+ }
+
@GuardedBy("mLock")
private boolean hasPendingNotifySignalLocked() {
if (Build.IS_DEBUGGABLE) {
diff --git a/services/core/java/com/android/server/vibrator/VibratorControlService.java b/services/core/java/com/android/server/vibrator/VibratorControlService.java
index 9d75249..8f8fe3c 100644
--- a/services/core/java/com/android/server/vibrator/VibratorControlService.java
+++ b/services/core/java/com/android/server/vibrator/VibratorControlService.java
@@ -16,11 +16,13 @@
package com.android.server.vibrator;
+import static android.os.VibrationAttributes.USAGE_ACCESSIBILITY;
import static android.os.VibrationAttributes.USAGE_ALARM;
import static android.os.VibrationAttributes.USAGE_COMMUNICATION_REQUEST;
import static android.os.VibrationAttributes.USAGE_HARDWARE_FEEDBACK;
import static android.os.VibrationAttributes.USAGE_MEDIA;
import static android.os.VibrationAttributes.USAGE_NOTIFICATION;
+import static android.os.VibrationAttributes.USAGE_PHYSICAL_EMULATION;
import static android.os.VibrationAttributes.USAGE_RINGTONE;
import static android.os.VibrationAttributes.USAGE_TOUCH;
import static android.os.VibrationAttributes.USAGE_UNKNOWN;
@@ -32,12 +34,18 @@
import android.frameworks.vibrator.IVibratorController;
import android.frameworks.vibrator.ScaleParam;
import android.frameworks.vibrator.VibrationParam;
+import android.os.Binder;
import android.os.IBinder;
import android.os.RemoteException;
+import android.os.VibrationAttributes;
import android.util.Slog;
-import android.util.SparseArray;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.ArrayUtils;
import java.util.Objects;
+import java.util.concurrent.CompletableFuture;
/**
* Implementation of {@link IVibratorControlService} which allows the registration of
@@ -47,16 +55,25 @@
*/
public final class VibratorControlService extends IVibratorControlService.Stub {
private static final String TAG = "VibratorControlService";
+ private static final int UNRECOGNIZED_VIBRATION_TYPE = -1;
+ private static final int NO_SCALE = -1;
private final VibratorControllerHolder mVibratorControllerHolder;
private final VibrationScaler mVibrationScaler;
private final Object mLock;
+ private final int[] mRequestVibrationParamsForUsages;
+
+ @GuardedBy("mLock")
+ private CompletableFuture<Void> mRequestVibrationParamsFuture = null;
+ @GuardedBy("mLock")
+ private IBinder mRequestVibrationParamsToken;
public VibratorControlService(VibratorControllerHolder vibratorControllerHolder,
- VibrationScaler vibrationScaler, Object lock) {
+ VibrationScaler vibrationScaler, VibrationSettings vibrationSettings, Object lock) {
mVibratorControllerHolder = vibratorControllerHolder;
mVibrationScaler = vibrationScaler;
mLock = lock;
+ mRequestVibrationParamsForUsages = vibrationSettings.getRequestVibrationParamsForUsages();
}
@Override
@@ -85,8 +102,9 @@
+ "controller doesn't match the registered one. " + this);
return;
}
- updateAdaptiveHapticsScales(/* params= */ null);
+ mVibrationScaler.clearAdaptiveHapticsScales();
mVibratorControllerHolder.setVibratorController(null);
+ endOngoingRequestVibrationParamsLocked(/* wasCancelled= */ true);
}
}
@@ -131,10 +149,8 @@
+ "controller doesn't match the registered one. " + this);
return;
}
- //TODO(305942827): Update this method to only clear the specified vibration types.
- // Perhaps look into whether it makes more sense to have this clear all scales and
- // rely on setVibrationParams for clearing the scales for specific vibrations.
- updateAdaptiveHapticsScales(/* params= */ null);
+
+ updateAdaptiveHapticsScales(types, NO_SCALE);
}
}
@@ -142,7 +158,26 @@
public void onRequestVibrationParamsComplete(
@NonNull IBinder requestToken, @SuppressLint("ArrayReturn") VibrationParam[] result)
throws RemoteException {
- // TODO(305942827): Cache the vibration params in VibrationScaler
+ Objects.requireNonNull(requestToken);
+
+ synchronized (mLock) {
+ if (mRequestVibrationParamsToken == null) {
+ Slog.wtf(TAG,
+ "New vibration params received but no token was cached in the service. "
+ + "New vibration params ignored.");
+ return;
+ }
+
+ if (!Objects.equals(requestToken, mRequestVibrationParamsToken)) {
+ Slog.w(TAG,
+ "New vibration params received but the provided token does not match the "
+ + "cached one. New vibration params ignored.");
+ return;
+ }
+
+ updateAdaptiveHapticsScales(result);
+ endOngoingRequestVibrationParamsLocked(/* wasCancelled= */ false);
+ }
}
@Override
@@ -156,50 +191,190 @@
}
/**
- * Extracts the vibration scales and caches them in {@link VibrationScaler}.
+ * If an {@link IVibratorController} is registered to the service, it will request the latest
+ * vibration params and return a {@link CompletableFuture} that completes when the request is
+ * fulfilled. Otherwise, ignores the call and returns null.
*
- * @param params the new vibration params to cache.
+ * @param usage a {@link android.os.VibrationAttributes} usage.
+ * @param timeoutInMillis the request's timeout in millis.
+ * @return a {@link CompletableFuture} to track the completion of the vibration param
+ * request, or null if no {@link IVibratorController} is registered.
*/
- private void updateAdaptiveHapticsScales(@Nullable VibrationParam[] params) {
- if (params == null || params.length == 0) {
- mVibrationScaler.updateAdaptiveHapticsScales(null);
- return;
- }
+ @Nullable
+ public CompletableFuture<Void> triggerVibrationParamsRequest(
+ @VibrationAttributes.Usage int usage, int timeoutInMillis) {
+ synchronized (mLock) {
+ IVibratorController vibratorController =
+ mVibratorControllerHolder.getVibratorController();
+ if (vibratorController == null) {
+ Slog.d(TAG, "Unable to request vibration params. There is no registered "
+ + "IVibrationController.");
+ return null;
+ }
- SparseArray<Float> vibrationScales = new SparseArray<>();
- for (int i = 0; i < params.length; i++) {
- ScaleParam scaleParam = params[i].getScale();
- extractVibrationScales(scaleParam, vibrationScales);
+ int vibrationType = mapToAdaptiveVibrationType(usage);
+ if (vibrationType == UNRECOGNIZED_VIBRATION_TYPE) {
+ Slog.d(TAG, "Unable to request vibration params. The provided usage " + usage
+ + " is unrecognized.");
+ return null;
+ }
+
+ try {
+ endOngoingRequestVibrationParamsLocked(/* wasCancelled= */ true);
+ mRequestVibrationParamsFuture = new CompletableFuture<>();
+ mRequestVibrationParamsToken = new Binder();
+ vibratorController.requestVibrationParams(vibrationType, timeoutInMillis,
+ mRequestVibrationParamsToken);
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Failed to request vibration params.", e);
+ endOngoingRequestVibrationParamsLocked(/* wasCancelled= */ true);
+ }
+
+ return mRequestVibrationParamsFuture;
}
- mVibrationScaler.updateAdaptiveHapticsScales(vibrationScales);
}
/**
- * Extracts the vibration scales and map them to their corresponding
- * {@link android.os.VibrationAttributes} usages.
+ * If an {@link IVibratorController} is registered to the service, then it checks whether to
+ * request new vibration params before playing the vibration. Returns true if the
+ * usage is for high latency vibrations, e.g. ringtone and notification, and can be delayed
+ * slightly. Otherwise, returns false.
+ *
+ * @param usage a {@link android.os.VibrationAttributes} usage.
+ * @return true if usage is for high latency vibrations, false otherwise.
*/
- private void extractVibrationScales(ScaleParam scaleParam, SparseArray<Float> vibrationScales) {
- if ((ScaleParam.TYPE_ALARM & scaleParam.typesMask) != 0) {
- vibrationScales.put(USAGE_ALARM, scaleParam.scale);
+ public boolean shouldRequestVibrationParams(@VibrationAttributes.Usage int usage) {
+ synchronized (mLock) {
+ IVibratorController vibratorController =
+ mVibratorControllerHolder.getVibratorController();
+ if (vibratorController == null) {
+ Slog.d(TAG, "Unable to check if should request vibration params. "
+ + "There is no registered IVibrationController.");
+ return false;
+ }
+
+ return ArrayUtils.contains(mRequestVibrationParamsForUsages, usage);
+ }
+ }
+
+ /**
+ * Returns the {@link #mRequestVibrationParamsToken} which is used to validate
+ * {@link #onRequestVibrationParamsComplete(IBinder, VibrationParam[])} calls.
+ */
+ @VisibleForTesting
+ public IBinder getRequestVibrationParamsToken() {
+ synchronized (mLock) {
+ return mRequestVibrationParamsToken;
+ }
+ }
+
+ /**
+ * Completes or cancels the vibration params request future and resets the future and token
+ * to null.
+ * @param wasCancelled specifies whether the future should be ended by being cancelled or not.
+ */
+ @GuardedBy("mLock")
+ private void endOngoingRequestVibrationParamsLocked(boolean wasCancelled) {
+ mRequestVibrationParamsToken = null;
+ if (mRequestVibrationParamsFuture == null) {
+ return;
}
- if ((ScaleParam.TYPE_NOTIFICATION & scaleParam.typesMask) != 0) {
- vibrationScales.put(USAGE_NOTIFICATION, scaleParam.scale);
- vibrationScales.put(USAGE_COMMUNICATION_REQUEST, scaleParam.scale);
+ if (wasCancelled) {
+ mRequestVibrationParamsFuture.cancel(/* mayInterruptIfRunning= */ true);
+ } else {
+ mRequestVibrationParamsFuture.complete(null);
}
- if ((ScaleParam.TYPE_RINGTONE & scaleParam.typesMask) != 0) {
- vibrationScales.put(USAGE_RINGTONE, scaleParam.scale);
+ mRequestVibrationParamsFuture = null;
+ }
+
+ private static int mapToAdaptiveVibrationType(@VibrationAttributes.Usage int usage) {
+ switch (usage) {
+ case USAGE_ALARM -> {
+ return ScaleParam.TYPE_ALARM;
+ }
+ case USAGE_NOTIFICATION, USAGE_COMMUNICATION_REQUEST -> {
+ return ScaleParam.TYPE_NOTIFICATION;
+ }
+ case USAGE_RINGTONE -> {
+ return ScaleParam.TYPE_RINGTONE;
+ }
+ case USAGE_MEDIA, USAGE_UNKNOWN -> {
+ return ScaleParam.TYPE_MEDIA;
+ }
+ case USAGE_TOUCH, USAGE_HARDWARE_FEEDBACK, USAGE_ACCESSIBILITY,
+ USAGE_PHYSICAL_EMULATION -> {
+ return ScaleParam.TYPE_INTERACTIVE;
+ }
+ default -> {
+ Slog.w(TAG, "Unrecognized vibration usage " + usage);
+ return UNRECOGNIZED_VIBRATION_TYPE;
+ }
+ }
+ }
+
+ /**
+ * Updates the adaptive haptics scales cached in {@link VibrationScaler} with the
+ * provided params.
+ *
+ * @param params the new vibration params.
+ */
+ private void updateAdaptiveHapticsScales(@Nullable VibrationParam[] params) {
+ for (VibrationParam param : params) {
+ ScaleParam scaleParam = param.getScale();
+ updateAdaptiveHapticsScales(scaleParam.typesMask, scaleParam.scale);
+ }
+ }
+
+ /**
+ * Updates the adaptive haptics scales, cached in {@link VibrationScaler}, for the provided
+ * vibration types.
+ *
+ * @param types The type of vibrations.
+ * @param scale The scaling factor that should be applied to the vibrations.
+ */
+ private void updateAdaptiveHapticsScales(int types, float scale) {
+ if ((ScaleParam.TYPE_ALARM & types) != 0) {
+ updateOrRemoveAdaptiveHapticsScale(USAGE_ALARM, scale);
}
- if ((ScaleParam.TYPE_MEDIA & scaleParam.typesMask) != 0) {
- vibrationScales.put(USAGE_MEDIA, scaleParam.scale);
- vibrationScales.put(USAGE_UNKNOWN, scaleParam.scale);
+ if ((ScaleParam.TYPE_NOTIFICATION & types) != 0) {
+ updateOrRemoveAdaptiveHapticsScale(USAGE_NOTIFICATION, scale);
+ updateOrRemoveAdaptiveHapticsScale(USAGE_COMMUNICATION_REQUEST, scale);
}
- if ((ScaleParam.TYPE_INTERACTIVE & scaleParam.typesMask) != 0) {
- vibrationScales.put(USAGE_TOUCH, scaleParam.scale);
- vibrationScales.put(USAGE_HARDWARE_FEEDBACK, scaleParam.scale);
+ if ((ScaleParam.TYPE_RINGTONE & types) != 0) {
+ updateOrRemoveAdaptiveHapticsScale(USAGE_RINGTONE, scale);
}
+
+ if ((ScaleParam.TYPE_MEDIA & types) != 0) {
+ updateOrRemoveAdaptiveHapticsScale(USAGE_MEDIA, scale);
+ updateOrRemoveAdaptiveHapticsScale(USAGE_UNKNOWN, scale);
+ }
+
+ if ((ScaleParam.TYPE_INTERACTIVE & types) != 0) {
+ updateOrRemoveAdaptiveHapticsScale(USAGE_TOUCH, scale);
+ updateOrRemoveAdaptiveHapticsScale(USAGE_HARDWARE_FEEDBACK, scale);
+ }
+ }
+
+ /**
+ * Updates or removes the adaptive haptics scale for the specified usage. If the scale is set
+ * to {@link #NO_SCALE} then it will be removed from the cached usage scales in
+ * {@link VibrationScaler}. Otherwise, the cached usage scale will be updated by the new value.
+ *
+ * @param usageHint one of VibrationAttributes.USAGE_*.
+ * @param scale The scaling factor that should be applied to the vibrations. If set to
+ * {@link #NO_SCALE} then the scale will be removed.
+ */
+ private void updateOrRemoveAdaptiveHapticsScale(@VibrationAttributes.Usage int usageHint,
+ float scale) {
+ if (scale == NO_SCALE) {
+ mVibrationScaler.removeAdaptiveHapticsScale(usageHint);
+ return;
+ }
+
+ mVibrationScaler.updateAdaptiveHapticsScale(usageHint, scale);
}
}
diff --git a/services/core/java/com/android/server/vibrator/VibratorControllerHolder.java b/services/core/java/com/android/server/vibrator/VibratorControllerHolder.java
index 63e69db..79a99b3 100644
--- a/services/core/java/com/android/server/vibrator/VibratorControllerHolder.java
+++ b/services/core/java/com/android/server/vibrator/VibratorControllerHolder.java
@@ -57,7 +57,7 @@
@Override
public void binderDied(@NonNull IBinder deadBinder) {
- if (deadBinder == mVibratorController.asBinder()) {
+ if (mVibratorController != null && deadBinder == mVibratorController.asBinder()) {
setVibratorController(null);
}
}
diff --git a/services/core/java/com/android/server/vibrator/VibratorManagerService.java b/services/core/java/com/android/server/vibrator/VibratorManagerService.java
index 2c1ab95..759450b 100644
--- a/services/core/java/com/android/server/vibrator/VibratorManagerService.java
+++ b/services/core/java/com/android/server/vibrator/VibratorManagerService.java
@@ -53,6 +53,7 @@
import android.os.VibrationAttributes;
import android.os.VibrationEffect;
import android.os.VibratorInfo;
+import android.os.vibrator.Flags;
import android.os.vibrator.PrebakedSegment;
import android.os.vibrator.VibrationEffectSegment;
import android.os.vibrator.VibratorInfoFactory;
@@ -84,6 +85,7 @@
import java.util.LinkedList;
import java.util.List;
import java.util.Objects;
+import java.util.concurrent.CompletableFuture;
import java.util.function.Consumer;
import java.util.function.Function;
@@ -161,6 +163,7 @@
private final VibrationSettings mVibrationSettings;
private final VibrationScaler mVibrationScaler;
+ private final VibratorControlService mVibratorControlService;
private final InputDeviceDelegate mInputDeviceDelegate;
private final DeviceAdapter mDeviceAdapter;
@@ -212,6 +215,9 @@
mVibrationSettings = new VibrationSettings(mContext, mHandler);
mVibrationScaler = new VibrationScaler(mContext, mVibrationSettings);
+ mVibratorControlService = new VibratorControlService(
+ injector.createVibratorControllerHolder(), mVibrationScaler, mVibrationSettings,
+ mLock);
mInputDeviceDelegate = new InputDeviceDelegate(mContext, mHandler);
VibrationCompleteListener listener = new VibrationCompleteListener(this);
@@ -272,9 +278,7 @@
injector.addService(EXTERNAL_VIBRATOR_SERVICE, new ExternalVibratorService());
if (ServiceManager.isDeclared(VIBRATOR_CONTROL_SERVICE)) {
- injector.addService(VIBRATOR_CONTROL_SERVICE,
- new VibratorControlService(new VibratorControllerHolder(), mVibrationScaler,
- mLock));
+ injector.addService(VIBRATOR_CONTROL_SERVICE, mVibratorControlService);
}
}
@@ -783,19 +787,12 @@
private Vibration.EndInfo startVibrationLocked(HalVibration vib) {
Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "startVibrationLocked");
try {
- if (!vib.callerInfo.attrs.isFlagSet(
- VibrationAttributes.FLAG_BYPASS_USER_VIBRATION_INTENSITY_SCALE)) {
- // Scale effect before dispatching it to the input devices or the vibration thread.
- vib.scaleEffects(mVibrationScaler::scale);
- }
- boolean inputDevicesAvailable = mInputDeviceDelegate.vibrateIfAvailable(
- vib.callerInfo, vib.getEffectToPlay());
- if (inputDevicesAvailable) {
- return new Vibration.EndInfo(Vibration.Status.FORWARDED_TO_INPUT_DEVICES);
+ if (mInputDeviceDelegate.isAvailable()) {
+ return startVibrationOnInputDevicesLocked(vib);
}
- VibrationStepConductor conductor = new VibrationStepConductor(vib, mVibrationSettings,
- mDeviceAdapter, mVibrationThreadCallbacks);
+ VibrationStepConductor conductor = createVibrationStepConductor(vib);
+
if (mCurrentVibration == null) {
return startVibrationOnThreadLocked(conductor);
}
@@ -866,6 +863,34 @@
vib.getStatsInfo(/* completionUptimeMillis= */ SystemClock.uptimeMillis()));
}
+ private VibrationStepConductor createVibrationStepConductor(HalVibration vib) {
+ CompletableFuture<Void> requestVibrationParamsFuture = null;
+
+ if (Flags.adaptiveHapticsEnabled() && !vib.callerInfo.attrs.isFlagSet(
+ VibrationAttributes.FLAG_BYPASS_USER_VIBRATION_INTENSITY_SCALE)
+ && mVibratorControlService.shouldRequestVibrationParams(
+ vib.callerInfo.attrs.getUsage())) {
+ requestVibrationParamsFuture =
+ mVibratorControlService.triggerVibrationParamsRequest(
+ vib.callerInfo.attrs.getUsage(),
+ mVibrationSettings.getRequestVibrationParamsTimeoutMs());
+ }
+
+ return new VibrationStepConductor(vib, mVibrationSettings, mDeviceAdapter, mVibrationScaler,
+ requestVibrationParamsFuture, mVibrationThreadCallbacks);
+ }
+
+ private Vibration.EndInfo startVibrationOnInputDevicesLocked(HalVibration vib) {
+ if (!vib.callerInfo.attrs.isFlagSet(
+ VibrationAttributes.FLAG_BYPASS_USER_VIBRATION_INTENSITY_SCALE)) {
+ // Scale effect before dispatching it to the input devices.
+ vib.scaleEffects(mVibrationScaler::scale);
+ }
+ mInputDeviceDelegate.vibrateIfAvailable(vib.callerInfo, vib.getEffectToPlay());
+
+ return new Vibration.EndInfo(Vibration.Status.FORWARDED_TO_INPUT_DEVICES);
+ }
+
private void logVibrationStatus(int uid, VibrationAttributes attrs,
Vibration.Status status) {
switch (status) {
@@ -1395,6 +1420,10 @@
void addService(String name, IBinder service) {
ServiceManager.addService(name, service);
}
+
+ VibratorControllerHolder createVibratorControllerHolder() {
+ return new VibratorControllerHolder();
+ }
}
/**
diff --git a/services/core/java/com/android/server/wm/AccessibilityController.java b/services/core/java/com/android/server/wm/AccessibilityController.java
index fa8c35a..19ea9f9 100644
--- a/services/core/java/com/android/server/wm/AccessibilityController.java
+++ b/services/core/java/com/android/server/wm/AccessibilityController.java
@@ -626,6 +626,8 @@
private boolean mForceShowMagnifiableBounds = false;
+ private final MagnificationSpec mMagnificationSpec = new MagnificationSpec();
+
DisplayMagnifier(WindowManagerService windowManagerService,
DisplayContent displayContent,
Display display,
@@ -655,13 +657,28 @@
mAccessibilityTracing.logTrace(LOG_TAG + ".setMagnificationSpec",
FLAGS_MAGNIFICATION_CALLBACK, "spec={" + spec + "}");
}
- mMagnifedViewport.updateMagnificationSpec(spec);
+ updateMagnificationSpec(spec);
mMagnifedViewport.recomputeBounds();
mService.applyMagnificationSpecLocked(mDisplay.getDisplayId(), spec);
mService.scheduleAnimationLocked();
}
+ void updateMagnificationSpec(MagnificationSpec spec) {
+ if (spec != null) {
+ mMagnificationSpec.initialize(spec.scale, spec.offsetX, spec.offsetY);
+ } else {
+ mMagnificationSpec.clear();
+ }
+ // If this message is pending we are in a rotation animation and do not want
+ // to show the border. We will do so when the pending message is handled.
+ if (!mHandler.hasMessages(
+ MyHandler.MESSAGE_SHOW_MAGNIFIED_REGION_BOUNDS_IF_NEEDED)) {
+ mMagnifedViewport.setMagnifiedRegionBorderShown(
+ isForceShowingMagnifiableBounds(), true);
+ }
+ }
+
void setForceShowMagnifiableBounds(boolean show) {
if (mAccessibilityTracing.isTracingEnabled(FLAGS_MAGNIFICATION_CALLBACK)) {
mAccessibilityTracing.logTrace(LOG_TAG + ".setForceShowMagnifiableBounds",
@@ -800,8 +817,7 @@
case WindowManager.LayoutParams.TYPE_QS_DIALOG:
case WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL: {
Rect magnifiedRegionBounds = mTempRect2;
- mMagnifedViewport.getMagnifiedFrameInContentCoords(
- magnifiedRegionBounds);
+ getMagnifiedFrameInContentCoords(magnifiedRegionBounds);
Rect touchableRegionBounds = mTempRect1;
windowState.getTouchableRegion(mTempRegion1);
mTempRegion1.getBounds(touchableRegionBounds);
@@ -818,6 +834,14 @@
}
}
+ void getMagnifiedFrameInContentCoords(Rect rect) {
+ Region magnificationRegion = new Region();
+ mMagnifedViewport.getMagnificationRegion(magnificationRegion);
+ magnificationRegion.getBounds(rect);
+ rect.offset((int) -mMagnificationSpec.offsetX, (int) -mMagnificationSpec.offsetY);
+ rect.scale(1.0f / mMagnificationSpec.scale);
+ }
+
void notifyImeWindowVisibilityChanged(boolean shown) {
if (mAccessibilityTracing.isTracingEnabled(FLAGS_MAGNIFICATION_CALLBACK)) {
mAccessibilityTracing.logTrace(LOG_TAG + ".notifyImeWindowVisibilityChanged",
@@ -832,13 +856,13 @@
mAccessibilityTracing.logTrace(LOG_TAG + ".getMagnificationSpecForWindow",
FLAGS_MAGNIFICATION_CALLBACK, "windowState={" + windowState + "}");
}
- MagnificationSpec spec = mMagnifedViewport.getMagnificationSpec();
- if (spec != null && !spec.isNop()) {
+
+ if (mMagnificationSpec != null && !mMagnificationSpec.isNop()) {
if (!windowState.shouldMagnify()) {
return null;
}
}
- return spec;
+ return mMagnificationSpec;
}
void getMagnificationRegion(Region outMagnificationRegion) {
@@ -852,6 +876,10 @@
mMagnifedViewport.getMagnificationRegion(outMagnificationRegion);
}
+ boolean isMagnifying() {
+ return mMagnificationSpec.scale > 1.0f;
+ }
+
void destroy() {
if (mAccessibilityTracing.isTracingEnabled(FLAGS_MAGNIFICATION_CALLBACK)) {
mAccessibilityTracing.logTrace(LOG_TAG + ".destroy", FLAGS_MAGNIFICATION_CALLBACK);
@@ -897,8 +925,6 @@
private final Path mCircularPath;
- private final MagnificationSpec mMagnificationSpec = new MagnificationSpec();
-
private final float mBorderWidth;
private final int mHalfBorderWidth;
private final int mDrawBorderInset;
@@ -932,20 +958,6 @@
outMagnificationRegion.set(mMagnificationRegion);
}
- void updateMagnificationSpec(MagnificationSpec spec) {
- if (spec != null) {
- mMagnificationSpec.initialize(spec.scale, spec.offsetX, spec.offsetY);
- } else {
- mMagnificationSpec.clear();
- }
- // If this message is pending we are in a rotation animation and do not want
- // to show the border. We will do so when the pending message is handled.
- if (!mHandler.hasMessages(
- MyHandler.MESSAGE_SHOW_MAGNIFIED_REGION_BOUNDS_IF_NEEDED)) {
- setMagnifiedRegionBorderShown(isForceShowingMagnifiableBounds(), true);
- }
- }
-
void recomputeBounds() {
getDisplaySizeLocked(mScreenSize);
final int screenWidth = mScreenSize.x;
@@ -1127,21 +1139,6 @@
}
}
- void getMagnifiedFrameInContentCoords(Rect rect) {
- MagnificationSpec spec = mMagnificationSpec;
- mMagnificationRegion.getBounds(rect);
- rect.offset((int) -spec.offsetX, (int) -spec.offsetY);
- rect.scale(1.0f / spec.scale);
- }
-
- boolean isMagnifying() {
- return mMagnificationSpec.scale > 1.0f;
- }
-
- MagnificationSpec getMagnificationSpec() {
- return mMagnificationSpec;
- }
-
void drawWindowIfNeeded() {
recomputeBounds();
mWindow.postDrawIfNeeded();
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index 03d55d9..b1d04c9 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -545,9 +545,6 @@
boolean launchFailed; // set if a launched failed, to abort on 2nd try
boolean delayedResume; // not yet resumed because of stopped app switches?
boolean finishing; // activity in pending finish list?
- boolean deferRelaunchUntilPaused; // relaunch of activity is being deferred until pause is
- // completed
- boolean preserveWindowOnDeferredRelaunch; // activity windows are preserved on deferred relaunch
int configChangeFlags; // which config values have changed
private boolean keysPaused; // has key dispatching been paused for it?
int launchMode; // the launch mode activity attribute.
@@ -1277,10 +1274,8 @@
if (mDeferHidingClient) {
pw.println(prefix + "mDeferHidingClient=" + mDeferHidingClient);
}
- if (deferRelaunchUntilPaused || configChangeFlags != 0) {
- pw.print(prefix); pw.print("deferRelaunchUntilPaused=");
- pw.print(deferRelaunchUntilPaused);
- pw.print(" configChangeFlags=");
+ if (configChangeFlags != 0) {
+ pw.print(prefix); pw.print(" configChangeFlags=");
pw.println(Integer.toHexString(configChangeFlags));
}
if (mServiceConnectionsHolder != null) {
@@ -2137,7 +2132,6 @@
launchFailed = false;
delayedResume = false;
finishing = false;
- deferRelaunchUntilPaused = false;
keysPaused = false;
inHistory = false;
nowVisible = false;
@@ -4096,8 +4090,6 @@
// Clean up the splash screen if it was still displayed.
cleanUpSplashScreen();
- deferRelaunchUntilPaused = false;
-
if (setState) {
setState(DESTROYED, "cleanUp");
if (DEBUG_APP) Slog.v(TAG_APP, "Clearing app during cleanUp for activity " + this);
@@ -6481,9 +6473,6 @@
mAppStopped = true;
ProtoLog.v(WM_DEBUG_STATES, "Stop failed; moving to STOPPED: %s", this);
setState(STOPPED, "stopIfPossible");
- if (deferRelaunchUntilPaused) {
- destroyImmediately("stop-except");
- }
}
}
@@ -6539,12 +6528,7 @@
if (finishing) {
abortAndClearOptionsAnimation();
} else {
- if (deferRelaunchUntilPaused) {
- destroyImmediately("stop-config");
- mRootWindowContainer.resumeFocusedTasksTopActivities();
- } else {
- mAtmService.updatePreviousProcess(this);
- }
+ mAtmService.updatePreviousProcess(this);
}
mTaskSupervisor.checkReadyForSleepLocked(true /* allowDelay */);
}
@@ -9724,23 +9708,12 @@
} else {
mRelaunchReason = RELAUNCH_REASON_NONE;
}
- if (mState == PAUSING) {
- // A little annoying: we are waiting for this activity to finish pausing. Let's not
- // do anything now, but just flag that it needs to be restarted when done pausing.
- ProtoLog.v(WM_DEBUG_CONFIGURATION,
- "Config is skipping already pausing %s", this);
- deferRelaunchUntilPaused = true;
- preserveWindowOnDeferredRelaunch = preserveWindow;
- return true;
- } else {
- ProtoLog.v(WM_DEBUG_CONFIGURATION, "Config is relaunching %s",
- this);
- if (!mVisibleRequested) {
- ProtoLog.v(WM_DEBUG_STATES, "Config is relaunching invisible "
- + "activity %s called by %s", this, Debug.getCallers(4));
- }
- relaunchActivityLocked(preserveWindow);
+ ProtoLog.v(WM_DEBUG_CONFIGURATION, "Config is relaunching %s", this);
+ if (!mVisibleRequested) {
+ ProtoLog.v(WM_DEBUG_STATES, "Config is relaunching invisible "
+ + "activity %s called by %s", this, Debug.getCallers(4));
}
+ relaunchActivityLocked(preserveWindow);
// All done... tell the caller we weren't able to keep this activity around.
return false;
@@ -9958,8 +9931,6 @@
mTaskSupervisor.mStoppingActivities.remove(this);
configChangeFlags = 0;
- deferRelaunchUntilPaused = false;
- preserveWindowOnDeferredRelaunch = false;
}
/**
diff --git a/services/core/java/com/android/server/wm/BackNavigationController.java b/services/core/java/com/android/server/wm/BackNavigationController.java
index 83ccbdc..13f6a5f 100644
--- a/services/core/java/com/android/server/wm/BackNavigationController.java
+++ b/services/core/java/com/android/server/wm/BackNavigationController.java
@@ -1527,6 +1527,12 @@
setLaunchBehind(visibleOpenActivities[i]);
}
}
+ // Force update mLastSurfaceShowing for opening activity and its task.
+ if (mWindowManagerService.mRoot.mTransitionController.isShellTransitionsEnabled()) {
+ for (int i = visibleOpenActivities.length - 1; i >= 0; --i) {
+ WindowContainer.enforceSurfaceVisible(visibleOpenActivities[i]);
+ }
+ }
}
@Nullable Runnable build() {
diff --git a/services/core/java/com/android/server/wm/DisplayPolicy.java b/services/core/java/com/android/server/wm/DisplayPolicy.java
index e2bc59b..a7bbc25 100644
--- a/services/core/java/com/android/server/wm/DisplayPolicy.java
+++ b/services/core/java/com/android/server/wm/DisplayPolicy.java
@@ -1913,12 +1913,9 @@
*/
final Rect mConfigFrame = new Rect();
- /** The count of insets sources when calculating this info. */
- int mLastInsetsSourceCount;
-
private boolean mNeedUpdate = true;
- void update(DisplayContent dc, int rotation, int w, int h) {
+ InsetsState update(DisplayContent dc, int rotation, int w, int h) {
final DisplayFrames df = new DisplayFrames();
dc.updateDisplayFrames(df, rotation, w, h);
dc.getDisplayPolicy().simulateLayoutDisplay(df);
@@ -1935,8 +1932,8 @@
mNonDecorFrame.inset(mNonDecorInsets);
mConfigFrame.set(displayFrame);
mConfigFrame.inset(mConfigInsets);
- mLastInsetsSourceCount = dc.getDisplayPolicy().mInsetsSourceWindowsExceptIme.size();
mNeedUpdate = false;
+ return insetsState;
}
void set(Info other) {
@@ -1944,7 +1941,6 @@
mConfigInsets.set(other.mConfigInsets);
mNonDecorFrame.set(other.mNonDecorFrame);
mConfigFrame.set(other.mConfigFrame);
- mLastInsetsSourceCount = other.mLastInsetsSourceCount;
mNeedUpdate = false;
}
@@ -1997,6 +1993,29 @@
}
}
+ static boolean hasInsetsFrameDiff(InsetsState s1, InsetsState s2, int insetsTypes) {
+ int insetsCount1 = 0;
+ for (int i = s1.sourceSize() - 1; i >= 0; i--) {
+ final InsetsSource source1 = s1.sourceAt(i);
+ if ((source1.getType() & insetsTypes) == 0) {
+ continue;
+ }
+ insetsCount1++;
+ final InsetsSource source2 = s2.peekSource(source1.getId());
+ if (source2 == null || !source2.getFrame().equals(source1.getFrame())) {
+ return true;
+ }
+ }
+ int insetsCount2 = 0;
+ for (int i = s2.sourceSize() - 1; i >= 0; i--) {
+ final InsetsSource source2 = s2.sourceAt(i);
+ if ((source2.getType() & insetsTypes) != 0) {
+ insetsCount2++;
+ }
+ }
+ return insetsCount1 != insetsCount2;
+ }
+
private static class Cache {
/**
* If {@link #mPreserveId} is this value, it is in the middle of updating display
@@ -2031,12 +2050,14 @@
final int dw = displayFrames.mWidth;
final int dh = displayFrames.mHeight;
final DecorInsets.Info newInfo = mDecorInsets.mTmpInfo;
- newInfo.update(mDisplayContent, rotation, dw, dh);
+ final InsetsState newInsetsState = newInfo.update(mDisplayContent, rotation, dw, dh);
final DecorInsets.Info currentInfo = getDecorInsetsInfo(rotation, dw, dh);
if (newInfo.mConfigFrame.equals(currentInfo.mConfigFrame)) {
// Even if the config frame is not changed in current rotation, it may change the
- // insets in other rotations if the source count is changed.
- if (newInfo.mLastInsetsSourceCount != currentInfo.mLastInsetsSourceCount) {
+ // insets in other rotations if the frame of insets source is changed.
+ final InsetsState currentInsetsState = mDisplayContent.mDisplayFrames.mInsetsState;
+ if (DecorInsets.hasInsetsFrameDiff(
+ newInsetsState, currentInsetsState, mService.mConfigTypes)) {
for (int i = mDecorInsets.mInfoForRotation.length - 1; i >= 0; i--) {
if (i != rotation) {
final boolean flipSize = (i + rotation) % 2 == 1;
diff --git a/services/core/java/com/android/server/wm/RecentsAnimation.java b/services/core/java/com/android/server/wm/RecentsAnimation.java
index 7b23004..7a3124d 100644
--- a/services/core/java/com/android/server/wm/RecentsAnimation.java
+++ b/services/core/java/com/android/server/wm/RecentsAnimation.java
@@ -160,7 +160,8 @@
// Invisible activity should be stopped. If the recents activity is alive and its doesn't
// need to relaunch by current configuration, then it may be already in stopped state.
- if (!targetActivity.isState(STOPPING, STOPPED)) {
+ if (!targetActivity.finishing && targetActivity.isAttached()
+ && !targetActivity.isState(STOPPING, STOPPED)) {
// Add to stopping instead of stop immediately. So the client has the chance to perform
// traversal in non-stopped state (ViewRootImpl.mStopped) that would initialize more
// things (e.g. the measure can be done earlier). The actual stop will be performed when
diff --git a/services/core/java/com/android/server/wm/ScreenRecordingCallbackController.java b/services/core/java/com/android/server/wm/ScreenRecordingCallbackController.java
index 5f488b7..bdb4588 100644
--- a/services/core/java/com/android/server/wm/ScreenRecordingCallbackController.java
+++ b/services/core/java/com/android/server/wm/ScreenRecordingCallbackController.java
@@ -97,7 +97,7 @@
mRecordedWC = (WindowContainer) mWms.mRoot.getDefaultDisplay();
} else {
mRecordedWC = mWms.mRoot.getActivity(activity -> activity.mLaunchCookie
- == mediaProjectionInfo.getLaunchCookie()).getTask();
+ == mediaProjectionInfo.getLaunchCookie().binder).getTask();
}
}
diff --git a/services/core/java/com/android/server/wm/TaskFragment.java b/services/core/java/com/android/server/wm/TaskFragment.java
index 11e7bb0..838ce86 100644
--- a/services/core/java/com/android/server/wm/TaskFragment.java
+++ b/services/core/java/com/android/server/wm/TaskFragment.java
@@ -1915,11 +1915,7 @@
ProtoLog.v(WM_DEBUG_STATES, "Enqueue pending stop if needed: %s "
+ "wasStopping=%b visibleRequested=%b", prev, wasStopping,
prev.isVisibleRequested());
- if (prev.deferRelaunchUntilPaused) {
- // Complete the deferred relaunch that was waiting for pause to complete.
- ProtoLog.v(WM_DEBUG_STATES, "Re-launching after pause: %s", prev);
- prev.relaunchActivityLocked(prev.preserveWindowOnDeferredRelaunch);
- } else if (wasStopping) {
+ if (wasStopping) {
// We are also stopping, the stop request must have gone soon after the pause.
// We can't clobber it, because the stop confirmation will not be handled.
// We don't need to schedule another stop, we only need to let it happen.
diff --git a/services/core/java/com/android/server/wm/TaskLaunchParamsModifier.java b/services/core/java/com/android/server/wm/TaskLaunchParamsModifier.java
index 4e7a9bd..f4e9957 100644
--- a/services/core/java/com/android/server/wm/TaskLaunchParamsModifier.java
+++ b/services/core/java/com/android/server/wm/TaskLaunchParamsModifier.java
@@ -303,8 +303,15 @@
} else {
if (DEBUG) appendLog("non-freeform-task-display-area");
}
+ final boolean isUpdatingExistingTaskWindowingMode = task != null
+ && task.getRequestedOverrideWindowingMode() != WINDOWING_MODE_UNDEFINED
+ && launchMode != task.getRequestedOverrideWindowingMode();
+ if (DEBUG && isUpdatingExistingTaskWindowingMode) {
+ appendLog("updating-existing-task-windowing-mode");
+ }
// If launch mode matches display windowing mode, let it inherit from display.
outParams.mWindowingMode = launchMode == suggestedDisplayArea.getWindowingMode()
+ && !isUpdatingExistingTaskWindowingMode
? WINDOWING_MODE_UNDEFINED : launchMode;
if (phase == PHASE_WINDOWING_MODE) {
diff --git a/services/core/java/com/android/server/wm/TransitionController.java b/services/core/java/com/android/server/wm/TransitionController.java
index 25b5630f..70775530 100644
--- a/services/core/java/com/android/server/wm/TransitionController.java
+++ b/services/core/java/com/android/server/wm/TransitionController.java
@@ -994,39 +994,18 @@
Slog.e(TAG, "Set visible without transition " + wc + " playing=" + isPlaying
+ " caller=" + caller);
if (!isPlaying) {
- enforceSurfaceVisible(wc);
+ WindowContainer.enforceSurfaceVisible(wc);
return;
}
// Update surface visibility after the playing transitions are finished, so the last
// visibility won't be replaced by the finish transaction of transition.
mStateValidators.add(() -> {
if (wc.isVisibleRequested()) {
- enforceSurfaceVisible(wc);
+ WindowContainer.enforceSurfaceVisible(wc);
}
});
}
- private void enforceSurfaceVisible(WindowContainer<?> wc) {
- if (wc.mSurfaceControl == null) return;
- wc.getSyncTransaction().show(wc.mSurfaceControl);
- final ActivityRecord ar = wc.asActivityRecord();
- if (ar != null) {
- ar.mLastSurfaceShowing = true;
- }
- // Force showing the parents because they may be hidden by previous transition.
- for (WindowContainer<?> p = wc.getParent(); p != null && p != wc.mDisplayContent;
- p = p.getParent()) {
- if (p.mSurfaceControl != null) {
- p.getSyncTransaction().show(p.mSurfaceControl);
- final Task task = p.asTask();
- if (task != null) {
- task.mLastSurfaceShowing = true;
- }
- }
- }
- wc.scheduleAnimation();
- }
-
/**
* Called when the transition has a complete set of participants for its operation. In other
* words, it is when the transition is "ready" but is still waiting for participants to draw.
diff --git a/services/core/java/com/android/server/wm/WindowContainer.java b/services/core/java/com/android/server/wm/WindowContainer.java
index 286182e..2d2857a 100644
--- a/services/core/java/com/android/server/wm/WindowContainer.java
+++ b/services/core/java/com/android/server/wm/WindowContainer.java
@@ -3625,6 +3625,29 @@
return mSurfaceControl.getHeight();
}
+ static void enforceSurfaceVisible(@NonNull WindowContainer<?> wc) {
+ if (wc.mSurfaceControl == null) {
+ return;
+ }
+ wc.getSyncTransaction().show(wc.mSurfaceControl);
+ final ActivityRecord ar = wc.asActivityRecord();
+ if (ar != null) {
+ ar.mLastSurfaceShowing = true;
+ }
+ // Force showing the parents because they may be hidden by previous transition.
+ for (WindowContainer<?> p = wc.getParent(); p != null && p != wc.mDisplayContent;
+ p = p.getParent()) {
+ if (p.mSurfaceControl != null) {
+ p.getSyncTransaction().show(p.mSurfaceControl);
+ final Task task = p.asTask();
+ if (task != null) {
+ task.mLastSurfaceShowing = true;
+ }
+ }
+ }
+ wc.scheduleAnimation();
+ }
+
@CallSuper
void dump(PrintWriter pw, String prefix, boolean dumpAll) {
if (mSurfaceAnimator.isAnimating()) {
diff --git a/services/core/java/com/android/server/wm/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java
index 3f889c0..8cd399f 100644
--- a/services/core/java/com/android/server/wm/WindowOrganizerController.java
+++ b/services/core/java/com/android/server/wm/WindowOrganizerController.java
@@ -1471,12 +1471,11 @@
final int index = task.mChildren.indexOf(topTaskFragment);
task.mChildren.remove(taskFragment);
task.mChildren.add(index, taskFragment);
- if (taskFragment.hasChild()) {
- effects |= TRANSACT_EFFECTS_LIFECYCLE;
- } else {
+ if (!taskFragment.hasChild()) {
// Ensure that the child layers are updated if the TaskFragment is empty
task.assignChildLayers();
}
+ effects |= TRANSACT_EFFECTS_LIFECYCLE;
}
}
break;
@@ -1491,12 +1490,11 @@
if (task != null) {
task.mChildren.remove(taskFragment);
task.mChildren.add(0, taskFragment);
- if (taskFragment.hasChild()) {
- effects |= TRANSACT_EFFECTS_LIFECYCLE;
- } else {
+ if (!taskFragment.hasChild()) {
// Ensure that the child layers are updated if the TaskFragment is empty.
task.assignChildLayers();
}
+ effects |= TRANSACT_EFFECTS_LIFECYCLE;
}
break;
}
@@ -1505,12 +1503,11 @@
if (task != null) {
task.mChildren.remove(taskFragment);
task.mChildren.add(taskFragment);
- if (taskFragment.hasChild()) {
- effects |= TRANSACT_EFFECTS_LIFECYCLE;
- } else {
+ if (!taskFragment.hasChild()) {
// Ensure that the child layers are updated if the TaskFragment is empty.
task.assignChildLayers();
}
+ effects |= TRANSACT_EFFECTS_LIFECYCLE;
}
break;
}
diff --git a/services/core/jni/com_android_server_am_CachedAppOptimizer.cpp b/services/core/jni/com_android_server_am_CachedAppOptimizer.cpp
index 7b08413..4403bce 100644
--- a/services/core/jni/com_android_server_am_CachedAppOptimizer.cpp
+++ b/services/core/jni/com_android_server_am_CachedAppOptimizer.cpp
@@ -571,8 +571,8 @@
}
static jboolean com_android_server_am_CachedAppOptimizer_isFreezerProfileValid(JNIEnv* env) {
- int uid = getuid();
- int pid = getpid();
+ uid_t uid = getuid();
+ pid_t pid = getpid();
return isProfileValidForProcess("Frozen", uid, pid) &&
isProfileValidForProcess("Unfrozen", uid, pid);
diff --git a/services/core/jni/com_android_server_input_InputManagerService.cpp b/services/core/jni/com_android_server_input_InputManagerService.cpp
index cbc301b..4a6b31c 100644
--- a/services/core/jni/com_android_server_input_InputManagerService.cpp
+++ b/services/core/jni/com_android_server_input_InputManagerService.cpp
@@ -290,6 +290,7 @@
void setTouchpadPointerSpeed(int32_t speed);
void setTouchpadNaturalScrollingEnabled(bool enabled);
void setTouchpadTapToClickEnabled(bool enabled);
+ void setTouchpadTapDraggingEnabled(bool enabled);
void setTouchpadRightClickZoneEnabled(bool enabled);
void setInputDeviceEnabled(uint32_t deviceId, bool enabled);
void setShowTouches(bool enabled);
@@ -440,6 +441,9 @@
// True to enable tap-to-click on touchpads.
bool touchpadTapToClickEnabled{true};
+ // True to enable tap dragging on touchpads.
+ bool touchpadTapDraggingEnabled{false};
+
// True to enable a zone on the right-hand side of touchpads where clicks will be turned
// into context (a.k.a. "right") clicks.
bool touchpadRightClickZoneEnabled{false};
@@ -697,6 +701,7 @@
outConfig->touchpadPointerSpeed = mLocked.touchpadPointerSpeed;
outConfig->touchpadNaturalScrollingEnabled = mLocked.touchpadNaturalScrollingEnabled;
outConfig->touchpadTapToClickEnabled = mLocked.touchpadTapToClickEnabled;
+ outConfig->touchpadTapDraggingEnabled = mLocked.touchpadTapDraggingEnabled;
outConfig->touchpadRightClickZoneEnabled = mLocked.touchpadRightClickZoneEnabled;
outConfig->disabledDevices = mLocked.disabledInputDevices;
@@ -1301,6 +1306,22 @@
InputReaderConfiguration::Change::TOUCHPAD_SETTINGS);
}
+void NativeInputManager::setTouchpadTapDraggingEnabled(bool enabled) {
+ { // acquire lock
+ std::scoped_lock _l(mLock);
+
+ if (mLocked.touchpadTapDraggingEnabled == enabled) {
+ return;
+ }
+
+ ALOGI("Setting touchpad tap dragging to %s.", toString(enabled));
+ mLocked.touchpadTapDraggingEnabled = enabled;
+ } // release lock
+
+ mInputManager->getReader().requestRefreshConfiguration(
+ InputReaderConfiguration::Change::TOUCHPAD_SETTINGS);
+}
+
void NativeInputManager::setTouchpadRightClickZoneEnabled(bool enabled) {
{ // acquire lock
std::scoped_lock _l(mLock);
@@ -2223,6 +2244,13 @@
im->setTouchpadTapToClickEnabled(enabled);
}
+static void nativeSetTouchpadTapDraggingEnabled(JNIEnv* env, jobject nativeImplObj,
+ jboolean enabled) {
+ NativeInputManager* im = getNativeInputManager(env, nativeImplObj);
+
+ im->setTouchpadTapDraggingEnabled(enabled);
+}
+
static void nativeSetTouchpadRightClickZoneEnabled(JNIEnv* env, jobject nativeImplObj,
jboolean enabled) {
NativeInputManager* im = getNativeInputManager(env, nativeImplObj);
@@ -2844,6 +2872,7 @@
{"setTouchpadNaturalScrollingEnabled", "(Z)V",
(void*)nativeSetTouchpadNaturalScrollingEnabled},
{"setTouchpadTapToClickEnabled", "(Z)V", (void*)nativeSetTouchpadTapToClickEnabled},
+ {"setTouchpadTapDraggingEnabled", "(Z)V", (void*)nativeSetTouchpadTapDraggingEnabled},
{"setTouchpadRightClickZoneEnabled", "(Z)V", (void*)nativeSetTouchpadRightClickZoneEnabled},
{"setShowTouches", "(Z)V", (void*)nativeSetShowTouches},
{"setInteractive", "(Z)V", (void*)nativeSetInteractive},
diff --git a/services/credentials/java/com/android/server/credentials/CredentialManagerService.java b/services/credentials/java/com/android/server/credentials/CredentialManagerService.java
index 667e086..281fb1c 100644
--- a/services/credentials/java/com/android/server/credentials/CredentialManagerService.java
+++ b/services/credentials/java/com/android/server/credentials/CredentialManagerService.java
@@ -27,6 +27,7 @@
import android.annotation.UserIdInt;
import android.app.ActivityManager;
import android.content.ComponentName;
+import android.content.ContentResolver;
import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
@@ -201,7 +202,7 @@
@SuppressWarnings("GuardedBy") // ErrorProne requires service.mLock which is the same
// this.mLock
protected void handlePackageRemovedMultiModeLocked(String packageName, int userId) {
- updateProvidersWhenPackageRemoved(mContext, packageName);
+ updateProvidersWhenPackageRemoved(new SettingsWrapper(mContext), packageName);
List<CredentialManagerServiceImpl> services = peekServiceListForUserLocked(userId);
if (services == null) {
@@ -1134,13 +1135,14 @@
}
/** Updates the list of providers when an app is uninstalled. */
- public static void updateProvidersWhenPackageRemoved(Context context, String packageName) {
+ public static void updateProvidersWhenPackageRemoved(
+ SettingsWrapper settingsWrapper, String packageName) {
+ Slog.i(TAG, "updateProvidersWhenPackageRemoved");
+
// Get the current providers.
String rawProviders =
- Settings.Secure.getStringForUser(
- context.getContentResolver(),
- Settings.Secure.CREDENTIAL_SERVICE_PRIMARY,
- UserHandle.myUserId());
+ settingsWrapper.getStringForUser(
+ Settings.Secure.CREDENTIAL_SERVICE_PRIMARY, UserHandle.myUserId());
if (rawProviders == null) {
Slog.w(TAG, "settings key is null");
return;
@@ -1148,44 +1150,44 @@
// Remove any providers from the primary setting that contain the package name
// being removed.
- Set<String> primaryProviders =
- getStoredProviders(rawProviders, packageName);
- if (!Settings.Secure.putString(
- context.getContentResolver(),
+ Set<String> primaryProviders = getStoredProviders(rawProviders, packageName);
+ if (!settingsWrapper.putStringForUser(
Settings.Secure.CREDENTIAL_SERVICE_PRIMARY,
- String.join(":", primaryProviders))) {
- Slog.w(TAG, "Failed to remove primary package: " + packageName);
+ String.join(":", primaryProviders),
+ UserHandle.myUserId(),
+ /* overrideableByRestore= */ true)) {
+ Slog.e(TAG, "Failed to remove primary package: " + packageName);
return;
}
// Read the autofill provider so we don't accidentally erase it.
String autofillProvider =
- Settings.Secure.getStringForUser(
- context.getContentResolver(),
- Settings.Secure.AUTOFILL_SERVICE,
- UserHandle.myUserId());
+ settingsWrapper.getStringForUser(
+ Settings.Secure.AUTOFILL_SERVICE, UserHandle.myUserId());
// If there is an autofill provider and it is the placeholder indicating
// that the currently selected primary provider does not support autofill
// then we should wipe the setting to keep it in sync.
if (autofillProvider != null && primaryProviders.isEmpty()) {
if (autofillProvider.equals(AUTOFILL_PLACEHOLDER_VALUE)) {
- if (!Settings.Secure.putString(
- context.getContentResolver(),
+ if (!settingsWrapper.putStringForUser(
Settings.Secure.AUTOFILL_SERVICE,
- "")) {
- Slog.w(TAG, "Failed to remove autofill package: " + packageName);
+ "",
+ UserHandle.myUserId(),
+ /* overrideableByRestore= */ true)) {
+ Slog.e(TAG, "Failed to remove autofill package: " + packageName);
}
} else {
// If the existing autofill provider is from the app being removed
// then erase the autofill service setting.
ComponentName cn = ComponentName.unflattenFromString(autofillProvider);
if (cn != null && cn.getPackageName().equals(packageName)) {
- if (!Settings.Secure.putString(
- context.getContentResolver(),
+ if (!settingsWrapper.putStringForUser(
Settings.Secure.AUTOFILL_SERVICE,
- "")) {
- Slog.w(TAG, "Failed to remove autofill package: " + packageName);
+ "",
+ UserHandle.myUserId(),
+ /* overrideableByRestore= */ true)) {
+ Slog.e(TAG, "Failed to remove autofill package: " + packageName);
}
}
}
@@ -1193,19 +1195,17 @@
// Read the credential providers to remove any reference of the removed app.
String rawCredentialProviders =
- Settings.Secure.getStringForUser(
- context.getContentResolver(),
- Settings.Secure.CREDENTIAL_SERVICE,
- UserHandle.myUserId());
+ settingsWrapper.getStringForUser(
+ Settings.Secure.CREDENTIAL_SERVICE, UserHandle.myUserId());
// Remove any providers that belong to the removed app.
- Set<String> credentialProviders =
- getStoredProviders(rawCredentialProviders, packageName);
- if (!Settings.Secure.putString(
- context.getContentResolver(),
+ Set<String> credentialProviders = getStoredProviders(rawCredentialProviders, packageName);
+ if (!settingsWrapper.putStringForUser(
Settings.Secure.CREDENTIAL_SERVICE,
- String.join(":", credentialProviders))) {
- Slog.w(TAG, "Failed to remove secondary package: " + packageName);
+ String.join(":", credentialProviders),
+ UserHandle.myUserId(),
+ /* overrideableByRestore= */ true)) {
+ Slog.e(TAG, "Failed to remove secondary package: " + packageName);
}
}
@@ -1232,4 +1232,38 @@
return providers;
}
+
+ /** A wrapper class that can be used by tests for intercepting reads/writes. */
+ public static class SettingsWrapper {
+ private final Context mContext;
+
+ public SettingsWrapper(@NonNull Context context) {
+ this.mContext = context;
+ }
+
+ ContentResolver getContentResolver() {
+ return mContext.getContentResolver();
+ }
+
+ /** Retrieves the string value of a system setting */
+ public String getStringForUser(String name, int userHandle) {
+ return Settings.Secure.getStringForUser(getContentResolver(), name, userHandle);
+ }
+
+ /** Updates the string value of a system setting */
+ public boolean putStringForUser(
+ String name,
+ String value,
+ int userHandle,
+ boolean overrideableByRestore) {
+ return Settings.Secure.putStringForUser(
+ getContentResolver(),
+ name,
+ value,
+ null,
+ false,
+ userHandle,
+ overrideableByRestore);
+ }
+ }
}
diff --git a/services/foldables/devicestateprovider/src/com/android/server/policy/FoldableDeviceStateProvider.java b/services/foldables/devicestateprovider/src/com/android/server/policy/FoldableDeviceStateProvider.java
index 4c487a7..ba72977 100644
--- a/services/foldables/devicestateprovider/src/com/android/server/policy/FoldableDeviceStateProvider.java
+++ b/services/foldables/devicestateprovider/src/com/android/server/policy/FoldableDeviceStateProvider.java
@@ -367,8 +367,11 @@
// TODO(b/312397262): consider virtual displays cases
synchronized (mLock) {
if (mIsDualDisplayBlockingEnabled
- && !mExternalDisplaysConnected.get(displayId, false)
- && mDisplayManager.getDisplay(displayId).getType() == TYPE_EXTERNAL) {
+ && !mExternalDisplaysConnected.get(displayId, false)) {
+ var display = mDisplayManager.getDisplay(displayId);
+ if (display == null || display.getType() != TYPE_EXTERNAL) {
+ return;
+ }
mExternalDisplaysConnected.put(displayId, true);
// Only update the supported state when going from 0 external display to 1
diff --git a/services/foldables/devicestateprovider/tests/src/com/android/server/policy/FoldableDeviceStateProviderTest.java b/services/foldables/devicestateprovider/tests/src/com/android/server/policy/FoldableDeviceStateProviderTest.java
index ddf4a08..04cebab 100644
--- a/services/foldables/devicestateprovider/tests/src/com/android/server/policy/FoldableDeviceStateProviderTest.java
+++ b/services/foldables/devicestateprovider/tests/src/com/android/server/policy/FoldableDeviceStateProviderTest.java
@@ -591,6 +591,20 @@
}
@Test
+ public void testOnDisplayAddedWithNullDisplayDoesNotThrowNPE() {
+ createProvider(
+ createConfig(
+ /* identifier= */ 1, /* name= */ "ONE",
+ /* flags= */0, (c) -> true,
+ FoldableDeviceStateProvider::hasNoConnectedExternalDisplay)
+ );
+
+ when(mDisplayManager.getDisplay(1)).thenReturn(null);
+ // This call should not throw NPE.
+ mProvider.onDisplayAdded(1);
+ }
+
+ @Test
public void hasNoConnectedDisplay_afterExternalDisplayAddedAndRemoved_returnsTrue() {
createProvider(
createConfig(
diff --git a/services/incremental/OWNERS b/services/incremental/OWNERS
index 7ebb962..c18a9e5 100644
--- a/services/incremental/OWNERS
+++ b/services/incremental/OWNERS
@@ -1,8 +1,4 @@
# Bug component: 554432
-include /services/core/java/com/android/server/pm/OWNERS
+include /PACKAGE_MANAGER_OWNERS
-alexbuy@google.com
-schfan@google.com
-toddke@google.com
-zyy@google.com
-patb@google.com
+zyy@google.com
\ No newline at end of file
diff --git a/services/permission/java/com/android/server/permission/access/appop/AppOpService.kt b/services/permission/java/com/android/server/permission/access/appop/AppOpService.kt
index 8f464d4..fc2eb26 100644
--- a/services/permission/java/com/android/server/permission/access/appop/AppOpService.kt
+++ b/services/permission/java/com/android/server/permission/access/appop/AppOpService.kt
@@ -17,6 +17,7 @@
package com.android.server.permission.access.appop
import android.app.AppOpsManager
+import android.companion.virtual.VirtualDeviceManager
import android.os.Handler
import android.os.UserHandle
import android.util.ArrayMap
@@ -213,7 +214,10 @@
val uid = key.first
val appOpCode = key.second
- listener.onUidModeChanged(uid, appOpCode, mode)
+ listener.onUidModeChanged(uid,
+ appOpCode,
+ mode,
+ VirtualDeviceManager.PERSISTENT_DEVICE_ID_DEFAULT)
}
}
diff --git a/services/permission/java/com/android/server/permission/access/permission/AppIdPermissionUpgrade.kt b/services/permission/java/com/android/server/permission/access/permission/AppIdPermissionUpgrade.kt
index edacda0..15c9b9f 100644
--- a/services/permission/java/com/android/server/permission/access/permission/AppIdPermissionUpgrade.kt
+++ b/services/permission/java/com/android/server/permission/access/permission/AppIdPermissionUpgrade.kt
@@ -20,7 +20,6 @@
import android.os.Build
import android.util.Slog
import com.android.server.permission.access.MutateStateScope
-import com.android.server.permission.access.collection.* // ktlint-disable no-wildcard-imports
import com.android.server.permission.access.immutable.* // ktlint-disable no-wildcard-imports
import com.android.server.permission.access.util.andInv
import com.android.server.permission.access.util.hasAnyBit
@@ -61,10 +60,11 @@
if (version <= 12 /*&& SdkLevel.isAtLeastT()*/) {
Slog.v(
LOG_TAG,
- "Upgrading scoped permissions for package: $packageName" +
+ "Upgrading scoped media and body sensor permissions for package: $packageName" +
", version: $version, user: $userId"
)
upgradeAuralVisualMediaPermissions(packageState, userId)
+ upgradeBodySensorPermissions(packageState, userId)
}
// TODO Enable isAtLeastU check, when moving subsystem to mainline.
if (version <= 14 /*&& SdkLevel.isAtLeastU()*/) {
@@ -182,6 +182,50 @@
}
}
+ private fun MutateStateScope.upgradeBodySensorPermissions(
+ packageState: PackageState,
+ userId: Int
+ ) {
+ if (
+ Manifest.permission.BODY_SENSORS_BACKGROUND !in
+ packageState.androidPackage!!.requestedPermissions
+ ) {
+ return
+ }
+
+ // Should have been granted when first getting exempt as if the perm was just split
+ val appId = packageState.appId
+ val backgroundBodySensorsFlags =
+ with(policy) {
+ getPermissionFlags(appId, userId, Manifest.permission.BODY_SENSORS_BACKGROUND)
+ }
+ if (backgroundBodySensorsFlags.hasAnyBit(PermissionFlags.MASK_EXEMPT)) {
+ return
+ }
+
+ // Add Upgrade Exemption - BODY_SENSORS_BACKGROUND is a restricted permission
+ with(policy) {
+ updatePermissionFlags(
+ appId,
+ userId,
+ Manifest.permission.BODY_SENSORS_BACKGROUND,
+ PermissionFlags.UPGRADE_EXEMPT,
+ PermissionFlags.UPGRADE_EXEMPT,
+ )
+ }
+
+ val bodySensorsFlags =
+ with(policy) { getPermissionFlags(appId, userId, Manifest.permission.BODY_SENSORS) }
+ val isForegroundBodySensorsGranted = PermissionFlags.isAppOpGranted(bodySensorsFlags)
+ if (isForegroundBodySensorsGranted) {
+ grantRuntimePermission(
+ packageState,
+ userId,
+ Manifest.permission.BODY_SENSORS_BACKGROUND
+ )
+ }
+ }
+
/** Upgrade permission based on the grant in [Manifest.permission_group.READ_MEDIA_VISUAL] */
private fun MutateStateScope.upgradeUserSelectedVisualMediaPermission(
packageState: PackageState,
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 b9f1ea0..dc9631a 100644
--- a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/ClientControllerTest.java
+++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/ClientControllerTest.java
@@ -116,7 +116,7 @@
ANY_CALLER_PID);
verify(invoker.asBinder()).linkToDeath(any(IBinder.DeathRecipient.class), eq(0));
- assertThat(mController.mClients).containsEntry(invoker.asBinder(), added);
+ assertThat(mController.getClient(invoker.asBinder())).isSameInstanceAs(added);
}
}
@@ -133,7 +133,7 @@
var invoker = IInputMethodClientInvoker.create(mClient, mHandler);
added = mController.addClient(invoker, mConnection, ANY_DISPLAY_ID, ANY_CALLER_UID,
ANY_CALLER_PID);
- assertThat(mController.mClients).containsEntry(invoker.asBinder(), added);
+ assertThat(mController.getClient(invoker.asBinder())).isSameInstanceAs(added);
assertThat(mController.removeClient(mClient)).isTrue();
}
diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java
index 02e3ef4..75febd9 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java
@@ -61,6 +61,7 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.app.ActivityOptions.LaunchCookie;
import android.app.PropertyInvalidatedCache;
import android.companion.virtual.IVirtualDevice;
import android.companion.virtual.IVirtualDeviceManager;
@@ -1557,7 +1558,7 @@
when(mMockProjectionService
.setContentRecordingSession(any(ContentRecordingSession.class), eq(projection)))
.thenReturn(true);
- doReturn(mock(IBinder.class)).when(projection).getLaunchCookie();
+ doReturn(new LaunchCookie()).when(projection).getLaunchCookie();
doReturn(true).when(mMockProjectionService).isCurrentProjection(eq(projection));
final VirtualDisplayConfig.Builder builder = new VirtualDisplayConfig.Builder(
diff --git a/services/tests/displayservicetests/src/com/android/server/display/mode/DisplayModeDirectorTest.java b/services/tests/displayservicetests/src/com/android/server/display/mode/DisplayModeDirectorTest.java
index c556aca..64076e6 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/mode/DisplayModeDirectorTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/mode/DisplayModeDirectorTest.java
@@ -1727,6 +1727,7 @@
var desiredSpecs = director.getDesiredDisplayModeSpecs(DISPLAY_ID_2);
assertThat(desiredSpecs.primary.render.max).isEqualTo(expectedMaxRenderFrameRate);
+ assertThat(desiredSpecs.appRequest.render.max).isEqualTo(expectedMaxRenderFrameRate);
}
@Test
diff --git a/services/tests/mockingservicestests/Android.bp b/services/tests/mockingservicestests/Android.bp
index 321d945..d928306 100644
--- a/services/tests/mockingservicestests/Android.bp
+++ b/services/tests/mockingservicestests/Android.bp
@@ -46,7 +46,6 @@
"androidx.test.espresso.core",
"androidx.test.espresso.contrib",
"androidx.test.ext.truth",
- "backup_flags_lib",
"flag-junit",
"frameworks-base-testutils",
"hamcrest-library",
diff --git a/services/tests/mockingservicestests/src/com/android/server/AppStateTrackerTest.java b/services/tests/mockingservicestests/src/com/android/server/AppStateTrackerTest.java
index 47ae97f..0e85626 100644
--- a/services/tests/mockingservicestests/src/com/android/server/AppStateTrackerTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/AppStateTrackerTest.java
@@ -44,6 +44,7 @@
import android.app.IActivityManager;
import android.app.IUidObserver;
import android.app.usage.UsageStatsManager;
+import android.companion.virtual.VirtualDeviceManager;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
@@ -309,7 +310,8 @@
mRestrictedPackages.remove(p);
}
if (mAppOpsCallback != null) {
- mAppOpsCallback.opChanged(AppOpsManager.OP_RUN_ANY_IN_BACKGROUND, uid, packageName);
+ mAppOpsCallback.opChanged(AppOpsManager.OP_RUN_ANY_IN_BACKGROUND, uid, packageName,
+ VirtualDeviceManager.PERSISTENT_DEVICE_ID_DEFAULT);
}
}
diff --git a/services/tests/mockingservicestests/src/com/android/server/alarm/AlarmManagerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/alarm/AlarmManagerServiceTest.java
index b39cd04..a476155 100644
--- a/services/tests/mockingservicestests/src/com/android/server/alarm/AlarmManagerServiceTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/alarm/AlarmManagerServiceTest.java
@@ -125,6 +125,7 @@
import android.app.compat.CompatChanges;
import android.app.tare.EconomyManager;
import android.app.usage.UsageStatsManagerInternal;
+import android.companion.virtual.VirtualDeviceManager;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
@@ -2980,7 +2981,8 @@
mService.mLastOpScheduleExactAlarm.put(TEST_CALLING_UID, MODE_ALLOWED);
mockScheduleExactAlarmStatePreT(true, MODE_ERRORED);
- mIAppOpsCallback.opChanged(OP_SCHEDULE_EXACT_ALARM, TEST_CALLING_UID, TEST_CALLING_PACKAGE);
+ mIAppOpsCallback.opChanged(OP_SCHEDULE_EXACT_ALARM, TEST_CALLING_UID, TEST_CALLING_PACKAGE,
+ VirtualDeviceManager.PERSISTENT_DEVICE_ID_DEFAULT);
assertAndHandleMessageSync(REMOVE_EXACT_ALARMS);
verify(mService).removeExactAlarmsOnPermissionRevoked(TEST_CALLING_UID,
TEST_CALLING_PACKAGE, true);
@@ -2993,7 +2995,8 @@
mService.mLastOpScheduleExactAlarm.put(TEST_CALLING_UID, MODE_ALLOWED);
mockScheduleExactAlarmStatePreT(true, MODE_ERRORED);
- mIAppOpsCallback.opChanged(OP_SCHEDULE_EXACT_ALARM, TEST_CALLING_UID, TEST_CALLING_PACKAGE);
+ mIAppOpsCallback.opChanged(OP_SCHEDULE_EXACT_ALARM, TEST_CALLING_UID, TEST_CALLING_PACKAGE,
+ VirtualDeviceManager.PERSISTENT_DEVICE_ID_DEFAULT);
verify(mService.mHandler, never()).sendMessageAtTime(
argThat(m -> m.what == REMOVE_EXACT_ALARMS), anyLong());
@@ -3008,7 +3011,8 @@
mService.mLastOpScheduleExactAlarm.put(TEST_CALLING_UID, MODE_ERRORED);
mockScheduleExactAlarmStatePreT(true, MODE_ALLOWED);
- mIAppOpsCallback.opChanged(OP_SCHEDULE_EXACT_ALARM, TEST_CALLING_UID, TEST_CALLING_PACKAGE);
+ mIAppOpsCallback.opChanged(OP_SCHEDULE_EXACT_ALARM, TEST_CALLING_UID, TEST_CALLING_PACKAGE,
+ VirtualDeviceManager.PERSISTENT_DEVICE_ID_DEFAULT);
final ArgumentCaptor<Intent> intentCaptor = ArgumentCaptor.forClass(Intent.class);
final ArgumentCaptor<Bundle> bundleCaptor = ArgumentCaptor.forClass(Bundle.class);
diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/PackageArchiverTest.java b/services/tests/mockingservicestests/src/com/android/server/pm/PackageArchiverTest.java
index a65ef00..bf00b75 100644
--- a/services/tests/mockingservicestests/src/com/android/server/pm/PackageArchiverTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/pm/PackageArchiverTest.java
@@ -552,20 +552,22 @@
when(mComputer.getPackageStateFiltered(eq(PACKAGE), anyInt(), anyInt())).thenReturn(
null);
- assertThat(mArchiveManager.getArchivedAppIcon(PACKAGE, UserHandle.CURRENT)).isNull();
+ assertThat(mArchiveManager.getArchivedAppIcon(PACKAGE, UserHandle.CURRENT,
+ CALLER_PACKAGE)).isNull();
}
@Test
public void getArchivedAppIcon_notArchived() {
- assertThat(mArchiveManager.getArchivedAppIcon(PACKAGE, UserHandle.CURRENT)).isNull();
+ assertThat(mArchiveManager.getArchivedAppIcon(PACKAGE, UserHandle.CURRENT,
+ CALLER_PACKAGE)).isNull();
}
@Test
public void getArchivedAppIcon_success() {
mUserState.setArchiveState(createArchiveState()).setInstalled(false);
- assertThat(mArchiveManager.getArchivedAppIcon(PACKAGE, UserHandle.CURRENT)).isEqualTo(
- mIcon);
+ assertThat(mArchiveManager.getArchivedAppIcon(PACKAGE, UserHandle.CURRENT,
+ CALLER_PACKAGE)).isEqualTo(mIcon);
}
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsHistoryTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsHistoryTest.java
index bb70080..9251376 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsHistoryTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsHistoryTest.java
@@ -21,6 +21,9 @@
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.when;
import android.content.Context;
@@ -96,18 +99,11 @@
mClock.realtime = 123;
mHistory = new BatteryStatsHistory(mHistoryBuffer, mSystemDir, 32, 1024,
- mStepDetailsCalculator, mClock, mMonotonicClock, mTracer) {
- @Override
- public boolean readFileToParcel(Parcel out, AtomicFile file) {
- mReadFiles.add(file.getBaseFile().getName());
- return super.readFileToParcel(out, file);
- }
- };
+ mStepDetailsCalculator, mClock, mMonotonicClock, mTracer);
when(mStepDetailsCalculator.getHistoryStepDetails())
.thenReturn(new BatteryStats.HistoryStepDetails());
-
mHistoryPrinter = new BatteryStats.HistoryPrinter();
}
@@ -276,6 +272,15 @@
mReadFiles.clear();
+ // Make an immutable copy and spy on it
+ mHistory = spy(mHistory.copy());
+
+ doAnswer(invocation -> {
+ AtomicFile file = invocation.getArgument(1);
+ mReadFiles.add(file.getBaseFile().getName());
+ return invocation.callRealMethod();
+ }).when(mHistory).readFileToParcel(any(), any());
+
// Prepare history for iteration
mHistory.iterate(0, MonotonicClock.UNDEFINED);
@@ -309,6 +314,15 @@
mReadFiles.clear();
+ // Make an immutable copy and spy on it
+ mHistory = spy(mHistory.copy());
+
+ doAnswer(invocation -> {
+ AtomicFile file = invocation.getArgument(1);
+ mReadFiles.add(file.getBaseFile().getName());
+ return invocation.callRealMethod();
+ }).when(mHistory).readFileToParcel(any(), any());
+
// Prepare history for iteration
mHistory.iterate(1000, 3000);
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/SystemServicePowerCalculatorTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/SystemServicePowerCalculatorTest.java
index 4dae2d5..8e53d52 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/SystemServicePowerCalculatorTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/SystemServicePowerCalculatorTest.java
@@ -28,6 +28,9 @@
import android.os.BatteryConsumer;
import android.os.Binder;
import android.os.Process;
+import android.platform.test.annotations.RequiresFlagsDisabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
@@ -38,6 +41,7 @@
import com.android.internal.os.KernelSingleUidTimeReader;
import com.android.internal.os.PowerProfile;
import com.android.internal.power.EnergyConsumerStats;
+import com.android.server.power.optimization.Flags;
import org.junit.Before;
import org.junit.Rule;
@@ -54,6 +58,8 @@
@RunWith(AndroidJUnit4.class)
@SuppressWarnings("GuardedBy")
public class SystemServicePowerCalculatorTest {
+ @Rule
+ public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
private static final double PRECISION = 0.000001;
private static final int APP_UID1 = 100;
@@ -108,6 +114,7 @@
}
@Test
+ @RequiresFlagsDisabled(Flags.FLAG_DISABLE_SYSTEM_SERVICE_POWER_ATTR)
public void testPowerProfileBasedModel() {
prepareBatteryStats(null);
@@ -135,6 +142,7 @@
}
@Test
+ @RequiresFlagsDisabled(Flags.FLAG_DISABLE_SYSTEM_SERVICE_POWER_ATTR)
public void testMeasuredEnergyBasedModel() {
final boolean[] supportedPowerBuckets =
new boolean[EnergyConsumerStats.NUMBER_STANDARD_POWER_BUCKETS];
diff --git a/services/tests/servicestests/Android.bp b/services/tests/servicestests/Android.bp
index 8958fac..5611415 100644
--- a/services/tests/servicestests/Android.bp
+++ b/services/tests/servicestests/Android.bp
@@ -36,6 +36,7 @@
"-Werror",
],
static_libs: [
+ "cts-input-lib",
"frameworks-base-testutils",
"services.accessibility",
"services.appwidget",
@@ -83,6 +84,7 @@
"flag-junit",
"ravenwood-junit",
"net_flags_lib",
+ "CtsVirtualDeviceCommonLib",
],
libs: [
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityInputFilterInputTest.kt b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityInputFilterInputTest.kt
new file mode 100644
index 0000000..52c7d8d
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityInputFilterInputTest.kt
@@ -0,0 +1,280 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.server.accessibility
+
+import android.hardware.display.DisplayManagerGlobal
+import android.os.SystemClock
+import android.view.Display
+import android.view.Display.DEFAULT_DISPLAY
+import android.view.DisplayAdjustments
+import android.view.DisplayInfo
+import android.view.IInputFilterHost
+import android.view.InputDevice.SOURCE_TOUCHSCREEN
+import android.view.InputEvent
+import android.view.MotionEvent
+import android.view.MotionEvent.ACTION_DOWN
+import android.view.MotionEvent.ACTION_MOVE
+import android.view.MotionEvent.ACTION_UP
+import android.view.MotionEvent.ACTION_HOVER_ENTER
+import android.view.MotionEvent.ACTION_HOVER_EXIT
+import android.view.MotionEvent.ACTION_HOVER_MOVE
+import android.view.WindowManagerPolicyConstants.FLAG_PASS_TO_USER
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.cts.input.inputeventmatchers.withDeviceId
+import com.android.cts.input.inputeventmatchers.withMotionAction
+import com.android.server.LocalServices
+import com.android.server.accessibility.magnification.MagnificationProcessor
+import com.android.server.wm.WindowManagerInternal
+import java.util.concurrent.LinkedBlockingQueue
+import org.hamcrest.Matchers.allOf
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.any
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.Mock
+import org.mockito.Mockito
+import org.mockito.Mockito.doAnswer
+import org.mockito.Mockito.`when`
+import org.mockito.junit.MockitoJUnit
+import org.mockito.junit.MockitoRule
+import org.mockito.stubbing.OngoingStubbing
+
+
+/**
+ * Create a MotionEvent with the provided action, eventTime, and source
+ */
+fun createMotionEvent(action: Int, downTime: Long, eventTime: Long, source: Int, deviceId: Int):
+ MotionEvent {
+ val x = 1f
+ val y = 2f
+ val pressure = 3f
+ val size = 1f
+ val metaState = 0
+ val xPrecision = 0f
+ val yPrecision = 0f
+ val edgeFlags = 0
+ val displayId = 0
+ return MotionEvent.obtain(downTime, eventTime, action, x, y, pressure, size, metaState,
+ xPrecision, yPrecision, deviceId, edgeFlags, source, displayId)
+}
+
+/**
+ * Tests for AccessibilityInputFilter, focusing on the input event processing as seen by the callers
+ * of the InputFilter interface.
+ * The main interaction with AccessibilityInputFilter in these tests is with the filterInputEvent
+ * and sendInputEvent APIs of InputFilter.
+ */
+@RunWith(AndroidJUnit4::class)
+class AccessibilityInputFilterInputTest {
+ private val instrumentation = InstrumentationRegistry.getInstrumentation()
+
+ private companion object{
+ const val ALL_A11Y_FEATURES = (AccessibilityInputFilter.FLAG_FEATURE_AUTOCLICK
+ or AccessibilityInputFilter.FLAG_FEATURE_TOUCH_EXPLORATION
+ or AccessibilityInputFilter.FLAG_FEATURE_CONTROL_SCREEN_MAGNIFIER
+ or AccessibilityInputFilter.FLAG_FEATURE_TRIGGERED_SCREEN_MAGNIFIER
+ or AccessibilityInputFilter.FLAG_FEATURE_INJECT_MOTION_EVENTS
+ or AccessibilityInputFilter.FLAG_FEATURE_FILTER_KEY_EVENTS)
+ }
+
+ @Rule
+ @JvmField
+ val mocks: MockitoRule = MockitoJUnit.rule()
+
+ @Mock
+ private lateinit var mockA11yController: WindowManagerInternal.AccessibilityControllerInternal
+
+ @Mock
+ private lateinit var mockWindowManagerService: WindowManagerInternal
+
+ @Mock
+ private lateinit var mockMagnificationProcessor: MagnificationProcessor
+
+ private val inputEvents = LinkedBlockingQueue<InputEvent>()
+ private val verifier = BlockingQueueEventVerifier(inputEvents)
+
+ @Mock
+ private lateinit var host: IInputFilterHost
+ private lateinit var ams: AccessibilityManagerService
+ private lateinit var a11yInputFilter: AccessibilityInputFilter
+ private val touchDeviceId = 1
+
+ @Before
+ fun setUp() {
+ val context = instrumentation.context
+ LocalServices.removeServiceForTest(WindowManagerInternal::class.java)
+ LocalServices.addService(WindowManagerInternal::class.java, mockWindowManagerService)
+
+ whenever(mockA11yController.isAccessibilityTracingEnabled).thenReturn(false)
+ whenever(
+ mockWindowManagerService.accessibilityController).thenReturn(
+ mockA11yController)
+
+ ams = Mockito.spy(AccessibilityManagerService(context))
+ val displayList = arrayListOf(createStubDisplay(DEFAULT_DISPLAY, DisplayInfo()))
+ whenever(ams.validDisplayList).thenReturn(displayList)
+ whenever(ams.magnificationProcessor).thenReturn(mockMagnificationProcessor)
+
+ doAnswer {
+ val event = it.getArgument(0) as MotionEvent
+ inputEvents.add(MotionEvent.obtain(event))
+ }.`when`(host).sendInputEvent(any(), anyInt())
+
+ a11yInputFilter = AccessibilityInputFilter(context, ams)
+ a11yInputFilter.install(host)
+ }
+
+ @After
+ fun tearDown() {
+ if (this::a11yInputFilter.isInitialized) {
+ a11yInputFilter.uninstall()
+ }
+ }
+
+ /**
+ * When no features are enabled, the events pass through the filter without getting modified.
+ */
+ @Test
+ fun testSingleDeviceTouchEventsWithoutA11yFeatures() {
+ enableFeatures(0)
+
+ val downTime = SystemClock.uptimeMillis()
+ val downEvent = createMotionEvent(
+ ACTION_DOWN, downTime, downTime, SOURCE_TOUCHSCREEN, touchDeviceId)
+ send(downEvent)
+ verifier.assertReceivedMotion(
+ allOf(withMotionAction(ACTION_DOWN), withDeviceId(touchDeviceId)))
+
+ val moveEvent = createMotionEvent(
+ ACTION_MOVE, downTime, SystemClock.uptimeMillis(), SOURCE_TOUCHSCREEN, touchDeviceId)
+ send(moveEvent)
+ verifier.assertReceivedMotion(
+ allOf(withMotionAction(ACTION_MOVE), withDeviceId(touchDeviceId)))
+
+ val upEvent = createMotionEvent(
+ ACTION_UP, downTime, SystemClock.uptimeMillis(), SOURCE_TOUCHSCREEN, touchDeviceId)
+ send(upEvent)
+ verifier.assertReceivedMotion(
+ allOf(withMotionAction(ACTION_UP), withDeviceId(touchDeviceId)))
+
+ verifier.assertNoEvents()
+ }
+
+ /**
+ * Enable all a11y features and send a touchscreen stream of DOWN -> MOVE -> UP events.
+ * These get converted into HOVER_ENTER -> HOVER_MOVE -> HOVER_EXIT events by the input filter.
+ */
+ @Test
+ fun testSingleDeviceTouchEventsWithAllA11yFeatures() {
+ enableFeatures(ALL_A11Y_FEATURES)
+
+ val downTime = SystemClock.uptimeMillis()
+ val downEvent = createMotionEvent(
+ ACTION_DOWN, downTime, downTime, SOURCE_TOUCHSCREEN, touchDeviceId)
+ send(MotionEvent.obtain(downEvent))
+
+ // DOWN event gets transformed to HOVER_ENTER
+ verifier.assertReceivedMotion(
+ allOf(withMotionAction(ACTION_HOVER_ENTER), withDeviceId(touchDeviceId)))
+
+ // MOVE becomes HOVER_MOVE
+ val moveEvent = createMotionEvent(
+ ACTION_MOVE, downTime, SystemClock.uptimeMillis(), SOURCE_TOUCHSCREEN, touchDeviceId)
+ send(moveEvent)
+ verifier.assertReceivedMotion(
+ allOf(withMotionAction(ACTION_HOVER_MOVE), withDeviceId(touchDeviceId)))
+
+ // UP becomes HOVER_EXIT
+ val upEvent = createMotionEvent(
+ ACTION_UP, downTime, SystemClock.uptimeMillis(), SOURCE_TOUCHSCREEN, touchDeviceId)
+ send(upEvent)
+
+ verifier.assertReceivedMotion(
+ allOf(withMotionAction(ACTION_HOVER_EXIT), withDeviceId(touchDeviceId)))
+
+ verifier.assertNoEvents()
+ }
+
+ /**
+ * Enable all a11y features and send a touchscreen event stream. In the middle of the gesture,
+ * disable the a11y features.
+ * When the a11y features are disabled, the filter generates HOVER_EXIT without further input
+ * from the dispatcher.
+ */
+ @Test
+ fun testSingleDeviceTouchEventsDisableFeaturesMidGesture() {
+ enableFeatures(ALL_A11Y_FEATURES)
+
+ val downTime = SystemClock.uptimeMillis()
+ val downEvent = createMotionEvent(
+ ACTION_DOWN, downTime, downTime, SOURCE_TOUCHSCREEN, touchDeviceId)
+ send(MotionEvent.obtain(downEvent))
+
+ // DOWN event gets transformed to HOVER_ENTER
+ verifier.assertReceivedMotion(
+ allOf(withMotionAction(ACTION_HOVER_ENTER), withDeviceId(touchDeviceId)))
+ verifier.assertNoEvents()
+
+ enableFeatures(0)
+ verifier.assertReceivedMotion(
+ allOf(withMotionAction(ACTION_HOVER_EXIT), withDeviceId(touchDeviceId)))
+ verifier.assertNoEvents()
+
+ val moveEvent = createMotionEvent(
+ ACTION_MOVE, downTime, SystemClock.uptimeMillis(), SOURCE_TOUCHSCREEN, touchDeviceId)
+ send(moveEvent)
+ val upEvent = createMotionEvent(
+ ACTION_UP, downTime, SystemClock.uptimeMillis(), SOURCE_TOUCHSCREEN, touchDeviceId)
+ send(upEvent)
+ // As the original gesture continues, no additional events should be getting sent by the
+ // filter because the HOVER_EXIT above already effectively finished the current gesture and
+ // the DOWN event was never sent to the host.
+
+ // Bug: the down event was swallowed, so the remainder of the gesture should be swallowed
+ // too. However, the MOVE and UP events are currently passed back to the dispatcher.
+ // TODO(b/310014874) - ensure a11y sends consistent input streams to the dispatcher
+ verifier.assertReceivedMotion(
+ allOf(withMotionAction(ACTION_MOVE), withDeviceId(touchDeviceId)))
+ verifier.assertReceivedMotion(
+ allOf(withMotionAction(ACTION_UP), withDeviceId(touchDeviceId)))
+
+ verifier.assertNoEvents()
+ }
+
+ private fun createStubDisplay(displayId: Int, displayInfo: DisplayInfo): Display {
+ val display = Display(DisplayManagerGlobal.getInstance(), displayId,
+ displayInfo, DisplayAdjustments.DEFAULT_DISPLAY_ADJUSTMENTS)
+ return display
+ }
+
+ private fun send(event: InputEvent) {
+ // We need to make a copy of the event before sending it to the filter, because the filter
+ // will recycle it, but the caller of this function might want to still be able to use
+ // this event for subsequent checks
+ val eventCopy = if (event is MotionEvent) MotionEvent.obtain(event) else event
+ a11yInputFilter.filterInputEvent(eventCopy, FLAG_PASS_TO_USER)
+ }
+
+ private fun enableFeatures(features: Int) {
+ instrumentation.runOnMainSync { a11yInputFilter.setUserAndEnabledFeatures(0, features) }
+ }
+}
+
+private fun <T> whenever(methodCall: T): OngoingStubbing<T> = `when`(methodCall)
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/BlockingQueueEventVerifier.kt b/services/tests/servicestests/src/com/android/server/accessibility/BlockingQueueEventVerifier.kt
new file mode 100644
index 0000000..b12f537
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/accessibility/BlockingQueueEventVerifier.kt
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.server.accessibility
+
+import android.os.InputConstants.DEFAULT_DISPATCHING_TIMEOUT_MILLIS
+import android.view.InputEvent
+import android.view.MotionEvent
+import java.time.Duration
+import java.util.concurrent.BlockingQueue
+import java.util.concurrent.TimeUnit
+import org.junit.Assert.fail
+
+import org.hamcrest.Matcher
+import org.hamcrest.MatcherAssert.assertThat
+import org.junit.Assert.assertNull
+
+private fun <T> getEvent(queue: BlockingQueue<T>, timeout: Duration): T? {
+ return queue.poll(timeout.toMillis(), TimeUnit.MILLISECONDS)
+}
+
+class BlockingQueueEventVerifier(val queue: BlockingQueue<InputEvent>) {
+ fun assertReceivedMotion(matcher: Matcher<MotionEvent>) {
+ val event = getMotionEvent()
+ assertThat("MotionEvent checks", event, matcher)
+ }
+
+ fun assertNoEvents() {
+ val event = getEvent(queue, Duration.ofMillis(50))
+ assertNull(event)
+ }
+
+ private fun getMotionEvent(): MotionEvent {
+ val event = getEvent(queue, Duration.ofMillis(DEFAULT_DISPATCHING_TIMEOUT_MILLIS.toLong()))
+ if (event == null) {
+ fail("Did not get an event")
+ }
+ if (event is MotionEvent) {
+ return event
+ }
+ fail("Instead of motion, got $event")
+ throw RuntimeException("should not reach here")
+ }
+}
+
diff --git a/services/tests/servicestests/src/com/android/server/appop/AppOpsActiveWatcherTest.java b/services/tests/servicestests/src/com/android/server/appop/AppOpsActiveWatcherTest.java
index a2aaccc..b487dc6 100644
--- a/services/tests/servicestests/src/com/android/server/appop/AppOpsActiveWatcherTest.java
+++ b/services/tests/servicestests/src/com/android/server/appop/AppOpsActiveWatcherTest.java
@@ -16,6 +16,8 @@
package com.android.server.appop;
+import static com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity;
+
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertFalse;
@@ -31,17 +33,23 @@
import android.app.AppOpsManager;
import android.app.AppOpsManager.OnOpActiveChangedListener;
+import android.companion.virtual.VirtualDeviceManager;
+import android.companion.virtual.VirtualDeviceParams;
+import android.content.AttributionSource;
import android.content.Context;
import android.os.Process;
+import android.virtualdevice.cts.common.FakeAssociationRule;
import androidx.test.InstrumentationRegistry;
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
+import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.util.List;
+import java.util.concurrent.atomic.AtomicInteger;
/**
* Tests app ops version upgrades
@@ -50,6 +58,8 @@
@RunWith(AndroidJUnit4.class)
public class AppOpsActiveWatcherTest {
+ @Rule
+ public FakeAssociationRule mFakeAssociationRule = new FakeAssociationRule();
private static final long NOTIFICATION_TIMEOUT_MILLIS = 5000;
@Test
@@ -69,7 +79,7 @@
verify(listener, timeout(NOTIFICATION_TIMEOUT_MILLIS)
.times(1)).onOpActiveChanged(eq(AppOpsManager.OPSTR_CAMERA),
eq(Process.myUid()), eq(getContext().getPackageName()),
- isNull(), eq(true), anyInt(), anyInt());
+ isNull(), eq(Context.DEVICE_ID_DEFAULT), eq(true), anyInt(), anyInt());
// This should be the only callback we got
verifyNoMoreInteractions(listener);
@@ -88,7 +98,7 @@
verify(listener, timeout(NOTIFICATION_TIMEOUT_MILLIS)
.times(1)).onOpActiveChanged(eq(AppOpsManager.OPSTR_CAMERA),
eq(Process.myUid()), eq(getContext().getPackageName()), isNull(),
- eq(false), anyInt(), anyInt());
+ eq(Context.DEVICE_ID_DEFAULT), eq(false), anyInt(), anyInt());
// Verify that the op is not active
assertThat(appOpsManager.isOperationActive(AppOpsManager.OP_CAMERA,
@@ -126,7 +136,7 @@
verify(listener, timeout(NOTIFICATION_TIMEOUT_MILLIS)
.times(1)).onOpActiveChanged(eq(AppOpsManager.OPSTR_CAMERA),
eq(Process.myUid()), eq(getContext().getPackageName()), isNull(),
- eq(true), anyInt(), anyInt());
+ eq(Context.DEVICE_ID_DEFAULT), eq(true), anyInt(), anyInt());
// Finish up
appOpsManager.finishOp(AppOpsManager.OP_CAMERA);
@@ -134,6 +144,64 @@
}
@Test
+ public void testWatchActiveOpsForExternalDevice() {
+ final VirtualDeviceManager virtualDeviceManager = getContext().getSystemService(
+ VirtualDeviceManager.class);
+ AtomicInteger virtualDeviceId = new AtomicInteger();
+ runWithShellPermissionIdentity(() -> {
+ final VirtualDeviceManager.VirtualDevice virtualDevice =
+ virtualDeviceManager.createVirtualDevice(
+ mFakeAssociationRule.getAssociationInfo().getId(),
+ new VirtualDeviceParams.Builder().setName("virtual_device").build());
+ virtualDeviceId.set(virtualDevice.getDeviceId());
+ });
+ final OnOpActiveChangedListener listener = mock(OnOpActiveChangedListener.class);
+ AttributionSource attributionSource = new AttributionSource(Process.myUid(),
+ getContext().getOpPackageName(), getContext().getAttributionTag(),
+ virtualDeviceId.get());
+
+ final AppOpsManager appOpsManager = getContext().getSystemService(AppOpsManager.class);
+ appOpsManager.startWatchingActive(new String[]{AppOpsManager.OPSTR_CAMERA,
+ AppOpsManager.OPSTR_RECORD_AUDIO}, getContext().getMainExecutor(), listener);
+
+ appOpsManager.startOpNoThrow(getContext().getAttributionSource().getToken(),
+ AppOpsManager.OP_CAMERA, attributionSource, false, "",
+ AppOpsManager.ATTRIBUTION_FLAGS_NONE, AppOpsManager.ATTRIBUTION_CHAIN_ID_NONE);
+
+ verify(listener, timeout(NOTIFICATION_TIMEOUT_MILLIS)
+ .times(1)).onOpActiveChanged(eq(AppOpsManager.OPSTR_CAMERA),
+ eq(Process.myUid()), eq(getContext().getOpPackageName()),
+ eq(getContext().getAttributionTag()), eq(virtualDeviceId.get()), eq(true),
+ eq(AppOpsManager.ATTRIBUTION_FLAGS_NONE),
+ eq(AppOpsManager.ATTRIBUTION_CHAIN_ID_NONE));
+ verifyNoMoreInteractions(listener);
+
+ appOpsManager.finishOp(getContext().getAttributionSource().getToken(),
+ AppOpsManager.OP_CAMERA, attributionSource);
+
+ verify(listener, timeout(NOTIFICATION_TIMEOUT_MILLIS)
+ .times(1)).onOpActiveChanged(eq(AppOpsManager.OPSTR_CAMERA),
+ eq(Process.myUid()), eq(getContext().getOpPackageName()),
+ eq(getContext().getAttributionTag()), eq(virtualDeviceId.get()), eq(false),
+ eq(AppOpsManager.ATTRIBUTION_FLAGS_NONE),
+ eq(AppOpsManager.ATTRIBUTION_CHAIN_ID_NONE));
+ verifyNoMoreInteractions(listener);
+
+ appOpsManager.stopWatchingActive(listener);
+
+ appOpsManager.startOpNoThrow(getContext().getAttributionSource().getToken(),
+ AppOpsManager.OP_CAMERA, attributionSource, false, "",
+ AppOpsManager.ATTRIBUTION_FLAGS_NONE, AppOpsManager.ATTRIBUTION_CHAIN_ID_NONE);
+
+ verifyNoMoreInteractions(listener);
+
+ appOpsManager.finishOp(getContext().getAttributionSource().getToken(),
+ AppOpsManager.OP_CAMERA, attributionSource);
+
+ verifyNoMoreInteractions(listener);
+ }
+
+ @Test
public void testIsRunning() throws Exception {
final AppOpsManager appOpsManager = getContext().getSystemService(AppOpsManager.class);
// Start the op
diff --git a/services/tests/servicestests/src/com/android/server/appop/AppOpsNotedWatcherTest.java b/services/tests/servicestests/src/com/android/server/appop/AppOpsNotedWatcherTest.java
index b5229d8..1abd4eb 100644
--- a/services/tests/servicestests/src/com/android/server/appop/AppOpsNotedWatcherTest.java
+++ b/services/tests/servicestests/src/com/android/server/appop/AppOpsNotedWatcherTest.java
@@ -22,27 +22,36 @@
import static org.mockito.Mockito.timeout;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity;
import android.app.AppOpsManager;
import android.app.AppOpsManager.OnOpNotedListener;
+import android.companion.virtual.VirtualDeviceManager;
+import android.companion.virtual.VirtualDeviceParams;
+import android.content.AttributionSource;
import android.content.Context;
import android.os.Process;
+import android.virtualdevice.cts.common.FakeAssociationRule;
import androidx.test.InstrumentationRegistry;
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
+import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InOrder;
+import java.util.concurrent.atomic.AtomicInteger;
+
/**
* Tests watching noted ops.
*/
@SmallTest
@RunWith(AndroidJUnit4.class)
public class AppOpsNotedWatcherTest {
-
+ @Rule
+ public FakeAssociationRule mFakeAssociationRule = new FakeAssociationRule();
private static final long NOTIFICATION_TIMEOUT_MILLIS = 5000;
@Test
@@ -66,12 +75,14 @@
inOrder.verify(listener, timeout(NOTIFICATION_TIMEOUT_MILLIS)
.times(1)).onOpNoted(eq(AppOpsManager.OPSTR_FINE_LOCATION),
eq(Process.myUid()), eq(getContext().getPackageName()),
- eq(getContext().getAttributionTag()), eq(AppOpsManager.OP_FLAG_SELF),
+ eq(getContext().getAttributionTag()), eq(Context.DEVICE_ID_DEFAULT),
+ eq(AppOpsManager.OP_FLAG_SELF),
eq(AppOpsManager.MODE_ALLOWED));
inOrder.verify(listener, timeout(NOTIFICATION_TIMEOUT_MILLIS)
.times(1)).onOpNoted(eq(AppOpsManager.OPSTR_CAMERA),
eq(Process.myUid()), eq(getContext().getPackageName()),
- eq(getContext().getAttributionTag()), eq(AppOpsManager.OP_FLAG_SELF),
+ eq(getContext().getAttributionTag()), eq(Context.DEVICE_ID_DEFAULT),
+ eq(AppOpsManager.OP_FLAG_SELF),
eq(AppOpsManager.MODE_ALLOWED));
// Stop watching
@@ -96,13 +107,54 @@
verify(listener, timeout(NOTIFICATION_TIMEOUT_MILLIS)
.times(2)).onOpNoted(eq(AppOpsManager.OPSTR_FINE_LOCATION),
eq(Process.myUid()), eq(getContext().getPackageName()),
- eq(getContext().getAttributionTag()), eq(AppOpsManager.OP_FLAG_SELF),
+ eq(getContext().getAttributionTag()), eq(Context.DEVICE_ID_DEFAULT),
+ eq(AppOpsManager.OP_FLAG_SELF),
eq(AppOpsManager.MODE_ALLOWED));
// Finish up
appOpsManager.stopWatchingNoted(listener);
}
+ @Test
+ public void testWatchNotedOpsForExternalDevice() {
+ final AppOpsManager.OnOpNotedListener listener = mock(
+ AppOpsManager.OnOpNotedListener.class);
+ final VirtualDeviceManager virtualDeviceManager = getContext().getSystemService(
+ VirtualDeviceManager.class);
+ AtomicInteger virtualDeviceId = new AtomicInteger();
+ runWithShellPermissionIdentity(() -> {
+ final VirtualDeviceManager.VirtualDevice virtualDevice =
+ virtualDeviceManager.createVirtualDevice(
+ mFakeAssociationRule.getAssociationInfo().getId(),
+ new VirtualDeviceParams.Builder().setName("virtual_device").build());
+ virtualDeviceId.set(virtualDevice.getDeviceId());
+ });
+ AttributionSource attributionSource = new AttributionSource(Process.myUid(),
+ getContext().getOpPackageName(), getContext().getAttributionTag(),
+ virtualDeviceId.get());
+
+ final AppOpsManager appOpsManager = getContext().getSystemService(AppOpsManager.class);
+ appOpsManager.startWatchingNoted(new int[]{AppOpsManager.OP_FINE_LOCATION,
+ AppOpsManager.OP_CAMERA}, listener);
+
+ appOpsManager.noteOpNoThrow(AppOpsManager.OP_FINE_LOCATION, attributionSource, "message");
+
+ verify(listener, timeout(NOTIFICATION_TIMEOUT_MILLIS)
+ .times(1)).onOpNoted(eq(AppOpsManager.OPSTR_FINE_LOCATION),
+ eq(Process.myUid()), eq(getContext().getOpPackageName()),
+ eq(getContext().getAttributionTag()), eq(virtualDeviceId.get()),
+ eq(AppOpsManager.OP_FLAG_SELF), eq(AppOpsManager.MODE_ALLOWED));
+
+ appOpsManager.finishOp(getContext().getAttributionSource().getToken(),
+ AppOpsManager.OP_FINE_LOCATION, attributionSource);
+
+ verifyNoMoreInteractions(listener);
+
+ appOpsManager.stopWatchingNoted(listener);
+
+ verifyNoMoreInteractions(listener);
+ }
+
private static Context getContext() {
return InstrumentationRegistry.getContext();
}
diff --git a/services/tests/servicestests/src/com/android/server/appop/AppOpsStartedWatcherTest.java b/services/tests/servicestests/src/com/android/server/appop/AppOpsStartedWatcherTest.java
index e98a4dd..2890078 100644
--- a/services/tests/servicestests/src/com/android/server/appop/AppOpsStartedWatcherTest.java
+++ b/services/tests/servicestests/src/com/android/server/appop/AppOpsStartedWatcherTest.java
@@ -16,6 +16,8 @@
package com.android.server.appop;
+import static com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity;
+
import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.mock;
@@ -25,22 +27,31 @@
import android.app.AppOpsManager;
import android.app.AppOpsManager.OnOpStartedListener;
+import android.companion.virtual.VirtualDeviceManager;
+import android.companion.virtual.VirtualDeviceParams;
+import android.content.AttributionSource;
import android.content.Context;
import android.os.Process;
+import android.virtualdevice.cts.common.FakeAssociationRule;
import androidx.test.InstrumentationRegistry;
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
+import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InOrder;
+import java.util.concurrent.atomic.AtomicInteger;
+
/** Tests watching started ops. */
@SmallTest
@RunWith(AndroidJUnit4.class)
public class AppOpsStartedWatcherTest {
+ @Rule
+ public FakeAssociationRule mFakeAssociationRule = new FakeAssociationRule();
private static final long NOTIFICATION_TIMEOUT_MILLIS = 5000;
@Test
@@ -63,15 +74,17 @@
inOrder.verify(listener, timeout(NOTIFICATION_TIMEOUT_MILLIS)
.times(1)).onOpStarted(eq(AppOpsManager.OP_FINE_LOCATION),
eq(Process.myUid()), eq(getContext().getPackageName()),
- eq(getContext().getAttributionTag()), eq(AppOpsManager.OP_FLAG_SELF),
- eq(AppOpsManager.MODE_ALLOWED), eq(OnOpStartedListener.START_TYPE_STARTED),
+ eq(getContext().getAttributionTag()), eq(Context.DEVICE_ID_DEFAULT),
+ eq(AppOpsManager.OP_FLAG_SELF), eq(AppOpsManager.MODE_ALLOWED),
+ eq(OnOpStartedListener.START_TYPE_STARTED),
eq(AppOpsManager.ATTRIBUTION_FLAGS_NONE),
eq(AppOpsManager.ATTRIBUTION_CHAIN_ID_NONE));
inOrder.verify(listener, timeout(NOTIFICATION_TIMEOUT_MILLIS)
.times(1)).onOpStarted(eq(AppOpsManager.OP_CAMERA),
eq(Process.myUid()), eq(getContext().getPackageName()),
- eq(getContext().getAttributionTag()), eq(AppOpsManager.OP_FLAG_SELF),
- eq(AppOpsManager.MODE_ALLOWED), eq(OnOpStartedListener.START_TYPE_STARTED),
+ eq(getContext().getAttributionTag()), eq(Context.DEVICE_ID_DEFAULT),
+ eq(AppOpsManager.OP_FLAG_SELF), eq(AppOpsManager.MODE_ALLOWED),
+ eq(OnOpStartedListener.START_TYPE_STARTED),
eq(AppOpsManager.ATTRIBUTION_FLAGS_NONE),
eq(AppOpsManager.ATTRIBUTION_CHAIN_ID_NONE));
@@ -97,8 +110,9 @@
verify(listener, timeout(NOTIFICATION_TIMEOUT_MILLIS)
.times(2)).onOpStarted(eq(AppOpsManager.OP_CAMERA),
eq(Process.myUid()), eq(getContext().getPackageName()),
- eq(getContext().getAttributionTag()), eq(AppOpsManager.OP_FLAG_SELF),
- eq(AppOpsManager.MODE_ALLOWED), eq(OnOpStartedListener.START_TYPE_STARTED),
+ eq(getContext().getAttributionTag()), eq(Context.DEVICE_ID_DEFAULT),
+ eq(AppOpsManager.OP_FLAG_SELF), eq(AppOpsManager.MODE_ALLOWED),
+ eq(OnOpStartedListener.START_TYPE_STARTED),
eq(AppOpsManager.ATTRIBUTION_FLAGS_NONE),
eq(AppOpsManager.ATTRIBUTION_CHAIN_ID_NONE));
verifyNoMoreInteractions(listener);
@@ -109,6 +123,50 @@
appOpsManager.stopWatchingStarted(listener);
}
+ @Test
+ public void testWatchStartedOpsForExternalDevice() {
+ final VirtualDeviceManager virtualDeviceManager = getContext().getSystemService(
+ VirtualDeviceManager.class);
+ AtomicInteger virtualDeviceId = new AtomicInteger();
+ runWithShellPermissionIdentity(() -> {
+ final VirtualDeviceManager.VirtualDevice virtualDevice =
+ virtualDeviceManager.createVirtualDevice(
+ mFakeAssociationRule.getAssociationInfo().getId(),
+ new VirtualDeviceParams.Builder().setName("virtual_device").build());
+ virtualDeviceId.set(virtualDevice.getDeviceId());
+ });
+ final OnOpStartedListener listener = mock(OnOpStartedListener.class);
+ AttributionSource attributionSource = new AttributionSource(Process.myUid(),
+ getContext().getOpPackageName(), getContext().getAttributionTag(),
+ virtualDeviceId.get());
+
+ final AppOpsManager appOpsManager = getContext().getSystemService(AppOpsManager.class);
+ appOpsManager.startWatchingStarted(new int[]{AppOpsManager.OP_FINE_LOCATION,
+ AppOpsManager.OP_CAMERA}, listener);
+
+ appOpsManager.startOpNoThrow(getContext().getAttributionSource().getToken(),
+ AppOpsManager.OP_FINE_LOCATION, attributionSource, false,
+ "message", 0, AppOpsManager.ATTRIBUTION_CHAIN_ID_NONE);
+
+ verify(listener, timeout(NOTIFICATION_TIMEOUT_MILLIS)
+ .times(1)).onOpStarted(eq(AppOpsManager.OP_FINE_LOCATION),
+ eq(Process.myUid()), eq(getContext().getOpPackageName()),
+ eq(getContext().getAttributionTag()), eq(virtualDeviceId.get()),
+ eq(AppOpsManager.OP_FLAG_SELF),
+ eq(AppOpsManager.MODE_ALLOWED), eq(OnOpStartedListener.START_TYPE_STARTED),
+ eq(AppOpsManager.ATTRIBUTION_FLAGS_NONE),
+ eq(AppOpsManager.ATTRIBUTION_CHAIN_ID_NONE));
+
+ appOpsManager.finishOp(getContext().getAttributionSource().getToken(),
+ AppOpsManager.OP_FINE_LOCATION, attributionSource);
+
+ verifyNoMoreInteractions(listener);
+
+ appOpsManager.stopWatchingStarted(listener);
+
+ verifyNoMoreInteractions(listener);
+ }
+
private static Context getContext() {
return InstrumentationRegistry.getContext();
}
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/BaseClientMonitorTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/BaseClientMonitorTest.java
index 3a9c0f0..a1f0dbd 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/BaseClientMonitorTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/BaseClientMonitorTest.java
@@ -70,7 +70,7 @@
mClientMonitor.binderDied();
assertThat(mClientMonitor.mCanceled).isTrue();
- assertThat(mClientMonitor.getListener()).isNull();
+ assertThat(mClientMonitor.getListener()).isNotNull();
}
@Test
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceGenerateChallengeClientTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceGenerateChallengeClientTest.java
index c8bfaa9..5b81277 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceGenerateChallengeClientTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceGenerateChallengeClientTest.java
@@ -87,14 +87,6 @@
verify(mCallback).onClientFinished(mClient, true);
}
- @Test
- public void generateChallenge_nullListener() {
- createClient(null);
- mClient.start(mCallback);
-
- verify(mCallback).onClientFinished(mClient, false);
- }
-
private void createClient(ClientMonitorCallbackConverter listener) {
mClient = new FaceGenerateChallengeClient(mContext, () -> mAidlSession, mToken, listener,
USER_ID, TAG, SENSOR_ID, mBiometricLogger, mBiometricContext);
diff --git a/services/tests/servicestests/src/com/android/server/credentials/CredentialManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/credentials/CredentialManagerServiceTest.java
index d850c73..57f3cc0 100644
--- a/services/tests/servicestests/src/com/android/server/credentials/CredentialManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/credentials/CredentialManagerServiceTest.java
@@ -22,6 +22,7 @@
import android.os.UserHandle;
import android.provider.Settings;
+import androidx.annotation.NonNull;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
@@ -40,10 +41,12 @@
public final class CredentialManagerServiceTest {
Context mContext = null;
+ MockSettingsWrapper mSettingsWrapper = null;
@Before
public void setUp() throws CertificateException {
mContext = ApplicationProvider.getApplicationContext();
+ mSettingsWrapper = new MockSettingsWrapper(mContext);
}
@Test
@@ -81,7 +84,8 @@
Settings.Secure.CREDENTIAL_SERVICE_PRIMARY,
"com.example.test/com.example.test.TestActivity");
- CredentialManagerService.updateProvidersWhenPackageRemoved(mContext, "com.example.test");
+ CredentialManagerService.updateProvidersWhenPackageRemoved(
+ mSettingsWrapper, "com.example.test");
assertThat(getSettingsKey(Settings.Secure.AUTOFILL_SERVICE)).isEqualTo("");
assertThat(getSettingsKey(Settings.Secure.CREDENTIAL_SERVICE))
@@ -101,7 +105,8 @@
setSettingsKey(Settings.Secure.CREDENTIAL_SERVICE, testCredentialValue);
setSettingsKey(Settings.Secure.CREDENTIAL_SERVICE_PRIMARY, testCredentialPrimaryValue);
- CredentialManagerService.updateProvidersWhenPackageRemoved(mContext, "com.example.test3");
+ CredentialManagerService.updateProvidersWhenPackageRemoved(
+ mSettingsWrapper, "com.example.test3");
// Since the provider removed was not a primary provider then we should do nothing.
assertThat(getSettingsKey(Settings.Secure.AUTOFILL_SERVICE))
@@ -125,7 +130,8 @@
Settings.Secure.CREDENTIAL_SERVICE_PRIMARY,
"com.example.test/com.example.test.TestActivity");
- CredentialManagerService.updateProvidersWhenPackageRemoved(mContext, "com.example.test");
+ CredentialManagerService.updateProvidersWhenPackageRemoved(
+ mSettingsWrapper, "com.example.test");
assertThat(getSettingsKey(Settings.Secure.AUTOFILL_SERVICE)).isEqualTo("");
assertThat(getSettingsKey(Settings.Secure.CREDENTIAL_SERVICE))
@@ -144,7 +150,8 @@
setSettingsKey(Settings.Secure.CREDENTIAL_SERVICE, testCredentialValue);
setSettingsKey(Settings.Secure.CREDENTIAL_SERVICE_PRIMARY, testCredentialPrimaryValue);
- CredentialManagerService.updateProvidersWhenPackageRemoved(mContext, "com.example.test3");
+ CredentialManagerService.updateProvidersWhenPackageRemoved(
+ mSettingsWrapper, "com.example.test3");
// Since the provider removed was not a primary provider then we should do nothing.
assertCredentialPropertyEquals(
@@ -176,12 +183,36 @@
assertThat(actualValueSet).isEqualTo(newValueSet);
}
- private void setSettingsKey(String key, String value) {
- assertThat(Settings.Secure.putString(mContext.getContentResolver(), key, value)).isTrue();
+ private void setSettingsKey(String name, String value) {
+ assertThat(
+ mSettingsWrapper.putStringForUser(
+ name, value, UserHandle.myUserId(), true))
+ .isTrue();
}
- private String getSettingsKey(String key) {
- return Settings.Secure.getStringForUser(
- mContext.getContentResolver(), key, UserHandle.myUserId());
+ private String getSettingsKey(String name) {
+ return mSettingsWrapper.getStringForUser(name, UserHandle.myUserId());
+ }
+
+ private static final class MockSettingsWrapper
+ extends CredentialManagerService.SettingsWrapper {
+
+ MockSettingsWrapper(@NonNull Context context) {
+ super(context);
+ }
+
+ /** Updates the string value of a system setting */
+ @Override
+ public boolean putStringForUser(
+ String name,
+ String value,
+ int userHandle,
+ boolean overrideableByRestore) {
+ // This will ensure that when the settings putStringForUser method is called by
+ // CredentialManagerService that the overrideableByRestore bit is true.
+ assertThat(overrideableByRestore).isTrue();
+
+ return Settings.Secure.putStringForUser(getContentResolver(), name, value, userHandle);
+ }
}
}
diff --git a/services/tests/servicestests/src/com/android/server/graphics/fonts/UpdatableFontDirTest.java b/services/tests/servicestests/src/com/android/server/graphics/fonts/UpdatableFontDirTest.java
index 3de167e..dacff4c 100644
--- a/services/tests/servicestests/src/com/android/server/graphics/fonts/UpdatableFontDirTest.java
+++ b/services/tests/servicestests/src/com/android/server/graphics/fonts/UpdatableFontDirTest.java
@@ -23,7 +23,6 @@
import android.content.Context;
import android.graphics.FontListParser;
-import android.graphics.fonts.FontFamily;
import android.graphics.fonts.FontManager;
import android.graphics.fonts.FontStyle;
import android.graphics.fonts.FontUpdateRequest;
@@ -324,15 +323,16 @@
Function<Map<String, File>, FontConfig> configSupplier = (map) -> {
FontConfig.Font fooFont = new FontConfig.Font(
new File(mPreinstalledFontDirs.get(0), "foo.ttf"), null, "foo",
- new FontStyle(400, FontStyle.FONT_SLANT_UPRIGHT), 0, null, null);
+ new FontStyle(400, FontStyle.FONT_SLANT_UPRIGHT), 0, null, null,
+ FontConfig.Font.VAR_TYPE_AXES_NONE);
FontConfig.Font barFont = new FontConfig.Font(
new File(mPreinstalledFontDirs.get(1), "bar.ttf"), null, "bar",
- new FontStyle(400, FontStyle.FONT_SLANT_UPRIGHT), 0, null, null);
+ new FontStyle(400, FontStyle.FONT_SLANT_UPRIGHT), 0, null, null,
+ FontConfig.Font.VAR_TYPE_AXES_NONE);
FontConfig.FontFamily family = new FontConfig.FontFamily(
Arrays.asList(fooFont, barFont), null,
- FontConfig.FontFamily.VARIANT_DEFAULT,
- FontFamily.Builder.VARIABLE_FONT_FAMILY_TYPE_NONE);
+ FontConfig.FontFamily.VARIANT_DEFAULT);
return new FontConfig(Collections.emptyList(),
Collections.emptyList(),
Collections.singletonList(new FontConfig.NamedFamilyList(
@@ -492,10 +492,9 @@
mConfigFile, mCurrentTimeSupplier, (map) -> {
FontConfig.Font font = new FontConfig.Font(
file, null, "bar", new FontStyle(400, FontStyle.FONT_SLANT_UPRIGHT),
- 0, null, null);
+ 0, null, null, FontConfig.Font.VAR_TYPE_AXES_NONE);
FontConfig.FontFamily family = new FontConfig.FontFamily(
- Collections.singletonList(font), null, FontConfig.FontFamily.VARIANT_DEFAULT,
- FontFamily.Builder.VARIABLE_FONT_FAMILY_TYPE_NONE);
+ Collections.singletonList(font), null, FontConfig.FontFamily.VARIANT_DEFAULT);
return new FontConfig(
Collections.emptyList(),
Collections.emptyList(),
@@ -647,10 +646,9 @@
mConfigFile, mCurrentTimeSupplier, (map) -> {
FontConfig.Font font = new FontConfig.Font(
file, null, "test", new FontStyle(400, FontStyle.FONT_SLANT_UPRIGHT), 0, null,
- null);
+ null, FontConfig.Font.VAR_TYPE_AXES_NONE);
FontConfig.FontFamily family = new FontConfig.FontFamily(
- Collections.singletonList(font), null, FontConfig.FontFamily.VARIANT_DEFAULT,
- FontFamily.Builder.VARIABLE_FONT_FAMILY_TYPE_NONE);
+ Collections.singletonList(font), null, FontConfig.FontFamily.VARIANT_DEFAULT);
return new FontConfig(Collections.emptyList(), Collections.emptyList(),
Collections.singletonList(new FontConfig.NamedFamilyList(
Collections.singletonList(family), "sans-serif")),
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDevicePlaybackTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDevicePlaybackTest.java
index 6207349..1bd6e29 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDevicePlaybackTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDevicePlaybackTest.java
@@ -659,6 +659,64 @@
mTestLooper.dispatchAll();
assertThat(mActiveMediaSessionsPaused).isFalse();
}
+ @Test
+ public void handleRoutingInformation_physicalAddressOfSender_Tv_activeSourceChange() {
+ mHdmiCecLocalDevicePlayback.mService.getHdmiCecConfig().setStringValue(
+ HdmiControlManager.CEC_SETTING_NAME_POWER_STATE_CHANGE_ON_ACTIVE_SOURCE_LOST,
+ HdmiControlManager.POWER_STATE_CHANGE_ON_ACTIVE_SOURCE_LOST_STANDBY_NOW);
+ mHdmiCecLocalDevicePlayback.setActiveSource(mPlaybackLogicalAddress,
+ mPlaybackPhysicalAddress, "HdmiCecLocalDevicePlaybackTest");
+ mPowerManager.setInteractive(true);
+ // Physical address reported in this message is the same as message sender's (TV) physical
+ // address.
+ HdmiCecMessage message =
+ HdmiCecMessageBuilder.buildRoutingInformation(Constants.ADDR_TV, 0x0000);
+
+ assertThat(mHdmiCecLocalDevicePlayback.handleRoutingInformation(message))
+ .isEqualTo(Constants.HANDLED);
+ assertThat(mHdmiCecLocalDevicePlayback.isActiveSource()).isFalse();
+ assertThat(mHdmiCecLocalDevicePlayback.getActiveSource().physicalAddress).isEqualTo(
+ 0x0000);
+ // Active source's logical address is invalidated.
+ // See {@link HdmiCecLocalDevicePlayback#handleRoutingChangeAndInformation}.
+ assertThat(mHdmiCecLocalDevicePlayback.getActiveSource().logicalAddress).isEqualTo(
+ ADDR_INVALID);
+ assertThat(mPowerManager.isInteractive()).isFalse();
+ }
+
+ @Test
+ public void handleRoutingInformation_physicalAddressOfSender_notTv_noActiveSourceChange() {
+ mHdmiCecLocalDevicePlayback.mService.getHdmiCecConfig().setStringValue(
+ HdmiControlManager.CEC_SETTING_NAME_POWER_STATE_CHANGE_ON_ACTIVE_SOURCE_LOST,
+ HdmiControlManager.POWER_STATE_CHANGE_ON_ACTIVE_SOURCE_LOST_STANDBY_NOW);
+ mHdmiCecLocalDevicePlayback.setActiveSource(mPlaybackLogicalAddress,
+ mPlaybackPhysicalAddress, "HdmiCecLocalDevicePlaybackTest");
+ // Add a device to the network and assert that this device is included in the list of
+ // devices.
+ HdmiDeviceInfo infoPlayback = HdmiDeviceInfo.cecDeviceBuilder()
+ .setLogicalAddress(Constants.ADDR_PLAYBACK_3)
+ .setPhysicalAddress(0x1000)
+ .setPortId(PORT_1)
+ .setDeviceType(HdmiDeviceInfo.DEVICE_PLAYBACK)
+ .setVendorId(0x1000)
+ .setDisplayName("Playback 3")
+ .build();
+ mHdmiControlService.getHdmiCecNetwork().addCecDevice(infoPlayback);
+ mPowerManager.setInteractive(true);
+ // Physical address reported in this message is the same as message sender's (Playback_3)
+ // physical address.
+ HdmiCecMessage message =
+ HdmiCecMessageBuilder.buildRoutingInformation(Constants.ADDR_PLAYBACK_3, 0x1000);
+
+ assertThat(mHdmiCecLocalDevicePlayback.handleRoutingInformation(message))
+ .isEqualTo(Constants.HANDLED);
+ assertThat(mHdmiCecLocalDevicePlayback.isActiveSource()).isTrue();
+ assertThat(mHdmiCecLocalDevicePlayback.getActiveSource().physicalAddress).isEqualTo(
+ mPlaybackPhysicalAddress);
+ assertThat(mHdmiCecLocalDevicePlayback.getActiveSource().logicalAddress).isEqualTo(
+ mPlaybackLogicalAddress);
+ assertThat(mPowerManager.isInteractive()).isTrue();
+ }
@Test
public void handleSetStreamPath() {
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/OneTouchPlayActionTest.java b/services/tests/servicestests/src/com/android/server/hdmi/OneTouchPlayActionTest.java
index 1172a87..4641802 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/OneTouchPlayActionTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/OneTouchPlayActionTest.java
@@ -19,6 +19,7 @@
import static com.android.server.SystemService.PHASE_SYSTEM_SERVICES_READY;
import static com.android.server.hdmi.Constants.ADDR_TV;
import static com.android.server.hdmi.HdmiControlService.INITIATED_BY_ENABLE_CEC;
+import static com.android.server.hdmi.HdmiControlService.WAKE_UP_SCREEN_ON;
import static com.android.server.hdmi.OneTouchPlayAction.STATE_WAITING_FOR_REPORT_POWER_STATUS;
import static com.google.common.truth.Truth.assertThat;
@@ -46,6 +47,7 @@
import java.util.ArrayList;
import java.util.Collections;
+import java.util.concurrent.TimeUnit;
/** Tests for {@link OneTouchPlayAction} */
@SmallTest
@@ -70,6 +72,7 @@
private Context mContextSpy;
private HdmiControlService mHdmiControlService;
+ private HdmiCecController mHdmiCecController;
private FakeNativeWrapper mNativeWrapper;
private FakePowerManagerWrapper mPowerManager;
private FakeHdmiCecConfig mHdmiCecConfig;
@@ -108,10 +111,10 @@
mHdmiControlService.setHdmiCecConfig(mHdmiCecConfig);
setHdmiControlEnabled(hdmiControlEnabled);
mNativeWrapper = new FakeNativeWrapper();
- HdmiCecController hdmiCecController = HdmiCecController.createWithNativeWrapper(
+ mHdmiCecController = HdmiCecController.createWithNativeWrapper(
this.mHdmiControlService, mNativeWrapper, mHdmiControlService.getAtomWriter());
mHdmiControlService.setDeviceConfig(new FakeDeviceConfigWrapper());
- mHdmiControlService.setCecController(hdmiCecController);
+ mHdmiControlService.setCecController(mHdmiCecController);
mHdmiControlService.setHdmiMhlController(HdmiMhlControllerStub.create(mHdmiControlService));
mHdmiControlService.initService();
mHdmiControlService.onBootPhase(PHASE_SYSTEM_SERVICES_READY);
@@ -618,6 +621,53 @@
assertThat(callback.getResult()).isEqualTo(HdmiControlManager.RESULT_SUCCESS);
}
+ @Test
+ public void onWakeUp_notInteractive_startOneTouchPlay() throws Exception {
+ setUp(true);
+
+ mHdmiControlService.onWakeUp(WAKE_UP_SCREEN_ON);
+ mPowerManager.setInteractive(false);
+ mTestLooper.dispatchAll();
+
+ assertThat(mPowerManager.isInteractive()).isFalse();
+ mNativeWrapper.clearResultMessages();
+ mTestLooper.moveTimeForward(HdmiConfig.TIMEOUT_MS);
+ mTestLooper.dispatchAll();
+
+ HdmiCecMessage textViewOn =
+ HdmiCecMessageBuilder.buildTextViewOn(
+ mHdmiControlService.playback().getDeviceInfo().getLogicalAddress(),
+ ADDR_TV);
+ assertThat(mNativeWrapper.getResultMessages()).contains(textViewOn);
+ }
+
+
+ @Test
+ public void onWakeUp_interruptedByOnStandby_notInteractive_OneTouchPlayNotStarted()
+ throws Exception {
+ setUp(true);
+ long allocationDelay = TimeUnit.SECONDS.toMillis(1);
+ mHdmiCecController.setLogicalAddressAllocationDelay(allocationDelay);
+ mTestLooper.dispatchAll();
+
+ mHdmiControlService.onWakeUp(WAKE_UP_SCREEN_ON);
+ mHdmiControlService.onStandby(HdmiControlService.STANDBY_SCREEN_OFF);
+ mPowerManager.setInteractive(false);
+ mTestLooper.dispatchAll();
+
+ assertThat(mPowerManager.isInteractive()).isFalse();
+ mNativeWrapper.clearResultMessages();
+ mTestLooper.moveTimeForward(HdmiConfig.TIMEOUT_MS);
+ mTestLooper.dispatchAll();
+
+ HdmiCecMessage textViewOn =
+ HdmiCecMessageBuilder.buildTextViewOn(
+ mHdmiControlService.playback().getDeviceInfo().getLogicalAddress(),
+ ADDR_TV);
+ assertThat(mNativeWrapper.getResultMessages()).doesNotContain(textViewOn);
+
+ }
+
private static class TestActionTimer implements ActionTimer {
private int mState;
diff --git a/services/tests/servicestests/src/com/android/server/locksettings/SyntheticPasswordTests.java b/services/tests/servicestests/src/com/android/server/locksettings/SyntheticPasswordTests.java
index eca19c8..2da2f50 100644
--- a/services/tests/servicestests/src/com/android/server/locksettings/SyntheticPasswordTests.java
+++ b/services/tests/servicestests/src/com/android/server/locksettings/SyntheticPasswordTests.java
@@ -506,6 +506,14 @@
}
@Test
+ public void testUnlockUserWithTokenWithBadHandleReturnsFalse() {
+ final long badTokenHandle = 123456789;
+ final byte[] token = "some-high-entropy-secure-token".getBytes();
+ mService.initializeSyntheticPassword(PRIMARY_USER_ID);
+ assertFalse(mLocalService.unlockUserWithToken(badTokenHandle, token, PRIMARY_USER_ID));
+ }
+
+ @Test
public void testGetHashFactorPrimaryUser() throws RemoteException {
LockscreenCredential password = newPassword("password");
initSpAndSetCredential(PRIMARY_USER_ID, password);
diff --git a/services/tests/servicestests/src/com/android/server/media/projection/MediaProjectionManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/media/projection/MediaProjectionManagerServiceTest.java
index 097cc51..abd3abe 100644
--- a/services/tests/servicestests/src/com/android/server/media/projection/MediaProjectionManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/media/projection/MediaProjectionManagerServiceTest.java
@@ -49,6 +49,7 @@
import static org.testng.Assert.assertThrows;
import android.app.ActivityManagerInternal;
+import android.app.ActivityOptions.LaunchCookie;
import android.content.Context;
import android.content.ContextWrapper;
import android.content.pm.ApplicationInfo;
@@ -784,7 +785,7 @@
@RecordContent int recordedContent)
throws NameNotFoundException {
MediaProjectionManagerService.MediaProjection projection = startProjectionPreconditions();
- projection.setLaunchCookie(mock(IBinder.class));
+ projection.setLaunchCookie(new LaunchCookie());
projection.start(mIMediaProjectionCallback);
projection.notifyVirtualDisplayCreated(10);
// Waiting for user to review consent.
@@ -825,7 +826,7 @@
public void testSetUserReviewGrantedConsentResult_displayMirroring_noPriorSession()
throws NameNotFoundException {
MediaProjectionManagerService.MediaProjection projection = startProjectionPreconditions();
- projection.setLaunchCookie(mock(IBinder.class));
+ projection.setLaunchCookie(new LaunchCookie());
projection.start(mIMediaProjectionCallback);
// Skip setting the prior session details.
@@ -844,7 +845,7 @@
public void testSetUserReviewGrantedConsentResult_displayMirroring_sessionNotWaiting()
throws NameNotFoundException {
MediaProjectionManagerService.MediaProjection projection = startProjectionPreconditions();
- projection.setLaunchCookie(mock(IBinder.class));
+ projection.setLaunchCookie(new LaunchCookie());
projection.start(mIMediaProjectionCallback);
// Session is not waiting for user's consent.
doReturn(true).when(mWindowManagerInternal).setContentRecordingSession(
diff --git a/services/tests/servicestests/src/com/android/server/pm/BaseShortcutManagerTest.java b/services/tests/servicestests/src/com/android/server/pm/BaseShortcutManagerTest.java
index 0805485..81df597 100644
--- a/services/tests/servicestests/src/com/android/server/pm/BaseShortcutManagerTest.java
+++ b/services/tests/servicestests/src/com/android/server/pm/BaseShortcutManagerTest.java
@@ -157,6 +157,7 @@
return mMockDevicePolicyManager;
case Context.APP_SEARCH_SERVICE:
case Context.ROLE_SERVICE:
+ case Context.APP_OPS_SERVICE:
// RoleManager is final and cannot be mocked, so we only override the inject
// accessor methods in ShortcutService.
return getTestContext().getSystemService(name);
diff --git a/services/tests/servicestests/src/com/android/server/pm/SuspendPackagesTest.java b/services/tests/servicestests/src/com/android/server/pm/SuspendPackagesTest.java
index 398148f..2e5feff 100644
--- a/services/tests/servicestests/src/com/android/server/pm/SuspendPackagesTest.java
+++ b/services/tests/servicestests/src/com/android/server/pm/SuspendPackagesTest.java
@@ -273,7 +273,7 @@
final CountDownLatch latch = new CountDownLatch(1);
final IAppOpsCallback watcher = new IAppOpsCallback.Stub() {
@Override
- public void opChanged(int op, int uid, String packageName) {
+ public void opChanged(int op, int uid, String packageName, String persistentDeviceId) {
if (op == code && packageName.equals(TEST_APP_PACKAGE_NAME)) {
latch.countDown();
}
diff --git a/services/tests/servicestests/src/com/android/server/pm/parsing/AndroidPackageParsingValidationTest.kt b/services/tests/servicestests/src/com/android/server/pm/parsing/AndroidPackageParsingValidationTest.kt
index 757abde..e3ee21a 100644
--- a/services/tests/servicestests/src/com/android/server/pm/parsing/AndroidPackageParsingValidationTest.kt
+++ b/services/tests/servicestests/src/com/android/server/pm/parsing/AndroidPackageParsingValidationTest.kt
@@ -436,6 +436,13 @@
validateTagCount("action", 20000, tag)
validateTagCount("category", 40000, tag)
validateTagCount("data", 40000, tag)
+ validateTagCount("uri-relative-filter-group", 100, tag)
+ }
+
+ @Test
+ fun parseUriRelativeFilterGroupTag() {
+ val tag = "uri-relative-filter-group"
+ validateTagCount("data", 100, tag)
}
@Test
@@ -465,6 +472,54 @@
R.styleable.AndroidManifestData_pathAdvancedPattern,
4000
)
+ validateTagAttr(tag, "query", R.styleable.AndroidManifestData_query, 4000)
+ validateTagAttr(
+ tag,
+ "queryPattern",
+ R.styleable.AndroidManifestData_queryPattern,
+ 4000
+ )
+ validateTagAttr(
+ tag,
+ "queryPrefix",
+ R.styleable.AndroidManifestData_queryPrefix,
+ 4000
+ )
+ validateTagAttr(tag,
+ "querySuffix",
+ R.styleable.AndroidManifestData_querySuffix,
+ 4000
+ )
+ validateTagAttr(
+ tag,
+ "queryAdvancedPattern",
+ R.styleable.AndroidManifestData_queryAdvancedPattern,
+ 4000
+ )
+ validateTagAttr(tag, "fragment", R.styleable.AndroidManifestData_query, 4000)
+ validateTagAttr(
+ tag,
+ "fragmentPattern",
+ R.styleable.AndroidManifestData_fragmentPattern,
+ 4000
+ )
+ validateTagAttr(
+ tag,
+ "fragmentPrefix",
+ R.styleable.AndroidManifestData_fragmentPrefix,
+ 4000
+ )
+ validateTagAttr(tag,
+ "fragmentSuffix",
+ R.styleable.AndroidManifestData_fragmentSuffix,
+ 4000
+ )
+ validateTagAttr(
+ tag,
+ "fragmentAdvancedPattern",
+ R.styleable.AndroidManifestData_fragmentAdvancedPattern,
+ 4000
+ )
validateTagAttr(tag, "mimeType", R.styleable.AndroidManifestData_mimeType, 255)
validateTagAttr(tag, "mimeGroup", R.styleable.AndroidManifestData_mimeGroup, 1024)
}
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 227265a..5d114f4 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java
@@ -2123,6 +2123,25 @@
}
@Test
+ @EnableFlags(Flags.FLAG_MODES_API)
+ public void testDefaultRulesFromConfig_modesApi_getPolicies() {
+ // After mZenModeHelper was created, set some things in the policy so it's changed from
+ // default.
+ setupZenConfig();
+
+ // Find default rules; check they have non-null policies; check that they match the default
+ // and not whatever has been set up in setupZenConfig.
+ ArrayMap<String, ZenModeConfig.ZenRule> rules = mZenModeHelper.mConfig.automaticRules;
+ for (String defaultId : ZenModeConfig.DEFAULT_RULE_IDS) {
+ assertThat(rules).containsKey(defaultId);
+ ZenRule rule = rules.get(defaultId);
+ assertThat(rule.zenPolicy).isNotNull();
+
+ assertThat(rule.zenPolicy).isEqualTo(mZenModeHelper.getDefaultZenPolicy());
+ }
+ }
+
+ @Test
public void testAddAutomaticZenRule_beyondSystemLimit() {
for (int i = 0; i < RULE_LIMIT_PER_PACKAGE; i++) {
ScheduleInfo si = new ScheduleInfo();
@@ -4762,6 +4781,7 @@
.setShouldDimWallpaper(true)
.setShouldUseNightMode(true)
.build();
+ user1Rule.zenPolicy = new ZenPolicy();
verifyNoMoreInteractions(mDeviceEffectsApplier);
mZenModeHelper.onUserSwitched(1);
@@ -5042,6 +5062,109 @@
}
@Test
+ @EnableFlags(Flags.FLAG_MODES_API)
+ public void removeAndAddAutomaticZenRule_wasActive_isRestoredAsInactive() {
+ // Start with a rule.
+ mZenModeHelper.mConfig.automaticRules.clear();
+ AutomaticZenRule rule = new AutomaticZenRule.Builder("Test", CONDITION_ID)
+ .setConditionId(CONDITION_ID)
+ .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
+ .build();
+ String ruleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), rule,
+ UPDATE_ORIGIN_APP, "add it", CUSTOM_PKG_UID);
+
+ // User customizes it.
+ AutomaticZenRule userUpdate = new AutomaticZenRule.Builder(rule)
+ .setInterruptionFilter(INTERRUPTION_FILTER_ALARMS)
+ .build();
+ mZenModeHelper.updateAutomaticZenRule(ruleId, userUpdate, UPDATE_ORIGIN_USER, "userUpdate",
+ Process.SYSTEM_UID);
+ assertThat(mZenModeHelper.getZenMode()).isEqualTo(ZEN_MODE_OFF);
+
+ // App activates it.
+ mZenModeHelper.setAutomaticZenRuleState(ruleId, CONDITION_TRUE, UPDATE_ORIGIN_APP,
+ CUSTOM_PKG_UID);
+ assertThat(mZenModeHelper.getZenMode()).isEqualTo(ZEN_MODE_ALARMS);
+
+ // App deletes it.
+ mZenModeHelper.removeAutomaticZenRule(ruleId, UPDATE_ORIGIN_APP, "delete it",
+ CUSTOM_PKG_UID);
+ assertThat(mZenModeHelper.mConfig.automaticRules).hasSize(0);
+ assertThat(mZenModeHelper.mConfig.deletedRules).hasSize(1);
+ assertThat(mZenModeHelper.getZenMode()).isEqualTo(ZEN_MODE_OFF);
+
+ // App adds it again.
+ String newRuleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), rule,
+ UPDATE_ORIGIN_APP, "add it again", CUSTOM_PKG_UID);
+
+ // The rule is restored...
+ assertThat(newRuleId).isEqualTo(ruleId);
+ AutomaticZenRule finalRule = mZenModeHelper.getAutomaticZenRule(newRuleId);
+ assertThat(finalRule.getInterruptionFilter()).isEqualTo(INTERRUPTION_FILTER_ALARMS);
+
+ // ... but it is NOT active
+ ZenRule storedRule = mZenModeHelper.mConfig.automaticRules.get(newRuleId);
+ assertThat(storedRule.isAutomaticActive()).isFalse();
+ assertThat(storedRule.isTrueOrUnknown()).isFalse();
+ assertThat(storedRule.condition).isNull();
+ assertThat(mZenModeHelper.getZenMode()).isEqualTo(ZEN_MODE_OFF);
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_MODES_API)
+ public void removeAndAddAutomaticZenRule_wasSnoozed_isRestoredAsInactive() {
+ // Start with a rule.
+ mZenModeHelper.mConfig.automaticRules.clear();
+ AutomaticZenRule rule = new AutomaticZenRule.Builder("Test", CONDITION_ID)
+ .setConditionId(CONDITION_ID)
+ .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
+ .build();
+ String ruleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), rule,
+ UPDATE_ORIGIN_APP, "add it", CUSTOM_PKG_UID);
+
+ // User customizes it.
+ AutomaticZenRule userUpdate = new AutomaticZenRule.Builder(rule)
+ .setInterruptionFilter(INTERRUPTION_FILTER_ALARMS)
+ .build();
+ mZenModeHelper.updateAutomaticZenRule(ruleId, userUpdate, UPDATE_ORIGIN_USER, "userUpdate",
+ Process.SYSTEM_UID);
+ assertThat(mZenModeHelper.getZenMode()).isEqualTo(ZEN_MODE_OFF);
+
+ // App activates it.
+ mZenModeHelper.setAutomaticZenRuleState(ruleId, CONDITION_TRUE, UPDATE_ORIGIN_APP,
+ CUSTOM_PKG_UID);
+ assertThat(mZenModeHelper.getZenMode()).isEqualTo(ZEN_MODE_ALARMS);
+
+ // User snoozes it.
+ mZenModeHelper.setManualZenMode(ZEN_MODE_OFF, null, UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI,
+ "snoozing", "systemui", Process.SYSTEM_UID);
+ assertThat(mZenModeHelper.getZenMode()).isEqualTo(ZEN_MODE_OFF);
+
+ // App deletes it.
+ mZenModeHelper.removeAutomaticZenRule(ruleId, UPDATE_ORIGIN_APP, "delete it",
+ CUSTOM_PKG_UID);
+ assertThat(mZenModeHelper.mConfig.automaticRules).hasSize(0);
+ assertThat(mZenModeHelper.mConfig.deletedRules).hasSize(1);
+
+ // App adds it again.
+ String newRuleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), rule,
+ UPDATE_ORIGIN_APP, "add it again", CUSTOM_PKG_UID);
+
+ // The rule is restored...
+ assertThat(newRuleId).isEqualTo(ruleId);
+ AutomaticZenRule finalRule = mZenModeHelper.getAutomaticZenRule(newRuleId);
+ assertThat(finalRule.getInterruptionFilter()).isEqualTo(INTERRUPTION_FILTER_ALARMS);
+
+ // ... but it is NEITHER active NOR snoozed.
+ ZenRule storedRule = mZenModeHelper.mConfig.automaticRules.get(newRuleId);
+ assertThat(storedRule.isAutomaticActive()).isFalse();
+ assertThat(storedRule.isTrueOrUnknown()).isFalse();
+ assertThat(storedRule.condition).isNull();
+ assertThat(storedRule.snoozing).isFalse();
+ assertThat(mZenModeHelper.getZenMode()).isEqualTo(ZEN_MODE_OFF);
+ }
+
+ @Test
public void testRuleCleanup() throws Exception {
mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API);
Instant now = Instant.ofEpochMilli(1701796461000L);
diff --git a/services/tests/vibrator/src/com/android/server/vibrator/VibrationScalerTest.java b/services/tests/vibrator/src/com/android/server/vibrator/VibrationScalerTest.java
index f9fe6a9..b431888 100644
--- a/services/tests/vibrator/src/com/android/server/vibrator/VibrationScalerTest.java
+++ b/services/tests/vibrator/src/com/android/server/vibrator/VibrationScalerTest.java
@@ -53,7 +53,6 @@
import android.platform.test.flag.junit.CheckFlagsRule;
import android.platform.test.flag.junit.DeviceFlagsValueProvider;
import android.provider.Settings;
-import android.util.SparseArray;
import androidx.test.InstrumentationRegistry;
@@ -270,10 +269,8 @@
setDefaultIntensity(USAGE_RINGTONE, VIBRATION_INTENSITY_HIGH);
setDefaultIntensity(USAGE_NOTIFICATION, VIBRATION_INTENSITY_HIGH);
- SparseArray<Float> adaptiveHapticsScales = new SparseArray<>();
- adaptiveHapticsScales.put(USAGE_RINGTONE, 0.5f);
- adaptiveHapticsScales.put(USAGE_NOTIFICATION, 0.5f);
- mVibrationScaler.updateAdaptiveHapticsScales(adaptiveHapticsScales);
+ mVibrationScaler.updateAdaptiveHapticsScale(USAGE_RINGTONE, 0.5f);
+ mVibrationScaler.updateAdaptiveHapticsScale(USAGE_NOTIFICATION, 0.5f);
StepSegment scaled = getFirstSegment(mVibrationScaler.scale(
VibrationEffect.createOneShot(128, 128), USAGE_RINGTONE));
@@ -287,6 +284,50 @@
assertTrue(scaled.getAmplitude() < 0.5);
}
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_ADAPTIVE_HAPTICS_ENABLED)
+ public void scale_clearAdaptiveHapticsScales_clearsAllCachedScales() {
+ setDefaultIntensity(USAGE_RINGTONE, VIBRATION_INTENSITY_HIGH);
+ setDefaultIntensity(USAGE_NOTIFICATION, VIBRATION_INTENSITY_HIGH);
+
+ mVibrationScaler.updateAdaptiveHapticsScale(USAGE_RINGTONE, 0.5f);
+ mVibrationScaler.updateAdaptiveHapticsScale(USAGE_NOTIFICATION, 0.5f);
+ mVibrationScaler.clearAdaptiveHapticsScales();
+
+ StepSegment scaled = getFirstSegment(mVibrationScaler.scale(
+ VibrationEffect.createOneShot(128, 128), USAGE_RINGTONE));
+ // Ringtone scales up.
+ assertTrue(scaled.getAmplitude() > 0.5);
+
+ scaled = getFirstSegment(mVibrationScaler.scale(
+ VibrationEffect.createWaveform(new long[]{128}, new int[]{128}, -1),
+ USAGE_NOTIFICATION));
+ // Notification scales up.
+ assertTrue(scaled.getAmplitude() > 0.5);
+ }
+
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_ADAPTIVE_HAPTICS_ENABLED)
+ public void scale_removeAdaptiveHapticsScale_removesCachedScale() {
+ setDefaultIntensity(USAGE_RINGTONE, VIBRATION_INTENSITY_HIGH);
+ setDefaultIntensity(USAGE_NOTIFICATION, VIBRATION_INTENSITY_HIGH);
+
+ mVibrationScaler.updateAdaptiveHapticsScale(USAGE_RINGTONE, 0.5f);
+ mVibrationScaler.updateAdaptiveHapticsScale(USAGE_NOTIFICATION, 0.5f);
+ mVibrationScaler.removeAdaptiveHapticsScale(USAGE_NOTIFICATION);
+
+ StepSegment scaled = getFirstSegment(mVibrationScaler.scale(
+ VibrationEffect.createOneShot(128, 128), USAGE_RINGTONE));
+ // Ringtone scales down.
+ assertTrue(scaled.getAmplitude() < 0.5);
+
+ scaled = getFirstSegment(mVibrationScaler.scale(
+ VibrationEffect.createWaveform(new long[]{128}, new int[]{128}, -1),
+ USAGE_NOTIFICATION));
+ // Notification scales up.
+ assertTrue(scaled.getAmplitude() > 0.5);
+ }
+
private void setDefaultIntensity(@VibrationAttributes.Usage int usage,
@Vibrator.VibrationIntensity int intensity) {
when(mVibrationConfigMock.getDefaultVibrationIntensity(eq(usage))).thenReturn(intensity);
diff --git a/services/tests/vibrator/src/com/android/server/vibrator/VibrationThreadTest.java b/services/tests/vibrator/src/com/android/server/vibrator/VibrationThreadTest.java
index b0aef47..6e478d8 100644
--- a/services/tests/vibrator/src/com/android/server/vibrator/VibrationThreadTest.java
+++ b/services/tests/vibrator/src/com/android/server/vibrator/VibrationThreadTest.java
@@ -16,6 +16,7 @@
package com.android.server.vibrator;
+import static android.os.VibrationAttributes.USAGE_RINGTONE;
import static android.os.VibrationEffect.VibrationParameter.targetAmplitude;
import static android.os.VibrationEffect.VibrationParameter.targetFrequency;
@@ -54,12 +55,16 @@
import android.os.VibrationEffect;
import android.os.Vibrator;
import android.os.test.TestLooper;
+import android.os.vibrator.Flags;
import android.os.vibrator.PrebakedSegment;
import android.os.vibrator.PrimitiveSegment;
import android.os.vibrator.RampSegment;
import android.os.vibrator.StepSegment;
import android.os.vibrator.VibrationConfig;
import android.os.vibrator.VibrationEffectSegment;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
import android.util.SparseArray;
import androidx.test.InstrumentationRegistry;
@@ -81,6 +86,7 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.concurrent.CompletableFuture;
import java.util.function.BooleanSupplier;
import java.util.stream.Collectors;
@@ -95,6 +101,9 @@
private static final int TEST_RAMP_STEP_DURATION = 5;
@Rule public MockitoRule mMockitoRule = MockitoJUnit.rule();
+ @Rule
+ public final CheckFlagsRule mCheckFlagsRule =
+ DeviceFlagsValueProvider.createCheckFlagsRule();
@Mock private PackageManagerInternal mPackageManagerInternalMock;
@Mock private VibrationThread.VibratorManagerHooks mManagerHooks;
@@ -104,6 +113,7 @@
private final Map<Integer, FakeVibratorControllerProvider> mVibratorProviders = new HashMap<>();
private VibrationSettings mVibrationSettings;
+ private VibrationScaler mVibrationScaler;
private TestLooper mTestLooper;
private TestLooperAutoDispatcher mCustomTestLooperDispatcher;
private VibrationThread mThread;
@@ -132,6 +142,7 @@
Context context = InstrumentationRegistry.getContext();
mVibrationSettings = new VibrationSettings(context, new Handler(mTestLooper.getLooper()),
mVibrationConfigMock);
+ mVibrationScaler = new VibrationScaler(context, mVibrationSettings);
mockVibrators(VIBRATOR_ID);
@@ -231,6 +242,45 @@
}
@Test
+ @RequiresFlagsEnabled(Flags.FLAG_ADAPTIVE_HAPTICS_ENABLED)
+ public void vibrate_singleWaveformWithAdaptiveHapticsScaling_scalesAmplitudesProperly() {
+ mVibratorProviders.get(VIBRATOR_ID).setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL);
+
+ VibrationEffect effect = VibrationEffect.createWaveform(
+ new long[]{5, 5, 5}, new int[]{1, 1, 1}, -1);
+ CompletableFuture<Void> mRequestVibrationParamsFuture = CompletableFuture.runAsync(() -> {
+ mVibrationScaler.updateAdaptiveHapticsScale(USAGE_RINGTONE, 0.5f);
+ });
+ long vibrationId = startThreadAndDispatcher(effect, mRequestVibrationParamsFuture,
+ USAGE_RINGTONE);
+ waitForCompletion();
+
+ assertEquals(Arrays.asList(expectedOneShot(15)),
+ mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibrationId));
+ List<Float> amplitudes = mVibratorProviders.get(VIBRATOR_ID).getAmplitudes();
+ for (int i = 0; i < amplitudes.size(); i++) {
+ assertTrue(amplitudes.get(i) < 1 / 255f);
+ }
+ }
+
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_ADAPTIVE_HAPTICS_ENABLED)
+ public void vibrate_withVibrationParamsRequestStalling_timeoutRequestAndApplyNoScaling() {
+ mVibratorProviders.get(VIBRATOR_ID).setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL);
+ VibrationEffect effect = VibrationEffect.createWaveform(
+ new long[]{5, 5, 5}, new int[]{1, 1, 1}, -1);
+
+ CompletableFuture<Void> neverCompletingFuture = new CompletableFuture<>();
+ long vibrationId = startThreadAndDispatcher(effect, neverCompletingFuture, USAGE_RINGTONE);
+ waitForCompletion();
+
+ assertEquals(Arrays.asList(expectedOneShot(15)),
+ mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibrationId));
+ assertEquals(expectedAmplitudes(1, 1, 1),
+ mVibratorProviders.get(VIBRATOR_ID).getAmplitudes());
+ }
+
+ @Test
public void vibrate_singleVibratorRepeatingWaveform_runsVibrationUntilThreadCancelled()
throws Exception {
FakeVibratorControllerProvider fakeVibrator = mVibratorProviders.get(VIBRATOR_ID);
@@ -1610,10 +1660,26 @@
}
private long startThreadAndDispatcher(HalVibration vib) {
+ return startThreadAndDispatcher(vib, /* requestVibrationParamsFuture= */ null);
+ }
+
+ private long startThreadAndDispatcher(VibrationEffect effect,
+ CompletableFuture<Void> requestVibrationParamsFuture, int usage) {
+ VibrationAttributes attrs = new VibrationAttributes.Builder()
+ .setUsage(usage)
+ .build();
+ HalVibration vib = new HalVibration(mVibrationToken,
+ CombinedVibration.createParallel(effect),
+ new Vibration.CallerInfo(attrs, UID, DEVICE_ID, PACKAGE_NAME, "reason"));
+ return startThreadAndDispatcher(vib, requestVibrationParamsFuture);
+ }
+
+ private long startThreadAndDispatcher(HalVibration vib,
+ CompletableFuture<Void> requestVibrationParamsFuture) {
mControllers = createVibratorControllers();
DeviceAdapter deviceAdapter = new DeviceAdapter(mVibrationSettings, mControllers);
- mVibrationConductor =
- new VibrationStepConductor(vib, mVibrationSettings, deviceAdapter, mManagerHooks);
+ mVibrationConductor = new VibrationStepConductor(vib, mVibrationSettings, deviceAdapter,
+ mVibrationScaler, requestVibrationParamsFuture, mManagerHooks);
assertTrue(mThread.runVibrationOnVibrationThread(mVibrationConductor));
return mVibrationConductor.getVibration().id;
}
diff --git a/services/tests/vibrator/src/com/android/server/vibrator/VibratorControlServiceTest.java b/services/tests/vibrator/src/com/android/server/vibrator/VibratorControlServiceTest.java
index 1e0b1df..2823223 100644
--- a/services/tests/vibrator/src/com/android/server/vibrator/VibratorControlServiceTest.java
+++ b/services/tests/vibrator/src/com/android/server/vibrator/VibratorControlServiceTest.java
@@ -18,29 +18,47 @@
import static android.os.VibrationAttributes.USAGE_ALARM;
import static android.os.VibrationAttributes.USAGE_COMMUNICATION_REQUEST;
+import static android.os.VibrationAttributes.USAGE_HARDWARE_FEEDBACK;
+import static android.os.VibrationAttributes.USAGE_MEDIA;
import static android.os.VibrationAttributes.USAGE_NOTIFICATION;
+import static android.os.VibrationAttributes.USAGE_RINGTONE;
+import static android.os.VibrationAttributes.USAGE_TOUCH;
+import static android.os.VibrationAttributes.USAGE_UNKNOWN;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.verifyZeroInteractions;
+import static org.mockito.Mockito.when;
+import android.content.ComponentName;
+import android.content.pm.PackageManagerInternal;
import android.frameworks.vibrator.ScaleParam;
import android.frameworks.vibrator.VibrationParam;
+import android.os.Binder;
+import android.os.Handler;
+import android.os.IBinder;
import android.os.RemoteException;
+import android.os.test.TestLooper;
import android.util.SparseArray;
+import androidx.test.core.app.ApplicationProvider;
+
+import com.android.internal.util.ArrayUtils;
+import com.android.server.LocalServices;
+
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
-import org.mockito.ArgumentCaptor;
-import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
import java.util.ArrayList;
import java.util.List;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.TimeUnit;
public class VibratorControlServiceTest {
@@ -49,35 +67,45 @@
@Mock
private VibrationScaler mMockVibrationScaler;
- @Captor
- private ArgumentCaptor<SparseArray<Float>> mVibrationScalesCaptor;
+ @Mock
+ private PackageManagerInternal mPackageManagerInternalMock;
+ private FakeVibratorController mFakeVibratorController;
private VibratorControlService mVibratorControlService;
+ private VibrationSettings mVibrationSettings;
private final Object mLock = new Object();
@Before
public void setUp() throws Exception {
+ when(mPackageManagerInternalMock.getSystemUiServiceComponent())
+ .thenReturn(new ComponentName("", ""));
+ LocalServices.removeServiceForTest(PackageManagerInternal.class);
+ LocalServices.addService(PackageManagerInternal.class, mPackageManagerInternalMock);
+
+ TestLooper testLooper = new TestLooper();
+ mVibrationSettings = new VibrationSettings(
+ ApplicationProvider.getApplicationContext(), new Handler(testLooper.getLooper()));
+
+ mFakeVibratorController = new FakeVibratorController();
mVibratorControlService = new VibratorControlService(new VibratorControllerHolder(),
- mMockVibrationScaler, mLock);
+ mMockVibrationScaler, mVibrationSettings, mLock);
}
@Test
public void testRegisterVibratorController() throws RemoteException {
- FakeVibratorController fakeController = new FakeVibratorController();
- mVibratorControlService.registerVibratorController(fakeController);
+ mVibratorControlService.registerVibratorController(mFakeVibratorController);
- assertThat(fakeController.isLinkedToDeath).isTrue();
+ assertThat(mFakeVibratorController.isLinkedToDeath).isTrue();
}
@Test
public void testUnregisterVibratorController_providingTheRegisteredController_performsRequest()
throws RemoteException {
- FakeVibratorController fakeController = new FakeVibratorController();
- mVibratorControlService.registerVibratorController(fakeController);
- mVibratorControlService.unregisterVibratorController(fakeController);
+ mVibratorControlService.registerVibratorController(mFakeVibratorController);
+ mVibratorControlService.unregisterVibratorController(mFakeVibratorController);
- verify(mMockVibrationScaler).updateAdaptiveHapticsScales(null);
- assertThat(fakeController.isLinkedToDeath).isFalse();
+ verify(mMockVibrationScaler).clearAdaptiveHapticsScales();
+ assertThat(mFakeVibratorController.isLinkedToDeath).isFalse();
}
@Test
@@ -93,41 +121,80 @@
}
@Test
+ public void testOnRequestVibrationParamsComplete_cachesAdaptiveHapticsScalesCorrectly()
+ throws RemoteException {
+ mVibratorControlService.registerVibratorController(mFakeVibratorController);
+ int timeoutInMillis = 10;
+ CompletableFuture<Void> future =
+ mVibratorControlService.triggerVibrationParamsRequest(USAGE_RINGTONE,
+ timeoutInMillis);
+ IBinder token = mVibratorControlService.getRequestVibrationParamsToken();
+
+ SparseArray<Float> vibrationScales = new SparseArray<>();
+ vibrationScales.put(ScaleParam.TYPE_ALARM, 0.7f);
+ vibrationScales.put(ScaleParam.TYPE_NOTIFICATION, 0.4f);
+
+ mVibratorControlService.onRequestVibrationParamsComplete(token,
+ generateVibrationParams(vibrationScales));
+
+ verify(mMockVibrationScaler).updateAdaptiveHapticsScale(USAGE_ALARM, 0.7f);
+ verify(mMockVibrationScaler).updateAdaptiveHapticsScale(USAGE_NOTIFICATION, 0.4f);
+ // Setting ScaleParam.TYPE_NOTIFICATION will update vibration scaling for both
+ // notification and communication request usages.
+ verify(mMockVibrationScaler).updateAdaptiveHapticsScale(USAGE_COMMUNICATION_REQUEST, 0.4f);
+ verifyNoMoreInteractions(mMockVibrationScaler);
+
+ assertThat(future.isDone()).isTrue();
+ assertThat(future.isCompletedExceptionally()).isFalse();
+ }
+
+ @Test
+ public void testOnRequestVibrationParamsComplete_withIncorrectToken_ignoresRequest()
+ throws RemoteException, InterruptedException {
+ mVibratorControlService.registerVibratorController(mFakeVibratorController);
+ int timeoutInMillis = 10;
+ CompletableFuture<Void> unusedFuture =
+ mVibratorControlService.triggerVibrationParamsRequest(USAGE_RINGTONE,
+ timeoutInMillis);
+
+ SparseArray<Float> vibrationScales = new SparseArray<>();
+ vibrationScales.put(ScaleParam.TYPE_ALARM, 0.7f);
+ vibrationScales.put(ScaleParam.TYPE_NOTIFICATION, 0.4f);
+
+ mVibratorControlService.onRequestVibrationParamsComplete(new Binder(),
+ generateVibrationParams(vibrationScales));
+
+ verifyZeroInteractions(mMockVibrationScaler);
+ }
+
+ @Test
public void testSetVibrationParams_cachesAdaptiveHapticsScalesCorrectly()
throws RemoteException {
- FakeVibratorController fakeController = new FakeVibratorController();
- mVibratorControlService.registerVibratorController(fakeController);
+ mVibratorControlService.registerVibratorController(mFakeVibratorController);
SparseArray<Float> vibrationScales = new SparseArray<>();
vibrationScales.put(ScaleParam.TYPE_ALARM, 0.7f);
vibrationScales.put(ScaleParam.TYPE_NOTIFICATION, 0.4f);
mVibratorControlService.setVibrationParams(generateVibrationParams(vibrationScales),
- fakeController);
+ mFakeVibratorController);
- verify(mMockVibrationScaler).updateAdaptiveHapticsScales(mVibrationScalesCaptor.capture());
- SparseArray<Float> cachedVibrationScales = mVibrationScalesCaptor.getValue();
- assertThat(cachedVibrationScales.size()).isEqualTo(3);
- assertThat(cachedVibrationScales.keyAt(0)).isEqualTo(USAGE_ALARM);
- assertThat(cachedVibrationScales.valueAt(0)).isEqualTo(0.7f);
- assertThat(cachedVibrationScales.keyAt(1)).isEqualTo(USAGE_NOTIFICATION);
- assertThat(cachedVibrationScales.valueAt(1)).isEqualTo(0.4f);
+ verify(mMockVibrationScaler).updateAdaptiveHapticsScale(USAGE_ALARM, 0.7f);
+ verify(mMockVibrationScaler).updateAdaptiveHapticsScale(USAGE_NOTIFICATION, 0.4f);
// Setting ScaleParam.TYPE_NOTIFICATION will update vibration scaling for both
// notification and communication request usages.
- assertThat(cachedVibrationScales.keyAt(2)).isEqualTo(USAGE_COMMUNICATION_REQUEST);
- assertThat(cachedVibrationScales.valueAt(2)).isEqualTo(0.4f);
+ verify(mMockVibrationScaler).updateAdaptiveHapticsScale(USAGE_COMMUNICATION_REQUEST, 0.4f);
+ verifyNoMoreInteractions(mMockVibrationScaler);
}
@Test
public void testSetVibrationParams_withUnregisteredController_ignoresRequest()
throws RemoteException {
- FakeVibratorController fakeController = new FakeVibratorController();
-
SparseArray<Float> vibrationScales = new SparseArray<>();
vibrationScales.put(ScaleParam.TYPE_ALARM, 0.7f);
vibrationScales.put(ScaleParam.TYPE_NOTIFICATION, 0.4f);
mVibratorControlService.setVibrationParams(generateVibrationParams(vibrationScales),
- fakeController);
+ mFakeVibratorController);
verifyZeroInteractions(mMockVibrationScaler);
}
@@ -135,23 +202,72 @@
@Test
public void testClearVibrationParams_clearsCachedAdaptiveHapticsScales()
throws RemoteException {
- FakeVibratorController fakeController = new FakeVibratorController();
- mVibratorControlService.registerVibratorController(fakeController);
- mVibratorControlService.clearVibrationParams(ScaleParam.TYPE_ALARM, fakeController);
+ mVibratorControlService.registerVibratorController(mFakeVibratorController);
+ int types = buildVibrationTypesMask(ScaleParam.TYPE_ALARM, ScaleParam.TYPE_NOTIFICATION);
- verify(mMockVibrationScaler).updateAdaptiveHapticsScales(null);
+ mVibratorControlService.clearVibrationParams(types, mFakeVibratorController);
+
+ verify(mMockVibrationScaler).removeAdaptiveHapticsScale(USAGE_ALARM);
+ verify(mMockVibrationScaler).removeAdaptiveHapticsScale(USAGE_NOTIFICATION);
+ // Clearing ScaleParam.TYPE_NOTIFICATION will clear vibration scaling for both
+ // notification and communication request usages.
+ verify(mMockVibrationScaler).removeAdaptiveHapticsScale(USAGE_COMMUNICATION_REQUEST);
}
@Test
public void testClearVibrationParams_withUnregisteredController_ignoresRequest()
throws RemoteException {
- FakeVibratorController fakeController = new FakeVibratorController();
-
- mVibratorControlService.clearVibrationParams(ScaleParam.TYPE_ALARM, fakeController);
+ mVibratorControlService.clearVibrationParams(ScaleParam.TYPE_ALARM,
+ mFakeVibratorController);
verifyZeroInteractions(mMockVibrationScaler);
}
+ @Test
+ public void testRequestVibrationParams_createsFutureRequestProperly()
+ throws RemoteException {
+ int timeoutInMillis = 10;
+ mVibratorControlService.registerVibratorController(mFakeVibratorController);
+ CompletableFuture<Void> future =
+ mVibratorControlService.triggerVibrationParamsRequest(USAGE_RINGTONE,
+ timeoutInMillis);
+ try {
+ future.orTimeout(timeoutInMillis, TimeUnit.MILLISECONDS).get();
+ } catch (Throwable ignored) {
+ }
+ assertThat(mFakeVibratorController.didRequestVibrationParams).isTrue();
+ assertThat(mFakeVibratorController.requestVibrationType).isEqualTo(
+ ScaleParam.TYPE_RINGTONE);
+ assertThat(mFakeVibratorController.requestTimeoutInMillis).isEqualTo(timeoutInMillis);
+ }
+
+ @Test
+ public void testShouldRequestVibrationParams_returnsTrueForVibrationsThatShouldRequestParams()
+ throws RemoteException {
+ int[] vibrations =
+ new int[]{USAGE_ALARM, USAGE_RINGTONE, USAGE_MEDIA, USAGE_TOUCH, USAGE_NOTIFICATION,
+ USAGE_HARDWARE_FEEDBACK, USAGE_UNKNOWN, USAGE_COMMUNICATION_REQUEST};
+ mVibratorControlService.registerVibratorController(mFakeVibratorController);
+
+ for (int vibration : vibrations) {
+ assertThat(mVibratorControlService.shouldRequestVibrationParams(vibration))
+ .isEqualTo(ArrayUtils.contains(
+ mVibrationSettings.getRequestVibrationParamsForUsages(), vibration));
+ }
+ }
+
+ @Test
+ public void testShouldRequestVibrationParams_unregisteredVibratorController_returnsFalse()
+ throws RemoteException {
+ int[] vibrations =
+ new int[]{USAGE_ALARM, USAGE_RINGTONE, USAGE_MEDIA, USAGE_TOUCH, USAGE_NOTIFICATION,
+ USAGE_HARDWARE_FEEDBACK, USAGE_UNKNOWN, USAGE_COMMUNICATION_REQUEST};
+
+ for (int vibration : vibrations) {
+ assertThat(mVibratorControlService.shouldRequestVibrationParams(vibration)).isFalse();
+ }
+ }
+
private VibrationParam[] generateVibrationParams(SparseArray<Float> vibrationScales) {
List<VibrationParam> vibrationParamList = new ArrayList<>();
for (int i = 0; i < vibrationScales.size(); i++) {
@@ -173,4 +289,12 @@
return vibrationParam;
}
+
+ private int buildVibrationTypesMask(int... types) {
+ int typesMask = 0;
+ for (int type : types) {
+ typesMask |= type;
+ }
+ return typesMask;
+ }
}
diff --git a/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java b/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java
index d6b2116..bdbb6c6 100644
--- a/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java
+++ b/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java
@@ -318,6 +318,12 @@
return new HapticFeedbackVibrationProvider(
resources, vibratorInfo, mHapticFeedbackVibrationMap);
}
+
+ VibratorControllerHolder createVibratorControllerHolder() {
+ VibratorControllerHolder holder = new VibratorControllerHolder();
+ holder.setVibratorController(new FakeVibratorController());
+ return holder;
+ }
});
return mService;
}
@@ -965,8 +971,9 @@
new long[]{10, 10_000}, new int[]{255, 0}, 1);
vibrate(service, repeatingEffect, ALARM_ATTRS);
- // VibrationThread will start this vibration async, wait until the off waveform step.
- assertTrue(waitUntil(s -> fakeVibrator.getOffCount() > 0, service, TEST_TIMEOUT_MILLIS));
+ // VibrationThread will start this vibration async, wait until it has started.
+ assertTrue(waitUntil(s -> !fakeVibrator.getAllEffectSegments().isEmpty(), service,
+ TEST_TIMEOUT_MILLIS));
vibrateAndWaitUntilFinished(service, VibrationEffect.get(VibrationEffect.EFFECT_CLICK),
ALARM_ATTRS);
@@ -1019,8 +1026,9 @@
new long[]{10, 10_000}, new int[]{255, 0}, -1);
vibrate(service, alarmEffect, ALARM_ATTRS);
- // VibrationThread will start this vibration async, wait until the off waveform step.
- assertTrue(waitUntil(s -> fakeVibrator.getOffCount() > 0, service, TEST_TIMEOUT_MILLIS));
+ // VibrationThread will start this vibration async, wait until it has started.
+ assertTrue(waitUntil(s -> !fakeVibrator.getAllEffectSegments().isEmpty(), service,
+ TEST_TIMEOUT_MILLIS));
vibrateAndWaitUntilFinished(service, VibrationEffect.get(VibrationEffect.EFFECT_CLICK),
UNKNOWN_ATTRS);
diff --git a/services/tests/vibrator/utils/com/android/server/vibrator/FakeVibratorController.java b/services/tests/vibrator/utils/com/android/server/vibrator/FakeVibratorController.java
index 7e23587..3912206 100644
--- a/services/tests/vibrator/utils/com/android/server/vibrator/FakeVibratorController.java
+++ b/services/tests/vibrator/utils/com/android/server/vibrator/FakeVibratorController.java
@@ -20,6 +20,7 @@
import android.frameworks.vibrator.IVibratorController;
import android.os.IBinder;
import android.os.RemoteException;
+import android.os.VibrationAttributes;
/**
* Provides a fake implementation of {@link android.frameworks.vibrator.IVibratorController} for
@@ -28,10 +29,16 @@
public final class FakeVibratorController extends IVibratorController.Stub {
public boolean isLinkedToDeath = false;
+ public boolean didRequestVibrationParams = false;
+ public int requestVibrationType = VibrationAttributes.USAGE_UNKNOWN;
+ public long requestTimeoutInMillis = 0;
@Override
- public void requestVibrationParams(int i, long l, IBinder iBinder) throws RemoteException {
-
+ public void requestVibrationParams(int vibrationType, long timeoutInMillis, IBinder iBinder)
+ throws RemoteException {
+ didRequestVibrationParams = true;
+ requestVibrationType = vibrationType;
+ requestTimeoutInMillis = timeoutInMillis;
}
@Override
diff --git a/services/tests/wmtests/src/com/android/server/policy/ModifierShortcutTests.java b/services/tests/wmtests/src/com/android/server/policy/ModifierShortcutTests.java
index 0382ca0..e21388e 100644
--- a/services/tests/wmtests/src/com/android/server/policy/ModifierShortcutTests.java
+++ b/services/tests/wmtests/src/com/android/server/policy/ModifierShortcutTests.java
@@ -34,6 +34,7 @@
import static android.view.KeyEvent.KEYCODE_SLASH;
import static android.view.KeyEvent.KEYCODE_SPACE;
import static android.view.KeyEvent.KEYCODE_TAB;
+import static android.view.KeyEvent.KEYCODE_SCREENSHOT;
import static android.view.KeyEvent.KEYCODE_U;
import static android.view.KeyEvent.KEYCODE_Z;
@@ -193,4 +194,26 @@
mPhoneWindowManager.verifyNewBrightness(newBrightness[i]);
}
}
+
+ /**
+ * Sends a KEYCODE_SCREENSHOT and validates screenshot is taken if flag is enabled
+ */
+ @Test
+ public void testTakeScreenshot_flagEnabled() {
+ mSetFlagsRule.enableFlags(com.android.hardware.input.Flags
+ .FLAG_EMOJI_AND_SCREENSHOT_KEYCODES_AVAILABLE);
+ sendKeyCombination(new int[]{KEYCODE_SCREENSHOT}, 0);
+ mPhoneWindowManager.assertTakeScreenshotCalled();
+ }
+
+ /**
+ * Sends a KEYCODE_SCREENSHOT and validates screenshot is not taken if flag is disabled
+ */
+ @Test
+ public void testTakeScreenshot_flagDisabled() {
+ mSetFlagsRule.disableFlags(com.android.hardware.input.Flags
+ .FLAG_EMOJI_AND_SCREENSHOT_KEYCODES_AVAILABLE);
+ sendKeyCombination(new int[]{KEYCODE_SCREENSHOT}, 0);
+ mPhoneWindowManager.assertTakeScreenshotNotCalled();
+ }
}
diff --git a/services/tests/wmtests/src/com/android/server/policy/ShortcutKeyTestBase.java b/services/tests/wmtests/src/com/android/server/policy/ShortcutKeyTestBase.java
index 157d162..fee6582 100644
--- a/services/tests/wmtests/src/com/android/server/policy/ShortcutKeyTestBase.java
+++ b/services/tests/wmtests/src/com/android/server/policy/ShortcutKeyTestBase.java
@@ -46,6 +46,7 @@
import static java.util.Collections.unmodifiableMap;
import android.content.Context;
+import android.platform.test.flag.junit.SetFlagsRule;
import android.util.ArrayMap;
import android.view.InputDevice;
import android.view.KeyCharacterMap;
@@ -57,11 +58,17 @@
import org.junit.After;
import org.junit.Rule;
+import org.junit.rules.RuleChain;
import java.util.Map;
class ShortcutKeyTestBase {
- @Rule public FakeSettingsProviderRule mSettingsProviderRule = FakeSettingsProvider.rule();
+
+ public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+ public final FakeSettingsProviderRule mSettingsProviderRule = FakeSettingsProvider.rule();
+
+ @Rule
+ public RuleChain rules = RuleChain.outerRule(mSettingsProviderRule).around(mSetFlagsRule);
TestPhoneWindowManager mPhoneWindowManager;
DispatchedKeyHandler mDispatchedKeyHandler = event -> false;
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 c8abd8d..2904c03 100644
--- a/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java
+++ b/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java
@@ -596,6 +596,11 @@
verify(mDisplayPolicy).takeScreenshot(anyInt(), anyInt());
}
+ void assertTakeScreenshotNotCalled() {
+ mTestLooper.dispatchAll();
+ verify(mDisplayPolicy, never()).takeScreenshot(anyInt(), anyInt());
+ }
+
void assertShowGlobalActionsCalled() {
mTestLooper.dispatchAll();
verify(mPhoneWindowManager).showGlobalActions();
diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTests.java
index 9e00f92..5d14334 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTests.java
@@ -417,6 +417,8 @@
di.logicalWidth, di.logicalHeight).mConfigInsets.top);
}
+ // Flush the pending change (DecorInsets.Info#mNeedUpdate) for the rotation to be tested.
+ displayPolicy.getDecorInsetsInfo(Surface.ROTATION_90, di.logicalHeight, di.logicalWidth);
// Add a window that provides the same insets in current rotation. But it specifies
// different insets in other rotations.
final WindowState bar2 = createWindow(null, navbar.mAttrs.type, "bar2");
@@ -446,6 +448,12 @@
// The insets in other rotations should be still updated.
assertEquals(doubleHeightFor90, displayPolicy.getDecorInsetsInfo(Surface.ROTATION_90,
di.logicalHeight, di.logicalWidth).mConfigInsets.bottom);
+ // Restore to previous height and the insets can still be updated.
+ bar2.mAttrs.paramsForRotation[Surface.ROTATION_90].providedInsets[0].setInsetsSize(
+ Insets.of(0, 0, 0, NAV_BAR_HEIGHT));
+ assertFalse(displayPolicy.updateDecorInsetsInfo());
+ assertEquals(NAV_BAR_HEIGHT, displayPolicy.getDecorInsetsInfo(Surface.ROTATION_90,
+ di.logicalHeight, di.logicalWidth).mConfigInsets.bottom);
navbar.removeIfPossible();
bar2.removeIfPossible();
diff --git a/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java b/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java
index 3b4b220..9930c88 100644
--- a/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java
@@ -19,6 +19,7 @@
import static android.app.ActivityManager.RECENT_WITH_EXCLUDED;
import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED;
+import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
@@ -1207,6 +1208,29 @@
}
@Test
+ public void addTask_tasksAreAddedAccordingToZOrder() {
+ final Task firstTask = new TaskBuilder(mSupervisor).setTaskId(1)
+ .setWindowingMode(WINDOWING_MODE_FREEFORM).build();
+ final Task secondTask = new TaskBuilder(mSupervisor).setTaskId(2)
+ .setWindowingMode(WINDOWING_MODE_FREEFORM).build();
+
+ assertEquals(-1, firstTask.compareTo(secondTask));
+
+ // initial addition when tasks are created
+ mRecentTasks.add(firstTask);
+ mRecentTasks.add(secondTask);
+
+ assertRecentTasksOrder(secondTask, firstTask);
+
+ // Tasks are added in a different order
+ mRecentTasks.add(secondTask);
+ mRecentTasks.add(firstTask);
+
+ // order in recents don't change as first task has lower z-order
+ assertRecentTasksOrder(secondTask, firstTask);
+ }
+
+ @Test
public void removeTask_callsTaskNotificationController() {
final Task task = createTaskBuilder(".Task").build();
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskLaunchParamsModifierTests.java b/services/tests/wmtests/src/com/android/server/wm/TaskLaunchParamsModifierTests.java
index 07cfbf0..16f963f 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskLaunchParamsModifierTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskLaunchParamsModifierTests.java
@@ -628,6 +628,27 @@
}
@Test
+ public void testLaunchWindowingModeUpdatesExistingTask() {
+ final TestDisplayContent freeformDisplay = createNewDisplayContent(
+ WINDOWING_MODE_FREEFORM);
+
+ mCurrent.mPreferredTaskDisplayArea = freeformDisplay.getDefaultTaskDisplayArea();
+ ActivityRecord activity = createSourceActivity(freeformDisplay);
+ final Task task = activity.getTask();
+ task.setWindowingMode(WINDOWING_MODE_FULLSCREEN);
+ final ActivityOptions options = ActivityOptions.makeBasic();
+ options.setLaunchWindowingMode(WINDOWING_MODE_FREEFORM);
+
+ assertEquals(RESULT_CONTINUE,
+ new CalculateRequestBuilder()
+ .setTask(task)
+ .setOptions(options)
+ .calculate());
+
+ assertEquals(WINDOWING_MODE_FREEFORM, mResult.mWindowingMode);
+ }
+
+ @Test
public void testBoundsInOptionsInfersFreeformWithResizeableActivity() {
final ActivityOptions options = ActivityOptions.makeBasic();
options.setLaunchBounds(new Rect(0, 0, 100, 100));
diff --git a/telephony/java/android/telephony/SubscriptionManager.java b/telephony/java/android/telephony/SubscriptionManager.java
index 1bf11df..eb7e67d 100644
--- a/telephony/java/android/telephony/SubscriptionManager.java
+++ b/telephony/java/android/telephony/SubscriptionManager.java
@@ -1125,7 +1125,7 @@
/**
* TelephonyProvider column name for satellite attach enabled for carrier. The value of this
* column is set based on user settings.
- * By default, it's disabled.
+ * By default, it's enabled.
* <P>Type: INTEGER (int)</P>
* @hide
*/
diff --git a/telephony/java/android/telephony/TelephonyManager.java b/telephony/java/android/telephony/TelephonyManager.java
index c1ceaef..89661a4 100644
--- a/telephony/java/android/telephony/TelephonyManager.java
+++ b/telephony/java/android/telephony/TelephonyManager.java
@@ -6638,11 +6638,7 @@
}
}
- /**
- * @hide
- */
- @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
- private ITelephony getITelephony() {
+ private static ITelephony getITelephony() {
// Keeps cache disabled until test fixes are checked into AOSP.
if (!sServiceHandleCacheEnabled) {
return ITelephony.Stub.asInterface(
@@ -18696,11 +18692,7 @@
@SimState
public static int getSimStateForSlotIndex(int slotIndex) {
try {
- ITelephony telephony = ITelephony.Stub.asInterface(
- TelephonyFrameworkInitializer
- .getTelephonyServiceManager()
- .getTelephonyServiceRegisterer()
- .get());
+ ITelephony telephony = getITelephony();
if (telephony != null) {
return telephony.getSimStateForSlotIndex(slotIndex);
}
@@ -18970,11 +18962,11 @@
@FlaggedApi(Flags.FLAG_ENABLE_MODEM_CIPHER_TRANSPARENCY)
@RequiresPermission(Manifest.permission.MODIFY_PHONE_STATE)
@SystemApi
- public void setEnableNullCipherNotifications(boolean enable) {
+ public void setNullCipherNotificationsEnabled(boolean enable) {
try {
ITelephony telephony = getITelephony();
if (telephony != null) {
- telephony.setEnableNullCipherNotifications(enable);
+ telephony.setNullCipherNotificationsEnabled(enable);
} else {
throw new IllegalStateException("telephony service is null.");
}
diff --git a/telephony/java/android/telephony/euicc/EuiccManager.java b/telephony/java/android/telephony/euicc/EuiccManager.java
index 09d2108..9b8e62f 100644
--- a/telephony/java/android/telephony/euicc/EuiccManager.java
+++ b/telephony/java/android/telephony/euicc/EuiccManager.java
@@ -43,6 +43,7 @@
import android.util.Log;
import com.android.internal.telephony.euicc.IEuiccController;
+import com.android.internal.telephony.flags.Flags;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -897,9 +898,7 @@
/** @hide */
public EuiccManager(Context context) {
mContext = context;
- TelephonyManager tm = (TelephonyManager)
- context.getSystemService(Context.TELEPHONY_SERVICE);
- mCardId = tm.getCardIdForDefaultEuicc();
+ mCardId = getCardIdForDefaultEuicc();
}
/** @hide */
@@ -1646,14 +1645,9 @@
private boolean refreshCardIdIfUninitialized() {
// Refresh mCardId if its UNINITIALIZED_CARD_ID
if (mCardId == TelephonyManager.UNINITIALIZED_CARD_ID) {
- TelephonyManager tm = (TelephonyManager)
- mContext.getSystemService(Context.TELEPHONY_SERVICE);
- mCardId = tm.getCardIdForDefaultEuicc();
+ mCardId = getCardIdForDefaultEuicc();
}
- if (mCardId == TelephonyManager.UNINITIALIZED_CARD_ID) {
- return false;
- }
- return true;
+ return mCardId != TelephonyManager.UNINITIALIZED_CARD_ID;
}
private static void sendUnavailableError(PendingIntent callbackIntent) {
@@ -1672,6 +1666,23 @@
.get());
}
+ private int getCardIdForDefaultEuicc() {
+ int cardId = TelephonyManager.UNINITIALIZED_CARD_ID;
+
+ if (Flags.enforceTelephonyFeatureMappingForPublicApis()) {
+ PackageManager pm = mContext.getPackageManager();
+ if (pm != null && pm.hasSystemFeature(PackageManager.FEATURE_TELEPHONY_EUICC)) {
+ TelephonyManager tm = mContext.getSystemService(TelephonyManager.class);
+ cardId = tm.getCardIdForDefaultEuicc();
+ }
+ } else {
+ TelephonyManager tm = mContext.getSystemService(TelephonyManager.class);
+ cardId = tm.getCardIdForDefaultEuicc();
+ }
+
+ return cardId;
+ }
+
/**
* Returns whether the passing portIndex is available.
* A port is available if it is active without enabled profile on it or
diff --git a/telephony/java/com/android/internal/telephony/ITelephony.aidl b/telephony/java/com/android/internal/telephony/ITelephony.aidl
index 43eb111..a1fc064 100644
--- a/telephony/java/com/android/internal/telephony/ITelephony.aidl
+++ b/telephony/java/com/android/internal/telephony/ITelephony.aidl
@@ -3247,7 +3247,7 @@
*/
@JavaPassthrough(annotation="@android.annotation.RequiresPermission("
+ "android.Manifest.permission.MODIFY_PHONE_STATE)")
- void setEnableNullCipherNotifications(boolean enable);
+ void setNullCipherNotificationsEnabled(boolean enable);
/**
* Get whether notifications are enabled for null cipher or integrity algorithms in use by the
diff --git a/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt b/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt
index 566e51a..cbec85e 100644
--- a/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt
+++ b/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt
@@ -146,6 +146,7 @@
verify(native).setTouchpadPointerSpeed(anyInt())
verify(native).setTouchpadNaturalScrollingEnabled(anyBoolean())
verify(native).setTouchpadTapToClickEnabled(anyBoolean())
+ verify(native).setTouchpadTapDraggingEnabled(anyBoolean())
verify(native).setTouchpadRightClickZoneEnabled(anyBoolean())
verify(native).setShowTouches(anyBoolean())
verify(native).setMotionClassifierEnabled(anyBoolean())
diff --git a/tests/vcn/java/com/android/server/vcn/routeselection/NetworkEvaluationTestBase.java b/tests/vcn/java/com/android/server/vcn/routeselection/NetworkEvaluationTestBase.java
index 6015e931..381c574 100644
--- a/tests/vcn/java/com/android/server/vcn/routeselection/NetworkEvaluationTestBase.java
+++ b/tests/vcn/java/com/android/server/vcn/routeselection/NetworkEvaluationTestBase.java
@@ -101,7 +101,7 @@
@Mock protected Context mContext;
@Mock protected Network mNetwork;
@Mock protected FeatureFlags mFeatureFlags;
- @Mock protected com.android.net.flags.FeatureFlags mCoreNetFeatureFlags;
+ @Mock protected android.net.platform.flags.FeatureFlags mCoreNetFeatureFlags;
@Mock protected TelephonySubscriptionSnapshot mSubscriptionSnapshot;
@Mock protected TelephonyManager mTelephonyManager;
@Mock protected IPowerManager mPowerManagerService;
diff --git a/tools/aapt2/Android.bp b/tools/aapt2/Android.bp
index 275a0e2..938a5ed 100644
--- a/tools/aapt2/Android.bp
+++ b/tools/aapt2/Android.bp
@@ -232,3 +232,39 @@
],
},
}
+
+cc_genrule {
+ name: "aapt2_results",
+ srcs: [
+ ":aapt2_tests",
+ "integration-tests/CompileTest/**/*",
+ "integration-tests/CommandTests/**/*",
+ "integration-tests/ConvertTest/**/*",
+ "integration-tests/DumpTest/**/*",
+ ],
+ host_supported: true,
+ device_supported: false,
+ target: {
+ windows: {
+ compile_multilib: "64",
+ },
+ },
+ out: ["result.xml"],
+ cmd: "mkdir -p $(genDir)/integration-tests/CompileTest/ && " +
+ "cp $(locations integration-tests/CompileTest/**/*) $(genDir)/integration-tests/CompileTest/ && " +
+ "mkdir -p $(genDir)/integration-tests/CommandTests/ && " +
+ "cp $(locations integration-tests/CommandTests/**/*) $(genDir)/integration-tests/CompileTest/ && " +
+ "mkdir -p $(genDir)/integration-tests/ConvertTest/ && " +
+ "cp $(locations integration-tests/ConvertTest/**/*) $(genDir)/integration-tests/ConvertTest/ && " +
+ "mkdir -p $(genDir)/integration-tests/DumpTest/ && " +
+ "cp $(locations integration-tests/DumpTest/**/*) $(genDir)/integration-tests/DumpTest/ && " +
+ "cp $(locations :aapt2_tests) $(genDir)/ && " +
+ "$(genDir)/aapt2_tests " +
+ "--gtest_output=xml:$(out) " +
+ ">/dev/null 2>&1 ; true",
+}
+
+phony_rule {
+ name: "aapt2_run_host_unit_tests",
+ phony_deps: ["aapt2_results"],
+}
diff --git a/tools/aapt2/Android.mk b/tools/aapt2/Android.mk
index 34a1b11..15ae2ba 100644
--- a/tools/aapt2/Android.mk
+++ b/tools/aapt2/Android.mk
@@ -1,22 +1,4 @@
-LOCAL_PATH := $(call my-dir)
-
include $(CLEAR_VARS)
-
-aapt2_results := $(call intermediates-dir-for,PACKAGING,aapt2_run_host_unit_tests)/result.xml
-
-# Target for running host unit tests on post/pre-submit.
-.PHONY: aapt2_run_host_unit_tests
-aapt2_run_host_unit_tests: $(aapt2_results)
-
-$(call dist-for-goals,aapt2_run_host_unit_tests,$(aapt2_results):gtest/aapt2_host_unit_tests_result.xml)
-
-# Always run the tests again, even if they haven't changed
-$(aapt2_results): .KATI_IMPLICIT_OUTPUTS := $(aapt2_results)-nocache
-$(aapt2_results): $(HOST_OUT_NATIVE_TESTS)/aapt2_tests/aapt2_tests
- -$(HOST_OUT_NATIVE_TESTS)/aapt2_tests/aapt2_tests --gtest_output=xml:$@ > /dev/null 2>&1
-
+aapt2_results := ./out/soong/.intermediates/frameworks/base/tools/aapt2/aapt2_results
$(call declare-1p-target,$(aapt2_results))
-
aapt2_results :=
-
-include $(call all-makefiles-under,$(LOCAL_PATH))
diff --git a/tools/aapt2/cmd/Link.cpp b/tools/aapt2/cmd/Link.cpp
index f3f1838..642a561 100644
--- a/tools/aapt2/cmd/Link.cpp
+++ b/tools/aapt2/cmd/Link.cpp
@@ -1988,6 +1988,8 @@
context_->SetNameManglerPolicy(NameManglerPolicy{context_->GetCompilationPackage()});
context_->SetSplitNameDependencies(app_info_.split_name_dependencies);
+ std::unique_ptr<xml::XmlResource> pre_flags_filter_manifest_xml = manifest_xml->Clone();
+
FeatureFlagsFilterOptions flags_filter_options;
if (context_->GetMinSdkVersion() > SDK_UPSIDE_DOWN_CAKE) {
// For API version > U, PackageManager will dynamically read the flag values and disable
@@ -2297,7 +2299,12 @@
}
if (options_.generate_java_class_path) {
- if (!WriteManifestJavaFile(manifest_xml.get())) {
+ // The FeatureFlagsFilter may remove <permission> and <permission-group> elements that
+ // generate constants in the Manifest Java file. While we want those permissions and
+ // permission groups removed in the SDK (i.e., if a feature flag is disabled), the
+ // constants should still remain so that code referencing it (e.g., within a feature
+ // flag check) will still compile. Therefore we use the manifest XML before the filter.
+ if (!WriteManifestJavaFile(pre_flags_filter_manifest_xml.get())) {
error = true;
}
}
diff --git a/tools/aapt2/cmd/Link_test.cpp b/tools/aapt2/cmd/Link_test.cpp
index 9323f3b..6cc42f1 100644
--- a/tools/aapt2/cmd/Link_test.cpp
+++ b/tools/aapt2/cmd/Link_test.cpp
@@ -1021,9 +1021,11 @@
.AddContents(manifest_contents)
.Build();
+ const std::string app_java = GetTestPath("app-java");
auto app_link_args = LinkCommandBuilder(this)
.SetManifestFile(app_manifest)
.AddParameter("-I", android_apk)
+ .AddParameter("--java", app_java)
.AddParameter("--feature-flags", "flag=false");
const std::string app_apk = GetTestPath("app.apk");
@@ -1038,6 +1040,12 @@
ASSERT_THAT(root, NotNull());
auto maybe_removed = root->FindChild({}, "permission");
ASSERT_THAT(maybe_removed, IsNull());
+
+ // Code for the permission should be generated even if the element is removed
+ const std::string manifest_java = app_java + "/com/example/app/Manifest.java";
+ std::string manifest_java_contents;
+ ASSERT_TRUE(android::base::ReadFileToString(manifest_java, &manifest_java_contents));
+ EXPECT_THAT(manifest_java_contents, HasSubstr(" public static final String FOO=\"FOO\";"));
}
TEST_F(LinkTest, FeatureFlagEnabled_SdkAtMostUDC) {
diff --git a/tools/hoststubgen/hoststubgen/helper-framework-runtime-src/framework/com/android/hoststubgen/nativesubstitution/EventLog_host.java b/tools/hoststubgen/hoststubgen/helper-framework-runtime-src/framework/com/android/hoststubgen/nativesubstitution/EventLog_host.java
index 292e8da..6480cfc 100644
--- a/tools/hoststubgen/hoststubgen/helper-framework-runtime-src/framework/com/android/hoststubgen/nativesubstitution/EventLog_host.java
+++ b/tools/hoststubgen/hoststubgen/helper-framework-runtime-src/framework/com/android/hoststubgen/nativesubstitution/EventLog_host.java
@@ -15,9 +15,9 @@
*/
package com.android.hoststubgen.nativesubstitution;
-import android.util.Log;
-import android.util.Log.Level;
+import com.android.internal.os.RuntimeInit;
+import java.io.PrintStream;
import java.util.Collection;
public class EventLog_host {
@@ -54,7 +54,7 @@
}
}
sb.append(']');
- System.out.println(sb.toString());
+ getRealOut().println(sb.toString());
return sb.length();
}
@@ -66,4 +66,16 @@
Collection<android.util.EventLog.Event> output) {
throw new UnsupportedOperationException();
}
+
+ /**
+ * Return the "real" {@code System.out} if it's been swapped by {@code RavenwoodRuleImpl}, so
+ * that we don't end up in a recursive loop.
+ */
+ private static PrintStream getRealOut() {
+ if (RuntimeInit.sOut$ravenwood != null) {
+ return RuntimeInit.sOut$ravenwood;
+ } else {
+ return System.out;
+ }
+ }
}
diff --git a/tools/hoststubgen/hoststubgen/helper-framework-runtime-src/framework/com/android/hoststubgen/nativesubstitution/Log_host.java b/tools/hoststubgen/hoststubgen/helper-framework-runtime-src/framework/com/android/hoststubgen/nativesubstitution/Log_host.java
index ee55c7a..cdfa302 100644
--- a/tools/hoststubgen/hoststubgen/helper-framework-runtime-src/framework/com/android/hoststubgen/nativesubstitution/Log_host.java
+++ b/tools/hoststubgen/hoststubgen/helper-framework-runtime-src/framework/com/android/hoststubgen/nativesubstitution/Log_host.java
@@ -18,6 +18,8 @@
import android.util.Log;
import android.util.Log.Level;
+import com.android.internal.os.RuntimeInit;
+
import java.io.PrintStream;
public class Log_host {
@@ -27,7 +29,6 @@
}
public static int println_native(int bufID, int priority, String tag, String msg) {
- final PrintStream out = System.out;
final String buffer;
switch (bufID) {
case Log.LOG_ID_MAIN: buffer = "main"; break;
@@ -50,7 +51,7 @@
};
for (String s : msg.split("\\n")) {
- out.println(String.format("logd: [%s] %s %s: %s", buffer, prio, tag, s));
+ getRealOut().println(String.format("logd: [%s] %s %s: %s", buffer, prio, tag, s));
}
return msg.length();
}
@@ -58,4 +59,16 @@
public static int logger_entry_max_payload_native() {
return 4068; // [ravenwood] This is what people use in various places.
}
+
+ /**
+ * Return the "real" {@code System.out} if it's been swapped by {@code RavenwoodRuleImpl}, so
+ * that we don't end up in a recursive loop.
+ */
+ private static PrintStream getRealOut() {
+ if (RuntimeInit.sOut$ravenwood != null) {
+ return RuntimeInit.sOut$ravenwood;
+ } else {
+ return System.out;
+ }
+ }
}
diff --git a/tools/hoststubgen/hoststubgen/helper-framework-runtime-src/framework/com/android/hoststubgen/nativesubstitution/MessageQueue_host.java b/tools/hoststubgen/hoststubgen/helper-framework-runtime-src/framework/com/android/hoststubgen/nativesubstitution/MessageQueue_host.java
index 2e47d48..65da4a1 100644
--- a/tools/hoststubgen/hoststubgen/helper-framework-runtime-src/framework/com/android/hoststubgen/nativesubstitution/MessageQueue_host.java
+++ b/tools/hoststubgen/hoststubgen/helper-framework-runtime-src/framework/com/android/hoststubgen/nativesubstitution/MessageQueue_host.java
@@ -28,6 +28,7 @@
private final Object mPoller = new Object();
private volatile boolean mPolling;
+ private volatile boolean mPendingWake;
private void validate() {
if (mDeleted) {
@@ -62,7 +63,9 @@
synchronized (q.mPoller) {
q.mPolling = true;
try {
- if (timeoutMillis == 0) {
+ if (q.mPendingWake) {
+ // Calling with pending wake returns immediately
+ } else if (timeoutMillis == 0) {
// Calling epoll_wait() with 0 returns immediately
} else if (timeoutMillis == -1) {
q.mPoller.wait();
@@ -72,6 +75,8 @@
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
+ // Any reason for returning counts as a "wake", so clear pending
+ q.mPendingWake = false;
q.mPolling = false;
}
}
@@ -79,6 +84,7 @@
public static void nativeWake(long ptr) {
var q = getInstance(ptr);
synchronized (q.mPoller) {
+ q.mPendingWake = true;
q.mPoller.notifyAll();
}
}
diff --git a/tools/hoststubgen/hoststubgen/helper-runtime-src/com/android/hoststubgen/hosthelper/HostTestUtils.java b/tools/hoststubgen/hoststubgen/helper-runtime-src/com/android/hoststubgen/hosthelper/HostTestUtils.java
index 7c6aa25..60eb47ee 100644
--- a/tools/hoststubgen/hoststubgen/helper-runtime-src/com/android/hoststubgen/hosthelper/HostTestUtils.java
+++ b/tools/hoststubgen/hoststubgen/helper-runtime-src/com/android/hoststubgen/hosthelper/HostTestUtils.java
@@ -63,7 +63,10 @@
*/
public static void onThrowMethodCalled() {
// TODO: Maybe add call tracking?
- throw new RuntimeException("This method is not supported on the host side");
+ throw new RuntimeException(
+ "This method is not yet supported under the Ravenwood deviceless testing "
+ + "environment; consider requesting support from the API owner or "
+ + "consider using Mockito; more details at go/ravenwood-docs");
}
/**
diff --git a/tools/hoststubgen/hoststubgen/test-tiny-framework/tiny-test/src/com/android/hoststubgen/test/tinyframework/TinyFrameworkClassTest.java b/tools/hoststubgen/hoststubgen/test-tiny-framework/tiny-test/src/com/android/hoststubgen/test/tinyframework/TinyFrameworkClassTest.java
index fc6b862..ba17c75 100644
--- a/tools/hoststubgen/hoststubgen/test-tiny-framework/tiny-test/src/com/android/hoststubgen/test/tinyframework/TinyFrameworkClassTest.java
+++ b/tools/hoststubgen/hoststubgen/test-tiny-framework/tiny-test/src/com/android/hoststubgen/test/tinyframework/TinyFrameworkClassTest.java
@@ -68,7 +68,7 @@
TinyFrameworkForTextPolicy tfc = new TinyFrameworkForTextPolicy();
thrown.expect(RuntimeException.class);
- thrown.expectMessage("This method is not supported on the host side");
+ thrown.expectMessage("not yet supported");
tfc.visibleButUsesUnsupportedMethod();
}
@@ -182,7 +182,7 @@
} catch (java.lang.reflect.InvocationTargetException e) {
var inner = e.getCause();
- assertThat(inner.getMessage()).contains("not supported on the host side");
+ assertThat(inner.getMessage()).contains("not yet supported");
}
}
diff --git a/tools/hoststubgen/hoststubgen/test-tiny-framework/tiny-test/src/com/android/hoststubgen/test/tinyframework/TinyFrameworkClassWithAnnotTest.java b/tools/hoststubgen/hoststubgen/test-tiny-framework/tiny-test/src/com/android/hoststubgen/test/tinyframework/TinyFrameworkClassWithAnnotTest.java
index 20cc2ec..288c716 100644
--- a/tools/hoststubgen/hoststubgen/test-tiny-framework/tiny-test/src/com/android/hoststubgen/test/tinyframework/TinyFrameworkClassWithAnnotTest.java
+++ b/tools/hoststubgen/hoststubgen/test-tiny-framework/tiny-test/src/com/android/hoststubgen/test/tinyframework/TinyFrameworkClassWithAnnotTest.java
@@ -60,7 +60,7 @@
TinyFrameworkClassAnnotations tfc = new TinyFrameworkClassAnnotations();
thrown.expect(RuntimeException.class);
- thrown.expectMessage("This method is not supported on the host side");
+ thrown.expectMessage("not yet supported");
tfc.visibleButUsesUnsupportedMethod();
}
}
diff --git a/wifi/java/src/android/net/wifi/nl80211/DeviceWiphyCapabilities.java b/wifi/java/src/android/net/wifi/nl80211/DeviceWiphyCapabilities.java
index 21700d5..6d57a87 100644
--- a/wifi/java/src/android/net/wifi/nl80211/DeviceWiphyCapabilities.java
+++ b/wifi/java/src/android/net/wifi/nl80211/DeviceWiphyCapabilities.java
@@ -16,11 +16,13 @@
package android.net.wifi.nl80211;
+import android.annotation.FlaggedApi;
import android.annotation.NonNull;
import android.annotation.SystemApi;
import android.net.wifi.ScanResult;
import android.net.wifi.WifiAnnotations.ChannelWidth;
import android.net.wifi.WifiAnnotations.WifiStandard;
+import android.net.wifi.flags.Flags;
import android.os.Parcel;
import android.os.Parcelable;
import android.util.Log;
@@ -48,6 +50,7 @@
private boolean mChannelWidth320MhzSupported;
private int mMaxNumberTxSpatialStreams;
private int mMaxNumberRxSpatialStreams;
+ private int mMaxNumberAkms;
/** public constructor */
@@ -61,6 +64,7 @@
mChannelWidth320MhzSupported = false;
mMaxNumberTxSpatialStreams = 1;
mMaxNumberRxSpatialStreams = 1;
+ mMaxNumberAkms = 1;
}
/**
@@ -199,6 +203,25 @@
}
/**
+ * Get the maximum number of AKM suites supported in the connection request to the driver.
+ *
+ * @return maximum number of AKMs
+ */
+ @FlaggedApi(Flags.FLAG_GET_DEVICE_CROSS_AKM_ROAMING_SUPPORT)
+ public int getMaxNumberAkms() {
+ return mMaxNumberAkms;
+ }
+
+ /**
+ * Set the maximum number of AKM suites supported in the connection request to the driver.
+ *
+ * @hide
+ */
+ public void setMaxNumberAkms(int akms) {
+ mMaxNumberAkms = akms;
+ }
+
+ /**
* Set maximum number of receive spatial streams
*
* @param streams number of streams
@@ -226,7 +249,8 @@
&& mChannelWidth80p80MhzSupported == capa.mChannelWidth80p80MhzSupported
&& mChannelWidth320MhzSupported == capa.mChannelWidth320MhzSupported
&& mMaxNumberTxSpatialStreams == capa.mMaxNumberTxSpatialStreams
- && mMaxNumberRxSpatialStreams == capa.mMaxNumberRxSpatialStreams;
+ && mMaxNumberRxSpatialStreams == capa.mMaxNumberRxSpatialStreams
+ && mMaxNumberAkms == capa.mMaxNumberAkms;
}
/** override hash code */
@@ -235,7 +259,7 @@
return Objects.hash(m80211nSupported, m80211acSupported, m80211axSupported,
m80211beSupported, mChannelWidth160MhzSupported, mChannelWidth80p80MhzSupported,
mChannelWidth320MhzSupported, mMaxNumberTxSpatialStreams,
- mMaxNumberRxSpatialStreams);
+ mMaxNumberRxSpatialStreams, mMaxNumberAkms);
}
/** implement Parcelable interface */
@@ -259,6 +283,7 @@
out.writeBoolean(mChannelWidth320MhzSupported);
out.writeInt(mMaxNumberTxSpatialStreams);
out.writeInt(mMaxNumberRxSpatialStreams);
+ out.writeInt(mMaxNumberAkms);
}
@Override
@@ -276,6 +301,7 @@
.append(mChannelWidth320MhzSupported ? "Yes" : "No");
sb.append("mMaxNumberTxSpatialStreams: ").append(mMaxNumberTxSpatialStreams);
sb.append("mMaxNumberRxSpatialStreams: ").append(mMaxNumberRxSpatialStreams);
+ sb.append("mMaxNumberAkms: ").append(mMaxNumberAkms);
return sb.toString();
}
@@ -298,6 +324,7 @@
capabilities.mChannelWidth320MhzSupported = in.readBoolean();
capabilities.mMaxNumberTxSpatialStreams = in.readInt();
capabilities.mMaxNumberRxSpatialStreams = in.readInt();
+ capabilities.mMaxNumberAkms = in.readInt();
return capabilities;
}
diff --git a/wifi/tests/src/android/net/wifi/nl80211/DeviceWiphyCapabilitiesTest.java b/wifi/tests/src/android/net/wifi/nl80211/DeviceWiphyCapabilitiesTest.java
index 7b900fe..5a3ca2b 100644
--- a/wifi/tests/src/android/net/wifi/nl80211/DeviceWiphyCapabilitiesTest.java
+++ b/wifi/tests/src/android/net/wifi/nl80211/DeviceWiphyCapabilitiesTest.java
@@ -49,6 +49,7 @@
capa.setChannelWidthSupported(ScanResult.CHANNEL_WIDTH_80MHZ_PLUS_MHZ, false);
capa.setMaxNumberTxSpatialStreams(2);
capa.setMaxNumberRxSpatialStreams(1);
+ capa.setMaxNumberAkms(2);
Parcel parcel = Parcel.obtain();
capa.writeToParcel(parcel, 0);
diff --git a/wifi/tests/src/android/net/wifi/nl80211/WifiNl80211ManagerTest.java b/wifi/tests/src/android/net/wifi/nl80211/WifiNl80211ManagerTest.java
index 362eb14..02da835 100644
--- a/wifi/tests/src/android/net/wifi/nl80211/WifiNl80211ManagerTest.java
+++ b/wifi/tests/src/android/net/wifi/nl80211/WifiNl80211ManagerTest.java
@@ -1130,6 +1130,7 @@
capaExpected.setChannelWidthSupported(ScanResult.CHANNEL_WIDTH_80MHZ_PLUS_MHZ, false);
capaExpected.setMaxNumberTxSpatialStreams(2);
capaExpected.setMaxNumberRxSpatialStreams(1);
+ capaExpected.setMaxNumberAkms(2);
when(mWificond.getDeviceWiphyCapabilities(TEST_INTERFACE_NAME))
.thenReturn(capaExpected);
diff --git a/wifi/wifi.aconfig b/wifi/wifi.aconfig
new file mode 100644
index 0000000..6ac986e
--- /dev/null
+++ b/wifi/wifi.aconfig
@@ -0,0 +1,9 @@
+package: "android.net.wifi.flags"
+
+flag {
+ name: "get_device_cross_akm_roaming_support"
+ namespace: "wifi"
+ description: "Add new API to get the device support for CROSS-AKM roaming"
+ bug: "313038031"
+ is_fixed_read_only: true
+}