Merge "[Networking] Use SharingStarted.Eagerly for broadcast" into main
diff --git a/AconfigFlags.bp b/AconfigFlags.bp
index 6ecd38f..3391698 100644
--- a/AconfigFlags.bp
+++ b/AconfigFlags.bp
@@ -335,6 +335,11 @@
aconfig_declarations: "android.os.flags-aconfig",
defaults: ["framework-minus-apex-aconfig-java-defaults"],
mode: "exported",
+ min_sdk_version: "30",
+ apex_available: [
+ "//apex_available:platform",
+ "com.android.mediaprovider",
+ ],
}
cc_aconfig_library {
@@ -716,6 +721,7 @@
name: "android.credentials.flags-aconfig",
package: "android.credentials.flags",
srcs: ["core/java/android/credentials/flags.aconfig"],
+ exportable: true,
}
java_aconfig_library {
@@ -724,6 +730,13 @@
defaults: ["framework-minus-apex-aconfig-java-defaults"],
}
+java_aconfig_library {
+ name: "android.credentials.flags-aconfig-java-export",
+ aconfig_declarations: "android.credentials.flags-aconfig",
+ defaults: ["framework-minus-apex-aconfig-java-defaults"],
+ mode: "exported",
+}
+
// Content Protection
aconfig_declarations {
name: "android.view.contentprotection.flags-aconfig",
diff --git a/Android.bp b/Android.bp
index 057b1d6..59e903e 100644
--- a/Android.bp
+++ b/Android.bp
@@ -389,7 +389,6 @@
// TODO(b/120066492): remove gps_debug and protolog.conf.json when the build
// system propagates "required" properly.
"gps_debug.conf",
- "protolog.conf.json.gz",
"core.protolog.pb",
"framework-res",
// any install dependencies should go into framework-minus-apex-install-dependencies
diff --git a/TEST_MAPPING b/TEST_MAPPING
index c904eb4..49384cd 100644
--- a/TEST_MAPPING
+++ b/TEST_MAPPING
@@ -232,30 +232,5 @@
}
]
}
- ],
- "auto-features-postsubmit": [
- // Test tag for automotive feature targets. These are only running in postsubmit.
- // This tag is used in targeted test features testing to limit resource use.
- // TODO(b/256932212): this tag to be removed once the above is no longer in use.
- {
- "name": "FrameworksMockingServicesTests",
- "options": [
- {
- "include-filter": "com.android.server.pm.UserVisibilityMediatorSUSDTest"
- },
- {
- "include-filter": "com.android.server.pm.UserVisibilityMediatorMUMDTest"
- },
- {
- "include-filter": "com.android.server.pm.UserVisibilityMediatorMUPANDTest"
- },
- {
- "exclude-annotation": "androidx.test.filters.FlakyTest"
- },
- {
- "exclude-annotation": "org.junit.Ignore"
- }
- ]
- }
]
}
diff --git a/apex/jobscheduler/framework/aconfig/job.aconfig b/apex/jobscheduler/framework/aconfig/job.aconfig
index 788e824..2c1a853 100644
--- a/apex/jobscheduler/framework/aconfig/job.aconfig
+++ b/apex/jobscheduler/framework/aconfig/job.aconfig
@@ -9,6 +9,7 @@
flag {
name: "job_debug_info_apis"
+ is_exported: true
namespace: "backstage_power"
description: "Add APIs to let apps attach debug information to jobs"
bug: "293491637"
@@ -16,6 +17,7 @@
flag {
name: "backup_jobs_exemption"
+ is_exported: true
namespace: "backstage_power"
description: "Introduce a new RUN_BACKUP_JOBS permission and exemption logic allowing for longer running jobs for apps whose primary purpose is to backup or sync content."
bug: "318731461"
diff --git a/core/api/current.txt b/core/api/current.txt
index 4d3ca13..93c34cd 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -10731,6 +10731,7 @@
field public static final String DROPBOX_SERVICE = "dropbox";
field public static final String EUICC_SERVICE = "euicc";
field public static final String FILE_INTEGRITY_SERVICE = "file_integrity";
+ field public static final String FINGERPRINT_SERVICE = "fingerprint";
field public static final String GAME_SERVICE = "game";
field public static final String GRAMMATICAL_INFLECTION_SERVICE = "grammatical_inflection";
field public static final String HARDWARE_PROPERTIES_SERVICE = "hardware_properties";
@@ -10764,6 +10765,7 @@
field public static final String OVERLAY_SERVICE = "overlay";
field public static final String PEOPLE_SERVICE = "people";
field public static final String PERFORMANCE_HINT_SERVICE = "performance_hint";
+ field @FlaggedApi("android.security.frp_enforcement") public static final String PERSISTENT_DATA_BLOCK_SERVICE = "persistent_data_block";
field public static final String POWER_SERVICE = "power";
field public static final String PRINT_SERVICE = "print";
field @FlaggedApi("android.os.telemetry_apis_framework_initialization") public static final String PROFILING_SERVICE = "profiling";
@@ -20235,10 +20237,10 @@
method public android.hardware.camera2.CaptureRequest getSessionParameters();
method public int getSessionType();
method public android.hardware.camera2.CameraCaptureSession.StateCallback getStateCallback();
- method @FlaggedApi("com.android.internal.camera.flags.camera_device_setup") public void setCallback(@NonNull java.util.concurrent.Executor, @NonNull android.hardware.camera2.CameraCaptureSession.StateCallback);
method public void setColorSpace(@NonNull android.graphics.ColorSpace.Named);
method public void setInputConfiguration(@NonNull android.hardware.camera2.params.InputConfiguration);
method public void setSessionParameters(android.hardware.camera2.CaptureRequest);
+ method @FlaggedApi("com.android.internal.camera.flags.camera_device_setup") public void setStateCallback(@NonNull java.util.concurrent.Executor, @NonNull android.hardware.camera2.CameraCaptureSession.StateCallback);
method public void writeToParcel(android.os.Parcel, int);
field @NonNull public static final android.os.Parcelable.Creator<android.hardware.camera2.params.SessionConfiguration> CREATOR;
field public static final int SESSION_HIGH_SPEED = 1; // 0x1
@@ -20386,6 +20388,54 @@
}
+package android.hardware.fingerprint {
+
+ @Deprecated public class FingerprintManager {
+ method @Deprecated @RequiresPermission(anyOf={android.Manifest.permission.USE_BIOMETRIC, android.Manifest.permission.USE_FINGERPRINT}) public void authenticate(@Nullable android.hardware.fingerprint.FingerprintManager.CryptoObject, @Nullable android.os.CancellationSignal, int, @NonNull android.hardware.fingerprint.FingerprintManager.AuthenticationCallback, @Nullable android.os.Handler);
+ method @Deprecated @RequiresPermission(android.Manifest.permission.USE_FINGERPRINT) public boolean hasEnrolledFingerprints();
+ method @Deprecated @RequiresPermission(android.Manifest.permission.USE_FINGERPRINT) public boolean isHardwareDetected();
+ field public static final int FINGERPRINT_ACQUIRED_GOOD = 0; // 0x0
+ field public static final int FINGERPRINT_ACQUIRED_IMAGER_DIRTY = 3; // 0x3
+ field public static final int FINGERPRINT_ACQUIRED_INSUFFICIENT = 2; // 0x2
+ field public static final int FINGERPRINT_ACQUIRED_PARTIAL = 1; // 0x1
+ field public static final int FINGERPRINT_ACQUIRED_TOO_FAST = 5; // 0x5
+ field public static final int FINGERPRINT_ACQUIRED_TOO_SLOW = 4; // 0x4
+ field public static final int FINGERPRINT_ERROR_CANCELED = 5; // 0x5
+ field public static final int FINGERPRINT_ERROR_HW_NOT_PRESENT = 12; // 0xc
+ field public static final int FINGERPRINT_ERROR_HW_UNAVAILABLE = 1; // 0x1
+ field public static final int FINGERPRINT_ERROR_LOCKOUT = 7; // 0x7
+ field public static final int FINGERPRINT_ERROR_LOCKOUT_PERMANENT = 9; // 0x9
+ field public static final int FINGERPRINT_ERROR_NO_FINGERPRINTS = 11; // 0xb
+ field public static final int FINGERPRINT_ERROR_NO_SPACE = 4; // 0x4
+ field public static final int FINGERPRINT_ERROR_TIMEOUT = 3; // 0x3
+ field public static final int FINGERPRINT_ERROR_UNABLE_TO_PROCESS = 2; // 0x2
+ field public static final int FINGERPRINT_ERROR_USER_CANCELED = 10; // 0xa
+ field public static final int FINGERPRINT_ERROR_VENDOR = 8; // 0x8
+ }
+
+ @Deprecated public abstract static class FingerprintManager.AuthenticationCallback {
+ ctor @Deprecated public FingerprintManager.AuthenticationCallback();
+ method @Deprecated public void onAuthenticationError(int, CharSequence);
+ method @Deprecated public void onAuthenticationFailed();
+ method @Deprecated public void onAuthenticationHelp(int, CharSequence);
+ method @Deprecated public void onAuthenticationSucceeded(android.hardware.fingerprint.FingerprintManager.AuthenticationResult);
+ }
+
+ @Deprecated public static class FingerprintManager.AuthenticationResult {
+ method @Deprecated public android.hardware.fingerprint.FingerprintManager.CryptoObject getCryptoObject();
+ }
+
+ @Deprecated public static final class FingerprintManager.CryptoObject {
+ ctor @Deprecated public FingerprintManager.CryptoObject(@NonNull java.security.Signature);
+ ctor @Deprecated public FingerprintManager.CryptoObject(@NonNull javax.crypto.Cipher);
+ ctor @Deprecated public FingerprintManager.CryptoObject(@NonNull javax.crypto.Mac);
+ method @Deprecated public javax.crypto.Cipher getCipher();
+ method @Deprecated public javax.crypto.Mac getMac();
+ method @Deprecated public java.security.Signature getSignature();
+ }
+
+}
+
package android.hardware.input {
public final class HostUsiVersion implements android.os.Parcelable {
diff --git a/core/api/removed.txt b/core/api/removed.txt
index c61f163..3c7c0d6 100644
--- a/core/api/removed.txt
+++ b/core/api/removed.txt
@@ -35,7 +35,6 @@
method @Deprecated @Nullable public String getFeatureId();
method public abstract android.content.SharedPreferences getSharedPreferences(java.io.File, int);
method public abstract java.io.File getSharedPreferencesPath(String);
- field public static final String FINGERPRINT_SERVICE = "fingerprint";
}
public class ContextWrapper extends android.content.Context {
@@ -146,54 +145,6 @@
}
-package android.hardware.fingerprint {
-
- @Deprecated public class FingerprintManager {
- method @Deprecated @RequiresPermission(anyOf={android.Manifest.permission.USE_BIOMETRIC, android.Manifest.permission.USE_FINGERPRINT}) public void authenticate(@Nullable android.hardware.fingerprint.FingerprintManager.CryptoObject, @Nullable android.os.CancellationSignal, int, @NonNull android.hardware.fingerprint.FingerprintManager.AuthenticationCallback, @Nullable android.os.Handler);
- method @Deprecated @RequiresPermission(android.Manifest.permission.USE_FINGERPRINT) public boolean hasEnrolledFingerprints();
- method @Deprecated @RequiresPermission(android.Manifest.permission.USE_FINGERPRINT) public boolean isHardwareDetected();
- field public static final int FINGERPRINT_ACQUIRED_GOOD = 0; // 0x0
- field public static final int FINGERPRINT_ACQUIRED_IMAGER_DIRTY = 3; // 0x3
- field public static final int FINGERPRINT_ACQUIRED_INSUFFICIENT = 2; // 0x2
- field public static final int FINGERPRINT_ACQUIRED_PARTIAL = 1; // 0x1
- field public static final int FINGERPRINT_ACQUIRED_TOO_FAST = 5; // 0x5
- field public static final int FINGERPRINT_ACQUIRED_TOO_SLOW = 4; // 0x4
- field public static final int FINGERPRINT_ERROR_CANCELED = 5; // 0x5
- field public static final int FINGERPRINT_ERROR_HW_NOT_PRESENT = 12; // 0xc
- field public static final int FINGERPRINT_ERROR_HW_UNAVAILABLE = 1; // 0x1
- field public static final int FINGERPRINT_ERROR_LOCKOUT = 7; // 0x7
- field public static final int FINGERPRINT_ERROR_LOCKOUT_PERMANENT = 9; // 0x9
- field public static final int FINGERPRINT_ERROR_NO_FINGERPRINTS = 11; // 0xb
- field public static final int FINGERPRINT_ERROR_NO_SPACE = 4; // 0x4
- field public static final int FINGERPRINT_ERROR_TIMEOUT = 3; // 0x3
- field public static final int FINGERPRINT_ERROR_UNABLE_TO_PROCESS = 2; // 0x2
- field public static final int FINGERPRINT_ERROR_USER_CANCELED = 10; // 0xa
- field public static final int FINGERPRINT_ERROR_VENDOR = 8; // 0x8
- }
-
- @Deprecated public abstract static class FingerprintManager.AuthenticationCallback {
- ctor public FingerprintManager.AuthenticationCallback();
- method public void onAuthenticationError(int, CharSequence);
- method public void onAuthenticationFailed();
- method public void onAuthenticationHelp(int, CharSequence);
- method public void onAuthenticationSucceeded(android.hardware.fingerprint.FingerprintManager.AuthenticationResult);
- }
-
- @Deprecated public static class FingerprintManager.AuthenticationResult {
- method public android.hardware.fingerprint.FingerprintManager.CryptoObject getCryptoObject();
- }
-
- @Deprecated public static final class FingerprintManager.CryptoObject {
- ctor public FingerprintManager.CryptoObject(@NonNull java.security.Signature);
- ctor public FingerprintManager.CryptoObject(@NonNull javax.crypto.Cipher);
- ctor public FingerprintManager.CryptoObject(@NonNull javax.crypto.Mac);
- method public javax.crypto.Cipher getCipher();
- method public javax.crypto.Mac getMac();
- method public java.security.Signature getSignature();
- }
-
-}
-
package android.media {
public final class AudioFormat implements android.os.Parcelable {
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index 8ceda62..5ead3e1 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -598,7 +598,6 @@
field public static final int FOREGROUND_SERVICE_API_TYPE_MICROPHONE = 6; // 0x6
field public static final int FOREGROUND_SERVICE_API_TYPE_PHONE_CALL = 7; // 0x7
field public static final int FOREGROUND_SERVICE_API_TYPE_USB = 8; // 0x8
- field @FlaggedApi("android.media.audio.foreground_audio_control") public static final int PROCESS_CAPABILITY_FOREGROUND_AUDIO_CONTROL = 64; // 0x40
field public static final int PROCESS_CAPABILITY_FOREGROUND_CAMERA = 2; // 0x2
field public static final int PROCESS_CAPABILITY_FOREGROUND_LOCATION = 1; // 0x1
field public static final int PROCESS_CAPABILITY_FOREGROUND_MICROPHONE = 4; // 0x4
@@ -3797,7 +3796,6 @@
field @FlaggedApi("android.app.ondeviceintelligence.flags.enable_on_device_intelligence") public static final String ON_DEVICE_INTELLIGENCE_SERVICE = "on_device_intelligence";
field public static final String PERMISSION_CONTROLLER_SERVICE = "permission_controller";
field public static final String PERMISSION_SERVICE = "permission";
- field public static final String PERSISTENT_DATA_BLOCK_SERVICE = "persistent_data_block";
field public static final String REBOOT_READINESS_SERVICE = "reboot_readiness";
field public static final String ROLLBACK_SERVICE = "rollback";
field public static final String SAFETY_CENTER_SERVICE = "safety_center";
@@ -4356,7 +4354,7 @@
field @Deprecated public static final int INTENT_FILTER_VERIFICATION_SUCCESS = 1; // 0x1
field @Deprecated public static final int MASK_PERMISSION_FLAGS = 255; // 0xff
field public static final int MATCH_ANY_USER = 4194304; // 0x400000
- field public static final int MATCH_CLONE_PROFILE = 536870912; // 0x20000000
+ field @Deprecated public static final int MATCH_CLONE_PROFILE = 536870912; // 0x20000000
field @FlaggedApi("android.content.pm.fix_duplicated_flags") public static final long MATCH_CLONE_PROFILE_LONG = 17179869184L; // 0x400000000L
field public static final int MATCH_FACTORY_ONLY = 2097152; // 0x200000
field public static final int MATCH_HIDDEN_UNTIL_INSTALLED_COMPONENTS = 536870912; // 0x20000000
diff --git a/core/api/test-current.txt b/core/api/test-current.txt
index 0a26490..a76aa67 100644
--- a/core/api/test-current.txt
+++ b/core/api/test-current.txt
@@ -1721,6 +1721,15 @@
}
+package android.hardware.fingerprint {
+
+ @Deprecated public class FingerprintManager {
+ method @Deprecated @NonNull @RequiresPermission(android.Manifest.permission.TEST_BIOMETRIC) public android.hardware.biometrics.BiometricTestSession createTestSession(int);
+ method @Deprecated @NonNull @RequiresPermission(android.Manifest.permission.TEST_BIOMETRIC) public java.util.List<android.hardware.biometrics.SensorProperties> getSensorProperties();
+ }
+
+}
+
package android.hardware.hdmi {
public final class HdmiControlServiceWrapper {
@@ -2460,6 +2469,7 @@
}
public class UserManager {
+ method @FlaggedApi("android.os.allow_private_profile") @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.CREATE_USERS}, conditional=true) public boolean canAddPrivateProfile();
method @Nullable @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.CREATE_USERS}) public android.content.pm.UserInfo createProfileForUser(@Nullable String, @NonNull String, int, int, @Nullable String[]);
method @Nullable @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.CREATE_USERS}) public android.content.pm.UserInfo createRestrictedProfile(@Nullable String);
method @Nullable @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.CREATE_USERS}) public android.content.pm.UserInfo createUser(@Nullable String, @NonNull String, int);
diff --git a/core/api/test-removed.txt b/core/api/test-removed.txt
index 2e44176..d802177 100644
--- a/core/api/test-removed.txt
+++ b/core/api/test-removed.txt
@@ -1,10 +1 @@
// Signature format: 2.0
-package android.hardware.fingerprint {
-
- @Deprecated public class FingerprintManager {
- method @NonNull @RequiresPermission(android.Manifest.permission.TEST_BIOMETRIC) public android.hardware.biometrics.BiometricTestSession createTestSession(int);
- method @NonNull @RequiresPermission(android.Manifest.permission.TEST_BIOMETRIC) public java.util.List<android.hardware.biometrics.SensorProperties> getSensorProperties();
- }
-
-}
-
diff --git a/core/java/android/app/ActivityManager.java b/core/java/android/app/ActivityManager.java
index fae4348..0c54351 100644
--- a/core/java/android/app/ActivityManager.java
+++ b/core/java/android/app/ActivityManager.java
@@ -20,7 +20,6 @@
import static android.app.WindowConfiguration.windowingModeToString;
import static android.content.Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS;
import static android.content.pm.ActivityInfo.RESIZE_MODE_RESIZEABLE;
-import static android.media.audio.Flags.FLAG_FOREGROUND_AUDIO_CONTROL;
import android.Manifest;
import android.annotation.ColorInt;
@@ -948,8 +947,6 @@
* @hide
* Process can access volume APIs and can request audio focus with GAIN.
*/
- @FlaggedApi(FLAG_FOREGROUND_AUDIO_CONTROL)
- @SystemApi
public static final int PROCESS_CAPABILITY_FOREGROUND_AUDIO_CONTROL = 1 << 6;
/**
diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java
index ae5cacd..fa9346e 100644
--- a/core/java/android/app/ActivityThread.java
+++ b/core/java/android/app/ActivityThread.java
@@ -712,16 +712,22 @@
stopped = false;
hideForNow = false;
activityConfigCallback = new ViewRootImpl.ActivityConfigCallback() {
+
@Override
- public void onConfigurationChanged(Configuration overrideConfig,
- int newDisplayId) {
+ public void onConfigurationChanged(@NonNull Configuration overrideConfig,
+ int newDisplayId, @Nullable ActivityWindowInfo activityWindowInfo) {
if (activity == null) {
throw new IllegalStateException(
"Received config update for non-existing activity");
}
+ if (activityWindowInfoFlag() && activityWindowInfo == null) {
+ Log.w(TAG, "Received empty ActivityWindowInfo update for r=" + activity);
+ activityWindowInfo = mActivityWindowInfo;
+ }
activity.mMainThread.handleActivityConfigurationChanged(
ActivityClientRecord.this, overrideConfig, newDisplayId,
- mActivityWindowInfo, false /* alwaysReportChange */);
+ activityWindowInfo,
+ false /* alwaysReportChange */);
}
@Override
diff --git a/core/java/android/app/AppOpsManager.java b/core/java/android/app/AppOpsManager.java
index a8352fa..ff713d0 100644
--- a/core/java/android/app/AppOpsManager.java
+++ b/core/java/android/app/AppOpsManager.java
@@ -1581,6 +1581,10 @@
* Allows an app to access location without the traditional location permissions and while the
* user location setting is off, but only during pre-defined emergency sessions.
*
+ * <p>This op is only used for tracking, not for permissions, so it is still the client's
+ * responsibility to check the {@link Manifest.permission.LOCATION_BYPASS} permission
+ * appropriately.
+ *
* @hide
*/
public static final int OP_EMERGENCY_LOCATION = AppProtoEnums.APP_OP_EMERGENCY_LOCATION;
@@ -2459,6 +2463,10 @@
* Allows an app to access location without the traditional location permissions and while the
* user location setting is off, but only during pre-defined emergency sessions.
*
+ * <p>This op is only used for tracking, not for permissions, so it is still the client's
+ * responsibility to check the {@link Manifest.permission.LOCATION_BYPASS} permission
+ * appropriately.
+ *
* @hide
*/
@SystemApi
@@ -3047,8 +3055,10 @@
new AppOpInfo.Builder(OP_UNARCHIVAL_CONFIRMATION, OPSTR_UNARCHIVAL_CONFIRMATION,
"UNARCHIVAL_CONFIRMATION")
.setDefaultMode(MODE_ALLOWED).build(),
- // TODO(b/301150056): STOPSHIP determine how this appop should work with the permission
new AppOpInfo.Builder(OP_EMERGENCY_LOCATION, OPSTR_EMERGENCY_LOCATION, "EMERGENCY_LOCATION")
+ .setDefaultMode(MODE_ALLOWED)
+ // even though this has a permission associated, this op is only used for tracking,
+ // and the client is responsible for checking the LOCATION_BYPASS permission.
.setPermission(Manifest.permission.LOCATION_BYPASS).build(),
};
diff --git a/core/java/android/app/ApplicationExitInfo.java b/core/java/android/app/ApplicationExitInfo.java
index 24cb9ea..cac10f5 100644
--- a/core/java/android/app/ApplicationExitInfo.java
+++ b/core/java/android/app/ApplicationExitInfo.java
@@ -487,6 +487,15 @@
*/
public static final int SUBREASON_FREEZER_BINDER_ASYNC_FULL = 31;
+ /**
+ * The process was killed because it was sending too many broadcasts while it is in the
+ * Cached state. This would be set only when the reason is {@link #REASON_OTHER}.
+ *
+ * For internal use only.
+ * @hide
+ */
+ public static final int SUBREASON_EXCESSIVE_OUTGOING_BROADCASTS_WHILE_CACHED = 32;
+
// If there is any OEM code which involves additional app kill reasons, it should
// be categorized in {@link #REASON_OTHER}, with subreason code starting from 1000.
@@ -665,6 +674,7 @@
SUBREASON_EXCESSIVE_BINDER_OBJECTS,
SUBREASON_OOM_KILL,
SUBREASON_FREEZER_BINDER_ASYNC_FULL,
+ SUBREASON_EXCESSIVE_OUTGOING_BROADCASTS_WHILE_CACHED,
})
@Retention(RetentionPolicy.SOURCE)
public @interface SubReason {}
@@ -1396,6 +1406,8 @@
return "OOM KILL";
case SUBREASON_FREEZER_BINDER_ASYNC_FULL:
return "FREEZER BINDER ASYNC FULL";
+ case SUBREASON_EXCESSIVE_OUTGOING_BROADCASTS_WHILE_CACHED:
+ return "EXCESSIVE_OUTGOING_BROADCASTS_WHILE_CACHED";
default:
return "UNKNOWN";
}
diff --git a/core/java/android/app/ContextImpl.java b/core/java/android/app/ContextImpl.java
index 6f6e091..716dee4 100644
--- a/core/java/android/app/ContextImpl.java
+++ b/core/java/android/app/ContextImpl.java
@@ -344,23 +344,37 @@
*/
private boolean mOwnsToken = false;
- private final Object mDirsLock = new Object();
- @GuardedBy("mDirsLock")
+ private final Object mDatabasesDirLock = new Object();
+ @GuardedBy("mDatabasesDirLock")
private File mDatabasesDir;
- @GuardedBy("mDirsLock")
+
+ private final Object mPreferencesDirLock = new Object();
@UnsupportedAppUsage
+ @GuardedBy("mPreferencesDirLock")
private File mPreferencesDir;
- @GuardedBy("mDirsLock")
+
+ private final Object mFilesDirLock = new Object();
+ @GuardedBy("mFilesDirLock")
private File mFilesDir;
- @GuardedBy("mDirsLock")
+
+ private final Object mCratesDirLock = new Object();
+ @GuardedBy("mCratesDirLock")
private File mCratesDir;
- @GuardedBy("mDirsLock")
+
+ private final Object mNoBackupFilesDirLock = new Object();
+ @GuardedBy("mNoBackupFilesDirLock")
private File mNoBackupFilesDir;
- @GuardedBy("mDirsLock")
+
+ private final Object mCacheDirLock = new Object();
+ @GuardedBy("mCacheDirLock")
private File mCacheDir;
- @GuardedBy("mDirsLock")
+
+ private final Object mCodeCacheDirLock = new Object();
+ @GuardedBy("mCodeCacheDirLock")
private File mCodeCacheDir;
+ private final Object mMiscDirsLock = new Object();
+
// The system service cache for the system services that are cached per-ContextImpl.
@UnsupportedAppUsage
final Object[] mServiceCache = SystemServiceRegistry.createServiceCache();
@@ -742,7 +756,7 @@
@UnsupportedAppUsage
private File getPreferencesDir() {
- synchronized (mDirsLock) {
+ synchronized (mPreferencesDirLock) {
if (mPreferencesDir == null) {
mPreferencesDir = new File(getDataDir(), "shared_prefs");
}
@@ -831,7 +845,7 @@
@Override
public File getFilesDir() {
- synchronized (mDirsLock) {
+ synchronized (mFilesDirLock) {
if (mFilesDir == null) {
mFilesDir = new File(getDataDir(), "files");
}
@@ -846,7 +860,7 @@
final Path absoluteNormalizedCratePath = cratesRootPath.resolve(crateId)
.toAbsolutePath().normalize();
- synchronized (mDirsLock) {
+ synchronized (mCratesDirLock) {
if (mCratesDir == null) {
mCratesDir = cratesRootPath.toFile();
}
@@ -859,7 +873,7 @@
@Override
public File getNoBackupFilesDir() {
- synchronized (mDirsLock) {
+ synchronized (mNoBackupFilesDirLock) {
if (mNoBackupFilesDir == null) {
mNoBackupFilesDir = new File(getDataDir(), "no_backup");
}
@@ -876,7 +890,7 @@
@Override
public File[] getExternalFilesDirs(String type) {
- synchronized (mDirsLock) {
+ synchronized (mMiscDirsLock) {
File[] dirs = Environment.buildExternalStorageAppFilesDirs(getPackageName());
if (type != null) {
dirs = Environment.buildPaths(dirs, type);
@@ -894,7 +908,7 @@
@Override
public File[] getObbDirs() {
- synchronized (mDirsLock) {
+ synchronized (mMiscDirsLock) {
File[] dirs = Environment.buildExternalStorageAppObbDirs(getPackageName());
return ensureExternalDirsExistOrFilter(dirs, true /* tryCreateInProcess */);
}
@@ -902,7 +916,7 @@
@Override
public File getCacheDir() {
- synchronized (mDirsLock) {
+ synchronized (mCacheDirLock) {
if (mCacheDir == null) {
mCacheDir = new File(getDataDir(), "cache");
}
@@ -912,7 +926,7 @@
@Override
public File getCodeCacheDir() {
- synchronized (mDirsLock) {
+ synchronized (mCodeCacheDirLock) {
if (mCodeCacheDir == null) {
mCodeCacheDir = getCodeCacheDirBeforeBind(getDataDir());
}
@@ -938,7 +952,7 @@
@Override
public File[] getExternalCacheDirs() {
- synchronized (mDirsLock) {
+ synchronized (mMiscDirsLock) {
File[] dirs = Environment.buildExternalStorageAppCacheDirs(getPackageName());
// We don't try to create cache directories in-process, because they need special
// setup for accurate quota tracking. This ensures the cache dirs are always
@@ -949,7 +963,7 @@
@Override
public File[] getExternalMediaDirs() {
- synchronized (mDirsLock) {
+ synchronized (mMiscDirsLock) {
File[] dirs = Environment.buildExternalStorageAppMediaDirs(getPackageName());
return ensureExternalDirsExistOrFilter(dirs, true /* tryCreateInProcess */);
}
@@ -1051,7 +1065,7 @@
}
private File getDatabasesDir() {
- synchronized (mDirsLock) {
+ synchronized (mDatabasesDirLock) {
if (mDatabasesDir == null) {
if ("android".equals(getPackageName())) {
mDatabasesDir = new File("/data/system");
diff --git a/core/java/android/app/SystemServiceRegistry.java b/core/java/android/app/SystemServiceRegistry.java
index 1cbec31..66ec865 100644
--- a/core/java/android/app/SystemServiceRegistry.java
+++ b/core/java/android/app/SystemServiceRegistry.java
@@ -450,6 +450,11 @@
new CachedServiceFetcher<VcnManager>() {
@Override
public VcnManager createService(ContextImpl ctx) throws ServiceNotFoundException {
+ if (!ctx.getPackageManager().hasSystemFeature(
+ PackageManager.FEATURE_TELEPHONY_SUBSCRIPTION)) {
+ return null;
+ }
+
IBinder b = ServiceManager.getService(Context.VCN_MANAGEMENT_SERVICE);
IVcnManagementService service = IVcnManagementService.Stub.asInterface(b);
return new VcnManager(ctx, service);
@@ -1736,6 +1741,13 @@
return fetcher;
}
+ private static boolean hasSystemFeatureOpportunistic(@NonNull ContextImpl ctx,
+ @NonNull String featureName) {
+ PackageManager manager = ctx.getPackageManager();
+ if (manager == null) return true;
+ return manager.hasSystemFeature(featureName);
+ }
+
/**
* Gets a system service from a given context.
* @hide
@@ -1758,12 +1770,18 @@
case Context.VIRTUALIZATION_SERVICE:
case Context.VIRTUAL_DEVICE_SERVICE:
return null;
- case Context.SEARCH_SERVICE:
- // Wear device does not support SEARCH_SERVICE so we do not print WTF here
- PackageManager manager = ctx.getPackageManager();
- if (manager != null && manager.hasSystemFeature(PackageManager.FEATURE_WATCH)) {
+ case Context.VCN_MANAGEMENT_SERVICE:
+ if (!hasSystemFeatureOpportunistic(ctx,
+ PackageManager.FEATURE_TELEPHONY_SUBSCRIPTION)) {
return null;
}
+ break;
+ case Context.SEARCH_SERVICE:
+ // Wear device does not support SEARCH_SERVICE so we do not print WTF here
+ if (hasSystemFeatureOpportunistic(ctx, PackageManager.FEATURE_WATCH)) {
+ return null;
+ }
+ break;
}
Slog.wtf(TAG, "Manager wrapper not available: " + name);
return null;
diff --git a/core/java/android/app/activity_manager.aconfig b/core/java/android/app/activity_manager.aconfig
index 350b1ed..b9aa18c 100644
--- a/core/java/android/app/activity_manager.aconfig
+++ b/core/java/android/app/activity_manager.aconfig
@@ -3,6 +3,7 @@
flag {
namespace: "system_performance"
name: "app_start_info"
+ is_exported: true
description: "Control collecting of ApplicationStartInfo records and APIs."
bug: "247814855"
}
@@ -10,6 +11,7 @@
flag {
namespace: "backstage_power"
name: "get_binding_uid_importance"
+ is_exported: true
description: "API to get importance of UID that's binding to the caller"
bug: "292533010"
}
@@ -17,6 +19,7 @@
flag {
namespace: "backstage_power"
name: "app_restrictions_api"
+ is_exported: true
description: "API to track and query restrictions applied to apps"
bug: "320150834"
}
@@ -24,6 +27,7 @@
flag {
namespace: "backstage_power"
name: "uid_importance_listener_for_uids"
+ is_exported: true
description: "API to add OnUidImportanceListener with targetted UIDs"
bug: "286258140"
}
@@ -31,12 +35,14 @@
flag {
namespace: "backstage_power"
name: "introduce_new_service_ontimeout_callback"
+ is_exported: true
description: "Add a new callback in Service to indicate a FGS has reached its timeout."
bug: "317799821"
}
flag {
name: "bcast_event_timestamps"
+ is_exported: true
namespace: "backstage_power"
description: "Add APIs for clients to provide broadcast event trigger timestamps"
bug: "325136414"
diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java
index a075ac5..60dffbd 100644
--- a/core/java/android/app/admin/DevicePolicyManager.java
+++ b/core/java/android/app/admin/DevicePolicyManager.java
@@ -6545,8 +6545,10 @@
}
/**
- * Flag for {@link #wipeData(int)}: also erase the device's external
- * storage (such as SD cards).
+ * Flag for {@link #wipeData(int)}: also erase the device's adopted external storage (such as
+ * adopted SD cards).
+ * @see <a href="{@docRoot}about/versions/marshmallow/android-6.0.html#adoptable-storage">
+ * Adoptable Storage Devices</a>
*/
public static final int WIPE_EXTERNAL_STORAGE = 0x0001;
diff --git a/core/java/android/app/admin/flags/flags.aconfig b/core/java/android/app/admin/flags/flags.aconfig
index 441d521..4fa45be 100644
--- a/core/java/android/app/admin/flags/flags.aconfig
+++ b/core/java/android/app/admin/flags/flags.aconfig
@@ -1,7 +1,11 @@
+# proto-file: build/make/tools/aconfig/aconfig_protos/protos/aconfig.proto
+# proto-message: flag_declarations
+
package: "android.app.admin.flags"
flag {
name: "policy_engine_migration_v2_enabled"
+ is_exported: true
namespace: "enterprise"
description: "V2 of the policy engine migrations for Android V"
bug: "289520697"
@@ -9,6 +13,7 @@
flag {
name: "device_policy_size_tracking_enabled"
+ is_exported: true
namespace: "enterprise"
description: "Add feature to track the total policy size and have a max threshold - public API changes"
bug: "281543351"
@@ -23,6 +28,7 @@
flag {
name: "onboarding_bugreport_v2_enabled"
+ is_exported: true
namespace: "enterprise"
description: "Add feature to track required changes for enabled V2 of auto-capturing of onboarding bug reports."
bug: "302517677"
@@ -44,6 +50,7 @@
flag {
name: "dedicated_device_control_api_enabled"
+ is_exported: true
namespace: "enterprise"
description: "(API) Allow the device management role holder to control which platform features are available on dedicated devices."
bug: "281964214"
@@ -51,6 +58,7 @@
flag {
name: "permission_migration_for_zero_trust_api_enabled"
+ is_exported: true
namespace: "enterprise"
description: "(API) Migrate existing APIs to permission based, and enable DMRH to call them to collect Zero Trust signals."
bug: "289520697"
@@ -65,6 +73,7 @@
flag {
name: "device_theft_api_enabled"
+ is_exported: true
namespace: "enterprise"
description: "Add new API for theft detection."
bug: "325073410"
@@ -86,6 +95,7 @@
flag {
name: "security_log_v2_enabled"
+ is_exported: true
namespace: "enterprise"
description: "Improve access to security logging in the context of Zero Trust."
bug: "295324350"
@@ -100,6 +110,7 @@
flag {
name: "allow_querying_profile_type"
+ is_exported: true
namespace: "enterprise"
description: "Public APIs to query if a user is a profile and what kind of profile type it is."
bug: "323001115"
@@ -114,6 +125,7 @@
flag {
name: "assist_content_user_restriction_enabled"
+ is_exported: true
namespace: "enterprise"
description: "Prevent work data leakage by sending assist content to privileged apps."
bug: "322975406"
@@ -131,6 +143,7 @@
flag {
name: "backup_service_security_log_event_enabled"
+ is_exported: true
namespace: "enterprise"
description: "Emit a security log event when DPM.setBackupServiceEnabled is called"
bug: "304999634"
@@ -138,6 +151,7 @@
flag {
name: "esim_management_enabled"
+ is_exported: true
namespace: "enterprise"
description: "Enable APIs to provision and manage eSIMs"
bug: "295301164"
@@ -145,6 +159,7 @@
flag {
name: "headless_device_owner_single_user_enabled"
+ is_exported: true
namespace: "enterprise"
description: "Add Headless DO support."
bug: "289515470"
@@ -152,6 +167,7 @@
flag {
name: "is_mte_policy_enforced"
+ is_exported: true
namespace: "enterprise"
description: "Allow to query whether MTE is enabled or not to check for compliance for enterprise policy"
bug: "322777918"
@@ -180,3 +196,10 @@
description: "Allow COPE admin to control screen brightness and timeout."
bug: "323894620"
}
+
+flag {
+ name: "is_recursive_required_app_merging_enabled"
+ namespace: "enterprise"
+ description: "Guards a new flow for recursive required enterprise app list merging"
+ bug: "319084618"
+}
diff --git a/core/java/android/app/background_install_control_manager.aconfig b/core/java/android/app/background_install_control_manager.aconfig
index 4473b95..5f3bb07 100644
--- a/core/java/android/app/background_install_control_manager.aconfig
+++ b/core/java/android/app/background_install_control_manager.aconfig
@@ -3,6 +3,7 @@
flag {
namespace: "preload_safety"
name: "bic_client"
+ is_exported: true
description: "System API for background install control."
is_fixed_read_only: true
bug: "287507984"
diff --git a/core/java/android/app/grammatical_inflection_manager.aconfig b/core/java/android/app/grammatical_inflection_manager.aconfig
index 68d12ba..0d7bf65 100644
--- a/core/java/android/app/grammatical_inflection_manager.aconfig
+++ b/core/java/android/app/grammatical_inflection_manager.aconfig
@@ -2,6 +2,7 @@
flag {
name: "system_terms_of_address_enabled"
+ is_exported: true
namespace: "globalintl"
description: "Feature flag for System Terms of Address"
bug: "297798866"
diff --git a/core/java/android/app/multitasking.aconfig b/core/java/android/app/multitasking.aconfig
index ab00891..dbf3173 100644
--- a/core/java/android/app/multitasking.aconfig
+++ b/core/java/android/app/multitasking.aconfig
@@ -2,6 +2,7 @@
flag {
name: "enable_pip_ui_state_callback_on_entering"
+ is_exported: true
namespace: "multitasking"
description: "Enables PiP UI state callback on entering"
bug: "303718131"
diff --git a/core/java/android/app/notification.aconfig b/core/java/android/app/notification.aconfig
index 274d02a..e9a7460 100644
--- a/core/java/android/app/notification.aconfig
+++ b/core/java/android/app/notification.aconfig
@@ -2,6 +2,7 @@
flag {
name: "modes_api"
+ is_exported: true
namespace: "systemui"
description: "This flag controls new and updated DND apis"
bug: "300477976"
@@ -16,6 +17,7 @@
flag {
name: "api_tvextender"
+ is_exported: true
namespace: "systemui"
description: "Guards new android.app.Notification.TvExtender api"
bug: "308164892"
@@ -24,6 +26,7 @@
flag {
name: "lifetime_extension_refactor"
+ is_exported: true
namespace: "systemui"
description: "Enables moving notification lifetime extension management from SystemUI to "
"Notification Manager Service"
@@ -46,6 +49,7 @@
flag {
name: "category_voicemail"
+ is_exported: true
namespace: "wear_sysui"
description: "Adds a new voicemail category for notifications"
bug: "322806700"
@@ -53,6 +57,7 @@
flag {
name: "notification_channel_vibration_effect_api"
+ is_exported: true
namespace: "systemui"
description: "This flag enables the API to allow setting VibrationEffect for NotificationChannels"
bug: "241732519"
diff --git a/core/java/android/app/ondeviceintelligence/IOnDeviceIntelligenceManager.aidl b/core/java/android/app/ondeviceintelligence/IOnDeviceIntelligenceManager.aidl
index 0dbe181..8bf288a 100644
--- a/core/java/android/app/ondeviceintelligence/IOnDeviceIntelligenceManager.aidl
+++ b/core/java/android/app/ondeviceintelligence/IOnDeviceIntelligenceManager.aidl
@@ -53,19 +53,22 @@
void getFeatureDetails(in Feature feature, in IFeatureDetailsCallback featureDetailsCallback) = 4;
@JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.USE_ON_DEVICE_INTELLIGENCE)")
- void requestFeatureDownload(in Feature feature, in ICancellationSignal signal, in IDownloadCallback callback) = 5;
+ void requestFeatureDownload(in Feature feature, in AndroidFuture cancellationSignalFuture, in IDownloadCallback callback) = 5;
@JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.USE_ON_DEVICE_INTELLIGENCE)")
- void requestTokenInfo(in Feature feature, in Bundle requestBundle, in ICancellationSignal signal,
+ void requestTokenInfo(in Feature feature, in Bundle requestBundle, in AndroidFuture cancellationSignalFuture,
in ITokenInfoCallback tokenInfocallback) = 6;
@JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.USE_ON_DEVICE_INTELLIGENCE)")
- void processRequest(in Feature feature, in Bundle requestBundle, int requestType, in ICancellationSignal cancellationSignal,
- in IProcessingSignal signal, in IResponseCallback responseCallback) = 7;
+ void processRequest(in Feature feature, in Bundle requestBundle, int requestType,
+ in AndroidFuture cancellationSignalFuture,
+ in AndroidFuture processingSignalFuture,
+ in IResponseCallback responseCallback) = 7;
@JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.USE_ON_DEVICE_INTELLIGENCE)")
void processRequestStreaming(in Feature feature,
- in Bundle requestBundle, int requestType, in ICancellationSignal cancellationSignal, in IProcessingSignal signal,
+ in Bundle requestBundle, int requestType, in AndroidFuture cancellationSignalFuture,
+ in AndroidFuture processingSignalFuture,
in IStreamingResponseCallback streamingCallback) = 8;
String getRemoteServicePackageName() = 9;
diff --git a/core/java/android/app/ondeviceintelligence/OnDeviceIntelligenceManager.java b/core/java/android/app/ondeviceintelligence/OnDeviceIntelligenceManager.java
index a465e3c..bc50d2e4 100644
--- a/core/java/android/app/ondeviceintelligence/OnDeviceIntelligenceManager.java
+++ b/core/java/android/app/ondeviceintelligence/OnDeviceIntelligenceManager.java
@@ -26,22 +26,23 @@
import android.annotation.RequiresPermission;
import android.annotation.SystemApi;
import android.annotation.SystemService;
-import android.content.ComponentName;
import android.content.Context;
import android.graphics.Bitmap;
import android.os.Binder;
import android.os.Bundle;
import android.os.CancellationSignal;
+import android.os.IBinder;
import android.os.ICancellationSignal;
import android.os.OutcomeReceiver;
import android.os.PersistableBundle;
import android.os.RemoteCallback;
import android.os.RemoteException;
import android.system.OsConstants;
+import android.util.Log;
import androidx.annotation.IntDef;
-import com.android.internal.R;
+import com.android.internal.infra.AndroidFuture;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
@@ -76,6 +77,8 @@
*/
public static final String AUGMENT_REQUEST_CONTENT_BUNDLE_KEY =
"AugmentRequestContentBundleKey";
+
+ private static final String TAG = "OnDeviceIntelligence";
private final Context mContext;
private final IOnDeviceIntelligenceManager mService;
@@ -121,9 +124,9 @@
@RequiresPermission(Manifest.permission.USE_ON_DEVICE_INTELLIGENCE)
public String getRemoteServicePackageName() {
String result;
- try{
- result = mService.getRemoteServicePackageName();
- } catch (RemoteException e){
+ try {
+ result = mService.getRemoteServicePackageName();
+ } catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
return result;
@@ -288,18 +291,15 @@
}
};
- ICancellationSignal transport = null;
- if (cancellationSignal != null) {
- transport = CancellationSignal.createTransport();
- cancellationSignal.setRemote(transport);
- }
-
- mService.requestFeatureDownload(feature, transport, downloadCallback);
+ mService.requestFeatureDownload(feature,
+ configureRemoteCancellationFuture(cancellationSignal, callbackExecutor),
+ downloadCallback);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
+
/**
* The methods computes the token related information for a given request payload using the
* provided {@link Feature}.
@@ -337,13 +337,9 @@
}
};
- ICancellationSignal transport = null;
- if (cancellationSignal != null) {
- transport = CancellationSignal.createTransport();
- cancellationSignal.setRemote(transport);
- }
-
- mService.requestTokenInfo(feature, request, transport, callback);
+ mService.requestTokenInfo(feature, request,
+ configureRemoteCancellationFuture(cancellationSignal, callbackExecutor),
+ callback);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -407,19 +403,9 @@
};
- IProcessingSignal transport = null;
- if (processingSignal != null) {
- transport = ProcessingSignal.createTransport();
- processingSignal.setRemote(transport);
- }
-
- ICancellationSignal cancellationTransport = null;
- if (cancellationSignal != null) {
- cancellationTransport = CancellationSignal.createTransport();
- cancellationSignal.setRemote(cancellationTransport);
- }
-
- mService.processRequest(feature, request, requestType, cancellationTransport, transport,
+ mService.processRequest(feature, request, requestType,
+ configureRemoteCancellationFuture(cancellationSignal, callbackExecutor),
+ configureRemoteProcessingSignalFuture(processingSignal, callbackExecutor),
callback);
} catch (RemoteException e) {
@@ -449,7 +435,8 @@
* @param callbackExecutor executor to run the callback on.
*/
@RequiresPermission(Manifest.permission.USE_ON_DEVICE_INTELLIGENCE)
- public void processRequestStreaming(@NonNull Feature feature, @NonNull @InferenceParams Bundle request,
+ public void processRequestStreaming(@NonNull Feature feature,
+ @NonNull @InferenceParams Bundle request,
@RequestType int requestType,
@Nullable CancellationSignal cancellationSignal,
@Nullable ProcessingSignal processingSignal,
@@ -500,20 +487,11 @@
}
};
- IProcessingSignal transport = null;
- if (processingSignal != null) {
- transport = ProcessingSignal.createTransport();
- processingSignal.setRemote(transport);
- }
-
- ICancellationSignal cancellationTransport = null;
- if (cancellationSignal != null) {
- cancellationTransport = CancellationSignal.createTransport();
- cancellationSignal.setRemote(cancellationTransport);
- }
-
mService.processRequestStreaming(
- feature, request, requestType, cancellationTransport, transport, callback);
+ feature, request, requestType,
+ configureRemoteCancellationFuture(cancellationSignal, callbackExecutor),
+ configureRemoteProcessingSignalFuture(processingSignal, callbackExecutor),
+ callback);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -574,4 +552,45 @@
@Target({ElementType.PARAMETER, ElementType.FIELD})
public @interface InferenceParams {
}
+
+
+ @Nullable
+ private static AndroidFuture<IBinder> configureRemoteCancellationFuture(
+ @Nullable CancellationSignal cancellationSignal,
+ @NonNull Executor callbackExecutor) {
+ if (cancellationSignal == null) {
+ return null;
+ }
+ AndroidFuture<IBinder> cancellationFuture = new AndroidFuture<>();
+ cancellationFuture.whenCompleteAsync(
+ (cancellationTransport, error) -> {
+ if (error != null || cancellationTransport == null) {
+ Log.e(TAG, "Unable to receive the remote cancellation signal.", error);
+ } else {
+ cancellationSignal.setRemote(
+ ICancellationSignal.Stub.asInterface(cancellationTransport));
+ }
+ }, callbackExecutor);
+ return cancellationFuture;
+ }
+
+ @Nullable
+ private static AndroidFuture<IBinder> configureRemoteProcessingSignalFuture(
+ ProcessingSignal processingSignal, Executor executor) {
+ if (processingSignal == null) {
+ return null;
+ }
+ AndroidFuture<IBinder> processingSignalFuture = new AndroidFuture<>();
+ processingSignalFuture.whenCompleteAsync(
+ (transport, error) -> {
+ if (error != null || transport == null) {
+ Log.e(TAG, "Unable to receive the remote processing signal.", error);
+ } else {
+ processingSignal.setRemote(IProcessingSignal.Stub.asInterface(transport));
+ }
+ }, executor);
+ return processingSignalFuture;
+ }
+
+
}
diff --git a/core/java/android/app/ondeviceintelligence/ProcessingSignal.java b/core/java/android/app/ondeviceintelligence/ProcessingSignal.java
index c275cc7..733f4fa 100644
--- a/core/java/android/app/ondeviceintelligence/ProcessingSignal.java
+++ b/core/java/android/app/ondeviceintelligence/ProcessingSignal.java
@@ -123,10 +123,10 @@
* Sets the processing signal callback to be called when signals are received.
*
* This method is intended to be used by the recipient of a processing signal
- * such as the remote implementation for {@link OnDeviceIntelligenceManager} to handle
- * cancellation requests while performing a long-running operation. This method is not
- * intended
- * to be used by applications themselves.
+ * such as the remote implementation in
+ * {@link android.service.ondeviceintelligence.OnDeviceSandboxedInferenceService} to handle
+ * processing signals while performing a long-running operation. This method is not
+ * intended to be used by the caller themselves.
*
* If {@link ProcessingSignal#sendSignal} has already been called, then the provided callback
* is invoked immediately and all previously queued actions are passed to remote signal.
@@ -200,7 +200,7 @@
}
/**
- * Given a locally created transport, returns its associated cancellation signal.
+ * Given a locally created transport, returns its associated processing signal.
*
* @param transport The locally created transport, or null if none.
* @return The associated processing signal, or null if none.
diff --git a/core/java/android/app/ondeviceintelligence/flags/ondevice_intelligence.aconfig b/core/java/android/app/ondeviceintelligence/flags/ondevice_intelligence.aconfig
index 44f3329..dd9210f 100644
--- a/core/java/android/app/ondeviceintelligence/flags/ondevice_intelligence.aconfig
+++ b/core/java/android/app/ondeviceintelligence/flags/ondevice_intelligence.aconfig
@@ -2,6 +2,7 @@
flag {
name: "enable_on_device_intelligence"
+ is_exported: true
namespace: "ondeviceintelligence"
description: "Make methods on OnDeviceIntelligenceManager available for local inference."
bug: "304755128"
diff --git a/core/java/android/app/pinner-client.aconfig b/core/java/android/app/pinner-client.aconfig
index b60ad9e..0f7fa14 100644
--- a/core/java/android/app/pinner-client.aconfig
+++ b/core/java/android/app/pinner-client.aconfig
@@ -3,6 +3,7 @@
flag {
namespace: "system_performance"
name: "pinner_service_client_api"
+ is_exported: true
description: "Control exposing PinnerService APIs."
bug: "307594624"
}
\ No newline at end of file
diff --git a/core/java/android/app/servertransaction/WindowStateResizeItem.java b/core/java/android/app/servertransaction/WindowStateResizeItem.java
index fedffe1..1817c5e 100644
--- a/core/java/android/app/servertransaction/WindowStateResizeItem.java
+++ b/core/java/android/app/servertransaction/WindowStateResizeItem.java
@@ -25,6 +25,7 @@
import android.app.ActivityThread;
import android.app.ClientTransactionHandler;
import android.content.Context;
+import android.os.IBinder;
import android.os.Parcel;
import android.os.RemoteException;
import android.os.Trace;
@@ -32,6 +33,7 @@
import android.util.MergedConfiguration;
import android.view.IWindow;
import android.view.InsetsState;
+import android.window.ActivityWindowInfo;
import android.window.ClientWindowFrames;
import java.util.Objects;
@@ -55,6 +57,14 @@
private int mSyncSeqId;
private boolean mDragResizing;
+ /** {@code null} if this is not an Activity window. */
+ @Nullable
+ private IBinder mActivityToken;
+
+ /** {@code null} if this is not an Activity window. */
+ @Nullable
+ private ActivityWindowInfo mActivityWindowInfo;
+
@Override
public void execute(@NonNull ClientTransactionHandler client,
@NonNull PendingTransactionActions pendingActions) {
@@ -65,7 +75,8 @@
}
try {
mWindow.resized(mFrames, mReportDraw, mConfiguration, mInsetsState, mForceLayout,
- mAlwaysConsumeSystemBars, mDisplayId, mSyncSeqId, mDragResizing);
+ mAlwaysConsumeSystemBars, mDisplayId, mSyncSeqId, mDragResizing,
+ mActivityWindowInfo);
} catch (RemoteException e) {
// Should be a local call.
// An exception could happen if the process is restarted. It is safe to ignore since
@@ -78,6 +89,7 @@
@Nullable
@Override
public Context getContextToUpdate(@NonNull ClientTransactionHandler client) {
+ // TODO(b/260873529): dispatch for mActivityToken as well.
// WindowStateResizeItem may update the global config with #mConfiguration.
return ActivityThread.currentApplication();
}
@@ -91,7 +103,8 @@
@NonNull ClientWindowFrames frames, boolean reportDraw,
@NonNull MergedConfiguration configuration, @NonNull InsetsState insetsState,
boolean forceLayout, boolean alwaysConsumeSystemBars, int displayId, int syncSeqId,
- boolean dragResizing) {
+ boolean dragResizing, @Nullable IBinder activityToken,
+ @Nullable ActivityWindowInfo activityWindowInfo) {
WindowStateResizeItem instance =
ObjectPool.obtain(WindowStateResizeItem.class);
if (instance == null) {
@@ -107,6 +120,10 @@
instance.mDisplayId = displayId;
instance.mSyncSeqId = syncSeqId;
instance.mDragResizing = dragResizing;
+ instance.mActivityToken = activityToken;
+ instance.mActivityWindowInfo = activityWindowInfo != null
+ ? new ActivityWindowInfo(activityWindowInfo)
+ : null;
return instance;
}
@@ -123,6 +140,8 @@
mDisplayId = INVALID_DISPLAY;
mSyncSeqId = -1;
mDragResizing = false;
+ mActivityToken = null;
+ mActivityWindowInfo = null;
ObjectPool.recycle(this);
}
@@ -141,6 +160,8 @@
dest.writeInt(mDisplayId);
dest.writeInt(mSyncSeqId);
dest.writeBoolean(mDragResizing);
+ dest.writeStrongBinder(mActivityToken);
+ dest.writeTypedObject(mActivityWindowInfo, flags);
}
/** Reads from Parcel. */
@@ -155,6 +176,8 @@
mDisplayId = in.readInt();
mSyncSeqId = in.readInt();
mDragResizing = in.readBoolean();
+ mActivityToken = in.readStrongBinder();
+ mActivityWindowInfo = in.readTypedObject(ActivityWindowInfo.CREATOR);
}
public static final @NonNull Creator<WindowStateResizeItem> CREATOR = new Creator<>() {
@@ -185,7 +208,9 @@
&& mAlwaysConsumeSystemBars == other.mAlwaysConsumeSystemBars
&& mDisplayId == other.mDisplayId
&& mSyncSeqId == other.mSyncSeqId
- && mDragResizing == other.mDragResizing;
+ && mDragResizing == other.mDragResizing
+ && Objects.equals(mActivityToken, other.mActivityToken)
+ && Objects.equals(mActivityWindowInfo, other.mActivityWindowInfo);
}
@Override
@@ -201,6 +226,8 @@
result = 31 * result + mDisplayId;
result = 31 * result + mSyncSeqId;
result = 31 * result + (mDragResizing ? 1 : 0);
+ result = 31 * result + Objects.hashCode(mActivityToken);
+ result = 31 * result + Objects.hashCode(mActivityWindowInfo);
return result;
}
@@ -209,6 +236,8 @@
return "WindowStateResizeItem{window=" + mWindow
+ ", reportDrawn=" + mReportDraw
+ ", configuration=" + mConfiguration
+ + ", activityToken=" + mActivityToken
+ + ", activityWindowInfo=" + mActivityWindowInfo
+ "}";
}
diff --git a/core/java/android/app/smartspace/flags.aconfig b/core/java/android/app/smartspace/flags.aconfig
index 12af888..e90ba67 100644
--- a/core/java/android/app/smartspace/flags.aconfig
+++ b/core/java/android/app/smartspace/flags.aconfig
@@ -2,6 +2,7 @@
flag {
name: "remote_views"
+ is_exported: true
namespace: "sysui_integrations"
description: "Flag to enable the FlaggedApi to include RemoteViews in SmartspaceTarget"
bug: "300157758"
@@ -9,6 +10,7 @@
flag {
name: "access_smartspace"
+ is_exported: true
namespace: "sysui_integrations"
description: "Flag to enable the ACCESS_SMARTSPACE check in SmartspaceManagerService"
bug: "297207196"
diff --git a/core/java/android/app/usage/flags.aconfig b/core/java/android/app/usage/flags.aconfig
index 4d9d911..9a2d2e5 100644
--- a/core/java/android/app/usage/flags.aconfig
+++ b/core/java/android/app/usage/flags.aconfig
@@ -2,6 +2,7 @@
flag {
name: "user_interaction_type_api"
+ is_exported: true
namespace: "backstage_power"
description: "Feature flag for user interaction event report/query API"
bug: "296061232"
@@ -9,6 +10,7 @@
flag {
name: "report_usage_stats_permission"
+ is_exported: true
namespace: "backstage_power"
description: "Feature flag for the new REPORT_USAGE_STATS permission."
bug: "296056771"
@@ -31,6 +33,7 @@
flag {
name: "filter_based_event_query_api"
+ is_exported: true
namespace: "backstage_power"
description: " Feature flag to support filter based event query API"
bug: "194321117"
@@ -38,6 +41,7 @@
flag {
name: "get_app_bytes_by_data_type_api"
+ is_exported: true
namespace: "system_performance"
description: "Feature flag for collecting app data size by file type API"
bug: "294088945"
diff --git a/core/java/android/app/wearable/flags.aconfig b/core/java/android/app/wearable/flags.aconfig
index b4f628f..d1d7b5d 100644
--- a/core/java/android/app/wearable/flags.aconfig
+++ b/core/java/android/app/wearable/flags.aconfig
@@ -2,6 +2,7 @@
flag {
name: "enable_unsupported_operation_status_code"
+ is_exported: true
namespace: "machine_learning"
description: "This flag enables the WearableSensingManager#STATUS_UNSUPPORTED_OPERATION status code API."
bug: "301427767"
@@ -9,6 +10,7 @@
flag {
name: "enable_data_request_observer_api"
+ is_exported: true
namespace: "machine_learning"
description: "This flag enables the API to register a data request observer on WearableSensingManager."
bug: "301427767"
@@ -16,6 +18,7 @@
flag {
name: "enable_provide_wearable_connection_api"
+ is_exported: true
namespace: "machine_learning"
description: "This flag enables the WearableSensingManager#provideWearableConnection API."
bug: "301427767"
@@ -30,6 +33,7 @@
flag {
name: "enable_hotword_wearable_sensing_api"
+ is_exported: true
namespace: "machine_learning"
description: "This flag enables the APIs related to hotword in WearableSensingManager and WearableSensingService."
bug: "310055381"
diff --git a/core/java/android/appwidget/flags.aconfig b/core/java/android/appwidget/flags.aconfig
index 822f02f..4511954 100644
--- a/core/java/android/appwidget/flags.aconfig
+++ b/core/java/android/appwidget/flags.aconfig
@@ -2,6 +2,7 @@
flag {
name: "generated_previews"
+ is_exported: true
namespace: "app_widgets"
description: "Enable support for generated previews in AppWidgetManager"
bug: "306546610"
@@ -26,6 +27,7 @@
flag {
name: "draw_data_parcel"
+ is_exported: true
namespace: "app_widgets"
description: "Enable support for transporting draw instructions as data parcel"
bug: "286130467"
diff --git a/core/java/android/companion/flags.aconfig b/core/java/android/companion/flags.aconfig
index d634b64..ecc5e1b 100644
--- a/core/java/android/companion/flags.aconfig
+++ b/core/java/android/companion/flags.aconfig
@@ -2,6 +2,7 @@
flag {
name: "new_association_builder"
+ is_exported: true
namespace: "companion"
description: "Controls if the new Builder is exposed to test apis."
bug: "296251481"
@@ -16,6 +17,7 @@
flag {
name: "association_tag"
+ is_exported: true
namespace: "companion"
description: "Enable Association tag APIs "
bug: "289241123"
@@ -23,6 +25,7 @@
flag {
name: "device_presence"
+ is_exported: true
namespace: "companion"
description: "Enable device presence APIs"
bug: "283000075"
@@ -30,6 +33,7 @@
flag {
name: "perm_sync_user_consent"
+ is_exported: true
namespace: "companion"
description: "Expose perm sync user consent API"
bug: "309528663"
diff --git a/core/java/android/companion/virtual/IVirtualDevice.aidl b/core/java/android/companion/virtual/IVirtualDevice.aidl
index 6eab363..30a1135 100644
--- a/core/java/android/companion/virtual/IVirtualDevice.aidl
+++ b/core/java/android/companion/virtual/IVirtualDevice.aidl
@@ -79,6 +79,11 @@
int getDevicePolicy(int policyType);
/**
+ * Returns whether the device has a valid microphone.
+ */
+ boolean hasCustomAudioInputSupport();
+
+ /**
* Closes the virtual device and frees all associated resources.
*/
@EnforcePermission("CREATE_VIRTUAL_DEVICE")
diff --git a/core/java/android/companion/virtual/VirtualDevice.java b/core/java/android/companion/virtual/VirtualDevice.java
index 97fa2ba..b9e9afe 100644
--- a/core/java/android/companion/virtual/VirtualDevice.java
+++ b/core/java/android/companion/virtual/VirtualDevice.java
@@ -17,7 +17,6 @@
package android.companion.virtual;
import static android.companion.virtual.VirtualDeviceParams.DEVICE_POLICY_CUSTOM;
-import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_AUDIO;
import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_CAMERA;
import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_SENSORS;
@@ -176,8 +175,7 @@
@FlaggedApi(Flags.FLAG_VDM_PUBLIC_APIS)
public boolean hasCustomAudioInputSupport() {
try {
- return mVirtualDevice.getDevicePolicy(POLICY_TYPE_AUDIO) == DEVICE_POLICY_CUSTOM;
- // TODO(b/291735254): also check for a custom audio injection mix for this device id.
+ return mVirtualDevice.hasCustomAudioInputSupport();
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
diff --git a/core/java/android/companion/virtual/VirtualDeviceManager.java b/core/java/android/companion/virtual/VirtualDeviceManager.java
index 3304475..ec59cf6 100644
--- a/core/java/android/companion/virtual/VirtualDeviceManager.java
+++ b/core/java/android/companion/virtual/VirtualDeviceManager.java
@@ -972,6 +972,7 @@
*
* @param config camera configuration.
* @return newly created camera.
+ * @throws UnsupportedOperationException if virtual camera isn't supported on this device.
* @see VirtualDeviceParams#POLICY_TYPE_CAMERA
*/
@RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
diff --git a/core/java/android/companion/virtual/flags.aconfig b/core/java/android/companion/virtual/flags.aconfig
index 588e4fc..a6a4f5e 100644
--- a/core/java/android/companion/virtual/flags.aconfig
+++ b/core/java/android/companion/virtual/flags.aconfig
@@ -19,6 +19,7 @@
flag {
name: "dynamic_policy"
+ is_exported: true
namespace: "virtual_devices"
description: "Enable dynamic policy API"
bug: "298401780"
@@ -26,6 +27,7 @@
flag {
name: "cross_device_clipboard"
+ is_exported: true
namespace: "virtual_devices"
description: "Enable cross-device clipboard API"
bug: "306622082"
@@ -40,6 +42,7 @@
flag {
name: "vdm_custom_ime"
+ is_exported: true
namespace: "virtual_devices"
description: "Enable custom IME API"
bug: "287269288"
@@ -47,6 +50,7 @@
flag {
name: "vdm_custom_home"
+ is_exported: true
namespace: "virtual_devices"
description: "Enable custom home API"
bug: "297168328"
@@ -54,6 +58,7 @@
flag {
name: "vdm_public_apis"
+ is_exported: true
namespace: "virtual_devices"
description: "Enable public VDM API for device capabilities"
bug: "297253526"
@@ -61,6 +66,7 @@
flag {
name: "virtual_camera"
+ is_exported: true
namespace: "virtual_devices"
description: "Enable Virtual Camera"
bug: "270352264"
@@ -82,6 +88,7 @@
flag {
name: "persistent_device_id_api"
+ is_exported: true
namespace: "virtual_devices"
description: "Enable persistent device ID notification API"
bug: "295258915"
@@ -96,6 +103,7 @@
flag {
name: "interactive_screen_mirror"
+ is_exported: true
namespace: "virtual_devices"
description: "Enable interactive screen mirroring using Virtual Devices"
bug: "292212199"
@@ -103,6 +111,7 @@
flag {
name: "virtual_stylus"
+ is_exported: true
namespace: "virtual_devices"
description: "Enable virtual stylus input"
bug: "304829446"
diff --git a/core/java/android/companion/virtual/flags/flags.aconfig b/core/java/android/companion/virtual/flags/flags.aconfig
index 24d6a5c..2904e7c 100644
--- a/core/java/android/companion/virtual/flags/flags.aconfig
+++ b/core/java/android/companion/virtual/flags/flags.aconfig
@@ -44,3 +44,11 @@
description: "Enable device awareness in camera service"
bug: "305170199"
}
+
+flag {
+ namespace: "virtual_devices"
+ name: "device_aware_drm"
+ description: "Makes MediaDrm APIs device-aware"
+ bug: "303535376"
+ is_fixed_read_only: true
+}
\ No newline at end of file
diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java
index 89300e3..c0c91cb 100644
--- a/core/java/android/content/Context.java
+++ b/core/java/android/content/Context.java
@@ -4208,7 +4208,7 @@
MEDIA_COMMUNICATION_SERVICE,
BATTERY_SERVICE,
JOB_SCHEDULER_SERVICE,
- //@hide: PERSISTENT_DATA_BLOCK_SERVICE,
+ PERSISTENT_DATA_BLOCK_SERVICE,
//@hide: OEM_LOCK_SERVICE,
MEDIA_PROJECTION_SERVICE,
MIDI_SERVICE,
@@ -5067,7 +5067,6 @@
* {@link android.hardware.fingerprint.FingerprintManager} for handling management
* of fingerprints.
*
- * @removed See {@link android.hardware.biometrics.BiometricPrompt}
* @see #getSystemService(String)
* @see android.hardware.fingerprint.FingerprintManager
*/
@@ -5930,9 +5929,8 @@
*
* @see #getSystemService(String)
* @see android.service.persistentdata.PersistentDataBlockManager
- * @hide
*/
- @SystemApi
+ @FlaggedApi(android.security.Flags.FLAG_FRP_ENFORCEMENT)
public static final String PERSISTENT_DATA_BLOCK_SERVICE = "persistent_data_block";
/**
diff --git a/core/java/android/content/flags/flags.aconfig b/core/java/android/content/flags/flags.aconfig
index 3445fb5..27bce5b 100644
--- a/core/java/android/content/flags/flags.aconfig
+++ b/core/java/android/content/flags/flags.aconfig
@@ -2,6 +2,7 @@
flag {
name: "enable_bind_package_isolated_process"
+ is_exported: true
namespace: "machine_learning"
description: "This flag enables the newly added flag for binding package-private isolated processes."
bug: "312706530"
diff --git a/core/java/android/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java
index 9f2f74b..b5809cf 100644
--- a/core/java/android/content/pm/PackageManager.java
+++ b/core/java/android/content/pm/PackageManager.java
@@ -895,7 +895,7 @@
GET_DISABLED_COMPONENTS,
GET_DISABLED_UNTIL_USED_COMPONENTS,
GET_UNINSTALLED_PACKAGES,
- MATCH_CLONE_PROFILE,
+ MATCH_CLONE_PROFILE_LONG,
MATCH_QUARANTINED_COMPONENTS,
})
@Retention(RetentionPolicy.SOURCE)
@@ -1235,10 +1235,11 @@
public static final int MATCH_DEBUG_TRIAGED_MISSING = MATCH_DIRECT_BOOT_AUTO;
/**
- * Use {@link #MATCH_CLONE_PROFILE_LONG} instead.
+ * @deprecated Use {@link #MATCH_CLONE_PROFILE_LONG} instead.
*
* @hide
*/
+ @Deprecated
@SystemApi
public static final int MATCH_CLONE_PROFILE = 0x20000000;
diff --git a/core/java/android/content/pm/flags.aconfig b/core/java/android/content/pm/flags.aconfig
index 92cb9cc..cde565b 100644
--- a/core/java/android/content/pm/flags.aconfig
+++ b/core/java/android/content/pm/flags.aconfig
@@ -2,6 +2,7 @@
flag {
name: "quarantined_enabled"
+ is_exported: true
namespace: "package_manager_service"
description: "Feature flag for Quarantined state"
bug: "269127435"
@@ -9,6 +10,7 @@
flag {
name: "archiving"
+ is_exported: true
namespace: "package_manager_service"
description: "Feature flag to enable the archiving feature."
bug: "278553670"
@@ -24,6 +26,7 @@
flag {
name: "stay_stopped"
+ is_exported: true
namespace: "backstage_power"
description: "Feature flag to improve stopped state enforcement"
bug: "296644915"
@@ -39,6 +42,7 @@
flag {
name: "get_package_info"
+ is_exported: true
namespace: "package_manager_service"
description: "Feature flag to enable the feature to retrieve package info without installation."
bug: "269149275"
@@ -54,6 +58,7 @@
flag {
name: "sdk_lib_independence"
+ is_exported: true
namespace: "package_manager_service"
description: "Feature flag to keep app working even if its declared sdk-library dependency is unavailable."
bug: "295827951"
@@ -78,6 +83,7 @@
flag {
name: "get_resolved_apk_path"
+ is_exported: true
namespace: "package_manager_service"
description: "Feature flag to retrieve resolved path of the base APK during an app install."
bug: "269728874"
@@ -92,6 +98,7 @@
flag {
name: "read_install_info"
+ is_exported: true
namespace: "package_manager_service"
description: "Feature flag to read install related information from an APK."
bug: "275658500"
@@ -113,6 +120,7 @@
flag {
name: "relative_reference_intent_filters"
+ is_exported: true
namespace: "package_manager_service"
description: "Feature flag to enable relative reference intent filters"
bug: "307556883"
@@ -121,6 +129,7 @@
flag {
name: "fix_duplicated_flags"
+ is_exported: true
namespace: "package_manager_service"
description: "Feature flag to fix duplicated PackageManager flag values"
bug: "314815969"
@@ -128,6 +137,7 @@
flag {
name: "provide_info_of_apk_in_apex"
+ is_exported: true
namespace: "package_manager_service"
description: "Feature flag to provide the information of APK-in-APEX"
bug: "306329516"
@@ -144,6 +154,7 @@
flag {
name: "introduce_media_processing_type"
+ is_exported: true
namespace: "backstage_power"
description: "Add a new FGS type for media processing use cases."
bug: "317788011"
@@ -182,6 +193,7 @@
flag {
name: "emergency_install_permission"
+ is_exported: true
namespace: "permissions"
description: "Feature flag to enable permission EMERGENCY_INSTALL_PACKAGES"
bug: "321080601"
@@ -189,6 +201,7 @@
flag {
name: "asl_in_apk_app_metadata_source"
+ is_exported: true
namespace: "package_manager_service"
description: "Feature flag to allow to know if the Android Safety Label (ASL) of an app is provided by the app's APK itself, or provided by an installer."
bug: "287487923"
@@ -205,6 +218,7 @@
flag {
name: "set_pre_verified_domains"
+ is_exported: true
namespace: "package_manager_service"
description: "Feature flag to enable pre-verified domains"
bug: "307327678"
diff --git a/core/java/android/content/pm/multiuser.aconfig b/core/java/android/content/pm/multiuser.aconfig
index 2d32aed..4963a4f 100644
--- a/core/java/android/content/pm/multiuser.aconfig
+++ b/core/java/android/content/pm/multiuser.aconfig
@@ -24,6 +24,7 @@
flag {
name: "support_communal_profile"
+ is_exported: true
namespace: "multiuser"
description: "Framework support for communal profile."
bug: "285426179"
@@ -31,6 +32,7 @@
flag {
name: "support_communal_profile_nextgen"
+ is_exported: true
namespace: "multiuser"
description: "Further framework support for communal profile, beyond the basics, for later releases."
bug: "285426179"
@@ -59,6 +61,7 @@
flag {
name: "enable_biometrics_to_unlock_private_space"
+ is_exported: true
namespace: "profile_experiences"
description: "Add support to unlock the private space using biometrics"
bug: "312184187"
@@ -102,6 +105,7 @@
flag {
name: "enable_system_user_only_for_services_and_providers"
+ is_exported: true
namespace: "multiuser"
description: "Enable systemUserOnly manifest attribute for services and providers."
bug: "302354856"
@@ -118,6 +122,7 @@
flag {
name: "enable_permission_to_access_hidden_profiles"
+ is_exported: true
namespace: "profile_experiences"
description: "Add permission to access API hidden users data via system APIs"
bug: "321988638"
diff --git a/core/java/android/content/res/Configuration.java b/core/java/android/content/res/Configuration.java
index d274792..885f4c5 100644
--- a/core/java/android/content/res/Configuration.java
+++ b/core/java/android/content/res/Configuration.java
@@ -802,14 +802,20 @@
public static final int SCREEN_WIDTH_DP_UNDEFINED = 0;
/**
- * The width of the available screen space in dp units excluding the area
- * occupied by {@link android.view.WindowInsets window insets}.
+ * The width of the available screen space in dp units.
*
- * <aside class="note"><b>Note:</b> The width measurement excludes window
- * insets even when the app is displayed edge to edge using
- * {@link android.view.Window#setDecorFitsSystemWindows(boolean)
+ * <aside class="note"><b>Note:</b> If the app targets
+ * {@link android.os.Build.VERSION_CODES#VANILLA_ICE_CREAM}
+ * or after, The width measurement reflects the window size without excluding insets.
+ * Otherwise, the measurement excludes window insets even when the app is displayed edge to edge
+ * using {@link android.view.Window#setDecorFitsSystemWindows(boolean)
* Window#setDecorFitsSystemWindows(boolean)}.</aside>
*
+ * Use {@link android.view.WindowMetrics#getBounds()} to always obtain the horizontal
+ * display area available to an app or embedded activity including the area
+ * occupied by window insets. A version of the API is also available for use on older platforms
+ * through {@link androidx.window.layout.WindowMetrics}.
+ *
* <p>Corresponds to the
* <a href="{@docRoot}guide/topics/resources/providing-resources.html#AvailableWidthHeightQualifier">
* available width</a> resource qualifier. Defaults to
@@ -831,14 +837,15 @@
* environment, {@code screenWidthDp} is the width of the screen on which
* the app is displayed excluding window insets.
*
- * <p>Differs from {@link android.view.WindowMetrics} by not including
+ * <p>If the app targets {@link android.os.Build.VERSION_CODES#VANILLA_ICE_CREAM} or after,
+ * it is the same as {@link android.view.WindowMetrics}, but is expressed rounded to the nearest
+ * dp rather than px.
+ *
+ * <p>Otherwise, differs from {@link android.view.WindowMetrics} by not including
* window insets in the width measurement and by expressing the measurement
* in dp rather than px. Use {@code screenWidthDp} to obtain the width of
* the display area available to an app or embedded activity excluding the
- * area occupied by window insets. Use
- * {@link android.view.WindowMetrics#getBounds()} to obtain the horizontal
- * display area available to an app or embedded activity including the area
- * occupied by window insets.
+ * area occupied by window insets.
*/
public int screenWidthDp;
@@ -849,15 +856,20 @@
public static final int SCREEN_HEIGHT_DP_UNDEFINED = 0;
/**
- * The height of the available screen space in dp units excluding the area
- * occupied by {@link android.view.WindowInsets window insets}, such as the
- * status bar, navigation bar, and cutouts.
+ * The height of the available screen space in dp units.
*
- * <aside class="note"><b>Note:</b> The height measurement excludes window
- * insets even when the app is displayed edge to edge using
- * {@link android.view.Window#setDecorFitsSystemWindows(boolean)
+ * <aside class="note"><b>Note:</b> If the app targets
+ * {@link android.os.Build.VERSION_CODES#VANILLA_ICE_CREAM}
+ * or after, the height measurement reflects the window size without excluding insets.
+ * Otherwise, the measurement excludes window insets even when the app is displayed edge to edge
+ * using {@link android.view.Window#setDecorFitsSystemWindows(boolean)
* Window#setDecorFitsSystemWindows(boolean)}.</aside>
*
+ * Use {@link android.view.WindowMetrics#getBounds()} to always obtain the vertical
+ * display area available to an app or embedded activity including the area
+ * occupied by window insets. A version of the API is also available for use on older platforms
+ * through {@link androidx.window.layout.WindowMetrics}.
+ *
* <p>Corresponds to the
* <a href="{@docRoot}guide/topics/resources/providing-resources.html#AvailableWidthHeightQualifier">
* available height</a> resource qualifier. Defaults to
@@ -879,14 +891,15 @@
* multiple-screen environment, {@code screenHeightDp} is the height of the
* screen on which the app is displayed excluding window insets.
*
- * <p>Differs from {@link android.view.WindowMetrics} by not including
+ * <p>If the app targets {@link android.os.Build.VERSION_CODES#VANILLA_ICE_CREAM} or after,
+ * it is the same as {@link android.view.WindowMetrics}, but is expressed rounded to the nearest
+ * dp rather than px.
+ *
+ * <p>Otherwise, differs from {@link android.view.WindowMetrics} by not including
* window insets in the height measurement and by expressing the measurement
* in dp rather than px. Use {@code screenHeightDp} to obtain the height of
* the display area available to an app or embedded activity excluding the
- * area occupied by window insets. Use
- * {@link android.view.WindowMetrics#getBounds()} to obtain the vertical
- * display area available to an app or embedded activity including the area
- * occupied by window insets.
+ * area occupied by window insets.
*/
public int screenHeightDp;
diff --git a/core/java/android/content/res/flags.aconfig b/core/java/android/content/res/flags.aconfig
index 7fd0b03..8f5c912 100644
--- a/core/java/android/content/res/flags.aconfig
+++ b/core/java/android/content/res/flags.aconfig
@@ -2,6 +2,7 @@
flag {
name: "default_locale"
+ is_exported: true
namespace: "resource_manager"
description: "Feature flag for default locale in LocaleConfig"
bug: "117306409"
@@ -11,6 +12,7 @@
flag {
name: "font_scale_converter_public"
+ is_exported: true
namespace: "accessibility"
description: "Enables the public API for FontScaleConverter, including enabling thread-safe caching."
bug: "239736383"
@@ -20,6 +22,7 @@
flag {
name: "asset_file_descriptor_frro"
+ is_exported: true
namespace: "resource_manager"
description: "Feature flag for passing in an AssetFileDescriptor to create an frro"
bug: "304478666"
@@ -27,6 +30,7 @@
flag {
name: "manifest_flagging"
+ is_exported: true
namespace: "resource_manager"
description: "Feature flag for flagging manifest entries"
bug: "297373084"
@@ -36,6 +40,7 @@
flag {
name: "nine_patch_frro"
+ is_exported: true
namespace: "resource_manager"
description: "Feature flag for creating an frro from a 9-patch"
bug: "296324826"
@@ -43,6 +48,7 @@
flag {
name: "register_resource_paths"
+ is_exported: true
namespace: "resource_manager"
description: "Feature flag for register resource paths for shared library"
bug: "306202569"
diff --git a/core/java/android/credentials/flags.aconfig b/core/java/android/credentials/flags.aconfig
index 47edba6..d077329 100644
--- a/core/java/android/credentials/flags.aconfig
+++ b/core/java/android/credentials/flags.aconfig
@@ -3,6 +3,7 @@
flag {
namespace: "credential_manager"
name: "settings_activity_enabled"
+ is_exported: true
description: "Enable the Credential Manager Settings Activity APIs"
bug: "300014059"
}
@@ -24,6 +25,7 @@
flag {
namespace: "credential_manager"
name: "new_settings_intents"
+ is_exported: true
description: "Enables settings intents to redirect to new settings page"
bug: "307587989"
}
@@ -45,8 +47,10 @@
flag {
namespace: "credential_manager"
name: "configurable_selector_ui_enabled"
+ is_exported: true
description: "Enables OEM configurable Credential Selector UI"
bug: "319448437"
+ is_exported: true
}
flag {
diff --git a/core/java/android/credentials/selection/IntentCreationResult.java b/core/java/android/credentials/selection/IntentCreationResult.java
new file mode 100644
index 0000000..189ff7b
--- /dev/null
+++ b/core/java/android/credentials/selection/IntentCreationResult.java
@@ -0,0 +1,155 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.credentials.selection;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Intent;
+
+/**
+ * Result of creating a Credential Manager UI intent.
+ *
+ * @hide
+ */
+public final class IntentCreationResult {
+ @NonNull
+ private final Intent mIntent;
+ @Nullable
+ private final String mFallbackUiPackageName;
+ @Nullable
+ private final String mOemUiPackageName;
+ @NonNull
+ private final OemUiUsageStatus mOemUiUsageStatus;
+
+ private IntentCreationResult(@NonNull Intent intent, @Nullable String fallbackUiPackageName,
+ @Nullable String oemUiPackageName, OemUiUsageStatus oemUiUsageStatus) {
+ mIntent = intent;
+ mFallbackUiPackageName = fallbackUiPackageName;
+ mOemUiPackageName = oemUiPackageName;
+ mOemUiUsageStatus = oemUiUsageStatus;
+ }
+
+ /** Returns the UI intent. */
+ @NonNull
+ public Intent getIntent() {
+ return mIntent;
+ }
+
+ /**
+ * Returns the result of attempting to use the config_oemCredentialManagerDialogComponent
+ * as the Credential Manager UI.
+ */
+ @NonNull
+ public OemUiUsageStatus getOemUiUsageStatus() {
+ return mOemUiUsageStatus;
+ }
+
+ /**
+ * Returns the package name of the ui component specified in
+ * config_fallbackCredentialManagerDialogComponent, or null if unspecified / not parsable
+ * successfully.
+ */
+ @Nullable
+ public String getFallbackUiPackageName() {
+ return mFallbackUiPackageName;
+ }
+
+ /**
+ * Returns the package name of the oem ui component specified in
+ * config_oemCredentialManagerDialogComponent, or null if unspecified / not parsable.
+ */
+ @Nullable
+ public String getOemUiPackageName() {
+ return mOemUiPackageName;
+ }
+
+ /**
+ * Result of attempting to use the config_oemCredentialManagerDialogComponent as the Credential
+ * Manager UI.
+ */
+ public enum OemUiUsageStatus {
+ UNKNOWN,
+ // Success: the UI specified in config_oemCredentialManagerDialogComponent was used to
+ // fulfill the request.
+ SUCCESS,
+ // The config value was not specified (e.g. left empty).
+ OEM_UI_CONFIG_NOT_SPECIFIED,
+ // The config value component was specified but not found (e.g. component doesn't exist or
+ // component isn't a system app).
+ OEM_UI_CONFIG_SPECIFIED_BUT_NOT_FOUND,
+ // The config value component was found but not enabled.
+ OEM_UI_CONFIG_SPECIFIED_FOUND_BUT_NOT_ENABLED,
+ }
+
+ /**
+ * Builder for {@link IntentCreationResult}.
+ *
+ * @hide
+ */
+ public static final class Builder {
+ @NonNull
+ private Intent mIntent;
+ @Nullable
+ private String mFallbackUiPackageName = null;
+ @Nullable
+ private String mOemUiPackageName = null;
+ @NonNull
+ private OemUiUsageStatus mOemUiUsageStatus = OemUiUsageStatus.UNKNOWN;
+
+ public Builder(Intent intent) {
+ mIntent = intent;
+ }
+
+ /**
+ * Sets the package name of the ui component specified in
+ * config_fallbackCredentialManagerDialogComponent, or null if unspecified / not parsable
+ * successfully.
+ */
+ @NonNull
+ public Builder setFallbackUiPackageName(@Nullable String fallbackUiPackageName) {
+ mFallbackUiPackageName = fallbackUiPackageName;
+ return this;
+ }
+
+ /**
+ * Sets the package name of the oem ui component specified in
+ * config_oemCredentialManagerDialogComponent, or null if unspecified / not parsable.
+ */
+ @NonNull
+ public Builder setOemUiPackageName(@Nullable String oemUiPackageName) {
+ mOemUiPackageName = oemUiPackageName;
+ return this;
+ }
+
+ /**
+ * Sets the result of attempting to use the config_oemCredentialManagerDialogComponent
+ * as the Credential Manager UI.
+ */
+ @NonNull
+ public Builder setOemUiUsageStatus(OemUiUsageStatus oemUiUsageStatus) {
+ mOemUiUsageStatus = oemUiUsageStatus;
+ return this;
+ }
+
+ /** Builds a {@link IntentCreationResult}. */
+ @NonNull
+ public IntentCreationResult build() {
+ return new IntentCreationResult(mIntent, mFallbackUiPackageName, mOemUiPackageName,
+ mOemUiUsageStatus);
+ }
+ }
+}
diff --git a/core/java/android/credentials/selection/IntentFactory.java b/core/java/android/credentials/selection/IntentFactory.java
index 79fba9b..b98a0d8 100644
--- a/core/java/android/credentials/selection/IntentFactory.java
+++ b/core/java/android/credentials/selection/IntentFactory.java
@@ -36,6 +36,8 @@
import android.text.TextUtils;
import android.util.Slog;
+import com.android.internal.annotations.VisibleForTesting;
+
import java.util.ArrayList;
/**
@@ -57,98 +59,48 @@
* @hide
*/
@NonNull
- public static Intent createCredentialSelectorIntentForAutofill(
+ public static IntentCreationResult createCredentialSelectorIntentForAutofill(
@NonNull Context context,
@NonNull RequestInfo requestInfo,
@SuppressLint("ConcreteCollection") // Concrete collection needed for marshalling.
@NonNull
ArrayList<DisabledProviderData> disabledProviderDataList,
@NonNull ResultReceiver resultReceiver) {
- return createCredentialSelectorIntent(context, requestInfo,
+ return createCredentialSelectorIntentInternal(context, requestInfo,
disabledProviderDataList, resultReceiver);
}
/**
* Generate a new launch intent to the Credential Selector UI.
+ *
+ * @param context the CredentialManager system service (only expected caller)
+ * context that may be used to query existence of the key UI
+ * application
+ * @param disabledProviderDataList the list of disabled provider data that when non-empty the
+ * UI should accordingly generate an entry suggesting the user
+ * to navigate to settings and enable them
+ * @param enabledProviderDataList the list of enabled provider that contain options for this
+ * request; the UI should render each option to the user for
+ * selection
+ * @param requestInfo the display information about the given app request
+ * @param resultReceiver used by the UI to send the UI selection result back
+ * @hide
*/
@NonNull
- private static Intent createCredentialSelectorIntent(
+ public static IntentCreationResult createCredentialSelectorIntentForCredMan(
@NonNull Context context,
@NonNull RequestInfo requestInfo,
@SuppressLint("ConcreteCollection") // Concrete collection needed for marshalling.
@NonNull
+ ArrayList<ProviderData> enabledProviderDataList,
+ @SuppressLint("ConcreteCollection") // Concrete collection needed for marshalling.
+ @NonNull
ArrayList<DisabledProviderData> disabledProviderDataList,
@NonNull ResultReceiver resultReceiver) {
- Intent intent = new Intent();
- setCredentialSelectorUiComponentName(context, intent);
- intent.putParcelableArrayListExtra(
- ProviderData.EXTRA_DISABLED_PROVIDER_DATA_LIST, disabledProviderDataList);
- intent.putExtra(RequestInfo.EXTRA_REQUEST_INFO, requestInfo);
- intent.putExtra(
- Constants.EXTRA_RESULT_RECEIVER, toIpcFriendlyResultReceiver(resultReceiver));
-
- return intent;
- }
-
- private static void setCredentialSelectorUiComponentName(@NonNull Context context,
- @NonNull Intent intent) {
- if (configurableSelectorUiEnabled()) {
- ComponentName componentName = getOemOverrideComponentName(context);
- if (componentName == null) {
- componentName = ComponentName.unflattenFromString(Resources.getSystem().getString(
- com.android.internal.R.string
- .config_fallbackCredentialManagerDialogComponent));
- }
- intent.setComponent(componentName);
- } else {
- ComponentName componentName = ComponentName.unflattenFromString(Resources.getSystem()
- .getString(com.android.internal.R.string
- .config_fallbackCredentialManagerDialogComponent));
- intent.setComponent(componentName);
- }
- }
-
- /**
- * Returns null if there is not an enabled and valid oem override component. It means the
- * default platform UI component name should be used instead.
- */
- @Nullable
- private static ComponentName getOemOverrideComponentName(@NonNull Context context) {
- ComponentName result = null;
- String oemComponentString =
- Resources.getSystem()
- .getString(
- com.android.internal.R.string
- .config_oemCredentialManagerDialogComponent);
- if (!TextUtils.isEmpty(oemComponentString)) {
- ComponentName oemComponentName = ComponentName.unflattenFromString(
- oemComponentString);
- if (oemComponentName != null) {
- try {
- ActivityInfo info = context.getPackageManager().getActivityInfo(
- oemComponentName,
- PackageManager.ComponentInfoFlags.of(
- PackageManager.MATCH_SYSTEM_ONLY));
- if (info.enabled && info.exported) {
- Slog.i(TAG,
- "Found enabled oem CredMan UI component."
- + oemComponentString);
- result = oemComponentName;
- } else {
- Slog.i(TAG,
- "Found enabled oem CredMan UI component but it was not "
- + "enabled.");
- }
- } catch (PackageManager.NameNotFoundException e) {
- Slog.i(TAG, "Unable to find oem CredMan UI component: "
- + oemComponentString + ".");
- }
- } else {
- Slog.i(TAG, "Invalid OEM ComponentName format.");
- }
- } else {
- Slog.i(TAG, "Invalid empty OEM component name.");
- }
+ IntentCreationResult result = createCredentialSelectorIntentInternal(context, requestInfo,
+ disabledProviderDataList, resultReceiver);
+ result.getIntent().putParcelableArrayListExtra(
+ ProviderData.EXTRA_ENABLED_PROVIDER_DATA_LIST, enabledProviderDataList);
return result;
}
@@ -167,6 +119,7 @@
* @param requestInfo the display information about the given app request
* @param resultReceiver used by the UI to send the UI selection result back
*/
+ @VisibleForTesting
@NonNull
public static Intent createCredentialSelectorIntent(
@NonNull Context context,
@@ -178,22 +131,21 @@
@NonNull
ArrayList<DisabledProviderData> disabledProviderDataList,
@NonNull ResultReceiver resultReceiver) {
- Intent intent = createCredentialSelectorIntent(context, requestInfo,
- disabledProviderDataList, resultReceiver);
- intent.putParcelableArrayListExtra(
- ProviderData.EXTRA_ENABLED_PROVIDER_DATA_LIST, enabledProviderDataList);
- return intent;
+ return createCredentialSelectorIntentForCredMan(context, requestInfo,
+ enabledProviderDataList, disabledProviderDataList, resultReceiver).getIntent();
}
/**
* Creates an Intent that cancels any UI matching the given request token id.
*/
+ @VisibleForTesting
@NonNull
public static Intent createCancelUiIntent(@NonNull Context context,
@NonNull IBinder requestToken, boolean shouldShowCancellationUi,
@NonNull String appPackageName) {
Intent intent = new Intent();
- setCredentialSelectorUiComponentName(context, intent);
+ IntentCreationResult.Builder intentResultBuilder = new IntentCreationResult.Builder(intent);
+ setCredentialSelectorUiComponentName(context, intent, intentResultBuilder);
intent.putExtra(CancelSelectionRequest.EXTRA_CANCEL_UI_REQUEST,
new CancelSelectionRequest(new RequestToken(requestToken), shouldShowCancellationUi,
appPackageName));
@@ -201,6 +153,119 @@
}
/**
+ * Generate a new launch intent to the Credential Selector UI.
+ */
+ @NonNull
+ private static IntentCreationResult createCredentialSelectorIntentInternal(
+ @NonNull Context context,
+ @NonNull RequestInfo requestInfo,
+ @SuppressLint("ConcreteCollection") // Concrete collection needed for marshalling.
+ @NonNull
+ ArrayList<DisabledProviderData> disabledProviderDataList,
+ @NonNull ResultReceiver resultReceiver) {
+ Intent intent = new Intent();
+ IntentCreationResult.Builder intentResultBuilder = new IntentCreationResult.Builder(intent);
+ setCredentialSelectorUiComponentName(context, intent, intentResultBuilder);
+ intent.putParcelableArrayListExtra(
+ ProviderData.EXTRA_DISABLED_PROVIDER_DATA_LIST, disabledProviderDataList);
+ intent.putExtra(RequestInfo.EXTRA_REQUEST_INFO, requestInfo);
+ intent.putExtra(
+ Constants.EXTRA_RESULT_RECEIVER, toIpcFriendlyResultReceiver(resultReceiver));
+ return intentResultBuilder.build();
+ }
+
+ private static void setCredentialSelectorUiComponentName(@NonNull Context context,
+ @NonNull Intent intent, @NonNull IntentCreationResult.Builder intentResultBuilder) {
+ if (configurableSelectorUiEnabled()) {
+ ComponentName componentName = getOemOverrideComponentName(context, intentResultBuilder);
+
+ ComponentName fallbackUiComponentName = null;
+ try {
+ fallbackUiComponentName = ComponentName.unflattenFromString(
+ Resources.getSystem().getString(
+ com.android.internal.R.string
+ .config_fallbackCredentialManagerDialogComponent));
+ intentResultBuilder.setFallbackUiPackageName(
+ fallbackUiComponentName.getPackageName());
+ } catch (Exception e) {
+ Slog.w(TAG, "Fallback CredMan IU not found: " + e);
+ }
+
+ if (componentName == null) {
+ componentName = fallbackUiComponentName;
+ }
+
+ intent.setComponent(componentName);
+ } else {
+ ComponentName componentName = ComponentName.unflattenFromString(Resources.getSystem()
+ .getString(com.android.internal.R.string
+ .config_fallbackCredentialManagerDialogComponent));
+ intent.setComponent(componentName);
+ }
+ }
+
+ /**
+ * Returns null if there is not an enabled and valid oem override component. It means the
+ * default platform UI component name should be used instead.
+ */
+ @Nullable
+ private static ComponentName getOemOverrideComponentName(@NonNull Context context,
+ @NonNull IntentCreationResult.Builder intentResultBuilder) {
+ ComponentName result = null;
+ String oemComponentString =
+ Resources.getSystem()
+ .getString(
+ com.android.internal.R.string
+ .config_oemCredentialManagerDialogComponent);
+ if (!TextUtils.isEmpty(oemComponentString)) {
+ ComponentName oemComponentName = null;
+ try {
+ oemComponentName = ComponentName.unflattenFromString(
+ oemComponentString);
+ } catch (Exception e) {
+ Slog.i(TAG, "Failed to parse OEM component name " + oemComponentString + ": " + e);
+ }
+ if (oemComponentName != null) {
+ try {
+ intentResultBuilder.setOemUiPackageName(oemComponentName.getPackageName());
+ ActivityInfo info = context.getPackageManager().getActivityInfo(
+ oemComponentName,
+ PackageManager.ComponentInfoFlags.of(
+ PackageManager.MATCH_SYSTEM_ONLY));
+ if (info.enabled && info.exported) {
+ intentResultBuilder.setOemUiUsageStatus(IntentCreationResult
+ .OemUiUsageStatus.SUCCESS);
+ Slog.i(TAG,
+ "Found enabled oem CredMan UI component."
+ + oemComponentString);
+ result = oemComponentName;
+ } else {
+ intentResultBuilder.setOemUiUsageStatus(IntentCreationResult
+ .OemUiUsageStatus.OEM_UI_CONFIG_SPECIFIED_FOUND_BUT_NOT_ENABLED);
+ Slog.i(TAG,
+ "Found enabled oem CredMan UI component but it was not "
+ + "enabled.");
+ }
+ } catch (PackageManager.NameNotFoundException e) {
+ intentResultBuilder.setOemUiUsageStatus(IntentCreationResult.OemUiUsageStatus
+ .OEM_UI_CONFIG_SPECIFIED_BUT_NOT_FOUND);
+ Slog.i(TAG, "Unable to find oem CredMan UI component: "
+ + oemComponentString + ".");
+ }
+ } else {
+ intentResultBuilder.setOemUiUsageStatus(IntentCreationResult.OemUiUsageStatus
+ .OEM_UI_CONFIG_SPECIFIED_BUT_NOT_FOUND);
+ Slog.i(TAG, "Invalid OEM ComponentName format.");
+ }
+ } else {
+ intentResultBuilder.setOemUiUsageStatus(
+ IntentCreationResult.OemUiUsageStatus.OEM_UI_CONFIG_NOT_SPECIFIED);
+ Slog.i(TAG, "Invalid empty OEM component name.");
+ }
+ return result;
+ }
+
+ /**
* Convert an instance of a "locally-defined" ResultReceiver to an instance of {@link
* android.os.ResultReceiver} itself, which the receiving process will be able to unmarshall.
*/
diff --git a/core/java/android/database/sqlite/flags.aconfig b/core/java/android/database/sqlite/flags.aconfig
index 92ef9c2..7ecffaf 100644
--- a/core/java/android/database/sqlite/flags.aconfig
+++ b/core/java/android/database/sqlite/flags.aconfig
@@ -2,6 +2,7 @@
flag {
name: "sqlite_apis_35"
+ is_exported: true
namespace: "system_performance"
is_fixed_read_only: true
description: "SQLite APIs held back for Android 15"
diff --git a/core/java/android/hardware/biometrics/flags.aconfig b/core/java/android/hardware/biometrics/flags.aconfig
index ff07498..9836eec 100644
--- a/core/java/android/hardware/biometrics/flags.aconfig
+++ b/core/java/android/hardware/biometrics/flags.aconfig
@@ -18,6 +18,7 @@
flag {
name: "get_op_id_crypto_object"
+ is_exported: true
namespace: "biometrics_framework"
description: "Feature flag for adding a get operation id api to CryptoObject."
bug: "307601768"
@@ -25,8 +26,8 @@
flag {
name: "custom_biometric_prompt"
+ is_exported: true
namespace: "biometrics_framework"
description: "Feature flag for adding a custom content view API to BiometricPrompt.Builder."
bug: "302735104"
}
-
diff --git a/core/java/android/hardware/camera2/CaptureRequest.java b/core/java/android/hardware/camera2/CaptureRequest.java
index 13d5c7e..6f901d7 100644
--- a/core/java/android/hardware/camera2/CaptureRequest.java
+++ b/core/java/android/hardware/camera2/CaptureRequest.java
@@ -2800,7 +2800,9 @@
* upright.</p>
* <p>Camera devices may either encode this value into the JPEG EXIF header, or
* rotate the image data to match this orientation. When the image data is rotated,
- * the thumbnail data will also be rotated.</p>
+ * the thumbnail data will also be rotated. Additionally, in the case where the image data
+ * is rotated, {@link android.media.Image#getWidth } and {@link android.media.Image#getHeight }
+ * will not be updated to reflect the height and width of the rotated image.</p>
* <p>Note that this orientation is relative to the orientation of the camera sensor, given
* by {@link CameraCharacteristics#SENSOR_ORIENTATION android.sensor.orientation}.</p>
* <p>To translate from the device orientation given by the Android sensor APIs for camera
diff --git a/core/java/android/hardware/camera2/CaptureResult.java b/core/java/android/hardware/camera2/CaptureResult.java
index 7145501..69b1c34 100644
--- a/core/java/android/hardware/camera2/CaptureResult.java
+++ b/core/java/android/hardware/camera2/CaptureResult.java
@@ -3091,7 +3091,9 @@
* upright.</p>
* <p>Camera devices may either encode this value into the JPEG EXIF header, or
* rotate the image data to match this orientation. When the image data is rotated,
- * the thumbnail data will also be rotated.</p>
+ * the thumbnail data will also be rotated. Additionally, in the case where the image data
+ * is rotated, {@link android.media.Image#getWidth } and {@link android.media.Image#getHeight }
+ * will not be updated to reflect the height and width of the rotated image.</p>
* <p>Note that this orientation is relative to the orientation of the camera sensor, given
* by {@link CameraCharacteristics#SENSOR_ORIENTATION android.sensor.orientation}.</p>
* <p>To translate from the device orientation given by the Android sensor APIs for camera
diff --git a/core/java/android/hardware/camera2/impl/CameraExtensionSessionImpl.java b/core/java/android/hardware/camera2/impl/CameraExtensionSessionImpl.java
index 5b32f33..c00e610 100644
--- a/core/java/android/hardware/camera2/impl/CameraExtensionSessionImpl.java
+++ b/core/java/android/hardware/camera2/impl/CameraExtensionSessionImpl.java
@@ -1757,7 +1757,8 @@
mCallbacks, result.getSequenceId());
}
if ((!mSingleCapture) && (mPreviewProcessorType ==
- IPreviewExtenderImpl.PROCESSOR_TYPE_REQUEST_UPDATE_ONLY)) {
+ IPreviewExtenderImpl.PROCESSOR_TYPE_REQUEST_UPDATE_ONLY)
+ && mInitialized) {
CaptureStageImpl captureStage = null;
try {
captureStage = mPreviewRequestUpdateProcessor.process(
@@ -1780,8 +1781,8 @@
} else {
mRequestUpdatedNeeded = false;
}
- } else if (mPreviewProcessorType ==
- IPreviewExtenderImpl.PROCESSOR_TYPE_IMAGE_PROCESSOR) {
+ } else if ((mPreviewProcessorType ==
+ IPreviewExtenderImpl.PROCESSOR_TYPE_IMAGE_PROCESSOR) && mInitialized) {
int idx = mPendingResultMap.indexOfKey(timestamp);
if ((idx >= 0) && (mPendingResultMap.get(timestamp).first == null)) {
@@ -1828,7 +1829,7 @@
} else {
// No special handling for PROCESSOR_TYPE_NONE
}
- if (notifyClient) {
+ if (notifyClient && mInitialized) {
final long ident = Binder.clearCallingIdentity();
try {
if (processStatus) {
diff --git a/core/java/android/hardware/camera2/params/SessionConfiguration.java b/core/java/android/hardware/camera2/params/SessionConfiguration.java
index b0f354f..3b2913c 100644
--- a/core/java/android/hardware/camera2/params/SessionConfiguration.java
+++ b/core/java/android/hardware/camera2/params/SessionConfiguration.java
@@ -133,7 +133,7 @@
* {@link CameraDeviceSetup.isSessionConfigurationSupported} and {@link
* CameraDeviceSetup.getSessionCharacteristics} to query a camera device's feature
* combination support and session specific characteristics. For the SessionConfiguration
- * object to be used to create a capture session, {@link #setCallback} must be called to
+ * object to be used to create a capture session, {@link #setStateCallback} must be called to
* specify the state callback function, and any incomplete OutputConfigurations must be
* completed via {@link OutputConfiguration#addSurface} or
* {@link OutputConfiguration#setSurfacesForMultiResolutionOutput} as appropriate.</p>
@@ -419,7 +419,7 @@
* @param cb A state callback interface implementation.
*/
@FlaggedApi(Flags.FLAG_CAMERA_DEVICE_SETUP)
- public void setCallback(
+ public void setStateCallback(
@NonNull @CallbackExecutor Executor executor,
@NonNull CameraCaptureSession.StateCallback cb) {
mStateCallback = cb;
diff --git a/core/java/android/hardware/camera2/params/StreamConfigurationMap.java b/core/java/android/hardware/camera2/params/StreamConfigurationMap.java
index b067095..978a8f9 100644
--- a/core/java/android/hardware/camera2/params/StreamConfigurationMap.java
+++ b/core/java/android/hardware/camera2/params/StreamConfigurationMap.java
@@ -1473,6 +1473,11 @@
* <li>ImageFormat.DEPTH_JPEG => HAL_DATASPACE_DYNAMIC_DEPTH
* <li>ImageFormat.HEIC => HAL_DATASPACE_HEIF
* <li>ImageFormat.JPEG_R => HAL_DATASPACE_JPEG_R
+ * <li>ImageFormat.YUV_420_888 => HAL_DATASPACE_JFIF
+ * <li>ImageFormat.RAW_SENSOR => HAL_DATASPACE_ARBITRARY
+ * <li>ImageFormat.RAW_OPAQUE => HAL_DATASPACE_ARBITRARY
+ * <li>ImageFormat.RAW10 => HAL_DATASPACE_ARBITRARY
+ * <li>ImageFormat.RAW12 => HAL_DATASPACE_ARBITRARY
* <li>others => HAL_DATASPACE_UNKNOWN
* </ul>
* </p>
@@ -1511,6 +1516,11 @@
return HAL_DATASPACE_JPEG_R;
case ImageFormat.YUV_420_888:
return HAL_DATASPACE_JFIF;
+ case ImageFormat.RAW_SENSOR:
+ case ImageFormat.RAW_PRIVATE:
+ case ImageFormat.RAW10:
+ case ImageFormat.RAW12:
+ return HAL_DATASPACE_ARBITRARY;
default:
return HAL_DATASPACE_UNKNOWN;
}
@@ -2005,6 +2015,12 @@
private static final int HAL_DATASPACE_RANGE_SHIFT = 27;
private static final int HAL_DATASPACE_UNKNOWN = 0x0;
+
+ /**
+ * @hide
+ */
+ public static final int HAL_DATASPACE_ARBITRARY = 0x1;
+
/** @hide */
public static final int HAL_DATASPACE_V0_JFIF =
(2 << HAL_DATASPACE_STANDARD_SHIFT) |
diff --git a/core/java/android/hardware/devicestate/DeviceState.java b/core/java/android/hardware/devicestate/DeviceState.java
index b214da2..689e343 100644
--- a/core/java/android/hardware/devicestate/DeviceState.java
+++ b/core/java/android/hardware/devicestate/DeviceState.java
@@ -173,7 +173,7 @@
public static final int PROPERTY_FEATURE_DUAL_DISPLAY_INTERNAL_DEFAULT = 17;
/** @hide */
- @IntDef(prefix = {"PROPERTY_"}, flag = true, value = {
+ @IntDef(prefix = {"PROPERTY_"}, flag = false, value = {
PROPERTY_FOLDABLE_HARDWARE_CONFIGURATION_FOLD_IN_CLOSED,
PROPERTY_FOLDABLE_HARDWARE_CONFIGURATION_FOLD_IN_HALF_OPEN,
PROPERTY_FOLDABLE_HARDWARE_CONFIGURATION_FOLD_IN_OPEN,
@@ -197,7 +197,7 @@
public @interface DeviceStateProperties {}
/** @hide */
- @IntDef(prefix = {"PROPERTY_"}, flag = true, value = {
+ @IntDef(prefix = {"PROPERTY_"}, flag = false, value = {
PROPERTY_FOLDABLE_HARDWARE_CONFIGURATION_FOLD_IN_CLOSED,
PROPERTY_FOLDABLE_HARDWARE_CONFIGURATION_FOLD_IN_HALF_OPEN,
PROPERTY_FOLDABLE_HARDWARE_CONFIGURATION_FOLD_IN_OPEN
@@ -207,7 +207,7 @@
public @interface PhysicalDeviceStateProperties {}
/** @hide */
- @IntDef(prefix = {"PROPERTY_"}, flag = true, value = {
+ @IntDef(prefix = {"PROPERTY_"}, flag = false, value = {
PROPERTY_POLICY_CANCEL_OVERRIDE_REQUESTS,
PROPERTY_POLICY_CANCEL_WHEN_REQUESTER_NOT_ON_TOP,
PROPERTY_POLICY_UNSUPPORTED_WHEN_THERMAL_STATUS_CRITICAL,
diff --git a/core/java/android/hardware/devicestate/feature/flags.aconfig b/core/java/android/hardware/devicestate/feature/flags.aconfig
index 73a9e34..e474603 100644
--- a/core/java/android/hardware/devicestate/feature/flags.aconfig
+++ b/core/java/android/hardware/devicestate/feature/flags.aconfig
@@ -2,6 +2,7 @@
flag {
name: "device_state_property_api"
+ is_exported: true
namespace: "windowing_sdk"
description: "Updated DeviceState hasProperty API"
bug: "293636629"
diff --git a/core/java/android/hardware/fingerprint/FingerprintManager.java b/core/java/android/hardware/fingerprint/FingerprintManager.java
index b0f69f5..81e321d 100644
--- a/core/java/android/hardware/fingerprint/FingerprintManager.java
+++ b/core/java/android/hardware/fingerprint/FingerprintManager.java
@@ -83,8 +83,7 @@
/**
* A class that coordinates access to the fingerprint hardware.
- *
- * @removed See {@link BiometricPrompt} which shows a system-provided dialog upon starting
+ * @deprecated See {@link BiometricPrompt} which shows a system-provided dialog upon starting
* authentication. In a world where devices may have different types of biometric authentication,
* it's much more realistic to have a system-provided authentication dialog since the method may
* vary by vendor/device.
@@ -95,6 +94,7 @@
@RequiresFeature(PackageManager.FEATURE_FINGERPRINT)
public class FingerprintManager implements BiometricAuthenticator, BiometricFingerprintConstants {
private static final String TAG = "FingerprintManager";
+ private static final boolean DEBUG = true;
private static final int MSG_ENROLL_RESULT = 100;
private static final int MSG_ACQUIRED = 101;
private static final int MSG_AUTHENTICATION_SUCCEEDED = 102;
@@ -196,7 +196,6 @@
/**
* Retrieves a test session for FingerprintManager.
- *
* @hide
*/
@TestApi
@@ -255,10 +254,9 @@
}
/**
- * A wrapper class for the crypto objects supported by FingerprintManager. Currently, the
+ * A wrapper class for the crypto objects supported by FingerprintManager. Currently the
* framework supports {@link Signature}, {@link Cipher} and {@link Mac} objects.
- *
- * @removed See {@link android.hardware.biometrics.BiometricPrompt.CryptoObject}
+ * @deprecated See {@link android.hardware.biometrics.BiometricPrompt.CryptoObject}
*/
@Deprecated
public static final class CryptoObject extends android.hardware.biometrics.CryptoObject {
@@ -332,8 +330,7 @@
/**
* Container for callback data from {@link FingerprintManager#authenticate(CryptoObject,
* CancellationSignal, int, AuthenticationCallback, Handler)}.
- *
- * @removed See {@link android.hardware.biometrics.BiometricPrompt.AuthenticationResult}
+ * @deprecated See {@link android.hardware.biometrics.BiometricPrompt.AuthenticationResult}
*/
@Deprecated
public static class AuthenticationResult {
@@ -395,8 +392,7 @@
* FingerprintManager#authenticate(CryptoObject, CancellationSignal,
* int, AuthenticationCallback, Handler) } must provide an implementation of this for listening to
* fingerprint events.
- *
- * @removed See {@link android.hardware.biometrics.BiometricPrompt.AuthenticationCallback}
+ * @deprecated See {@link android.hardware.biometrics.BiometricPrompt.AuthenticationCallback}
*/
@Deprecated
public static abstract class AuthenticationCallback
@@ -459,7 +455,6 @@
/**
* Callback structure provided for {@link #detectFingerprint(CancellationSignal,
* FingerprintDetectionCallback, int, Surface)}.
- *
* @hide
*/
public interface FingerprintDetectionCallback {
@@ -613,8 +608,7 @@
* by <a href="{@docRoot}training/articles/keystore.html">Android Keystore
* facility</a>.
* @throws IllegalStateException if the crypto primitive is not initialized.
- *
- * @removed See {@link BiometricPrompt#authenticate(CancellationSignal, Executor,
+ * @deprecated See {@link BiometricPrompt#authenticate(CancellationSignal, Executor,
* BiometricPrompt.AuthenticationCallback)} and {@link BiometricPrompt#authenticate(
* BiometricPrompt.CryptoObject, CancellationSignal, Executor,
* BiometricPrompt.AuthenticationCallback)}
@@ -629,7 +623,6 @@
/**
* Per-user version of authenticate.
* @deprecated use {@link #authenticate(CryptoObject, CancellationSignal, AuthenticationCallback, Handler, FingerprintAuthenticateOptions)}.
- *
* @hide
*/
@Deprecated
@@ -642,7 +635,6 @@
/**
* Per-user and per-sensor version of authenticate.
* @deprecated use {@link #authenticate(CryptoObject, CancellationSignal, AuthenticationCallback, Handler, FingerprintAuthenticateOptions)}.
- *
* @hide
*/
@Deprecated
@@ -659,7 +651,6 @@
/**
* Version of authenticate with additional options.
- *
* @hide
*/
@RequiresPermission(anyOf = {USE_BIOMETRIC, USE_FINGERPRINT})
@@ -707,7 +698,6 @@
/**
* Uses the fingerprint hardware to detect for the presence of a finger, without giving details
* about accept/reject/lockout.
- *
* @hide
*/
@RequiresPermission(USE_BIOMETRIC_INTERNAL)
@@ -750,7 +740,6 @@
* @param callback an object to receive enrollment events
* @param shouldLogMetrics a flag that indicates if enrollment failure/success metrics
* should be logged.
- *
* @hide
*/
@RequiresPermission(MANAGE_FINGERPRINT)
@@ -821,7 +810,6 @@
/**
* Same as {@link #generateChallenge(int, GenerateChallengeCallback)}, but assumes the first
* enumerated sensor.
- *
* @hide
*/
@RequiresPermission(MANAGE_FINGERPRINT)
@@ -836,7 +824,6 @@
/**
* Revokes the specified challenge.
- *
* @hide
*/
@RequiresPermission(MANAGE_FINGERPRINT)
@@ -862,7 +849,6 @@
* @param sensorId Sensor ID that this operation takes effect for
* @param userId User ID that this operation takes effect for.
* @param hardwareAuthToken An opaque token returned by password confirmation.
- *
* @hide
*/
@RequiresPermission(RESET_FINGERPRINT_LOCKOUT)
@@ -900,7 +886,6 @@
/**
* Removes all fingerprint templates for the given user.
- *
* @hide
*/
@RequiresPermission(MANAGE_FINGERPRINT)
@@ -1020,7 +1005,6 @@
/**
* Forwards BiometricStateListener to FingerprintService
* @param listener new BiometricStateListener being added
- *
* @hide
*/
public void registerBiometricStateListener(@NonNull BiometricStateListener listener) {
@@ -1172,8 +1156,7 @@
}
/**
- * This is triggered by SideFpsEventHandler.
- *
+ * This is triggered by SideFpsEventHandler
* @hide
*/
@RequiresPermission(USE_BIOMETRIC_INTERNAL)
@@ -1186,8 +1169,7 @@
* Determine if there is at least one fingerprint enrolled.
*
* @return true if at least one fingerprint is enrolled, false otherwise
- *
- * @removed See {@link BiometricPrompt} and
+ * @deprecated See {@link BiometricPrompt} and
* {@link FingerprintManager#FINGERPRINT_ERROR_NO_FINGERPRINTS}
*/
@Deprecated
@@ -1221,8 +1203,7 @@
* Determine if fingerprint hardware is present and functional.
*
* @return true if hardware is present and functional, false otherwise.
- *
- * @removed See {@link BiometricPrompt} and
+ * @deprecated See {@link BiometricPrompt} and
* {@link FingerprintManager#FINGERPRINT_ERROR_HW_UNAVAILABLE}
*/
@Deprecated
@@ -1248,7 +1229,6 @@
/**
* Get statically configured sensor properties.
- *
* @hide
*/
@RequiresPermission(USE_BIOMETRIC_INTERNAL)
@@ -1267,7 +1247,6 @@
/**
* Returns whether the device has a power button fingerprint sensor.
* @return boolean indicating whether power button is fingerprint sensor
- *
* @hide
*/
public boolean isPowerbuttonFps() {
diff --git a/core/java/android/hardware/flags/overlayproperties_flags.aconfig b/core/java/android/hardware/flags/overlayproperties_flags.aconfig
index c6a352e..1165e65 100644
--- a/core/java/android/hardware/flags/overlayproperties_flags.aconfig
+++ b/core/java/android/hardware/flags/overlayproperties_flags.aconfig
@@ -2,6 +2,7 @@
flag {
name: "overlayproperties_class_api"
+ is_exported: true
namespace: "core_graphics"
description: "public OverlayProperties class, OverlayProperties#supportMixedColorSpaces and Display#getOverlaySupport API"
bug: "267234573"
diff --git a/core/java/android/hardware/input/input_framework.aconfig b/core/java/android/hardware/input/input_framework.aconfig
index e070fe5..9684e64 100644
--- a/core/java/android/hardware/input/input_framework.aconfig
+++ b/core/java/android/hardware/input/input_framework.aconfig
@@ -27,6 +27,7 @@
flag {
namespace: "input_native"
name: "pointer_coords_is_resampled_api"
+ is_exported: true
description: "Makes MotionEvent.PointerCoords#isResampled() a public API"
bug: "298197511"
}
@@ -34,6 +35,7 @@
flag {
namespace: "input_native"
name: "emoji_and_screenshot_keycodes_available"
+ is_exported: true
description: "Add new KeyEvent keycodes for opening Emoji Picker and Taking Screenshots"
bug: "315307777"
}
diff --git a/core/java/android/hardware/radio/Announcement.java b/core/java/android/hardware/radio/Announcement.java
index 3ba3ebc..faa103c 100644
--- a/core/java/android/hardware/radio/Announcement.java
+++ b/core/java/android/hardware/radio/Announcement.java
@@ -71,7 +71,7 @@
/**
* An event called whenever a list of active announcements change.
*
- * The entire list is sent each time a new announcement appears or any ends broadcasting.
+ * <p>The entire list is sent each time a new announcement appears or any ends broadcasting.
*
* @param activeAnnouncements a full list of active announcements
*/
diff --git a/core/java/android/hardware/radio/ProgramList.java b/core/java/android/hardware/radio/ProgramList.java
index c5167db..6146df8 100644
--- a/core/java/android/hardware/radio/ProgramList.java
+++ b/core/java/android/hardware/radio/ProgramList.java
@@ -357,7 +357,7 @@
/**
* Constructor of program list filter.
*
- * Arrays passed to this constructor become owned by this object, do not modify them later.
+ * <p>Arrays passed to this constructor will be owned by this object, do not modify them.
*
* @param identifierTypes see getIdentifierTypes()
* @param identifiers see getIdentifiers()
@@ -438,12 +438,11 @@
/**
* Returns the list of identifier types that satisfy the filter.
*
- * If the program list entry contains at least one identifier of the type
- * listed, it satisfies this condition.
+ * <p>If the program list entry contains at least one identifier of the type
+ * listed, it satisfies this condition. Empty list means no filtering on
+ * identifier type.
*
- * Empty list means no filtering on identifier type.
- *
- * @return the list of accepted identifier types, must not be modified
+ * @return the set of accepted identifier types, must not be modified
*/
public @NonNull Set<Integer> getIdentifierTypes() {
return mIdentifierTypes;
@@ -452,12 +451,10 @@
/**
* Returns the list of identifiers that satisfy the filter.
*
- * If the program list entry contains at least one listed identifier,
- * it satisfies this condition.
+ * <p>If the program list entry contains at least one listed identifier,
+ * it satisfies this condition. Empty list means no filtering on identifier.
*
- * Empty list means no filtering on identifier.
- *
- * @return the list of accepted identifiers, must not be modified
+ * @return the set of accepted identifiers, must not be modified
*/
public @NonNull Set<ProgramSelector.Identifier> getIdentifiers() {
return mIdentifiers;
@@ -476,7 +473,7 @@
/**
* Checks, if updates on entry modifications should be disabled.
*
- * If true, 'modified' vector of ProgramListChunk must contain list
+ * <p>If true, 'modified' vector of ProgramListChunk must contain list
* additions only. Once the program is added to the list, it's not
* updated anymore.
*/
diff --git a/core/java/android/hardware/radio/ProgramSelector.java b/core/java/android/hardware/radio/ProgramSelector.java
index 0740374..42028f6 100644
--- a/core/java/android/hardware/radio/ProgramSelector.java
+++ b/core/java/android/hardware/radio/ProgramSelector.java
@@ -36,27 +36,31 @@
/**
* A set of identifiers necessary to tune to a given station.
*
- * This can hold various identifiers, like
- * - AM/FM frequency
- * - HD Radio subchannel
- * - DAB channel info
+ * <p>This can hold various identifiers, like
+ * <ui>
+ * <li>AM/FM frequency</li>
+ * <li>HD Radio subchannel</li>
+ * <li>DAB channel info</li>
+ * </ui>
*
- * The primary ID uniquely identifies a station and can be used for equality
+ * <p>The primary ID uniquely identifies a station and can be used for equality
* check. The secondary IDs are supplementary and can speed up tuning process,
* but the primary ID is sufficient (ie. after a full band scan).
*
- * Two selectors with different secondary IDs, but the same primary ID are
+ * <p>Two selectors with different secondary IDs, but the same primary ID are
* considered equal. In particular, secondary IDs vector may get updated for
* an entry on the program list (ie. when a better frequency for a given
* station is found).
*
- * The primaryId of a given programType MUST be of a specific type:
- * - AM, FM: RDS_PI if the station broadcasts RDS, AMFM_FREQUENCY otherwise;
- * - AM_HD, FM_HD: HD_STATION_ID_EXT;
- * - DAB: DAB_SIDECC;
- * - DRMO: DRMO_SERVICE_ID;
- * - SXM: SXM_SERVICE_ID;
- * - VENDOR: VENDOR_PRIMARY.
+ * <p>The primaryId of a given programType MUST be of a specific type:
+ * <ui>
+ * <li>AM, FM: RDS_PI if the station broadcasts RDS, AMFM_FREQUENCY otherwise;</li>
+ * <li>AM_HD, FM_HD: HD_STATION_ID_EXT;</li>
+ * <li>DAB: DAB_SIDECC;</li>
+ * <li>DRMO: DRMO_SERVICE_ID;</li>
+ * <li>SXM: SXM_SERVICE_ID;</li>
+ * <li>VENDOR: VENDOR_PRIMARY.</li>
+ * </ui>
* @hide
*/
@SystemApi
@@ -258,10 +262,10 @@
/**
* 64bit additional identifier for HD Radio.
*
- * <p>Due to Station ID abuse, some HD_STATION_ID_EXT identifiers may be not
- * globally unique. To provide a best-effort solution, a short version of
- * station name may be carried as additional identifier and may be used
- * by the tuner hardware to double-check tuning.
+ * <p>Due to Station ID abuse, some {@link #IDENTIFIER_TYPE_HD_STATION_ID_EXT}
+ * identifiers may be not globally unique. To provide a best-effort solution, a
+ * short version of station name may be carried as additional identifier and
+ * may be used by the tuner hardware to double-check tuning.
*
* <p>The name is limited to the first 8 A-Z0-9 characters (lowercase
* letters must be converted to uppercase). Encoded in little-endian
@@ -384,7 +388,7 @@
* The value format is determined by a vendor.
*
* <p>It must not be used in any other programType than corresponding VENDOR
- * type between VENDOR_START and VENDOR_END (eg. identifier type 1015 must
+ * type between VENDOR_START and VENDOR_END (e.g. identifier type 1015 must
* not be used in any program type other than 1015).
*/
public static final int IDENTIFIER_TYPE_VENDOR_START = PROGRAM_TYPE_VENDOR_START;
@@ -435,9 +439,10 @@
/**
* Constructor for ProgramSelector.
*
- * It's not desired to modify selector objects, so all its fields are initialized at creation.
+ * <p>It's not desired to modify selector objects, so all its fields are initialized at
+ * creation.
*
- * Identifier lists must not contain any nulls, but can itself be null to be interpreted
+ * <p>Identifier lists must not contain any nulls, but can itself be null to be interpreted
* as empty list at object creation.
*
* @param programType type of a radio technology.
@@ -492,8 +497,8 @@
/**
* Looks up an identifier of a given type (either primary or secondary).
*
- * If there are multiple identifiers if a given type, then first in order (where primary id is
- * before any secondary) is selected.
+ * <p>If there are multiple identifiers if a given type, then first in order (where primary id
+ * is before any secondary) is selected.
*
* @param type type of identifier.
* @return identifier value, if found.
@@ -510,11 +515,11 @@
/**
* Looks up all identifier of a given type (either primary or secondary).
*
- * Some identifiers may be provided multiple times, for example
- * IDENTIFIER_TYPE_AMFM_FREQUENCY for FM Alternate Frequencies.
+ * <p>Some identifiers may be provided multiple times, for example
+ * {@link #IDENTIFIER_TYPE_AMFM_FREQUENCY} for FM Alternate Frequencies.
*
* @param type type of identifier.
- * @return a list of identifiers, generated on each call. May be modified.
+ * @return an array of identifiers, generated on each call. May be modified.
*/
public @NonNull Identifier[] getAllIds(@IdentifierType int type) {
List<Identifier> out = new ArrayList<>();
@@ -543,14 +548,14 @@
/**
* Creates an equivalent ProgramSelector with a given secondary identifier preferred.
*
- * Used to point to a specific physical identifier for technologies that may broadcast the same
- * program on different channels. For example, with a DAB program broadcasted over multiple
+ * <p>Used to point to a specific physical identifier for technologies that may broadcast the
+ * same program on different channels. For example, with a DAB program broadcasted over multiple
* ensembles, the radio hardware may select the one with the strongest signal. The UI may select
* preferred ensemble though, so the radio hardware may try to use it in the first place.
*
- * This is a best-effort hint for the tuner, not a guaranteed behavior.
+ * <p>This is a best-effort hint for the tuner, not a guaranteed behavior.
*
- * Setting the given secondary identifier as preferred means filtering out other secondary
+ * <p>Setting the given secondary identifier as preferred means filtering out other secondary
* identifiers of its type and adding it to the list.
*
* @param preferred preferred secondary identifier
@@ -577,7 +582,7 @@
*
* @param band the band.
* @param frequencyKhz the frequency in kHz.
- * @return new ProgramSelector object representing given frequency.
+ * @return new {@link ProgramSelector} object representing given frequency.
* @throws IllegalArgumentException if provided frequency is out of bounds.
*/
public static @NonNull ProgramSelector createAmFmSelector(
@@ -588,13 +593,13 @@
/**
* Checks, if a given AM/FM frequency is roughly valid and in correct unit.
*
- * It does not check the range precisely: it may provide false positives, but not false
+ * <p>It does not check the range precisely: it may provide false positives, but not false
* negatives. In particular, it may be way off for certain regions.
- * The main purpose is to avoid passing inproper units, ie. MHz instead of kHz.
+ * The main purpose is to avoid passing improper units, ie. MHz instead of kHz.
*
* @param isAm true, if AM, false if FM.
* @param frequencyKhz the frequency in kHz.
- * @return true, if the frequency is rougly valid.
+ * @return true, if the frequency is roughly valid.
*/
private static boolean isValidAmFmFrequency(boolean isAm, int frequencyKhz) {
if (isAm) {
@@ -607,7 +612,7 @@
/**
* Builds new ProgramSelector for AM/FM frequency.
*
- * This method variant supports HD Radio subchannels, but it's undesirable to
+ * <p>This method variant supports HD Radio subchannels, but it's undesirable to
* select them manually. Instead, the value should be retrieved from program list.
*
* @param band the band.
@@ -741,9 +746,9 @@
};
/**
- * A single program identifier component, eg. frequency or channel ID.
+ * A single program identifier component, e.g. frequency or channel ID.
*
- * The long value field holds the value in format described in comments for
+ * <p>The long value field holds the value in format described in comments for
* IdentifierType constants.
*/
public static final class Identifier implements Parcelable {
@@ -776,11 +781,11 @@
}
/**
- * Returns whether this Identifier's type is considered a category when filtering
+ * Returns whether this identifier's type is considered a category when filtering
* ProgramLists for category entries.
*
* @see ProgramList.Filter#areCategoriesIncluded
- * @return False if this identifier's type is not tuneable (e.g. DAB ensemble or
+ * @return False if this identifier's type is not tunable (e.g. DAB ensemble or
* vendor-specified type). True otherwise.
*/
public boolean isCategoryType() {
@@ -791,14 +796,14 @@
/**
* Value of an identifier.
*
- * Its meaning depends on identifier type, ie. for IDENTIFIER_TYPE_AMFM_FREQUENCY type,
- * the value is a frequency in kHz.
+ * <p>Its meaning depends on identifier type, ie. for
+ * {@link #IDENTIFIER_TYPE_AMFM_FREQUENCY} type, the value is a frequency in kHz.
*
- * The range of a value depends on its type; it does not always require the whole long
+ * <p>The range of a value depends on its type; it does not always require the whole long
* range. Casting to necessary type (ie. int) without range checking is correct in front-end
* code - any range violations are either errors in the framework or in the
- * HAL implementation. For example, IDENTIFIER_TYPE_AMFM_FREQUENCY always fits in int,
- * as Integer.MAX_VALUE would mean 2.1THz.
+ * HAL implementation. For example, {@link #IDENTIFIER_TYPE_AMFM_FREQUENCY} always fits in
+ * int, as {@link Integer#MAX_VALUE} would mean 2.1THz.
*
* @return value of an identifier.
*/
diff --git a/core/java/android/hardware/radio/RadioManager.java b/core/java/android/hardware/radio/RadioManager.java
index da6c686..61854e4 100644
--- a/core/java/android/hardware/radio/RadioManager.java
+++ b/core/java/android/hardware/radio/RadioManager.java
@@ -102,7 +102,7 @@
public @interface RadioStatusType{}
- // keep in sync with radio_class_t in /system/core/incluse/system/radio.h
+ // keep in sync with radio_class_t in /system/core/include/system/radio.h
/** Radio module class supporting FM (including HD radio) and AM */
public static final int CLASS_AM_FM = 0;
/** Radio module class supporting satellite radio */
@@ -154,7 +154,7 @@
/**
* Forces mono audio stream reception.
*
- * Analog broadcasts can recover poor reception conditions by jointing
+ * <p>Analog broadcasts can recover poor reception conditions by jointing
* stereo channels into one. Mainly for, but not limited to AM/FM.
*/
public static final int CONFIG_FORCE_MONO = 1;
@@ -176,7 +176,7 @@
/**
* Forces the digital playback for the supporting radio technology.
*
- * User may disable digital-analog handover that happens with poor
+ * <p>User may disable digital-analog handover that happens with poor
* reception conditions. With digital forced, the radio will remain silent
* instead of switching to analog channel if it's available. This is purely
* user choice, it does not reflect the actual state of handover.
@@ -185,7 +185,7 @@
/**
* RDS Alternative Frequencies.
*
- * If set and the currently tuned RDS station broadcasts on multiple
+ * <p>If set and the currently tuned RDS station broadcasts on multiple
* channels, radio tuner automatically switches to the best available
* alternative.
*/
@@ -193,7 +193,7 @@
/**
* RDS region-specific program lock-down.
*
- * Allows user to lock to the current region as they move into the
+ * <p>Allows user to lock to the current region as they move into the
* other region.
*/
public static final int CONFIG_RDS_REG = 5;
@@ -247,11 +247,12 @@
@Retention(RetentionPolicy.SOURCE)
public @interface ConfigFlag {}
- /*****************************************************************************
+ /**
* Lists properties, options and radio bands supported by a given broadcast radio module.
- * Each module has a unique ID used to address it when calling RadioManager APIs.
- * Module properties are returned by {@link #listModules(List <ModuleProperties>)} method.
- ****************************************************************************/
+ *
+ * <p>Each module has a unique ID used to address it when calling RadioManager APIs.
+ * Module properties are returned by {@link #listModules(List)} method.
+ */
public static class ModuleProperties implements Parcelable {
private final int mId;
@@ -315,8 +316,11 @@
return set.stream().mapToInt(Integer::intValue).toArray();
}
- /** Unique module identifier provided by the native service.
- * For use with {@link #openTuner(int, BandConfig, boolean, RadioTuner.Callback, Handler)}.
+ /**
+ * Unique module identifier provided by the native service.
+ *
+ * <p>or use with
+ * {@link #openTuner(int, BandConfig, boolean, RadioTuner.Callback, Handler)}.
* @return the radio module unique identifier.
*/
public int getId() {
@@ -324,22 +328,24 @@
}
/**
- * Module service (driver) name as registered with HIDL.
+ * Module service (driver) name as registered with HIDL or AIDL HAL.
* @return the module service name.
*/
public @NonNull String getServiceName() {
return mServiceName;
}
- /** Module class identifier: {@link #CLASS_AM_FM}, {@link #CLASS_SAT}, {@link #CLASS_DT}
+ /**
+ * Module class identifier: {@link #CLASS_AM_FM}, {@link #CLASS_SAT}, {@link #CLASS_DT}
* @return the radio module class identifier.
*/
public int getClassId() {
return mClassId;
}
- /** Human readable broadcast radio module implementor
- * @return the name of the radio module implementator.
+ /**
+ * Human readable broadcast radio module implementor
+ * @return the name of the radio module implementer.
*/
public String getImplementor() {
return mImplementor;
@@ -352,31 +358,38 @@
return mProduct;
}
- /** Human readable broadcast radio module version number
+ /**
+ * Human readable broadcast radio module version number
* @return the radio module version.
*/
public String getVersion() {
return mVersion;
}
- /** Radio module serial number.
- * Can be used for subscription services.
+ /**
+ * Radio module serial number.
+ *
+ * <p>This can be used for subscription services.
* @return the radio module serial number.
*/
public String getSerial() {
return mSerial;
}
- /** Number of tuners available.
- * This is the number of tuners that can be open simultaneously.
+ /**
+ * Number of tuners available.
+ *
+ * <p>This is the number of tuners that can be open simultaneously.
* @return the number of tuners supported.
*/
public int getNumTuners() {
return mNumTuners;
}
- /** Number tuner audio sources available. Must be less or equal to getNumTuners().
- * When more than one tuner is supported, one is usually for playback and has one
+ /**
+ * Number tuner audio sources available. Must be less or equal to {@link #getNumTuners}.
+ *
+ * <p>When more than one tuner is supported, one is usually for playback and has one
* associated audio source and the other is for pre scanning and building a
* program list.
* @return the number of audio sources available.
@@ -387,20 +400,24 @@
}
/**
- * Checks, if BandConfig initialization (after {@link RadioManager#openTuner})
+ * Checks, if {@link BandConfig} initialization (after {@link RadioManager#openTuner})
* is required to be done before other operations or not.
*
- * If it is, the client has to wait for {@link RadioTuner.Callback#onConfigurationChanged}
- * callback before executing any other operations. Otherwise, such operation will fail
- * returning {@link RadioManager#STATUS_INVALID_OPERATION} error code.
+ * <p>If it is, the client has to wait for
+ * {@link RadioTuner.Callback#onConfigurationChanged} callback before executing any other
+ * operations. Otherwise, such operation will fail returning
+ * {@link RadioManager#STATUS_INVALID_OPERATION} error code.
*/
public boolean isInitializationRequired() {
return mIsInitializationRequired;
}
- /** {@code true} if audio capture is possible from radio tuner output.
- * This indicates if routing to audio devices not connected to the same HAL as the FM radio
- * is possible (e.g. to USB) or DAR (Digital Audio Recorder) feature can be implemented.
+ /**
+ * {@code true} if audio capture is possible from radio tuner output.
+ *
+ * <p>This indicates if routing to audio devices not connected to the same HAL as the FM
+ * radio is possible (e.g. to USB) or DAR (Digital Audio Recorder) feature can be
+ * implemented.
* @return {@code true} if audio capture is possible, {@code false} otherwise.
*/
public boolean isCaptureSupported() {
@@ -421,8 +438,8 @@
/**
* Checks, if a given program type is supported by this tuner.
*
- * If a program type is supported by radio module, it means it can tune
- * to ProgramSelector of a given type.
+ * <p>If a program type is supported by radio module, it means it can tune
+ * to {@link ProgramSelector} of a given type.
*
* @return {@code true} if a given program type is supported.
*/
@@ -433,8 +450,8 @@
/**
* Checks, if a given program identifier is supported by this tuner.
*
- * If an identifier is supported by radio module, it means it can use it for
- * tuning to ProgramSelector with either primary or secondary Identifier of
+ * <p>If an identifier is supported by radio module, it means it can use it for
+ * tuning to {@link ProgramSelector} with either primary or secondary Identifier of
* a given type.
*
* @return {@code true} if a given program type is supported.
@@ -446,9 +463,9 @@
/**
* A frequency table for Digital Audio Broadcasting (DAB).
*
- * The key is a channel name, i.e. 5A, 7B.
+ * <p>The key is a channel name, i.e. 5A, 7B.
*
- * The value is a frequency, in kHz.
+ * <p>The value is a frequency, in kHz.
*
* @return a frequency table, or {@code null} if the module doesn't support DAB
*/
@@ -460,17 +477,18 @@
* A map of vendor-specific opaque strings, passed from HAL without changes.
* Format of these strings can vary across vendors.
*
- * It may be used for extra features, that's not supported by a platform,
+ * <p>It may be used for extra features, that's not supported by a platform,
* for example: preset-slots=6; ultra-hd-capable=false.
*
- * Keys must be prefixed with unique vendor Java-style namespace,
- * eg. 'com.somecompany.parameter1'.
+ * <p>Keys must be prefixed with unique vendor Java-style namespace,
+ * e.g. 'com.somecompany.parameter1'.
*/
public @NonNull Map<String, String> getVendorInfo() {
return mVendorInfo;
}
- /** List of descriptors for all bands supported by this module.
+ /**
+ * List of descriptors for all bands supported by this module.
* @return an array of {@link BandDescriptor}.
*/
public BandDescriptor[] getBands() {
@@ -590,7 +608,9 @@
}
/** Radio band descriptor: an element in ModuleProperties bands array.
- * It is either an instance of {@link FmBandDescriptor} or {@link AmBandDescriptor} */
+ *
+ * <p>It is either an instance of {@link FmBandDescriptor} or {@link AmBandDescriptor}
+ */
public static class BandDescriptor implements Parcelable {
private final int mRegion;
@@ -610,16 +630,18 @@
mSpacing = spacing;
}
- /** Region this band applies to. E.g. {@link #REGION_ITU_1}
+ /**
+ * Region this band applies to. E.g. {@link #REGION_ITU_1}
* @return the region this band is associated to.
*/
public int getRegion() {
return mRegion;
}
- /** Band type, e.g {@link #BAND_FM}. Defines the subclass this descriptor can be cast to:
+ /**
+ * Band type, e.g. {@link #BAND_FM}. Defines the subclass this descriptor can be cast to:
* <ul>
- * <li>{@link #BAND_FM} or {@link #BAND_FM_HD} cast to {@link FmBandDescriptor}, </li>
- * <li>{@link #BAND_AM} cast to {@link AmBandDescriptor}, </li>
+ * <li>{@link #BAND_FM} or {@link #BAND_FM_HD} cast to {@link FmBandDescriptor}</li>
+ * <li>{@link #BAND_AM} cast to {@link AmBandDescriptor}</li>
* </ul>
* @return the band type.
*/
@@ -645,23 +667,29 @@
return mType == BAND_FM || mType == BAND_FM_HD;
}
- /** Lower band limit expressed in units according to band type.
- * Currently all defined band types express channels as frequency in kHz
+ /**
+ * Lower band limit expressed in units according to band type.
+ *
+ * <p>Currently all defined band types express channels as frequency in kHz.
* @return the lower band limit.
*/
public int getLowerLimit() {
return mLowerLimit;
}
- /** Upper band limit expressed in units according to band type.
- * Currently all defined band types express channels as frequency in kHz
+ /**
+ * Upper band limit expressed in units according to band type.
+ *
+ * <p>Currently all defined band types express channels as frequency in kHz.
* @return the upper band limit.
*/
public int getUpperLimit() {
return mUpperLimit;
}
- /** Channel spacing in units according to band type.
- * Currently all defined band types express channels as frequency in kHz
- * @return the channel spacing.
+ /**
+ * Channel spacing in units according to band type.
+ *
+ * <p>Currently all defined band types express channels as frequency in kHz
+ * @return the channel spacing.</p>
*/
public int getSpacing() {
return mSpacing;
@@ -758,9 +786,11 @@
}
}
- /** FM band descriptor
+ /**
+ * FM band descriptor
* @see #BAND_FM
- * @see #BAND_FM_HD */
+ * @see #BAND_FM_HD
+ */
public static class FmBandDescriptor extends BandDescriptor {
private final boolean mStereo;
private final boolean mRds;
@@ -779,19 +809,22 @@
mEa = ea;
}
- /** Stereo is supported
+ /**
+ * Stereo is supported
* @return {@code true} if stereo is supported, {@code false} otherwise.
*/
public boolean isStereoSupported() {
return mStereo;
}
- /** RDS or RBDS(if region is ITU2) is supported
+ /**
+ * RDS or RBDS(if region is ITU2) is supported
* @return {@code true} if RDS or RBDS is supported, {@code false} otherwise.
*/
public boolean isRdsSupported() {
return mRds;
}
- /** Traffic announcement is supported
+ /**
+ * Traffic announcement is supported
* @return {@code true} if TA is supported, {@code false} otherwise.
*/
public boolean isTaSupported() {
@@ -804,8 +837,9 @@
return mAf;
}
- /** Emergency Announcement is supported
- * @return {@code true} if Emergency annoucement is supported, {@code false} otherwise.
+ /**
+ * Emergency Announcement is supported
+ * @return {@code true} if Emergency announcement is supported, {@code false} otherwise.
*/
public boolean isEaSupported() {
return mEa;
@@ -890,8 +924,10 @@
}
}
- /** AM band descriptor.
- * @see #BAND_AM */
+ /**
+ * AM band descriptor.
+ * @see #BAND_AM
+ */
public static class AmBandDescriptor extends BandDescriptor {
private final boolean mStereo;
@@ -903,8 +939,9 @@
mStereo = stereo;
}
- /** Stereo is supported
- * @return {@code true} if stereo is supported, {@code false} otherwise.
+ /**
+ * Stereo is supported
+ * @return {@code true} if stereo is supported, {@code false} otherwise.
*/
public boolean isStereoSupported() {
return mStereo;
@@ -991,39 +1028,47 @@
return mDescriptor;
}
- /** Region this band applies to. E.g. {@link #REGION_ITU_1}
- * @return the region associated with this band.
+ /**
+ * Region this band applies to. E.g. {@link #REGION_ITU_1}
+ * @return the region associated with this band.
*/
public int getRegion() {
return mDescriptor.getRegion();
}
- /** Band type, e.g {@link #BAND_FM}. Defines the subclass this descriptor can be cast to:
+ /**
+ * Band type, e.g. {@link #BAND_FM}. Defines the subclass this descriptor can be cast to:
* <ul>
- * <li>{@link #BAND_FM} or {@link #BAND_FM_HD} cast to {@link FmBandDescriptor}, </li>
- * <li>{@link #BAND_AM} cast to {@link AmBandDescriptor}, </li>
+ * <li>{@link #BAND_FM} or {@link #BAND_FM_HD} cast to {@link FmBandDescriptor}</li>
+ * <li>{@link #BAND_AM} cast to {@link AmBandDescriptor}</li>
* </ul>
* @return the band type.
*/
public int getType() {
return mDescriptor.getType();
}
- /** Lower band limit expressed in units according to band type.
- * Currently all defined band types express channels as frequency in kHz
- * @return the lower band limit.
+ /**
+ * Lower band limit expressed in units according to band type.
+ *
+ * <p>Currently all defined band types express channels as frequency in kHz.
+ * @return the lower band limit.
*/
public int getLowerLimit() {
return mDescriptor.getLowerLimit();
}
- /** Upper band limit expressed in units according to band type.
- * Currently all defined band types express channels as frequency in kHz
- * @return the upper band limit.
+ /**
+ * Upper band limit expressed in units according to band type.
+ *
+ * <p>Currently all defined band types express channels as frequency in kHz.
+ * @return the upper band limit.
*/
public int getUpperLimit() {
return mDescriptor.getUpperLimit();
}
- /** Channel spacing in units according to band type.
- * Currently all defined band types express channels as frequency in kHz
- * @return the channel spacing.
+ /**
+ * Channel spacing in units according to band type.
+ *
+ * <p>Currently all defined band types express channels as frequency in kHz.
+ * @return the channel spacing.
*/
public int getSpacing() {
return mDescriptor.getSpacing();
@@ -1089,9 +1134,11 @@
}
}
- /** FM band configuration.
+ /**
+ * FM band configuration.
* @see #BAND_FM
- * @see #BAND_FM_HD */
+ * @see #BAND_FM_HD
+ */
public static class FmBandConfig extends BandConfig {
private final boolean mStereo;
private final boolean mRds;
@@ -1119,28 +1166,32 @@
mEa = ea;
}
- /** Get stereo enable state
+ /**
+ * Get stereo enable state
* @return the enable state.
*/
public boolean getStereo() {
return mStereo;
}
- /** Get RDS or RBDS(if region is ITU2) enable state
+ /**
+ * Get RDS or RBDS(if region is ITU2) enable state
* @return the enable state.
*/
public boolean getRds() {
return mRds;
}
- /** Get Traffic announcement enable state
+ /**
+ * Get Traffic announcement enable state
* @return the enable state.
*/
public boolean getTa() {
return mTa;
}
- /** Get Alternate Frequency Switching enable state
+ /**
+ * Get Alternate Frequency Switching enable state
* @return the enable state.
*/
public boolean getAf() {
@@ -1285,7 +1336,8 @@
return config;
}
- /** Set stereo enable state
+ /**
+ * Set stereo enable state
* @param state The new enable state.
* @return the same Builder instance.
*/
@@ -1294,7 +1346,8 @@
return this;
}
- /** Set RDS or RBDS(if region is ITU2) enable state
+ /**
+ * Set RDS or RBDS(if region is ITU2) enable state
* @param state The new enable state.
* @return the same Builder instance.
*/
@@ -1303,7 +1356,8 @@
return this;
}
- /** Set Traffic announcement enable state
+ /**
+ * Set Traffic announcement enable state
* @param state The new enable state.
* @return the same Builder instance.
*/
@@ -1312,7 +1366,8 @@
return this;
}
- /** Set Alternate Frequency Switching enable state
+ /**
+ * Set Alternate Frequency Switching enable state
* @param state The new enable state.
* @return the same Builder instance.
*/
@@ -1321,7 +1376,8 @@
return this;
}
- /** Set Emergency Announcement enable state
+ /**
+ * Set Emergency Announcement enable state
* @param state The new enable state.
* @return the same Builder instance.
*/
@@ -1332,8 +1388,10 @@
};
}
- /** AM band configuration.
- * @see #BAND_AM */
+ /**
+ * AM band configuration.
+ * @see #BAND_AM
+ */
public static class AmBandConfig extends BandConfig {
private final boolean mStereo;
@@ -1349,7 +1407,8 @@
mStereo = stereo;
}
- /** Get stereo enable state
+ /**
+ * Get stereo enable state
* @return the enable state.
*/
public boolean getStereo() {
@@ -1453,7 +1512,8 @@
return config;
}
- /** Set stereo enable state
+ /**
+ * Set stereo enable state
* @param state The new enable state.
* @return the same Builder instance.
*/
@@ -1467,7 +1527,8 @@
/** Radio program information. */
public static class ProgramInfo implements Parcelable {
- // sourced from hardware/interfaces/broadcastradio/2.0/types.hal
+ // sourced from
+ // hardware/interfaces/broadcastradio/aidl/android/hardware/broadcastradio/ProgramInfo.aidl
private static final int FLAG_LIVE = 1 << 0;
private static final int FLAG_MUTED = 1 << 1;
private static final int FLAG_TRAFFIC_PROGRAM = 1 << 2;
@@ -1521,10 +1582,10 @@
/**
* Identifier currently used for program selection.
*
- * This identifier can be used to determine which technology is
+ * <p>This identifier can be used to determine which technology is
* currently being used for reception.
*
- * Some program selectors contain tuning information for different radio
+ * <p>Some program selectors contain tuning information for different radio
* technologies (i.e. FM RDS and DAB). For example, user may tune using
* a ProgramSelector with RDS_PI primary identifier, but the tuner hardware
* may choose to use DAB technology to make actual tuning. This identifier
@@ -1537,7 +1598,7 @@
/**
* Identifier currently used by hardware to physically tune to a channel.
*
- * Some radio technologies broadcast the same program on multiple channels,
+ * <p>Some radio technologies broadcast the same program on multiple channels,
* i.e. with RDS AF the same program may be broadcasted on multiple
* alternative frequencies; the same DAB program may be broadcast on
* multiple ensembles. This identifier points to the channel to which the
@@ -1550,11 +1611,11 @@
/**
* Primary identifiers of related contents.
*
- * Some radio technologies provide pointers to other programs that carry
+ * <p>Some radio technologies provide pointers to other programs that carry
* related content (i.e. DAB soft-links). This field is a list of pointers
* to other programs on the program list.
*
- * Please note, that these identifiers does not have to exist on the program
+ * <p>Please note, that these identifiers does not have to exist on the program
* list - i.e. DAB tuner may provide information on FM RDS alternatives
* despite not supporting FM RDS. If the system has multiple tuners, another
* one may have it on its list.
@@ -1563,7 +1624,8 @@
return mRelatedContent;
}
- /** Main channel expressed in units according to band type.
+ /**
+ * Main channel expressed in units according to band type.
* Currently all defined band types express channels as frequency in kHz
* @return the program channel
* @deprecated Use {@link ProgramInfo#getSelector} instead.
@@ -1578,7 +1640,8 @@
}
}
- /** Sub channel ID. E.g 1 for HD radio HD1
+ /**
+ * Sub channel ID. E.g. 1 for HD radio HD1
* @return the program sub channel
* @deprecated Use {@link ProgramInfo#getSelector} instead.
*/
@@ -1600,14 +1663,16 @@
return (mInfoFlags & FLAG_TUNED) != 0;
}
- /** {@code true} if the received program is stereo
+ /**
+ * {@code true} if the received program is stereo
* @return {@code true} if stereo, {@code false} otherwise.
*/
public boolean isStereo() {
return (mInfoFlags & FLAG_STEREO) != 0;
}
- /** {@code true} if the received program is digital (e.g HD radio)
+ /**
+ * {@code true} if the received program is digital (e.g. HD radio)
* @return {@code true} if digital, {@code false} otherwise.
* @deprecated Use {@link ProgramInfo#getLogicallyTunedTo()} instead.
*/
@@ -1623,8 +1688,9 @@
/**
* {@code true} if the program is currently playing live stream.
- * This may result in a slightly altered reception parameters,
- * usually targetted at reduced latency.
+ *
+ * <p>This may result in a slightly altered reception parameters,
+ * usually targeted at reduced latency.
*/
public boolean isLive() {
return (mInfoFlags & FLAG_LIVE) != 0;
@@ -1634,7 +1700,8 @@
* {@code true} if radio stream is not playing, i.e. due to bad reception
* conditions or buffering. In this state volume knob MAY be disabled to
* prevent user increasing volume too much.
- * It does NOT mean the user has muted audio.
+ *
+ * <p>It does NOT mean the user has muted audio.
*/
public boolean isMuted() {
return (mInfoFlags & FLAG_MUTED) != 0;
@@ -1688,8 +1755,9 @@
}
/** Metadata currently received from this station.
- * null if no metadata have been received
- * @return current meta data received from this program.
+ *
+ * @return current meta data received from this program, {@code null} if no metadata have
+ * been received
*/
public RadioMetadata getMetadata() {
return mMetadata;
@@ -1699,11 +1767,11 @@
* A map of vendor-specific opaque strings, passed from HAL without changes.
* Format of these strings can vary across vendors.
*
- * It may be used for extra features, that's not supported by a platform,
+ * <p>It may be used for extra features, that's not supported by a platform,
* for example: paid-service=true; bitrate=320kbps.
*
- * Keys must be prefixed with unique vendor Java-style namespace,
- * eg. 'com.somecompany.parameter1'.
+ * <p>Keys must be prefixed with unique vendor Java-style namespace,
+ * e.g. 'com.somecompany.parameter1'.
*/
public @NonNull Map<String, String> getVendorInfo() {
return mVendorInfo;
@@ -1830,13 +1898,14 @@
/**
* Open an interface to control a tuner on a given broadcast radio module.
- * Optionally selects and applies the configuration passed as "config" argument.
+ *
+ * <p>Optionally selects and applies the configuration passed as "config" argument.
* @param moduleId radio module identifier {@link ModuleProperties#getId()}. Mandatory.
* @param config desired band and configuration to apply when enabling the hardware module.
* optional, can be null.
* @param withAudio {@code true} to request a tuner with an audio source.
* This tuner is intended for live listening or recording or a radio program.
- * If {@code false}, the tuner can only be used to retrieve program informations.
+ * If {@code false}, the tuner can only be used to retrieve program information.
* @param callback {@link RadioTuner.Callback} interface. Mandatory.
* @param handler the Handler on which the callbacks will be received.
* Can be null if default handler is OK.
diff --git a/core/java/android/hardware/radio/RadioMetadata.java b/core/java/android/hardware/radio/RadioMetadata.java
index 67381ec..31880fd 100644
--- a/core/java/android/hardware/radio/RadioMetadata.java
+++ b/core/java/android/hardware/radio/RadioMetadata.java
@@ -291,7 +291,7 @@
/**
* Provides a Clock that can be used to describe time as provided by the Radio.
*
- * The clock is defined by the seconds since epoch at the UTC + 0 timezone
+ * <p>The clock time is defined by the seconds since epoch at the UTC + 0 timezone
* and timezone offset from UTC + 0 represented in number of minutes.
*
* @hide
@@ -493,16 +493,16 @@
/**
* Retrieves an identifier for a bitmap.
*
- * The format of an identifier is opaque to the application,
+ * <p>The format of an identifier is opaque to the application,
* with a special case of value 0 being invalid.
* An identifier for a given image-tuner pair is unique, so an application
* may cache images and determine if there is a necessity to fetch them
* again - if identifier changes, it means the image has changed.
*
- * Only bitmap keys may be used with this method:
+ * <p>Only bitmap keys may be used with this method:
* <ul>
- * <li>{@link #METADATA_KEY_ICON}</li>
- * <li>{@link #METADATA_KEY_ART}</li>
+ * <li>{@link #METADATA_KEY_ICON}</li>
+ * <li>{@link #METADATA_KEY_ART}</li>
* </ul>
*
* @param key The key the value is stored under.
@@ -537,7 +537,7 @@
*
* <p>Only string array keys may be used with this method:
* <ul>
- * <li>{@link #METADATA_KEY_UFIDS}</li>
+ * <li>{@link #METADATA_KEY_UFIDS}</li>
* </ul>
*
* @param key The key the value is stored under
@@ -667,17 +667,17 @@
* the METADATA_KEYs defined in this class are used they may only be one
* of the following:
* <ul>
- * <li>{@link #METADATA_KEY_RDS_PS}</li>
- * <li>{@link #METADATA_KEY_RDS_RT}</li>
- * <li>{@link #METADATA_KEY_TITLE}</li>
- * <li>{@link #METADATA_KEY_ARTIST}</li>
- * <li>{@link #METADATA_KEY_ALBUM}</li>
- * <li>{@link #METADATA_KEY_GENRE}</li>
- * <li>{@link #METADATA_KEY_COMMENT_SHORT_DESCRIPTION}</li>
- * <li>{@link #METADATA_KEY_COMMENT_ACTUAL_TEXT}</li>
- * <li>{@link #METADATA_KEY_COMMERCIAL}</li>
- * <li>{@link #METADATA_KEY_HD_STATION_NAME_SHORT}</li>
- * <li>{@link #METADATA_KEY_HD_STATION_NAME_LONG}</li>
+ * <li>{@link #METADATA_KEY_RDS_PS}</li>
+ * <li>{@link #METADATA_KEY_RDS_RT}</li>
+ * <li>{@link #METADATA_KEY_TITLE}</li>
+ * <li>{@link #METADATA_KEY_ARTIST}</li>
+ * <li>{@link #METADATA_KEY_ALBUM}</li>
+ * <li>{@link #METADATA_KEY_GENRE}</li>
+ * <li>{@link #METADATA_KEY_COMMENT_SHORT_DESCRIPTION}</li>
+ * <li>{@link #METADATA_KEY_COMMENT_ACTUAL_TEXT}</li>
+ * <li>{@link #METADATA_KEY_COMMERCIAL}</li>
+ * <li>{@link #METADATA_KEY_HD_STATION_NAME_SHORT}</li>
+ * <li>{@link #METADATA_KEY_HD_STATION_NAME_LONG}</li>
* </ul>
*
* @param key The key for referencing this value
@@ -699,10 +699,10 @@
* the METADATA_KEYs defined in this class are used they may only be one
* of the following:
* <ul>
- * <li>{@link #METADATA_KEY_RDS_PI}</li>
- * <li>{@link #METADATA_KEY_RDS_PTY}</li>
- * <li>{@link #METADATA_KEY_RBDS_PTY}</li>
- * <li>{@link #METADATA_KEY_HD_SUBCHANNELS_AVAILABLE}</li>
+ * <li>{@link #METADATA_KEY_RDS_PI}</li>
+ * <li>{@link #METADATA_KEY_RDS_PTY}</li>
+ * <li>{@link #METADATA_KEY_RBDS_PTY}</li>
+ * <li>{@link #METADATA_KEY_HD_SUBCHANNELS_AVAILABLE}</li>
* </ul>
* or any bitmap represented by its identifier.
*
@@ -720,8 +720,8 @@
* if the METADATA_KEYs defined in this class are used they may only be
* one of the following:
* <ul>
- * <li>{@link #METADATA_KEY_ICON}</li>
- * <li>{@link #METADATA_KEY_ART}</li>
+ * <li>{@link #METADATA_KEY_ICON}</li>
+ * <li>{@link #METADATA_KEY_ART}</li>
* </ul>
* <p>
*
@@ -765,7 +765,7 @@
* the METADATA_KEYs defined in this class are used they may only be one
* of the following:
* <ul>
- * <li>{@link #METADATA_KEY_UFIDS}</li>
+ * <li>{@link #METADATA_KEY_UFIDS}</li>
* </ul>
*
* @param key The key for referencing this value
diff --git a/core/java/android/hardware/radio/flags.aconfig b/core/java/android/hardware/radio/flags.aconfig
index dbc1a4b..d0d10c1 100644
--- a/core/java/android/hardware/radio/flags.aconfig
+++ b/core/java/android/hardware/radio/flags.aconfig
@@ -2,6 +2,7 @@
flag {
name: "hd_radio_improved"
+ is_exported: true
namespace: "car_framework"
description: "Feature flag for improved HD radio support with less vendor extensions"
bug: "280300929"
diff --git a/core/java/android/hardware/usb/flags/system_sw_usb_flags.aconfig b/core/java/android/hardware/usb/flags/system_sw_usb_flags.aconfig
index 9e487e1..fac02ce 100644
--- a/core/java/android/hardware/usb/flags/system_sw_usb_flags.aconfig
+++ b/core/java/android/hardware/usb/flags/system_sw_usb_flags.aconfig
@@ -2,6 +2,7 @@
flag {
name: "enable_usb_data_compliance_warning"
+ is_exported: true
namespace: "system_sw_usb"
description: "Enable USB data compliance warnings when set"
bug: "296119135"
diff --git a/core/java/android/hardware/usb/flags/usb_framework_flags.aconfig b/core/java/android/hardware/usb/flags/usb_framework_flags.aconfig
index a495631..3dd746c 100644
--- a/core/java/android/hardware/usb/flags/usb_framework_flags.aconfig
+++ b/core/java/android/hardware/usb/flags/usb_framework_flags.aconfig
@@ -2,6 +2,7 @@
flag {
name: "enable_is_pd_compliant_api"
+ is_exported: true
namespace: "usb"
description: "Feature flag for the api to check if a port is PD compliant"
bug: "323470419"
@@ -9,6 +10,7 @@
flag {
name: "enable_is_mode_change_supported_api"
+ is_exported: true
namespace: "usb"
description: "Feature flag for the api to check if a port supports mode change"
bug: "323470419"
diff --git a/core/java/android/net/vcn/flags.aconfig b/core/java/android/net/vcn/flags.aconfig
index 97b773e..e64823a 100644
--- a/core/java/android/net/vcn/flags.aconfig
+++ b/core/java/android/net/vcn/flags.aconfig
@@ -20,4 +20,18 @@
namespace: "vcn"
description: "Feature flag for enabling network metric monitor"
bug: "282996138"
+}
+
+flag{
+ name: "validate_network_on_ipsec_loss"
+ namespace: "vcn"
+ description: "Trigger network validation when IPsec packet loss exceeds the threshold"
+ bug: "329139898"
+}
+
+flag{
+ name: "evaluate_ipsec_loss_on_lp_nc_change"
+ namespace: "vcn"
+ description: "Re-evaluate IPsec packet loss on LinkProperties or NetworkCapabilities change"
+ bug: "323238888"
}
\ No newline at end of file
diff --git a/core/java/android/os/Bundle.java b/core/java/android/os/Bundle.java
index 387eebe..ed4037c 100644
--- a/core/java/android/os/Bundle.java
+++ b/core/java/android/os/Bundle.java
@@ -18,6 +18,7 @@
import static java.util.Objects.requireNonNull;
+import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SuppressLint;
@@ -31,6 +32,8 @@
import com.android.internal.annotations.VisibleForTesting;
import java.io.Serializable;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.List;
@@ -53,6 +56,53 @@
@VisibleForTesting
static final int FLAG_ALLOW_FDS = 1 << 10;
+ @VisibleForTesting
+ static final int FLAG_HAS_BINDERS_KNOWN = 1 << 11;
+
+ @VisibleForTesting
+ static final int FLAG_HAS_BINDERS = 1 << 12;
+
+
+ /**
+ * Status when the Bundle can <b>assert</b> that the underlying Parcel DOES NOT contain
+ * Binder object(s).
+ *
+ * @hide
+ */
+ public static final int STATUS_BINDERS_NOT_PRESENT = 0;
+
+ /**
+ * Status when the Bundle can <b>assert</b> that there are Binder object(s) in the Parcel.
+ *
+ * @hide
+ */
+ public static final int STATUS_BINDERS_PRESENT = 1;
+
+ /**
+ * Status when the Bundle cannot be checked for Binders and there is no parcelled data
+ * available to check either.
+ * <p> This could happen when a Bundle is unparcelled or was never parcelled, and modified such
+ * that it is not possible to assert if the Bundle has any Binder objects in the current state.
+ *
+ * For e.g. calling {@link #putParcelable} or {@link #putBinder} could have added a Binder
+ * object to the Bundle but it is not possible to assert this fact unless the Bundle is written
+ * to a Parcel.
+ * </p>
+ *
+ * @hide
+ */
+ public static final int STATUS_BINDERS_UNKNOWN = 2;
+
+ /** @hide */
+ @IntDef(flag = true, prefix = {"STATUS_BINDERS_"}, value = {
+ STATUS_BINDERS_PRESENT,
+ STATUS_BINDERS_UNKNOWN,
+ STATUS_BINDERS_NOT_PRESENT
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface HasBinderStatus {
+ }
+
/** An unmodifiable {@code Bundle} that is always {@link #isEmpty() empty}. */
public static final Bundle EMPTY;
@@ -75,7 +125,7 @@
*/
public Bundle() {
super();
- mFlags = FLAG_HAS_FDS_KNOWN | FLAG_ALLOW_FDS;
+ mFlags = FLAG_HAS_FDS_KNOWN | FLAG_HAS_BINDERS_KNOWN | FLAG_ALLOW_FDS;
}
/**
@@ -111,7 +161,6 @@
*
* @param from The bundle to be copied.
* @param deep Whether is a deep or shallow copy.
- *
* @hide
*/
Bundle(Bundle from, boolean deep) {
@@ -143,7 +192,7 @@
*/
public Bundle(ClassLoader loader) {
super(loader);
- mFlags = FLAG_HAS_FDS_KNOWN | FLAG_ALLOW_FDS;
+ mFlags = FLAG_HAS_FDS_KNOWN | FLAG_HAS_BINDERS_KNOWN | FLAG_ALLOW_FDS;
}
/**
@@ -154,7 +203,7 @@
*/
public Bundle(int capacity) {
super(capacity);
- mFlags = FLAG_HAS_FDS_KNOWN | FLAG_ALLOW_FDS;
+ mFlags = FLAG_HAS_FDS_KNOWN | FLAG_HAS_BINDERS_KNOWN | FLAG_ALLOW_FDS;
}
/**
@@ -180,7 +229,7 @@
*/
public Bundle(PersistableBundle b) {
super(b);
- mFlags = FLAG_HAS_FDS_KNOWN | FLAG_ALLOW_FDS;
+ mFlags = FLAG_HAS_FDS_KNOWN | FLAG_HAS_BINDERS_KNOWN | FLAG_ALLOW_FDS;
}
/**
@@ -292,6 +341,9 @@
if ((mFlags & FLAG_HAS_FDS) != 0) {
mFlags &= ~FLAG_HAS_FDS_KNOWN;
}
+ if ((mFlags & FLAG_HAS_BINDERS) != 0) {
+ mFlags &= ~FLAG_HAS_BINDERS_KNOWN;
+ }
}
/**
@@ -306,13 +358,20 @@
bundle.mOwnsLazyValues = false;
mMap.putAll(bundle.mMap);
- // FD state is now known if and only if both bundles already knew
+ // FD and Binders state is now known if and only if both bundles already knew
if ((bundle.mFlags & FLAG_HAS_FDS) != 0) {
mFlags |= FLAG_HAS_FDS;
}
if ((bundle.mFlags & FLAG_HAS_FDS_KNOWN) == 0) {
mFlags &= ~FLAG_HAS_FDS_KNOWN;
}
+
+ if ((bundle.mFlags & FLAG_HAS_BINDERS) != 0) {
+ mFlags |= FLAG_HAS_BINDERS;
+ }
+ if ((bundle.mFlags & FLAG_HAS_BINDERS_KNOWN) == 0) {
+ mFlags &= ~FLAG_HAS_BINDERS_KNOWN;
+ }
}
/**
@@ -343,6 +402,33 @@
return (mFlags & FLAG_HAS_FDS) != 0;
}
+ /**
+ * Returns a status indicating whether the bundle contains any parcelled Binder objects.
+ * @hide
+ */
+ public @HasBinderStatus int hasBinders() {
+ if ((mFlags & FLAG_HAS_BINDERS_KNOWN) != 0) {
+ if ((mFlags & FLAG_HAS_BINDERS) != 0) {
+ return STATUS_BINDERS_PRESENT;
+ } else {
+ return STATUS_BINDERS_NOT_PRESENT;
+ }
+ }
+
+ final Parcel p = mParcelledData;
+ if (p == null) {
+ return STATUS_BINDERS_UNKNOWN;
+ }
+ if (p.hasBinders()) {
+ mFlags = mFlags | FLAG_HAS_BINDERS | FLAG_HAS_BINDERS_KNOWN;
+ return STATUS_BINDERS_PRESENT;
+ } else {
+ mFlags = mFlags & ~FLAG_HAS_BINDERS;
+ mFlags |= FLAG_HAS_BINDERS_KNOWN;
+ return STATUS_BINDERS_NOT_PRESENT;
+ }
+ }
+
/** {@hide} */
@Override
public void putObject(@Nullable String key, @Nullable Object value) {
@@ -464,6 +550,7 @@
unparcel();
mMap.put(key, value);
mFlags &= ~FLAG_HAS_FDS_KNOWN;
+ mFlags &= ~FLAG_HAS_BINDERS_KNOWN;
}
/**
@@ -502,6 +589,7 @@
unparcel();
mMap.put(key, value);
mFlags &= ~FLAG_HAS_FDS_KNOWN;
+ mFlags &= ~FLAG_HAS_BINDERS_KNOWN;
}
/**
@@ -517,6 +605,7 @@
unparcel();
mMap.put(key, value);
mFlags &= ~FLAG_HAS_FDS_KNOWN;
+ mFlags &= ~FLAG_HAS_BINDERS_KNOWN;
}
/** {@hide} */
@@ -525,6 +614,7 @@
unparcel();
mMap.put(key, value);
mFlags &= ~FLAG_HAS_FDS_KNOWN;
+ mFlags &= ~FLAG_HAS_BINDERS_KNOWN;
}
/**
@@ -540,6 +630,7 @@
unparcel();
mMap.put(key, value);
mFlags &= ~FLAG_HAS_FDS_KNOWN;
+ mFlags &= ~FLAG_HAS_BINDERS_KNOWN;
}
/**
@@ -680,6 +771,7 @@
public void putBinder(@Nullable String key, @Nullable IBinder value) {
unparcel();
mMap.put(key, value);
+ mFlags &= ~FLAG_HAS_BINDERS_KNOWN;
}
/**
@@ -697,6 +789,7 @@
public void putIBinder(@Nullable String key, @Nullable IBinder value) {
unparcel();
mMap.put(key, value);
+ mFlags &= ~FLAG_HAS_BINDERS_KNOWN;
}
/**
diff --git a/core/java/android/os/Parcel.java b/core/java/android/os/Parcel.java
index ccfb632..bcef815 100644
--- a/core/java/android/os/Parcel.java
+++ b/core/java/android/os/Parcel.java
@@ -475,6 +475,10 @@
private static native boolean nativeHasFileDescriptors(long nativePtr);
private static native boolean nativeHasFileDescriptorsInRange(
long nativePtr, int offset, int length);
+
+ private static native boolean nativeHasBinders(long nativePtr);
+ private static native boolean nativeHasBindersInRange(
+ long nativePtr, int offset, int length);
@RavenwoodThrow
private static native void nativeWriteInterfaceToken(long nativePtr, String interfaceName);
@RavenwoodThrow
@@ -970,6 +974,34 @@
}
/**
+ * Report whether the parcel contains any marshalled IBinder objects.
+ *
+ * @throws UnsupportedOperationException if binder kernel driver was disabled or if method was
+ * invoked in case of Binder RPC protocol.
+ * @hide
+ */
+ public boolean hasBinders() {
+ return nativeHasBinders(mNativePtr);
+ }
+
+ /**
+ * Report whether the parcel contains any marshalled {@link IBinder} objects in the range
+ * defined by {@code offset} and {@code length}.
+ *
+ * @param offset The offset from which the range starts. Should be between 0 and
+ * {@link #dataSize()}.
+ * @param length The length of the range. Should be between 0 and {@link #dataSize()} - {@code
+ * offset}.
+ * @return whether there are binders in the range or not.
+ * @throws IllegalArgumentException if the parameters are out of the permitted ranges.
+ *
+ * @hide
+ */
+ public boolean hasBinders(int offset, int length) {
+ return nativeHasBindersInRange(mNativePtr, offset, length);
+ }
+
+ /**
* Store or read an IBinder interface token in the parcel at the current
* {@link #dataPosition}. This is used to validate that the marshalled
* transaction is intended for the target interface. This is typically written
diff --git a/core/java/android/os/Process.java b/core/java/android/os/Process.java
index 7020a38..db06a6b 100644
--- a/core/java/android/os/Process.java
+++ b/core/java/android/os/Process.java
@@ -48,6 +48,7 @@
import java.io.FileDescriptor;
import java.io.IOException;
import java.util.Map;
+import java.util.NoSuchElementException;
import java.util.concurrent.TimeoutException;
/**
@@ -588,6 +589,8 @@
**/
public static final int THREAD_GROUP_RESTRICTED = 7;
+ /** @hide */
+ public static final int SIGNAL_DEFAULT = 0;
public static final int SIGNAL_QUIT = 3;
public static final int SIGNAL_KILL = 9;
public static final int SIGNAL_USR1 = 10;
@@ -1437,6 +1440,49 @@
sendSignal(pid, SIGNAL_KILL);
}
+ /**
+ * Check the tgid and tid pair to see if the tid still exists and belong to the tgid.
+ *
+ * TOCTOU warning: the status of the tid can change at the time this method returns. This should
+ * be used in very rare cases such as checking if a (tid, tgid) pair that is known to exist
+ * recently no longer exists now. As the possibility of the same tid to be reused under the same
+ * tgid during a short window is rare. And even if it happens the caller logic should be robust
+ * to handle it without error.
+ *
+ * @throws IllegalArgumentException if tgid or tid is not positive.
+ * @throws SecurityException if the caller doesn't have the permission, this method is expected
+ * to be used by system process with {@link #SYSTEM_UID} because it
+ * internally uses tkill(2).
+ * @throws NoSuchElementException if the Linux process with pid as the tid has exited or it
+ * doesn't belong to the tgid.
+ * @hide
+ */
+ public static final void checkTid(int tgid, int tid)
+ throws IllegalArgumentException, SecurityException, NoSuchElementException {
+ sendTgSignalThrows(tgid, tid, SIGNAL_DEFAULT);
+ }
+
+ /**
+ * Check if the pid still exists.
+ *
+ * TOCTOU warning: the status of the pid can change at the time this method returns. This should
+ * be used in very rare cases such as checking if a pid that belongs to an isolated process of a
+ * uid known to exist recently no longer exists now. As the possibility of the same pid to be
+ * reused again under the same uid during a short window is rare. And even if it happens the
+ * caller logic should be robust to handle it without error.
+ *
+ * @throws IllegalArgumentException if pid is not positive.
+ * @throws SecurityException if the caller doesn't have the permission, this method is expected
+ * to be used by system process with {@link #SYSTEM_UID} because it
+ * internally uses kill(2).
+ * @throws NoSuchElementException if the Linux process with the pid has exited.
+ * @hide
+ */
+ public static final void checkPid(int pid)
+ throws IllegalArgumentException, SecurityException, NoSuchElementException {
+ sendSignalThrows(pid, SIGNAL_DEFAULT);
+ }
+
/** @hide */
public static final native int setUid(int uid);
@@ -1451,6 +1497,12 @@
*/
public static final native void sendSignal(int pid, int signal);
+ private static native void sendSignalThrows(int pid, int signal)
+ throws IllegalArgumentException, SecurityException, NoSuchElementException;
+
+ private static native void sendTgSignalThrows(int pid, int tgid, int signal)
+ throws IllegalArgumentException, SecurityException, NoSuchElementException;
+
/**
* @hide
* Private impl for avoiding a log message... DO NOT USE without doing
diff --git a/core/java/android/os/UserManager.java b/core/java/android/os/UserManager.java
index 84619a0..f172c3e 100644
--- a/core/java/android/os/UserManager.java
+++ b/core/java/android/os/UserManager.java
@@ -3188,6 +3188,8 @@
* @return whether the context user can add a private profile.
* @hide
*/
+ @TestApi
+ @FlaggedApi(android.os.Flags.FLAG_ALLOW_PRIVATE_PROFILE)
@RequiresPermission(anyOf = {
Manifest.permission.MANAGE_USERS,
Manifest.permission.CREATE_USERS},
diff --git a/core/java/android/os/flags.aconfig b/core/java/android/os/flags.aconfig
index 375d729..311e991 100644
--- a/core/java/android/os/flags.aconfig
+++ b/core/java/android/os/flags.aconfig
@@ -26,6 +26,7 @@
flag {
name: "remove_app_profiler_pss_collection"
+ is_exported: true
namespace: "backstage_power"
description: "Replaces background PSS collection in AppProfiler with RSS"
bug: "297542292"
@@ -33,6 +34,7 @@
flag {
name: "allow_thermal_headroom_thresholds"
+ is_exported: true
namespace: "game"
description: "Enable thermal headroom thresholds API"
bug: "288119641"
@@ -41,6 +43,7 @@
# This flag guards the private space feature, its APIs, and some of the feature implementations. The flag android.multiuser.Flags.enable_private_space_features exclusively guards all the implementations.
flag {
name: "allow_private_profile"
+ is_exported: true
namespace: "profile_experiences"
description: "Guards a new Private Profile type in UserManager - everything from its setup to config to deletion."
bug: "299069460"
@@ -49,6 +52,7 @@
flag {
name: "bugreport_mode_max_value"
+ is_exported: true
namespace: "telephony"
description: "Introduce a constant as maximum value of bugreport mode."
bug: "305067125"
@@ -56,6 +60,7 @@
flag {
name: "adpf_prefer_power_efficiency"
+ is_exported: true
namespace: "game"
description: "Guards the ADPF power efficiency API"
bug: "288117936"
@@ -63,6 +68,7 @@
flag {
name: "security_state_service"
+ is_exported: true
namespace: "dynamic_spl"
description: "Guards the Security State API."
bug: "302189431"
@@ -70,6 +76,7 @@
flag {
name: "battery_saver_supported_check_api"
+ is_exported: true
namespace: "backstage_power"
description: "Guards a new API in PowerManager to check if battery saver is supported or not."
bug: "305067031"
@@ -77,6 +84,7 @@
flag {
name: "adpf_gpu_report_actual_work_duration"
+ is_exported: true
namespace: "game"
description: "Guards the ADPF GPU APIs."
bug: "284324521"
@@ -114,6 +122,7 @@
flag {
name: "battery_part_status_api"
+ is_exported: true
namespace: "phoenix"
description: "Feature flag for adding Health HAL v3 APIs."
is_fixed_read_only: true
@@ -122,6 +131,7 @@
flag {
name: "storage_lifetime_api"
+ is_exported: true
namespace: "phoenix"
description: "Feature flag for adding storage component health APIs."
is_fixed_read_only: true
@@ -131,6 +141,7 @@
flag {
namespace: "system_performance"
name: "telemetry_apis_framework_initialization"
+ is_exported: true
description: "Control framework initialization APIs of telemetry APIs feature."
is_fixed_read_only: true
bug: "324241334"
diff --git a/core/java/android/os/vibrator/flags.aconfig b/core/java/android/os/vibrator/flags.aconfig
index d485eca..bb0498e 100644
--- a/core/java/android/os/vibrator/flags.aconfig
+++ b/core/java/android/os/vibrator/flags.aconfig
@@ -10,6 +10,7 @@
flag {
namespace: "haptics"
name: "haptics_customization_enabled"
+ is_exported: true
description: "Enables the haptics customization feature"
bug: "241918098"
}
diff --git a/core/java/android/permission/flags.aconfig b/core/java/android/permission/flags.aconfig
index 999bc99..2710df2 100644
--- a/core/java/android/permission/flags.aconfig
+++ b/core/java/android/permission/flags.aconfig
@@ -2,6 +2,7 @@
flag {
name: "device_aware_permission_apis_enabled"
+ is_exported: true
is_fixed_read_only: true
namespace: "permissions"
description: "enable device aware permission APIs"
@@ -10,6 +11,7 @@
flag {
name: "voice_activation_permission_apis"
+ is_exported: true
namespace: "permissions"
description: "enable voice activation permission APIs"
bug: "287264308"
@@ -17,6 +19,7 @@
flag {
name: "system_server_role_controller_enabled"
+ is_exported: true
is_fixed_read_only: true
namespace: "permissions"
description: "enable role controller in system server"
@@ -25,6 +28,7 @@
flag {
name: "set_next_attribution_source"
+ is_exported: true
namespace: "permissions"
description: "enable AttributionSource.setNextAttributionSource"
bug: "304478648"
@@ -32,6 +36,7 @@
flag {
name: "should_register_attribution_source"
+ is_exported: true
namespace: "permissions"
description: "enable the shouldRegisterAttributionSource API"
bug: "305057691"
@@ -39,6 +44,7 @@
flag {
name: "attribution_source_constructor"
+ is_exported: true
namespace: "permissions"
description: "enable AttributionSource(int, int, String, String, IBinder, String[], AttributionSource)"
bug: "304478648"
@@ -46,6 +52,7 @@
flag {
name: "enhanced_confirmation_mode_apis_enabled"
+ is_exported: true
is_fixed_read_only: true
namespace: "permissions"
description: "enable enhanced confirmation mode apis"
@@ -54,6 +61,7 @@
flag {
name: "op_enable_mobile_data_by_user"
+ is_exported: true
namespace: "permissions"
description: "enables logging of the OP_ENABLE_MOBILE_DATA_BY_USER"
bug: "222650148"
@@ -61,6 +69,7 @@
flag {
name: "factory_reset_prep_permission_apis"
+ is_exported: true
namespace: "wallet_integration"
description: "enable Permission PREPARE_FACTORY_RESET."
bug: "302016478"
@@ -68,6 +77,7 @@
flag {
name: "retail_demo_role_enabled"
+ is_exported: true
namespace: "permissions"
description: "default retail demo role holder"
bug: "274132354"
@@ -82,6 +92,7 @@
flag {
name: "wallet_role_enabled"
+ is_exported: true
namespace: "wallet_integration"
description: "This flag is used to enabled the Wallet Role for all users on the device"
bug: "283989236"
@@ -114,6 +125,7 @@
flag {
name: "get_emergency_role_holder_api_enabled"
+ is_exported: true
is_fixed_read_only: true
namespace: "permissions"
description: "Enables the getEmergencyRoleHolder API."
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index e26dc73..d0593e7 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -11090,21 +11090,12 @@
"assist_long_press_home_enabled";
/**
- * Whether press and hold on nav handle can trigger search.
+ * Whether all entrypoints can trigger search. Replaces individual settings.
*
* @hide
*/
- public static final String SEARCH_PRESS_HOLD_NAV_HANDLE_ENABLED =
- "search_press_hold_nav_handle_enabled";
-
- /**
- * Whether long-pressing on the home button can trigger search.
- *
- * @hide
- */
- public static final String SEARCH_LONG_PRESS_HOME_ENABLED =
- "search_long_press_home_enabled";
-
+ public static final String SEARCH_ALL_ENTRYPOINTS_ENABLED =
+ "search_all_entrypoints_enabled";
/**
* Whether or not the accessibility data streaming is enbled for the
@@ -12395,6 +12386,17 @@
*/
public static final String HIDE_PRIVATESPACE_ENTRY_POINT = "hide_privatespace_entry_point";
+ /**
+ * Whether or not secure windows should be disabled. This only works on debuggable builds.
+ *
+ * <p>When this setting is set to a non-zero value, all windows are treated as non-secure.
+ * Content in windows with {@link android.view.WindowManager.LayoutParams#FLAG_SECURE} will
+ * appear in screenshots and recordings.
+ *
+ * @hide
+ */
+ public static final String DISABLE_SECURE_WINDOWS = "disable_secure_windows";
+
/** @hide */
public static final int PRIVATE_SPACE_AUTO_LOCK_ON_DEVICE_LOCK = 0;
/** @hide */
diff --git a/core/java/android/provider/flags.aconfig b/core/java/android/provider/flags.aconfig
index ea1ac27..9245557 100644
--- a/core/java/android/provider/flags.aconfig
+++ b/core/java/android/provider/flags.aconfig
@@ -2,6 +2,7 @@
flag {
name: "system_settings_default"
+ is_exported: true
namespace: "package_manager_service"
description: "Enable Settings.System.resetToDefault APIs."
bug: "279083734"
@@ -9,6 +10,7 @@
flag {
name: "user_keys"
+ is_exported: true
namespace: "privacy_infra_policy"
description: "This flag controls new E2EE contact keys API"
bug: "290696572"
@@ -16,6 +18,7 @@
flag {
name: "backup_tasks_settings_screen"
+ is_exported: true
namespace: "backstage_power"
description: "Add a new settings page for the RUN_BACKUP_JOBS permission."
bug: "320563660"
diff --git a/core/java/android/security/flags.aconfig b/core/java/android/security/flags.aconfig
index 3c77c44..7f5b550 100644
--- a/core/java/android/security/flags.aconfig
+++ b/core/java/android/security/flags.aconfig
@@ -10,6 +10,7 @@
flag {
name: "fsverity_api"
+ is_exported: true
namespace: "hardware_backed_security"
description: "Feature flag for fs-verity API"
bug: "285185747"
@@ -64,6 +65,7 @@
flag {
name: "frp_enforcement"
+ is_exported: true
namespace: "hardware_backed_security"
description: "This flag controls whether PDB enforces FRP"
bug: "290312729"
diff --git a/core/java/android/security/responsible_apis_flags.aconfig b/core/java/android/security/responsible_apis_flags.aconfig
index 0bae459..548f8aa 100644
--- a/core/java/android/security/responsible_apis_flags.aconfig
+++ b/core/java/android/security/responsible_apis_flags.aconfig
@@ -9,6 +9,7 @@
flag {
name: "asm_restrictions_enabled"
+ is_exported: true
namespace: "responsible_apis"
description: "Enables ASM restrictions for activity starts and finishes"
bug: "230590090"
@@ -23,6 +24,7 @@
flag {
name: "content_uri_permission_apis"
+ is_exported: true
namespace: "responsible_apis"
description: "Enables the content URI permission APIs"
bug: "293467489"
@@ -30,6 +32,7 @@
flag {
name: "enforce_intent_filter_match"
+ is_exported: true
namespace: "responsible_apis"
description: "Make delivered intents match components' intent filters"
bug: "293560872"
diff --git a/core/java/android/service/appprediction/flags/flags.aconfig b/core/java/android/service/appprediction/flags/flags.aconfig
index c7e47d4..7f9764e 100644
--- a/core/java/android/service/appprediction/flags/flags.aconfig
+++ b/core/java/android/service/appprediction/flags/flags.aconfig
@@ -2,6 +2,7 @@
flag {
name: "service_features_api"
+ is_exported: true
namespace: "systemui"
description: "Guards the new requestServiceFeatures api"
bug: "292565550"
diff --git a/core/java/android/service/chooser/flags.aconfig b/core/java/android/service/chooser/flags.aconfig
index d72441f..a3eff3b 100644
--- a/core/java/android/service/chooser/flags.aconfig
+++ b/core/java/android/service/chooser/flags.aconfig
@@ -2,6 +2,7 @@
flag {
name: "chooser_album_text"
+ is_exported: true
namespace: "intentresolver"
description: "Flag controlling the album text subtype hint for sharesheet"
bug: "323380224"
@@ -9,6 +10,7 @@
flag {
name: "enable_sharesheet_metadata_extra"
+ is_exported: true
namespace: "intentresolver"
description: "This flag enables sharesheet metadata to be displayed to users."
bug: "318942069"
@@ -16,6 +18,7 @@
flag {
name: "chooser_payload_toggling"
+ is_exported: true
namespace: "intentresolver"
description: "This flag controls content toggling in Chooser"
bug: "302691505"
@@ -23,18 +26,8 @@
flag {
name: "enable_chooser_result"
+ is_exported: true
namespace: "intentresolver"
description: "Provides additional callbacks with information about user actions in ChooserResult"
bug: "263474465"
}
-
-flag {
- name: "legacy_chooser_pinning_removal"
- namespace: "intentresolver"
- description: "Removing pinning functionality from the legacy chooser (used by partial screenshare)"
- bug: "301068735"
- metadata {
- purpose: PURPOSE_BUGFIX
- }
-}
-
diff --git a/core/java/android/service/controls/flags/flags.aconfig b/core/java/android/service/controls/flags/flags.aconfig
index 3a28844..197f1bc 100644
--- a/core/java/android/service/controls/flags/flags.aconfig
+++ b/core/java/android/service/controls/flags/flags.aconfig
@@ -2,6 +2,7 @@
flag {
name: "home_panel_dream"
+ is_exported: true
namespace: "systemui"
description: "Enables the home controls dream feature."
bug: "298025023"
diff --git a/core/java/android/service/notification/flags.aconfig b/core/java/android/service/notification/flags.aconfig
index c5acc2c..35cd3ed 100644
--- a/core/java/android/service/notification/flags.aconfig
+++ b/core/java/android/service/notification/flags.aconfig
@@ -10,6 +10,7 @@
flag {
name: "redact_sensitive_notifications_from_untrusted_listeners"
+ is_exported: true
namespace: "systemui"
description: "This flag controls the redacting of sensitive notifications from untrusted NotificationListenerServices"
bug: "306271190"
@@ -18,6 +19,7 @@
flag {
name: "callstyle_callback_api"
+ is_exported: true
namespace: "systemui"
description: "Guards the new CallStyleNotificationEventsCallback"
bug: "305095040"
diff --git a/core/java/android/service/ondeviceintelligence/IOnDeviceIntelligenceService.aidl b/core/java/android/service/ondeviceintelligence/IOnDeviceIntelligenceService.aidl
index 6dbff71..908ab5f 100644
--- a/core/java/android/service/ondeviceintelligence/IOnDeviceIntelligenceService.aidl
+++ b/core/java/android/service/ondeviceintelligence/IOnDeviceIntelligenceService.aidl
@@ -41,7 +41,9 @@
void getFeatureDetails(int callerUid, in Feature feature, in IFeatureDetailsCallback featureDetailsCallback);
void getReadOnlyFileDescriptor(in String fileName, in AndroidFuture<ParcelFileDescriptor> future);
void getReadOnlyFeatureFileDescriptorMap(in Feature feature, in RemoteCallback remoteCallback);
- void requestFeatureDownload(int callerUid, in Feature feature, in ICancellationSignal cancellationSignal, in IDownloadCallback downloadCallback);
+ void requestFeatureDownload(int callerUid, in Feature feature,
+ in AndroidFuture<ICancellationSignal> cancellationSignal,
+ in IDownloadCallback downloadCallback);
void registerRemoteServices(in IRemoteProcessingService remoteProcessingService);
void notifyInferenceServiceConnected();
void notifyInferenceServiceDisconnected();
diff --git a/core/java/android/service/ondeviceintelligence/IOnDeviceSandboxedInferenceService.aidl b/core/java/android/service/ondeviceintelligence/IOnDeviceSandboxedInferenceService.aidl
index 799c7545..4213a09 100644
--- a/core/java/android/service/ondeviceintelligence/IOnDeviceSandboxedInferenceService.aidl
+++ b/core/java/android/service/ondeviceintelligence/IOnDeviceSandboxedInferenceService.aidl
@@ -24,6 +24,7 @@
import android.os.ICancellationSignal;
import android.os.PersistableBundle;
import android.os.Bundle;
+import com.android.internal.infra.AndroidFuture;
import android.service.ondeviceintelligence.IRemoteStorageService;
import android.service.ondeviceintelligence.IProcessingUpdateStatusCallback;
@@ -34,13 +35,16 @@
*/
oneway interface IOnDeviceSandboxedInferenceService {
void registerRemoteStorageService(in IRemoteStorageService storageService);
- void requestTokenInfo(int callerUid, in Feature feature, in Bundle request, in ICancellationSignal cancellationSignal,
+ void requestTokenInfo(int callerUid, in Feature feature, in Bundle request,
+ in AndroidFuture<ICancellationSignal> cancellationSignal,
in ITokenInfoCallback tokenInfoCallback);
void processRequest(int callerUid, in Feature feature, in Bundle request, in int requestType,
- in ICancellationSignal cancellationSignal, in IProcessingSignal processingSignal,
+ in AndroidFuture<ICancellationSignal> cancellationSignal,
+ in AndroidFuture<IProcessingSignal> processingSignal,
in IResponseCallback callback);
void processRequestStreaming(int callerUid, in Feature feature, in Bundle request, in int requestType,
- in ICancellationSignal cancellationSignal, in IProcessingSignal processingSignal,
+ in AndroidFuture<ICancellationSignal> cancellationSignal,
+ in AndroidFuture<IProcessingSignal> processingSignal,
in IStreamingResponseCallback callback);
void updateProcessingState(in Bundle processingState,
in IProcessingUpdateStatusCallback callback);
diff --git a/core/java/android/service/ondeviceintelligence/OnDeviceIntelligenceService.java b/core/java/android/service/ondeviceintelligence/OnDeviceIntelligenceService.java
index 9321318..86320b8 100644
--- a/core/java/android/service/ondeviceintelligence/OnDeviceIntelligenceService.java
+++ b/core/java/android/service/ondeviceintelligence/OnDeviceIntelligenceService.java
@@ -148,14 +148,18 @@
@Override
public void requestFeatureDownload(int callerUid, Feature feature,
- ICancellationSignal cancellationSignal,
+ AndroidFuture cancellationSignalFuture,
IDownloadCallback downloadCallback) {
Objects.requireNonNull(feature);
Objects.requireNonNull(downloadCallback);
-
+ ICancellationSignal transport = null;
+ if (cancellationSignalFuture != null) {
+ transport = CancellationSignal.createTransport();
+ cancellationSignalFuture.complete(transport);
+ }
OnDeviceIntelligenceService.this.onDownloadFeature(callerUid,
feature,
- CancellationSignal.fromTransport(cancellationSignal),
+ CancellationSignal.fromTransport(transport),
wrapDownloadCallback(downloadCallback));
}
diff --git a/core/java/android/service/ondeviceintelligence/OnDeviceSandboxedInferenceService.java b/core/java/android/service/ondeviceintelligence/OnDeviceSandboxedInferenceService.java
index fc7a4c8..96c45ee 100644
--- a/core/java/android/service/ondeviceintelligence/OnDeviceSandboxedInferenceService.java
+++ b/core/java/android/service/ondeviceintelligence/OnDeviceSandboxedInferenceService.java
@@ -122,46 +122,72 @@
@Override
public void requestTokenInfo(int callerUid, Feature feature, Bundle request,
- ICancellationSignal cancellationSignal,
+ AndroidFuture cancellationSignalFuture,
ITokenInfoCallback tokenInfoCallback) {
Objects.requireNonNull(feature);
Objects.requireNonNull(tokenInfoCallback);
+ ICancellationSignal transport = null;
+ if (cancellationSignalFuture != null) {
+ transport = CancellationSignal.createTransport();
+ cancellationSignalFuture.complete(transport);
+ }
OnDeviceSandboxedInferenceService.this.onTokenInfoRequest(callerUid,
feature,
request,
- CancellationSignal.fromTransport(cancellationSignal),
+ CancellationSignal.fromTransport(transport),
wrapTokenInfoCallback(tokenInfoCallback));
}
@Override
public void processRequestStreaming(int callerUid, Feature feature, Bundle request,
- int requestType, ICancellationSignal cancellationSignal,
- IProcessingSignal processingSignal,
+ int requestType,
+ AndroidFuture cancellationSignalFuture,
+ AndroidFuture processingSignalFuture,
IStreamingResponseCallback callback) {
Objects.requireNonNull(feature);
Objects.requireNonNull(callback);
+ ICancellationSignal transport = null;
+ if (cancellationSignalFuture != null) {
+ transport = CancellationSignal.createTransport();
+ cancellationSignalFuture.complete(transport);
+ }
+ IProcessingSignal processingSignalTransport = null;
+ if (processingSignalFuture != null) {
+ processingSignalTransport = ProcessingSignal.createTransport();
+ processingSignalFuture.complete(processingSignalTransport);
+ }
OnDeviceSandboxedInferenceService.this.onProcessRequestStreaming(callerUid,
feature,
request,
requestType,
- CancellationSignal.fromTransport(cancellationSignal),
- ProcessingSignal.fromTransport(processingSignal),
+ CancellationSignal.fromTransport(transport),
+ ProcessingSignal.fromTransport(processingSignalTransport),
wrapStreamingResponseCallback(callback));
}
@Override
public void processRequest(int callerUid, Feature feature, Bundle request,
- int requestType, ICancellationSignal cancellationSignal,
- IProcessingSignal processingSignal,
+ int requestType,
+ AndroidFuture cancellationSignalFuture,
+ AndroidFuture processingSignalFuture,
IResponseCallback callback) {
Objects.requireNonNull(feature);
Objects.requireNonNull(callback);
-
+ ICancellationSignal transport = null;
+ if (cancellationSignalFuture != null) {
+ transport = CancellationSignal.createTransport();
+ cancellationSignalFuture.complete(transport);
+ }
+ IProcessingSignal processingSignalTransport = null;
+ if (processingSignalFuture != null) {
+ processingSignalTransport = ProcessingSignal.createTransport();
+ processingSignalFuture.complete(processingSignalTransport);
+ }
OnDeviceSandboxedInferenceService.this.onProcessRequest(callerUid, feature,
request, requestType,
- CancellationSignal.fromTransport(cancellationSignal),
- ProcessingSignal.fromTransport(processingSignal),
+ CancellationSignal.fromTransport(transport),
+ ProcessingSignal.fromTransport(processingSignalTransport),
wrapResponseCallback(callback));
}
@@ -206,7 +232,8 @@
* Invoked when caller provides a request for a particular feature to be processed in a
* streaming manner. The expectation from the implementation is that when processing the
* request,
- * it periodically populates the {@link StreamingProcessingCallback#onPartialResult} to continuously
+ * it periodically populates the {@link StreamingProcessingCallback#onPartialResult} to
+ * continuously
* provide partial Bundle results for the caller to utilize. Optionally the implementation can
* provide the complete response in the {@link StreamingProcessingCallback#onResult} upon
* processing completion.
diff --git a/core/java/android/service/voice/flags/flags.aconfig b/core/java/android/service/voice/flags/flags.aconfig
index 22e8cdd..633304b 100644
--- a/core/java/android/service/voice/flags/flags.aconfig
+++ b/core/java/android/service/voice/flags/flags.aconfig
@@ -2,6 +2,7 @@
flag {
name: "allow_training_data_egress_from_hds"
+ is_exported: true
namespace: "machine_learning"
description: "This flag allows the hotword detection service to egress training data to the default assistant."
bug: "296074924"
@@ -9,6 +10,7 @@
flag {
name: "allow_hotword_bump_egress"
+ is_exported: true
namespace: "machine_learning"
description: "This flag allows hotword detection service to egress reason code for hotword bump."
bug: "290951024"
@@ -16,6 +18,7 @@
flag {
name: "allow_foreground_activities_in_on_show"
+ is_exported: true
namespace: "machine_learning"
description: "This flag allows providing foreground app component along with onShow args."
bug: "319409708"
@@ -23,6 +26,7 @@
flag {
name: "allow_various_attention_types"
+ is_exported: true
namespace: "visual_query"
description: "This flag allows visual query detection service to set different attention types."
bug: "318617199"
@@ -30,6 +34,7 @@
flag {
name: "allow_complex_results_egress_from_vqds"
+ is_exported: true
namespace: "visual_query"
description: "This flag allows visual query detection service egress detailed results. "
bug: "318617199"
@@ -37,6 +42,7 @@
flag {
name: "allow_speaker_id_egress"
+ is_exported: true
namespace: "machine_learning"
description: "This flag allows hotword detection service and visual query detection service to egress current speaker profile id."
bug: "318617199"
diff --git a/core/java/android/service/wallpaper/WallpaperService.java b/core/java/android/service/wallpaper/WallpaperService.java
index bbda068..f6d197c 100644
--- a/core/java/android/service/wallpaper/WallpaperService.java
+++ b/core/java/android/service/wallpaper/WallpaperService.java
@@ -102,6 +102,7 @@
import android.view.WindowLayout;
import android.view.WindowManager;
import android.view.WindowManagerGlobal;
+import android.window.ActivityWindowInfo;
import android.window.ClientWindowFrames;
import android.window.ScreenCapture;
@@ -211,7 +212,7 @@
* @hide
*/
@ChangeId
- @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM)
public static final long WEAROS_WALLPAPER_HANDLES_SCALING = 272527315L;
static final class WallpaperCommand {
@@ -459,7 +460,8 @@
public void resized(ClientWindowFrames frames, boolean reportDraw,
MergedConfiguration mergedConfiguration, InsetsState insetsState,
boolean forceLayout, boolean alwaysConsumeSystemBars, int displayId,
- int syncSeqId, boolean dragResizing) {
+ int syncSeqId, boolean dragResizing,
+ @Nullable ActivityWindowInfo activityWindowInfo) {
Message msg = mCaller.obtainMessageIO(MSG_WINDOW_RESIZED,
reportDraw ? 1 : 0,
mergedConfiguration);
diff --git a/core/java/android/speech/flags/speech_flags.aconfig b/core/java/android/speech/flags/speech_flags.aconfig
index fd80127..fa33592 100644
--- a/core/java/android/speech/flags/speech_flags.aconfig
+++ b/core/java/android/speech/flags/speech_flags.aconfig
@@ -2,6 +2,7 @@
flag {
name: "multilang_extra_launch"
+ is_exported: true
namespace: "machine_learning"
description: "Feature flag for adding new extra for multi-lang feature"
bug: "312489931"
diff --git a/core/java/android/text/flags/flags.aconfig b/core/java/android/text/flags/flags.aconfig
index aff1d4a..8e1ac63 100644
--- a/core/java/android/text/flags/flags.aconfig
+++ b/core/java/android/text/flags/flags.aconfig
@@ -10,6 +10,7 @@
flag {
name: "new_fonts_fallback_xml"
+ is_exported: true
namespace: "text"
description: "Feature flag for deprecating fonts.xml. By setting true for this feature flag, the new font configuration XML, /system/etc/font_fallback.xml is used. The new XML has a new syntax and flexibility of variable font declarations, but it is not compatible with the apps that reads fonts.xml. So, fonts.xml is maintained as a subset of the font_fallback.xml"
# Make read only, as it could be used before the Settings provider is initialized.
@@ -26,6 +27,7 @@
flag {
name: "fix_line_height_for_locale"
+ is_exported: true
namespace: "text"
description: "Feature flag that preserve the line height of the TextView and EditText even if the the locale is different from Latin"
bug: "303326708"
@@ -33,6 +35,7 @@
flag {
name: "no_break_no_hyphenation_span"
+ is_exported: true
namespace: "text"
description: "A feature flag that adding new spans that prevents line breaking and hyphenation."
bug: "283193586"
@@ -57,6 +60,7 @@
flag {
name: "use_bounds_for_width"
+ is_exported: true
namespace: "text"
description: "Feature flag for preventing horizontal clipping."
bug: "63938206"
@@ -71,6 +75,7 @@
flag {
name: "word_style_auto"
+ is_exported: true
namespace: "text"
description: "A feature flag that implements line break word style auto."
bug: "280005585"
@@ -78,6 +83,7 @@
flag {
name: "letter_spacing_justification"
+ is_exported: true
namespace: "text"
description: "A feature flag that implement inter character justification."
bug: "283193133"
@@ -121,8 +127,22 @@
}
flag {
+ name: "handwriting_end_of_line_tap"
+ namespace: "text"
+ description: "Initiate handwriting when stylus taps at the end of a line in a focused non-empty TextView with the cursor at the end of that line"
+ bug: "323376217"
+}
+
+flag {
name: "handwriting_cursor_position"
namespace: "text"
description: "When handwriting is initiated in an unfocused TextView, cursor is placed at the end of the closest paragraph."
bug: "323376217"
}
+
+flag {
+ name: "handwriting_unsupported_message"
+ namespace: "text"
+ description: "Feature flag for showing error message when user tries stylus handwriting on a text field which doesn't support it"
+ bug: "297962571"
+}
diff --git a/core/java/android/view/HandwritingInitiator.java b/core/java/android/view/HandwritingInitiator.java
index 29c8350..f4dadbb 100644
--- a/core/java/android/view/HandwritingInitiator.java
+++ b/core/java/android/view/HandwritingInitiator.java
@@ -34,7 +34,9 @@
import android.widget.EditText;
import android.widget.Editor;
import android.widget.TextView;
+import android.widget.Toast;
+import com.android.internal.R;
import com.android.internal.annotations.VisibleForTesting;
import java.lang.ref.WeakReference;
@@ -223,24 +225,43 @@
View candidateView = findBestCandidateView(mState.mStylusDownX,
mState.mStylusDownY, /* isHover */ false);
if (candidateView != null && candidateView.isEnabled()) {
- if (candidateView == getConnectedOrFocusedView()) {
- if (!mInitiateWithoutConnection && !candidateView.hasFocus()) {
+ boolean candidateHasFocus = candidateView.hasFocus();
+ if (shouldShowHandwritingUnavailableMessageForView(candidateView)) {
+ int messagesResId = (candidateView instanceof TextView tv
+ && tv.isAnyPasswordInputType())
+ ? R.string.error_handwriting_unsupported_password
+ : R.string.error_handwriting_unsupported;
+ Toast.makeText(candidateView.getContext(), messagesResId,
+ Toast.LENGTH_SHORT).show();
+ if (!candidateView.hasFocus()) {
+ requestFocusWithoutReveal(candidateView);
+ }
+ mImm.showSoftInput(candidateView, 0);
+ mState.mHandled = true;
+ mState.mShouldInitHandwriting = false;
+ motionEvent.setAction((motionEvent.getAction()
+ & MotionEvent.ACTION_POINTER_INDEX_MASK)
+ | MotionEvent.ACTION_CANCEL);
+ candidateView.getRootView().dispatchTouchEvent(motionEvent);
+ } else if (candidateView == getConnectedOrFocusedView()) {
+ if (!candidateHasFocus) {
requestFocusWithoutReveal(candidateView);
}
startHandwriting(candidateView);
} else if (candidateView.getHandwritingDelegatorCallback() != null) {
prepareDelegation(candidateView);
} else {
- if (!mInitiateWithoutConnection) {
+ if (mInitiateWithoutConnection) {
+ if (!candidateHasFocus) {
+ // schedule for view focus.
+ mState.mPendingFocusedView = new WeakReference<>(candidateView);
+ requestFocusWithoutReveal(candidateView);
+ }
+ } else {
mState.mPendingConnectedView = new WeakReference<>(candidateView);
- }
- if (!candidateView.hasFocus()) {
- requestFocusWithoutReveal(candidateView);
- }
- if (mInitiateWithoutConnection
- && updateFocusedView(candidateView,
- /* fromTouchEvent */ true)) {
- startHandwriting(candidateView);
+ if (!candidateHasFocus) {
+ requestFocusWithoutReveal(candidateView);
+ }
}
}
}
@@ -266,6 +287,9 @@
* gained focus.
*/
public void onDelegateViewFocused(@NonNull View view) {
+ if (mInitiateWithoutConnection) {
+ onEditorFocused(view);
+ }
if (view == getConnectedView()) {
tryAcceptStylusHandwritingDelegation(view);
}
@@ -313,6 +337,33 @@
}
/**
+ * Notify HandwritingInitiator that a new editor is focused.
+ * @param view the view that received focus.
+ */
+ @VisibleForTesting
+ public void onEditorFocused(@NonNull View view) {
+ if (!mInitiateWithoutConnection) {
+ return;
+ }
+
+ if (!view.isAutoHandwritingEnabled()) {
+ clearFocusedView(view);
+ return;
+ }
+
+ final View focusedView = getFocusedView();
+ if (focusedView == view) {
+ return;
+ }
+ updateFocusedView(view);
+
+ if (mState != null && mState.mPendingFocusedView != null
+ && mState.mPendingFocusedView.get() == view) {
+ startHandwriting(view);
+ }
+ }
+
+ /**
* Notify HandwritingInitiator that the InputConnection has closed for the given view.
* The caller of this method should guarantee that each onInputConnectionClosed call
* is paired with a onInputConnectionCreated call.
@@ -359,7 +410,7 @@
* @return {@code true} if handwriting can initiate for given view.
*/
@VisibleForTesting
- public boolean updateFocusedView(@NonNull View view, boolean fromTouchEvent) {
+ public boolean updateFocusedView(@NonNull View view) {
if (!view.shouldInitiateHandwriting()) {
mFocusedView = null;
return false;
@@ -371,9 +422,7 @@
// A new view just gain focus. By default, we should show hover icon for it.
mShowHoverIconForConnectedView = true;
}
- if (!fromTouchEvent && view.isHandwritingDelegate()) {
- tryAcceptStylusHandwritingDelegation(view);
- }
+
return true;
}
@@ -484,6 +533,15 @@
return view.isStylusHandwritingAvailable();
}
+ private static boolean shouldShowHandwritingUnavailableMessageForView(@NonNull View view) {
+ return (view instanceof TextView) && !shouldTriggerStylusHandwritingForView(view);
+ }
+
+ private static boolean shouldTriggerHandwritingOrShowUnavailableMessageForView(
+ @NonNull View view) {
+ return (view instanceof TextView) || shouldTriggerStylusHandwritingForView(view);
+ }
+
/**
* Returns the pointer icon for the motion event, or null if it doesn't specify the icon.
* This gives HandwritingInitiator a chance to show the stylus handwriting icon over a
@@ -491,7 +549,7 @@
*/
public PointerIcon onResolvePointerIcon(Context context, MotionEvent event) {
final View hoverView = findHoverView(event);
- if (hoverView == null) {
+ if (hoverView == null || !shouldTriggerStylusHandwritingForView(hoverView)) {
return null;
}
@@ -594,7 +652,7 @@
/**
* Given the location of the stylus event, return the best candidate view to initialize
- * handwriting mode.
+ * handwriting mode or show the handwriting unavailable error message.
*
* @param x the x coordinates of the stylus event, in the coordinates of the window.
* @param y the y coordinates of the stylus event, in the coordinates of the window.
@@ -610,7 +668,8 @@
Rect handwritingArea = mTempRect;
if (getViewHandwritingArea(connectedOrFocusedView, handwritingArea)
&& isInHandwritingArea(handwritingArea, x, y, connectedOrFocusedView, isHover)
- && shouldTriggerStylusHandwritingForView(connectedOrFocusedView)) {
+ && shouldTriggerHandwritingOrShowUnavailableMessageForView(
+ connectedOrFocusedView)) {
if (!isHover && mState != null) {
mState.mStylusDownWithinEditorBounds =
contains(handwritingArea, x, y, 0f, 0f, 0f, 0f);
@@ -628,7 +687,7 @@
final View view = viewInfo.getView();
final Rect handwritingArea = viewInfo.getHandwritingArea();
if (!isInHandwritingArea(handwritingArea, x, y, view, isHover)
- || !shouldTriggerStylusHandwritingForView(view)) {
+ || !shouldTriggerHandwritingOrShowUnavailableMessageForView(view)) {
continue;
}
@@ -832,6 +891,12 @@
*/
private WeakReference<View> mPendingConnectedView = null;
+ /**
+ * A view which has requested focus and is yet to receive it.
+ * When view receives focus, a handwriting session should be started for the view.
+ */
+ private WeakReference<View> mPendingFocusedView = null;
+
/** The pointer id of the stylus pointer that is being tracked. */
private final int mStylusPointerId;
/** The time stamp when the stylus pointer goes down. */
@@ -856,7 +921,7 @@
/** The helper method to check if the given view is still active for handwriting. */
private static boolean isViewActive(@Nullable View view) {
return view != null && view.isAttachedToWindow() && view.isAggregatedVisible()
- && view.shouldInitiateHandwriting();
+ && view.shouldTrackHandwritingArea();
}
private CursorAnchorInfo getCursorAnchorInfoForConnectionless(View view) {
diff --git a/core/java/android/view/IWindow.aidl b/core/java/android/view/IWindow.aidl
index 5ee526e..1c0834f 100644
--- a/core/java/android/view/IWindow.aidl
+++ b/core/java/android/view/IWindow.aidl
@@ -30,6 +30,7 @@
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.inputmethod.ImeTracker;
+import android.window.ActivityWindowInfo;
import android.window.ClientWindowFrames;
import com.android.internal.os.IResultReceiver;
@@ -61,7 +62,8 @@
void resized(in ClientWindowFrames frames, boolean reportDraw,
in MergedConfiguration newMergedConfiguration, in InsetsState insetsState,
boolean forceLayout, boolean alwaysConsumeSystemBars, int displayId,
- int syncSeqId, boolean dragResizing);
+ int syncSeqId, boolean dragResizing,
+ in @nullable ActivityWindowInfo activityWindowInfo);
/**
* Called when this window retrieved control over a specified set of insets sources.
diff --git a/core/java/android/view/IWindowSession.aidl b/core/java/android/view/IWindowSession.aidl
index e126836..3a90841 100644
--- a/core/java/android/view/IWindowSession.aidl
+++ b/core/java/android/view/IWindowSession.aidl
@@ -47,6 +47,19 @@
* {@hide}
*/
interface IWindowSession {
+
+ /**
+ * Bundle key to store the latest sync seq id for the relayout configuration.
+ * @see #relayout
+ */
+ const String KEY_RELAYOUT_BUNDLE_SEQID = "seqid";
+ /**
+ * Bundle key to store the latest ActivityWindowInfo associated with the relayout configuration.
+ * Will only be set if the relayout window is an activity window.
+ * @see #relayout
+ */
+ const String KEY_RELAYOUT_BUNDLE_ACTIVITY_WINDOW_INFO = "activity_window_info";
+
int addToDisplay(IWindow window, in WindowManager.LayoutParams attrs,
in int viewVisibility, in int layerStackId, int requestedVisibleTypes,
out InputChannel outInputChannel, out InsetsState insetsState,
@@ -92,7 +105,7 @@
* @param outSurfaceControl Object in which is placed the new display surface.
* @param insetsState The current insets state in the system.
* @param activeControls Objects which allow controlling {@link InsetsSource}s.
- * @param bundle A temporary object to obtain the latest SyncSeqId.
+ * @param bundle A Bundle to contain the latest SyncSeqId and any extra relayout optional infos.
* @return int Result flags, defined in {@link WindowManagerGlobal}.
*/
int relayout(IWindow window, in WindowManager.LayoutParams attrs,
diff --git a/core/java/android/view/OWNERS b/core/java/android/view/OWNERS
index a2f767d..07d05a4 100644
--- a/core/java/android/view/OWNERS
+++ b/core/java/android/view/OWNERS
@@ -75,12 +75,14 @@
per-file View.java = file:/services/core/java/com/android/server/input/OWNERS
per-file View.java = file:/services/core/java/com/android/server/wm/OWNERS
per-file View.java = file:/core/java/android/view/inputmethod/OWNERS
+per-file View.java = file:/core/java/android/text/OWNERS
per-file ViewRootImpl.java = file:/services/accessibility/OWNERS
per-file ViewRootImpl.java = file:/core/java/android/service/autofill/OWNERS
per-file ViewRootImpl.java = file:/graphics/java/android/graphics/OWNERS
per-file ViewRootImpl.java = file:/services/core/java/com/android/server/input/OWNERS
per-file ViewRootImpl.java = file:/services/core/java/com/android/server/wm/OWNERS
per-file ViewRootImpl.java = file:/core/java/android/view/inputmethod/OWNERS
+per-file ViewRootImpl.java = file:/core/java/android/text/OWNERS
per-file AccessibilityInteractionController.java = file:/services/accessibility/OWNERS
per-file OnReceiveContentListener.java = file:/core/java/android/service/autofill/OWNERS
per-file OnReceiveContentListener.java = file:/core/java/android/widget/OWNERS
diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java
index 0a75f4e..0a9ac2f 100644
--- a/core/java/android/view/View.java
+++ b/core/java/android/view/View.java
@@ -44,6 +44,7 @@
import static android.view.flags.Flags.toolkitSetFrameRateReadOnly;
import static android.view.flags.Flags.viewVelocityApi;
import static android.view.inputmethod.Flags.FLAG_HOME_SCREEN_HANDWRITING_DELEGATOR;
+import static android.view.inputmethod.Flags.initiationWithoutInputConnection;
import static com.android.internal.util.FrameworkStatsLog.TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__DEEP_PRESS;
import static com.android.internal.util.FrameworkStatsLog.TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS;
@@ -8496,8 +8497,9 @@
* hierarchy
* @param refocus when propagate is true, specifies whether to request the
* root view place new focus
+ * @hide
*/
- void clearFocusInternal(View focused, boolean propagate, boolean refocus) {
+ public void clearFocusInternal(View focused, boolean propagate, boolean refocus) {
if ((mPrivateFlags & PFLAG_FOCUSED) != 0) {
mPrivateFlags &= ~PFLAG_FOCUSED;
clearParentsWantFocus();
@@ -8668,11 +8670,12 @@
onFocusLost();
} else if (hasWindowFocus()) {
notifyFocusChangeToImeFocusController(true /* hasFocus */);
-
- if (mIsHandwritingDelegate) {
- ViewRootImpl viewRoot = getViewRootImpl();
- if (viewRoot != null) {
+ ViewRootImpl viewRoot = getViewRootImpl();
+ if (viewRoot != null) {
+ if (mIsHandwritingDelegate) {
viewRoot.getHandwritingInitiator().onDelegateViewFocused(this);
+ } else if (initiationWithoutInputConnection() && onCheckIsTextEditor()) {
+ viewRoot.getHandwritingInitiator().onEditorFocused(this);
}
}
}
@@ -12695,7 +12698,7 @@
if (getSystemGestureExclusionRects().isEmpty()
&& collectPreferKeepClearRects().isEmpty()
&& collectUnrestrictedPreferKeepClearRects().isEmpty()
- && (info.mHandwritingArea == null || !shouldInitiateHandwriting())) {
+ && (info.mHandwritingArea == null || !shouldTrackHandwritingArea())) {
if (info.mPositionUpdateListener != null) {
mRenderNode.removePositionUpdateListener(info.mPositionUpdateListener);
info.mPositionUpdateListener = null;
@@ -13062,7 +13065,7 @@
void updateHandwritingArea() {
// If autoHandwritingArea is not enabled, do nothing.
- if (!shouldInitiateHandwriting()) return;
+ if (!shouldTrackHandwritingArea()) return;
final AttachInfo ai = mAttachInfo;
if (ai != null) {
ai.mViewRootImpl.getHandwritingInitiator().updateHandwritingAreasForView(this);
@@ -13080,6 +13083,16 @@
}
/**
+ * Returns whether the handwriting initiator should track the handwriting area for this view,
+ * either to initiate handwriting mode, or to prepare handwriting delegation, or to show the
+ * handwriting unsupported message.
+ * @hide
+ */
+ public boolean shouldTrackHandwritingArea() {
+ return shouldInitiateHandwriting();
+ }
+
+ /**
* Sets a callback which should be called when a stylus {@link MotionEvent} occurs within this
* view's bounds. The callback will be called from the UI thread.
*
@@ -16691,6 +16704,10 @@
onFocusLost();
} else if ((mPrivateFlags & PFLAG_FOCUSED) != 0) {
notifyFocusChangeToImeFocusController(true /* hasFocus */);
+ ViewRootImpl viewRoot = getViewRootImpl();
+ if (viewRoot != null && initiationWithoutInputConnection() && onCheckIsTextEditor()) {
+ viewRoot.getHandwritingInitiator().onEditorFocused(this);
+ }
}
refreshDrawableState();
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index cae6672..5fe8c00 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -114,6 +114,7 @@
import static android.view.inputmethod.InputMethodEditorTraceProto.InputMethodClientsTraceProto.ClientSideProto.INSETS_CONTROLLER;
import static com.android.input.flags.Flags.enablePointerChoreographer;
+import static com.android.window.flags.Flags.activityWindowInfoFlag;
import static com.android.window.flags.Flags.enableBufferTransformHintFromDisplay;
import android.Manifest;
@@ -233,6 +234,7 @@
import android.view.inputmethod.ImeTracker;
import android.view.inputmethod.InputMethodManager;
import android.widget.Scroller;
+import android.window.ActivityWindowInfo;
import android.window.BackEvent;
import android.window.ClientWindowFrames;
import android.window.CompatOnBackInvokedCallback;
@@ -435,13 +437,27 @@
* Callback for notifying activities.
*/
public interface ActivityConfigCallback {
-
/**
* Notifies about override config change and/or move to different display.
* @param overrideConfig New override config to apply to activity.
* @param newDisplayId New display id, {@link Display#INVALID_DISPLAY} if not changed.
*/
- void onConfigurationChanged(Configuration overrideConfig, int newDisplayId);
+ default void onConfigurationChanged(@NonNull Configuration overrideConfig,
+ int newDisplayId) {
+ // Must override one of the #onConfigurationChanged.
+ throw new IllegalStateException("Not implemented");
+ }
+
+ /**
+ * Notifies about override config change and/or move to different display.
+ * @param overrideConfig New override config to apply to activity.
+ * @param newDisplayId New display id, {@link Display#INVALID_DISPLAY} if not changed.
+ * @param activityWindowInfo New ActivityWindowInfo to apply to activity.
+ */
+ default void onConfigurationChanged(@NonNull Configuration overrideConfig,
+ int newDisplayId, @Nullable ActivityWindowInfo activityWindowInfo) {
+ onConfigurationChanged(overrideConfig, newDisplayId);
+ }
/**
* Notify the corresponding activity about the request to show or hide a camera compat
@@ -467,7 +483,7 @@
* In that case we receive a call back from {@link ActivityThread} and this flag is used to
* preserve the initial value.
*
- * @see #performConfigurationChange(MergedConfiguration, boolean, int)
+ * @see #performConfigurationChange
*/
private boolean mForceNextConfigUpdate;
@@ -814,6 +830,13 @@
/** Configurations waiting to be applied. */
private final MergedConfiguration mPendingMergedConfiguration = new MergedConfiguration();
+ /** Non-{@code null} if {@link #mActivityConfigCallback} is not {@code null}. */
+ @Nullable
+ private ActivityWindowInfo mPendingActivityWindowInfo;
+ /** Non-{@code null} if {@link #mActivityConfigCallback} is not {@code null}. */
+ @Nullable
+ private ActivityWindowInfo mLastReportedActivityWindowInfo;
+
boolean mScrollMayChange;
@SoftInputModeFlags
int mSoftInputMode;
@@ -1260,8 +1283,18 @@
* Add activity config callback to be notified about override config changes and camera
* compat control state updates.
*/
- public void setActivityConfigCallback(ActivityConfigCallback callback) {
+ public void setActivityConfigCallback(@Nullable ActivityConfigCallback callback) {
mActivityConfigCallback = callback;
+ if (!activityWindowInfoFlag()) {
+ return;
+ }
+ if (callback == null) {
+ mPendingActivityWindowInfo = null;
+ mLastReportedActivityWindowInfo = null;
+ } else {
+ mPendingActivityWindowInfo = new ActivityWindowInfo();
+ mLastReportedActivityWindowInfo = new ActivityWindowInfo();
+ }
}
public void setOnContentApplyWindowInsetsListener(OnContentApplyWindowInsetsListener listener) {
@@ -2096,7 +2129,8 @@
/** Handles messages {@link #MSG_RESIZED} and {@link #MSG_RESIZED_REPORT}. */
private void handleResized(ClientWindowFrames frames, boolean reportDraw,
MergedConfiguration mergedConfiguration, InsetsState insetsState, boolean forceLayout,
- boolean alwaysConsumeSystemBars, int displayId, int syncSeqId, boolean dragResizing) {
+ boolean alwaysConsumeSystemBars, int displayId, int syncSeqId, boolean dragResizing,
+ @Nullable ActivityWindowInfo activityWindowInfo) {
if (!mAdded) {
return;
}
@@ -2114,7 +2148,14 @@
mInsetsController.onStateChanged(insetsState);
final float compatScale = frames.compatScale;
final boolean frameChanged = !mWinFrame.equals(frame);
- final boolean configChanged = !mLastReportedMergedConfiguration.equals(mergedConfiguration);
+ final boolean shouldReportActivityWindowInfoChanged =
+ // Can be null if callbacks is not set
+ mLastReportedActivityWindowInfo != null
+ // Can be null if not activity window
+ && activityWindowInfo != null
+ && !mLastReportedActivityWindowInfo.equals(activityWindowInfo);
+ final boolean configChanged = !mLastReportedMergedConfiguration.equals(mergedConfiguration)
+ || shouldReportActivityWindowInfoChanged;
final boolean attachedFrameChanged =
!Objects.equals(mTmpFrames.attachedFrame, attachedFrame);
final boolean displayChanged = mDisplay.getDisplayId() != displayId;
@@ -2133,7 +2174,8 @@
if (configChanged) {
// If configuration changed - notify about that and, maybe, about move to display.
performConfigurationChange(mergedConfiguration, false /* force */,
- displayChanged ? displayId : INVALID_DISPLAY /* same display */);
+ displayChanged ? displayId : INVALID_DISPLAY /* same display */,
+ activityWindowInfo);
} else if (displayChanged) {
// Moved to display without config change - report last applied one.
onMovedToDisplay(displayId, mLastConfigurationFromResources);
@@ -3532,12 +3574,18 @@
// WindowManagerService has reported back a frame from a configuration not yet
// handled by the client. In this case, we need to accept the configuration so we
// do not lay out and draw with the wrong configuration.
- if (mRelayoutRequested
- && !mPendingMergedConfiguration.equals(mLastReportedMergedConfiguration)) {
+ boolean shouldPerformConfigurationUpdate =
+ !mPendingMergedConfiguration.equals(mLastReportedMergedConfiguration)
+ || !Objects.equals(mPendingActivityWindowInfo,
+ mLastReportedActivityWindowInfo);
+ if (mRelayoutRequested && shouldPerformConfigurationUpdate) {
if (DEBUG_CONFIGURATION) Log.v(mTag, "Visible with new config: "
+ mPendingMergedConfiguration.getMergedConfiguration());
performConfigurationChange(new MergedConfiguration(mPendingMergedConfiguration),
- !mFirst, INVALID_DISPLAY /* same display */);
+ !mFirst, INVALID_DISPLAY /* same display */,
+ mPendingActivityWindowInfo != null
+ ? new ActivityWindowInfo(mPendingActivityWindowInfo)
+ : null);
updatedConfiguration = true;
}
final boolean updateSurfaceNeeded = mUpdateSurfaceNeeded;
@@ -6063,9 +6111,11 @@
* @param force Flag indicating if we should force apply the config.
* @param newDisplayId Id of new display if moved, {@link Display#INVALID_DISPLAY} if not
* changed.
+ * @param activityWindowInfo New activity window info. {@code null} if it is non-app window, or
+ * this is not a Configuration change to the activity window (global).
*/
- private void performConfigurationChange(MergedConfiguration mergedConfiguration, boolean force,
- int newDisplayId) {
+ private void performConfigurationChange(@NonNull MergedConfiguration mergedConfiguration,
+ boolean force, int newDisplayId, @Nullable ActivityWindowInfo activityWindowInfo) {
if (mergedConfiguration == null) {
throw new IllegalArgumentException("No merged config provided.");
}
@@ -6105,6 +6155,9 @@
}
mLastReportedMergedConfiguration.setConfiguration(globalConfig, overrideConfig);
+ if (mLastReportedActivityWindowInfo != null && activityWindowInfo != null) {
+ mLastReportedActivityWindowInfo.set(activityWindowInfo);
+ }
mForceNextConfigUpdate = force;
if (mActivityConfigCallback != null) {
@@ -6112,7 +6165,8 @@
// This basically initiates a round trip to ActivityThread and back, which will ensure
// that corresponding activity and resources are updated before updating inner state of
// ViewRootImpl. Eventually it will call #updateConfiguration().
- mActivityConfigCallback.onConfigurationChanged(overrideConfig, newDisplayId);
+ mActivityConfigCallback.onConfigurationChanged(overrideConfig, newDisplayId,
+ activityWindowInfo);
} else {
// There is no activity callback - update the configuration right away.
updateConfiguration(newDisplayId);
@@ -6354,13 +6408,15 @@
final boolean reportDraw = msg.what == MSG_RESIZED_REPORT;
final MergedConfiguration mergedConfiguration = (MergedConfiguration) args.arg2;
final InsetsState insetsState = (InsetsState) args.arg3;
+ final ActivityWindowInfo activityWindowInfo = (ActivityWindowInfo) args.arg4;
final boolean forceLayout = args.argi1 != 0;
final boolean alwaysConsumeSystemBars = args.argi2 != 0;
final int displayId = args.argi3;
final int syncSeqId = args.argi4;
final boolean dragResizing = args.argi5 != 0;
handleResized(frames, reportDraw, mergedConfiguration, insetsState, forceLayout,
- alwaysConsumeSystemBars, displayId, syncSeqId, dragResizing);
+ alwaysConsumeSystemBars, displayId, syncSeqId, dragResizing,
+ activityWindowInfo);
args.recycle();
break;
}
@@ -6504,7 +6560,8 @@
mLastReportedMergedConfiguration.getOverrideConfiguration());
performConfigurationChange(new MergedConfiguration(mPendingMergedConfiguration),
- false /* force */, INVALID_DISPLAY /* same display */);
+ false /* force */, INVALID_DISPLAY /* same display */,
+ mLastReportedActivityWindowInfo);
} break;
case MSG_CLEAR_ACCESSIBILITY_FOCUS_HOST: {
setAccessibilityFocus(null, null);
@@ -8933,10 +8990,19 @@
mTempInsets, mTempControls, mRelayoutBundle);
mRelayoutRequested = true;
- final int maybeSyncSeqId = mRelayoutBundle.getInt("seqid");
+ final int maybeSyncSeqId = mRelayoutBundle.getInt(
+ IWindowSession.KEY_RELAYOUT_BUNDLE_SEQID);
if (maybeSyncSeqId > 0) {
mSyncSeqId = maybeSyncSeqId;
}
+ if (activityWindowInfoFlag() && mPendingActivityWindowInfo != null) {
+ final ActivityWindowInfo outInfo = mRelayoutBundle.getParcelable(
+ IWindowSession.KEY_RELAYOUT_BUNDLE_ACTIVITY_WINDOW_INFO,
+ ActivityWindowInfo.class);
+ if (outInfo != null) {
+ mPendingActivityWindowInfo.set(outInfo);
+ }
+ }
mWinFrameInScreen.set(mTmpFrames.frame);
if (mTranslator != null) {
mTranslator.translateRectInScreenToAppWindow(mTmpFrames.frame);
@@ -9357,6 +9423,10 @@
+ mLastReportedMergedConfiguration);
writer.println(innerPrefix + "mLastConfigurationFromResources="
+ mLastConfigurationFromResources);
+ if (mLastReportedActivityWindowInfo != null) {
+ writer.println(innerPrefix + "mLastReportedActivityWindowInfo="
+ + mLastReportedActivityWindowInfo);
+ }
writer.println(innerPrefix + "mIsAmbientMode=" + mIsAmbientMode);
writer.println(innerPrefix + "mUnbufferedInputSource="
+ Integer.toHexString(mUnbufferedInputSource));
@@ -9570,12 +9640,14 @@
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
private void dispatchResized(ClientWindowFrames frames, boolean reportDraw,
MergedConfiguration mergedConfiguration, InsetsState insetsState, boolean forceLayout,
- boolean alwaysConsumeSystemBars, int displayId, int syncSeqId, boolean dragResizing) {
+ boolean alwaysConsumeSystemBars, int displayId, int syncSeqId, boolean dragResizing,
+ @Nullable ActivityWindowInfo activityWindowInfo) {
Message msg = mHandler.obtainMessage(reportDraw ? MSG_RESIZED_REPORT : MSG_RESIZED);
SomeArgs args = SomeArgs.obtain();
args.arg1 = frames;
args.arg2 = mergedConfiguration;
args.arg3 = insetsState;
+ args.arg4 = activityWindowInfo;
args.argi1 = forceLayout ? 1 : 0;
args.argi2 = alwaysConsumeSystemBars ? 1 : 0;
args.argi3 = displayId;
@@ -11028,7 +11100,7 @@
public void resized(ClientWindowFrames frames, boolean reportDraw,
MergedConfiguration mergedConfiguration, InsetsState insetsState,
boolean forceLayout, boolean alwaysConsumeSystemBars, int displayId, int syncSeqId,
- boolean dragResizing) {
+ boolean dragResizing, @Nullable ActivityWindowInfo activityWindowInfo) {
final boolean isFromResizeItem = mIsFromResizeItem;
mIsFromResizeItem = false;
// Although this is a AIDL method, it will only be triggered in local process through
@@ -11047,7 +11119,8 @@
if (isFromResizeItem && viewAncestor.mHandler.getLooper()
== ActivityThread.currentActivityThread().getLooper()) {
viewAncestor.handleResized(frames, reportDraw, mergedConfiguration, insetsState,
- forceLayout, alwaysConsumeSystemBars, displayId, syncSeqId, dragResizing);
+ forceLayout, alwaysConsumeSystemBars, displayId, syncSeqId, dragResizing,
+ activityWindowInfo);
return;
}
// The the parameters from WindowStateResizeItem are already copied.
@@ -11059,7 +11132,8 @@
mergedConfiguration = new MergedConfiguration(mergedConfiguration);
}
viewAncestor.dispatchResized(frames, reportDraw, mergedConfiguration, insetsState,
- forceLayout, alwaysConsumeSystemBars, displayId, syncSeqId, dragResizing);
+ forceLayout, alwaysConsumeSystemBars, displayId, syncSeqId, dragResizing,
+ activityWindowInfo);
}
@Override
@@ -12715,7 +12789,10 @@
}
private boolean shouldEnableDvrr() {
- return sToolkitSetFrameRateReadOnlyFlagValue && mIsFrameRatePowerSavingsBalanced;
+ // uncomment this when we are ready for enabling dVRR
+ // return sToolkitSetFrameRateReadOnlyFlagValue && mIsFrameRatePowerSavingsBalanced;
+ return false;
+
}
private void checkIdleness() {
diff --git a/core/java/android/view/WindowManager.java b/core/java/android/view/WindowManager.java
index 86fc6f4..5666739 100644
--- a/core/java/android/view/WindowManager.java
+++ b/core/java/android/view/WindowManager.java
@@ -1476,15 +1476,26 @@
*/
@TestApi
static boolean hasWindowExtensionsEnabled() {
- return HAS_WINDOW_EXTENSIONS_ON_DEVICE
- && ActivityTaskManager.supportsMultiWindow(ActivityThread.currentApplication())
- // Since enableWmExtensionsForAllFlag, HAS_WINDOW_EXTENSIONS_ON_DEVICE is now true
- // on all devices by default as a build file property.
- // Until finishing flag ramp up, only return true when
- // ACTIVITY_EMBEDDING_GUARD_WITH_ANDROID_15 is false, which is set per device by
- // OEMs.
- && (Flags.enableWmExtensionsForAllFlag()
- || !ACTIVITY_EMBEDDING_GUARD_WITH_ANDROID_15);
+ if (!Flags.enableWmExtensionsForAllFlag() && ACTIVITY_EMBEDDING_GUARD_WITH_ANDROID_15) {
+ // Since enableWmExtensionsForAllFlag, HAS_WINDOW_EXTENSIONS_ON_DEVICE is now true
+ // on all devices by default as a build file property.
+ // Until finishing flag ramp up, only return true when
+ // ACTIVITY_EMBEDDING_GUARD_WITH_ANDROID_15 is false, which is set per device by
+ // OEMs.
+ return false;
+ }
+
+ if (!HAS_WINDOW_EXTENSIONS_ON_DEVICE) {
+ return false;
+ }
+
+ try {
+ return ActivityTaskManager.supportsMultiWindow(ActivityThread.currentApplication());
+ } catch (Exception e) {
+ // In case the PackageManager is not set up correctly in test.
+ Log.e("WindowManager", "Unable to read if the device supports multi window", e);
+ return false;
+ }
}
/**
diff --git a/core/java/android/view/WindowlessWindowManager.java b/core/java/android/view/WindowlessWindowManager.java
index 22d8ed9..7309080 100644
--- a/core/java/android/view/WindowlessWindowManager.java
+++ b/core/java/android/view/WindowlessWindowManager.java
@@ -638,7 +638,7 @@
mTmpConfig.setConfiguration(mConfiguration, mConfiguration);
s.mClient.resized(mTmpFrames, false /* reportDraw */, mTmpConfig, state,
false /* forceLayout */, false /* alwaysConsumeSystemBars */, s.mDisplayId,
- Integer.MAX_VALUE, false /* dragResizing */);
+ Integer.MAX_VALUE, false /* dragResizing */, null /* activityWindowInfo */);
} catch (RemoteException e) {
// Too bad
}
diff --git a/core/java/android/view/accessibility/flags/accessibility_flags.aconfig b/core/java/android/view/accessibility/flags/accessibility_flags.aconfig
index 5b99c71f..91bd4ea 100644
--- a/core/java/android/view/accessibility/flags/accessibility_flags.aconfig
+++ b/core/java/android/view/accessibility/flags/accessibility_flags.aconfig
@@ -4,6 +4,7 @@
flag {
name: "a11y_overlay_callbacks"
+ is_exported: true
namespace: "accessibility"
description: "Whether to allow the passing of result callbacks when attaching a11y overlays."
bug: "304478691"
@@ -26,6 +27,7 @@
flag {
namespace: "accessibility"
name: "braille_display_hid"
+ is_exported: true
description: "Enables new APIs for an AccessibilityService to communicate with a HID Braille display"
bug: "303522222"
}
@@ -40,6 +42,7 @@
flag {
namespace: "accessibility"
name: "collection_info_item_counts"
+ is_exported: true
description: "Fields for total items and the number of important for accessibility items in a collection"
bug: "302376158"
}
@@ -61,6 +64,7 @@
flag {
namespace: "accessibility"
name: "flash_notification_system_api"
+ is_exported: true
description: "Makes flash notification APIs as system APIs for calling from mainline module"
bug: "303131332"
}
@@ -74,6 +78,7 @@
flag {
name: "motion_event_observing"
+ is_exported: true
namespace: "accessibility"
description: "Allows accessibility services to intercept but not consume motion events from specified sources."
bug: "297595990"
@@ -82,6 +87,7 @@
flag {
namespace: "accessibility"
name: "granular_scrolling"
+ is_exported: true
description: "Allow the use of granular scrolling. This allows scrollable nodes to scroll by increments other than a full screen"
bug: "302376158"
}
@@ -103,6 +109,7 @@
flag {
namespace: "accessibility"
name: "add_type_window_control"
+ is_exported: true
description: "adds new TYPE_WINDOW_CONTROL to AccessibilityWindowInfo for detecting Window Decorations"
bug: "320445550"
}
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 5d3153c..4de0f29 100644
--- a/core/java/android/view/contentprotection/flags/content_protection_flags.aconfig
+++ b/core/java/android/view/contentprotection/flags/content_protection_flags.aconfig
@@ -23,6 +23,7 @@
flag {
name: "create_accessibility_overlay_app_op_enabled"
+ is_exported: true
namespace: "content_protection"
description: "If true, an appop is logged on creation of accessibility overlays."
bug: "289081465"
@@ -30,6 +31,7 @@
flag {
name: "rapid_clear_notifications_by_listener_app_op_enabled"
+ is_exported: true
namespace: "content_protection"
description: "If true, an appop is logged when a notification is rapidly cleared by a notification listener."
bug: "289080543"
@@ -37,6 +39,7 @@
flag {
name: "manage_device_policy_enabled"
+ is_exported: true
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/view/flags/refresh_rate_flags.aconfig b/core/java/android/view/flags/refresh_rate_flags.aconfig
index 05cabd5..06598b3 100644
--- a/core/java/android/view/flags/refresh_rate_flags.aconfig
+++ b/core/java/android/view/flags/refresh_rate_flags.aconfig
@@ -2,6 +2,7 @@
flag {
name: "view_velocity_api"
+ is_exported: true
namespace: "toolkit"
description: "Feature flag for view content velocity api"
bug: "293513816"
@@ -16,6 +17,7 @@
flag {
name: "toolkit_set_frame_rate_read_only"
+ is_exported: true
namespace: "toolkit"
description: "Feature flag for toolkit to set frame rate"
bug: "293512962"
@@ -24,6 +26,7 @@
flag {
name: "expected_presentation_time_api"
+ is_exported: true
namespace: "toolkit"
description: "Feature flag for using expected presentation time of the Choreographer"
bug: "278730197"
@@ -31,6 +34,7 @@
flag {
name: "expected_presentation_time_read_only"
+ is_exported: true
namespace: "toolkit"
description: "Feature flag for using expected presentation time of the Choreographer"
bug: "278730197"
diff --git a/core/java/android/view/flags/scroll_feedback_flags.aconfig b/core/java/android/view/flags/scroll_feedback_flags.aconfig
index d1d871c2..a7c4104 100644
--- a/core/java/android/view/flags/scroll_feedback_flags.aconfig
+++ b/core/java/android/view/flags/scroll_feedback_flags.aconfig
@@ -3,6 +3,7 @@
flag {
namespace: "toolkit"
name: "scroll_feedback_api"
+ is_exported: true
description: "Enable the scroll feedback APIs"
bug: "239594271"
}
diff --git a/core/java/android/view/flags/view_flags.aconfig b/core/java/android/view/flags/view_flags.aconfig
index 6cf89d6..c482f8b 100644
--- a/core/java/android/view/flags/view_flags.aconfig
+++ b/core/java/android/view/flags/view_flags.aconfig
@@ -28,6 +28,7 @@
flag {
name: "sensitive_content_app_protection_api"
+ is_exported: true
namespace: "permissions"
description: "This flag controls the new sensitive content protection API,"
" The API will be used by other ui toolkits (i.e. compose, webview, custom virtual views)."
diff --git a/core/java/android/view/flags/window_insets.aconfig b/core/java/android/view/flags/window_insets.aconfig
index 201b7ad..bf6df5c 100644
--- a/core/java/android/view/flags/window_insets.aconfig
+++ b/core/java/android/view/flags/window_insets.aconfig
@@ -2,6 +2,7 @@
flag {
name: "customizable_window_headers"
+ is_exported: true
namespace: "lse_desktop_experience"
description: "Flag to control the caption bar appearance and to fit app content in its empty space"
bug: "316387515"
diff --git a/core/java/android/view/inputmethod/InputMethodManager.java b/core/java/android/view/inputmethod/InputMethodManager.java
index 985f542..8efb201 100644
--- a/core/java/android/view/inputmethod/InputMethodManager.java
+++ b/core/java/android/view/inputmethod/InputMethodManager.java
@@ -3499,10 +3499,6 @@
return false;
}
mServedView = mNextServedView;
- if (initiationWithoutInputConnection() && mServedView.isHandwritingDelegate()) {
- mServedView.getViewRootImpl().getHandwritingInitiator().onDelegateViewFocused(
- mServedView);
- }
if (mServedInputConnection != null) {
mServedInputConnection.finishComposingTextFromImm();
}
diff --git a/core/java/android/view/inputmethod/flags.aconfig b/core/java/android/view/inputmethod/flags.aconfig
index 8d3920f..be74a65 100644
--- a/core/java/android/view/inputmethod/flags.aconfig
+++ b/core/java/android/view/inputmethod/flags.aconfig
@@ -10,6 +10,7 @@
flag {
name: "editorinfo_handwriting_enabled"
+ is_exported: true
namespace: "input_method"
description: "Feature flag for adding EditorInfo#mStylusHandwritingEnabled"
bug: "293898187"
@@ -18,6 +19,7 @@
flag {
name: "imm_userhandle_hostsidetests"
+ is_exported: true
namespace: "input_method"
description: "Feature flag for replacing UserIdInt with UserHandle in some helper IMM functions"
bug: "301713309"
@@ -26,6 +28,7 @@
flag {
name: "concurrent_input_methods"
+ is_exported: true
namespace: "input_method"
description: "Feature flag for concurrent multi-session IME"
bug: "284527000"
@@ -34,6 +37,7 @@
flag {
name: "home_screen_handwriting_delegator"
+ is_exported: true
namespace: "input_method"
description: "Feature flag for supporting stylus handwriting delegation from RemoteViews on the home screen"
bug: "279959705"
@@ -49,6 +53,7 @@
flag {
name: "use_zero_jank_proxy"
+ is_exported: true
namespace: "input_method"
description: "Feature flag for using a proxy that uses async calls to achieve zero jank for IMMS calls."
bug: "293640003"
@@ -57,6 +62,7 @@
flag {
name: "ime_switcher_revamp"
+ is_exported: true
namespace: "input_method"
description: "Feature flag for revamping the Input Method Switcher menu"
bug: "311791923"
@@ -73,6 +79,7 @@
flag {
name: "connectionless_handwriting"
+ is_exported: true
namespace: "input_method"
description: "Feature flag for connectionless stylus handwriting APIs"
bug: "300979854"
diff --git a/core/java/android/webkit/flags.aconfig b/core/java/android/webkit/flags.aconfig
index 6938b29..2d834a8 100644
--- a/core/java/android/webkit/flags.aconfig
+++ b/core/java/android/webkit/flags.aconfig
@@ -2,6 +2,7 @@
flag {
name: "update_service_ipc_wrapper"
+ is_exported: true
namespace: "webview"
description: "New API: proper wrapper for IWebViewUpdateService"
bug: "319292658"
diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java
index 0373539..fbb5116 100644
--- a/core/java/android/widget/TextView.java
+++ b/core/java/android/widget/TextView.java
@@ -9733,7 +9733,7 @@
return KEY_EVENT_HANDLED;
}
if (hasFocus()) {
- clearFocus();
+ clearFocusInternal(null, /* propagate */ true, /* refocus */ false);
InputMethodManager imm = getInputMethodManager();
if (imm != null) {
imm.hideSoftInputFromView(this, 0);
@@ -13118,6 +13118,16 @@
return superResult;
}
+ // At this point, the event is not a long press, otherwise it would be handled above.
+ if (Flags.handwritingEndOfLineTap() && action == MotionEvent.ACTION_UP
+ && shouldStartHandwritingForEndOfLineTap(event)) {
+ InputMethodManager imm = getInputMethodManager();
+ if (imm != null) {
+ imm.startStylusHandwriting(this);
+ return true;
+ }
+ }
+
final boolean touchIsFinished = (action == MotionEvent.ACTION_UP)
&& (mEditor == null || !mEditor.mIgnoreActionUpEvent) && isFocused();
@@ -13167,6 +13177,46 @@
}
/**
+ * If handwriting is supported, the TextView is already focused and not empty, and the cursor is
+ * at the end of a line, a stylus tap after the end of the line will trigger handwriting.
+ */
+ private boolean shouldStartHandwritingForEndOfLineTap(MotionEvent actionUpEvent) {
+ if (!onCheckIsTextEditor()
+ || !isEnabled()
+ || !isAutoHandwritingEnabled()
+ || TextUtils.isEmpty(mText)
+ || didTouchFocusSelect()
+ || mLayout == null
+ || !actionUpEvent.isStylusPointer()) {
+ return false;
+ }
+ int cursorOffset = getSelectionStart();
+ if (cursorOffset < 0 || getSelectionEnd() != cursorOffset) {
+ return false;
+ }
+ int cursorLine = mLayout.getLineForOffset(cursorOffset);
+ int cursorLineEnd = mLayout.getLineEnd(cursorLine);
+ if (cursorLine != mLayout.getLineCount() - 1) {
+ cursorLineEnd--;
+ }
+ if (cursorLineEnd != cursorOffset) {
+ return false;
+ }
+ // Check that the stylus down point is within the same line as the cursor.
+ if (getLineAtCoordinate(actionUpEvent.getY()) != cursorLine) {
+ return false;
+ }
+ // Check that the stylus down point is after the end of the line.
+ float localX = convertToLocalHorizontalCoordinate(actionUpEvent.getX());
+ if (mLayout.getParagraphDirection(cursorLine) == Layout.DIR_RIGHT_TO_LEFT
+ ? localX >= mLayout.getLineLeft(cursorLine)
+ : localX <= mLayout.getLineRight(cursorLine)) {
+ return false;
+ }
+ return isStylusHandwritingAvailable();
+ }
+
+ /**
* Returns true when need to show UIs, e.g. floating toolbar, etc, for finger based interaction.
*
* @return true if UIs need to show for finger interaciton. false if UIs are not necessary.
@@ -13565,6 +13615,15 @@
/** @hide */
@Override
+ public boolean shouldTrackHandwritingArea() {
+ // The handwriting initiator tracks all editable TextViews regardless of whether handwriting
+ // is supported, so that it can show an error message for unsupported editable TextViews.
+ return super.shouldTrackHandwritingArea()
+ || (Flags.handwritingUnsupportedMessage() && onCheckIsTextEditor());
+ }
+
+ /** @hide */
+ @Override
public boolean isStylusHandwritingAvailable() {
if (mTextOperationUser == null) {
return super.isStylusHandwritingAvailable();
diff --git a/core/java/android/window/InputTransferToken.java b/core/java/android/window/InputTransferToken.java
index 5fab48f..d2cefa8 100644
--- a/core/java/android/window/InputTransferToken.java
+++ b/core/java/android/window/InputTransferToken.java
@@ -57,6 +57,7 @@
private static native void nativeWriteToParcel(long nativeObject, Parcel out);
private static native long nativeReadFromParcel(Parcel in);
private static native IBinder nativeGetBinderToken(long nativeObject);
+ private static native long nativeGetBinderTokenRef(long nativeObject);
private static native long nativeGetNativeInputTransferTokenFinalizer();
private static native boolean nativeEquals(long nativeObject1, long nativeObject2);
@@ -130,7 +131,7 @@
*/
@Override
public int hashCode() {
- return Objects.hash(getToken());
+ return Objects.hash(nativeGetBinderTokenRef(mNativeObject));
}
/**
diff --git a/core/java/android/window/TaskFragmentOperation.java b/core/java/android/window/TaskFragmentOperation.java
index 7e77f15..43df4f9 100644
--- a/core/java/android/window/TaskFragmentOperation.java
+++ b/core/java/android/window/TaskFragmentOperation.java
@@ -112,10 +112,13 @@
/**
* Creates a decor surface in the parent Task of the TaskFragment. The created decor surface
* will be provided in {@link TaskFragmentTransaction#TYPE_TASK_FRAGMENT_PARENT_INFO_CHANGED}
- * event callback. The decor surface can be used to draw the divider between TaskFragments or
- * other decorations.
+ * event callback. If a decor surface already exists in the parent Task, the current
+ * TaskFragment will become the new owner of the decor surface and the decor surface will be
+ * moved above the TaskFragment.
+ *
+ * The decor surface can be used to draw the divider between TaskFragments or other decorations.
*/
- public static final int OP_TYPE_CREATE_TASK_FRAGMENT_DECOR_SURFACE = 14;
+ public static final int OP_TYPE_CREATE_OR_MOVE_TASK_FRAGMENT_DECOR_SURFACE = 14;
/**
* Removes the decor surface in the parent Task of the TaskFragment.
@@ -162,7 +165,7 @@
OP_TYPE_SET_ISOLATED_NAVIGATION,
OP_TYPE_REORDER_TO_BOTTOM_OF_TASK,
OP_TYPE_REORDER_TO_TOP_OF_TASK,
- OP_TYPE_CREATE_TASK_FRAGMENT_DECOR_SURFACE,
+ OP_TYPE_CREATE_OR_MOVE_TASK_FRAGMENT_DECOR_SURFACE,
OP_TYPE_REMOVE_TASK_FRAGMENT_DECOR_SURFACE,
OP_TYPE_SET_DIM_ON_TASK,
OP_TYPE_SET_MOVE_TO_BOTTOM_IF_CLEAR_WHEN_LAUNCH,
diff --git a/core/java/android/window/TransitionInfo.java b/core/java/android/window/TransitionInfo.java
index 3685bba..5227724 100644
--- a/core/java/android/window/TransitionInfo.java
+++ b/core/java/android/window/TransitionInfo.java
@@ -542,6 +542,9 @@
// independent either.
if (change.getMode() == TRANSIT_CHANGE) return false;
+ // Always fold the activity embedding change into the parent change.
+ if (change.hasFlags(FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY)) return false;
+
TransitionInfo.Change parentChg = info.getChange(change.getParent());
while (parentChg != null) {
// If the parent is a visibility change, it will include the results of all child
diff --git a/core/java/android/window/WindowTokenClient.java b/core/java/android/window/WindowTokenClient.java
index 7f5331b..4a3aba1 100644
--- a/core/java/android/window/WindowTokenClient.java
+++ b/core/java/android/window/WindowTokenClient.java
@@ -165,7 +165,8 @@
Log.d(TAG, "Configuration not dispatch to IME because configuration is up"
+ " to date. Current config=" + context.getResources().getConfiguration()
+ ", reported config=" + currentConfig
- + ", updated config=" + newConfig);
+ + ", updated config=" + newConfig
+ + ", updated display ID=" + newDisplayId);
}
// Update display first. In case callers want to obtain display information(
// ex: DisplayMetrics) in #onConfigurationChanged callback.
@@ -190,13 +191,18 @@
if (mShouldDumpConfigForIme) {
if (!shouldReportConfigChange) {
Log.d(TAG, "Only apply configuration update to Resources because "
- + "shouldReportConfigChange is false.\n" + Debug.getCallers(5));
+ + "shouldReportConfigChange is false. "
+ + "context=" + context
+ + ", config=" + context.getResources().getConfiguration()
+ + ", display ID=" + context.getDisplayId() + "\n"
+ + Debug.getCallers(5));
} else if (diff == 0) {
Log.d(TAG, "Configuration not dispatch to IME because configuration has no "
+ " public difference with updated config. "
+ " Current config=" + context.getResources().getConfiguration()
+ ", reported config=" + currentConfig
- + ", updated config=" + newConfig);
+ + ", updated config=" + newConfig
+ + ", display ID=" + context.getDisplayId());
}
}
}
diff --git a/core/java/android/window/flags/accessibility.aconfig b/core/java/android/window/flags/accessibility.aconfig
index 814c620..90b54bd 100644
--- a/core/java/android/window/flags/accessibility.aconfig
+++ b/core/java/android/window/flags/accessibility.aconfig
@@ -12,4 +12,7 @@
namespace: "accessibility"
description: "Always draw fullscreen orange border in fullscreen magnification"
bug: "291891390"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
}
\ No newline at end of file
diff --git a/core/java/android/window/flags/large_screen_experiences_app_compat.aconfig b/core/java/android/window/flags/large_screen_experiences_app_compat.aconfig
index 254f4f7..7fbec67 100644
--- a/core/java/android/window/flags/large_screen_experiences_app_compat.aconfig
+++ b/core/java/android/window/flags/large_screen_experiences_app_compat.aconfig
@@ -18,6 +18,7 @@
flag {
name: "density_390_api"
+ is_exported: true
namespace: "large_screen_experiences_app_compat"
description: "Whether the API DisplayMetrics.DENSITY_390 is available"
bug: "297550533"
@@ -26,6 +27,7 @@
flag {
name: "app_compat_properties_api"
+ is_exported: true
namespace: "large_screen_experiences_app_compat"
description: "Whether app compat property APIs are public. Which includes: /n"
"WindowManager.PROPERTY_COMPAT_ALLOW_MIN_ASPECT_RATIO_OVERRIDE,/n"
diff --git a/core/java/android/window/flags/wallpaper_manager.aconfig b/core/java/android/window/flags/wallpaper_manager.aconfig
index ea9da96..dea9497 100644
--- a/core/java/android/window/flags/wallpaper_manager.aconfig
+++ b/core/java/android/window/flags/wallpaper_manager.aconfig
@@ -2,6 +2,7 @@
flag {
name: "always_update_wallpaper_permission"
+ is_exported: true
namespace: "wear_frameworks"
description: "Allow out of focus process to update wallpaper complications"
bug: "271132915"
diff --git a/core/java/android/window/flags/window_surfaces.aconfig b/core/java/android/window/flags/window_surfaces.aconfig
index 3f48341..00b600c 100644
--- a/core/java/android/window/flags/window_surfaces.aconfig
+++ b/core/java/android/window/flags/window_surfaces.aconfig
@@ -45,6 +45,7 @@
flag {
namespace: "window_surfaces"
name: "trusted_presentation_listener_for_window"
+ is_exported: true
description: "Enable trustedPresentationListener on windows public API"
is_fixed_read_only: true
bug: "278027319"
@@ -53,6 +54,7 @@
flag {
namespace: "window_surfaces"
name: "sdk_desired_present_time"
+ is_exported: true
description: "Feature flag for the new SDK API to set desired present time"
is_fixed_read_only: true
bug: "295038072"
@@ -61,6 +63,7 @@
flag {
namespace: "window_surfaces"
name: "surface_control_input_receiver"
+ is_exported: true
description: "Enable public API to register an InputReceiver for a SurfaceControl"
is_fixed_read_only: true
bug: "278757236"
@@ -69,6 +72,7 @@
flag {
namespace: "window_surfaces"
name: "screen_recording_callbacks"
+ is_exported: true
description: "Enable screen recording callbacks public API"
is_fixed_read_only: true
bug: "304574518"
diff --git a/core/java/android/window/flags/windowing_frontend.aconfig b/core/java/android/window/flags/windowing_frontend.aconfig
index 14fb17c..247f28c 100644
--- a/core/java/android/window/flags/windowing_frontend.aconfig
+++ b/core/java/android/window/flags/windowing_frontend.aconfig
@@ -16,6 +16,7 @@
flag {
name: "enforce_edge_to_edge"
+ is_exported: true
namespace: "windowing_frontend"
description: "Make app go edge-to-edge when targeting SDK level 35 or greater"
bug: "309578419"
@@ -38,6 +39,17 @@
}
flag {
+ name: "skip_sleeping_when_switching_display"
+ namespace: "windowing_frontend"
+ description: "Reduce unnecessary visibility or lifecycle changes when changing fold state"
+ bug: "303241079"
+ is_fixed_read_only: true
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
+
+flag {
name: "introduce_smoother_dimmer"
namespace: "windowing_frontend"
description: "Refactor dim to fix flickers"
@@ -77,6 +89,7 @@
flag {
name: "supports_multi_instance_system_ui"
+ is_exported: true
namespace: "multitasking"
description: "Feature flag to enable a multi-instance system ui component property."
bug: "262864589"
@@ -85,6 +98,7 @@
flag {
name: "delegate_unhandled_drags"
+ is_exported: true
namespace: "multitasking"
description: "Enables delegating unhandled drags to SystemUI"
bug: "320797628"
diff --git a/core/java/android/window/flags/windowing_sdk.aconfig b/core/java/android/window/flags/windowing_sdk.aconfig
index 82e613e..4b3d8e8 100644
--- a/core/java/android/window/flags/windowing_sdk.aconfig
+++ b/core/java/android/window/flags/windowing_sdk.aconfig
@@ -43,6 +43,7 @@
flag {
namespace: "windowing_sdk"
name: "untrusted_embedding_any_app_permission"
+ is_exported: true
description: "Feature flag to enable the permission to embed any app in untrusted mode."
bug: "293647332"
is_fixed_read_only: true
@@ -59,6 +60,7 @@
flag {
namespace: "windowing_sdk"
name: "untrusted_embedding_state_sharing"
+ is_exported: true
description: "Feature flag to enable state sharing in untrusted embedding when apps opt in."
bug: "293647332"
is_fixed_read_only: true
@@ -74,6 +76,7 @@
flag {
namespace: "windowing_sdk"
name: "cover_display_opt_in"
+ is_exported: true
description: "Properties to allow apps and activities to opt-in to cover display rendering"
bug: "312530526"
is_fixed_read_only: true
diff --git a/core/java/com/android/internal/accessibility/dialog/AccessibilityShortcutChooserActivity.java b/core/java/com/android/internal/accessibility/dialog/AccessibilityShortcutChooserActivity.java
index 2e80b7e..c70febb 100644
--- a/core/java/com/android/internal/accessibility/dialog/AccessibilityShortcutChooserActivity.java
+++ b/core/java/com/android/internal/accessibility/dialog/AccessibilityShortcutChooserActivity.java
@@ -20,7 +20,6 @@
import static android.view.accessibility.AccessibilityManager.ShortcutType;
import static com.android.internal.accessibility.common.ShortcutConstants.ShortcutMenuMode;
-import static com.android.internal.accessibility.dialog.AccessibilityTargetHelper.createEnableDialogContentView;
import static com.android.internal.accessibility.dialog.AccessibilityTargetHelper.getInstalledTargets;
import static com.android.internal.accessibility.dialog.AccessibilityTargetHelper.getTargets;
import static com.android.internal.accessibility.util.AccessibilityUtils.isUserSetupCompleted;
@@ -115,39 +114,22 @@
private void onTargetChecked(AdapterView<?> parent, View view, int position, long id) {
final AccessibilityTarget target = mTargets.get(position);
- if (Flags.cleanupAccessibilityWarningDialog()) {
- if (target instanceof AccessibilityServiceTarget serviceTarget) {
- if (sendRestrictedDialogIntentIfNeeded(target)) {
- return;
- }
- final AccessibilityManager am = getSystemService(AccessibilityManager.class);
- if (am.isAccessibilityServiceWarningRequired(
- serviceTarget.getAccessibilityServiceInfo())) {
- showPermissionDialogIfNeeded(this, (AccessibilityServiceTarget) target,
- position, mTargetAdapter);
- return;
- }
+ if (target instanceof AccessibilityServiceTarget serviceTarget) {
+ if (sendRestrictedDialogIntentIfNeeded(target)) {
+ return;
}
- if (target instanceof AccessibilityActivityTarget activityTarget) {
- if (!activityTarget.isShortcutEnabled()
- && sendRestrictedDialogIntentIfNeeded(activityTarget)) {
- return;
- }
+ final AccessibilityManager am = getSystemService(AccessibilityManager.class);
+ if (am.isAccessibilityServiceWarningRequired(
+ serviceTarget.getAccessibilityServiceInfo())) {
+ showPermissionDialogIfNeeded(this, (AccessibilityServiceTarget) target,
+ position, mTargetAdapter);
+ return;
}
- } else {
- if (!target.isShortcutEnabled()) {
- if (target instanceof AccessibilityServiceTarget
- || target instanceof AccessibilityActivityTarget) {
- if (sendRestrictedDialogIntentIfNeeded(target)) {
- return;
- }
- }
-
- if (target instanceof AccessibilityServiceTarget) {
- showPermissionDialogIfNeeded(this, (AccessibilityServiceTarget) target,
- position, mTargetAdapter);
- return;
- }
+ }
+ if (target instanceof AccessibilityActivityTarget activityTarget) {
+ if (!activityTarget.isShortcutEnabled()
+ && sendRestrictedDialogIntentIfNeeded(activityTarget)) {
+ return;
}
}
@@ -178,37 +160,25 @@
return;
}
- if (Flags.cleanupAccessibilityWarningDialog()) {
- mPermissionDialog = AccessibilityServiceWarning
- .createAccessibilityServiceWarningDialog(context,
- serviceTarget.getAccessibilityServiceInfo(),
- v -> {
- serviceTarget.onCheckedChanged(true);
- targetAdapter.notifyDataSetChanged();
- mPermissionDialog.dismiss();
- }, v -> {
- serviceTarget.onCheckedChanged(false);
- mPermissionDialog.dismiss();
- },
- v -> {
- mTargets.remove(position);
- context.getPackageManager().getPackageInstaller().uninstall(
- serviceTarget.getComponentName().getPackageName(), null);
- targetAdapter.notifyDataSetChanged();
- mPermissionDialog.dismiss();
- });
- mPermissionDialog.setOnDismissListener(dialog -> mPermissionDialog = null);
- } else {
- mPermissionDialog = new AlertDialog.Builder(context)
- .setView(createEnableDialogContentView(context, serviceTarget,
- v -> {
- mPermissionDialog.dismiss();
- targetAdapter.notifyDataSetChanged();
- },
- v -> mPermissionDialog.dismiss()))
- .setOnDismissListener(dialog -> mPermissionDialog = null)
- .create();
- }
+ mPermissionDialog = AccessibilityServiceWarning
+ .createAccessibilityServiceWarningDialog(context,
+ serviceTarget.getAccessibilityServiceInfo(),
+ v -> {
+ serviceTarget.onCheckedChanged(true);
+ targetAdapter.notifyDataSetChanged();
+ mPermissionDialog.dismiss();
+ }, v -> {
+ serviceTarget.onCheckedChanged(false);
+ mPermissionDialog.dismiss();
+ },
+ v -> {
+ mTargets.remove(position);
+ context.getPackageManager().getPackageInstaller().uninstall(
+ serviceTarget.getComponentName().getPackageName(), null);
+ targetAdapter.notifyDataSetChanged();
+ mPermissionDialog.dismiss();
+ });
+ mPermissionDialog.setOnDismissListener(dialog -> mPermissionDialog = null);
mPermissionDialog.show();
}
diff --git a/core/java/com/android/internal/accessibility/dialog/AccessibilityTargetHelper.java b/core/java/com/android/internal/accessibility/dialog/AccessibilityTargetHelper.java
index 3d3db47..0d82d63 100644
--- a/core/java/com/android/internal/accessibility/dialog/AccessibilityTargetHelper.java
+++ b/core/java/com/android/internal/accessibility/dialog/AccessibilityTargetHelper.java
@@ -37,14 +37,8 @@
import android.os.Build;
import android.os.UserHandle;
import android.provider.Settings;
-import android.text.BidiFormatter;
-import android.view.LayoutInflater;
-import android.view.View;
import android.view.accessibility.AccessibilityManager;
import android.view.accessibility.AccessibilityManager.ShortcutType;
-import android.widget.Button;
-import android.widget.ImageView;
-import android.widget.TextView;
import com.android.internal.R;
import com.android.internal.accessibility.common.ShortcutConstants.AccessibilityFragmentType;
@@ -52,7 +46,6 @@
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
-import java.util.Locale;
/**
* Collection of utilities for accessibility target.
@@ -298,50 +291,6 @@
}
/**
- * @deprecated Use {@link AccessibilityServiceWarning}.
- */
- @Deprecated
- static View createEnableDialogContentView(Context context,
- AccessibilityServiceTarget target, View.OnClickListener allowListener,
- View.OnClickListener denyListener) {
- final LayoutInflater inflater = (LayoutInflater) context.getSystemService(
- Context.LAYOUT_INFLATER_SERVICE);
-
- final View content = inflater.inflate(
- R.layout.accessibility_enable_service_warning, /* root= */ null);
-
- final ImageView dialogIcon = content.findViewById(
- R.id.accessibility_permissionDialog_icon);
- dialogIcon.setImageDrawable(target.getIcon());
-
- final TextView dialogTitle = content.findViewById(
- R.id.accessibility_permissionDialog_title);
- dialogTitle.setText(context.getString(R.string.accessibility_enable_service_title,
- getServiceName(context, target.getLabel())));
-
- final Button allowButton = content.findViewById(
- R.id.accessibility_permission_enable_allow_button);
- final Button denyButton = content.findViewById(
- R.id.accessibility_permission_enable_deny_button);
- allowButton.setOnClickListener((view) -> {
- target.onCheckedChanged(/* isChecked= */ true);
- allowListener.onClick(view);
- });
- denyButton.setOnClickListener((view) -> {
- target.onCheckedChanged(/* isChecked= */ false);
- denyListener.onClick(view);
- });
-
- return content;
- }
-
- // Gets the service name and bidi wrap it to protect from bidi side effects.
- private static CharSequence getServiceName(Context context, CharSequence label) {
- final Locale locale = context.getResources().getConfiguration().getLocales().get(0);
- return BidiFormatter.getInstance(locale).unicodeWrap(label);
- }
-
- /**
* Determines if the{@link AccessibilityTarget} is allowed.
*/
public static boolean isAccessibilityTargetAllowed(Context context, String packageName,
diff --git a/core/java/com/android/internal/app/ChooserActivity.java b/core/java/com/android/internal/app/ChooserActivity.java
index 29669d3..ab456a8 100644
--- a/core/java/com/android/internal/app/ChooserActivity.java
+++ b/core/java/com/android/internal/app/ChooserActivity.java
@@ -96,7 +96,6 @@
import android.provider.OpenableColumns;
import android.provider.Settings;
import android.service.chooser.ChooserTarget;
-import android.service.chooser.Flags;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.util.HashedStringCache;
@@ -1801,54 +1800,6 @@
return getIntent().getBooleanExtra(Intent.EXTRA_AUTO_LAUNCH_SINGLE_CHOICE, true);
}
- private void showTargetDetails(TargetInfo targetInfo) {
- if (targetInfo == null) return;
-
- ArrayList<DisplayResolveInfo> targetList;
- ChooserTargetActionsDialogFragment fragment = new ChooserTargetActionsDialogFragment();
- Bundle bundle = new Bundle();
-
- if (targetInfo instanceof SelectableTargetInfo) {
- SelectableTargetInfo selectableTargetInfo = (SelectableTargetInfo) targetInfo;
- if (selectableTargetInfo.getDisplayResolveInfo() == null
- || selectableTargetInfo.getChooserTarget() == null) {
- Log.e(TAG, "displayResolveInfo or chooserTarget in selectableTargetInfo are null");
- return;
- }
- targetList = new ArrayList<>();
- targetList.add(selectableTargetInfo.getDisplayResolveInfo());
- bundle.putString(ChooserTargetActionsDialogFragment.SHORTCUT_ID_KEY,
- selectableTargetInfo.getChooserTarget().getIntentExtras().getString(
- Intent.EXTRA_SHORTCUT_ID));
- bundle.putBoolean(ChooserTargetActionsDialogFragment.IS_SHORTCUT_PINNED_KEY,
- selectableTargetInfo.isPinned());
- bundle.putParcelable(ChooserTargetActionsDialogFragment.INTENT_FILTER_KEY,
- getTargetIntentFilter());
- if (selectableTargetInfo.getDisplayLabel() != null) {
- bundle.putString(ChooserTargetActionsDialogFragment.SHORTCUT_TITLE_KEY,
- selectableTargetInfo.getDisplayLabel().toString());
- }
- } else if (targetInfo instanceof MultiDisplayResolveInfo) {
- // For multiple targets, include info on all targets
- MultiDisplayResolveInfo mti = (MultiDisplayResolveInfo) targetInfo;
- targetList = mti.getTargets();
- } else {
- targetList = new ArrayList<DisplayResolveInfo>();
- targetList.add((DisplayResolveInfo) targetInfo);
- }
- // Adding userHandle from ResolveInfo allows the app icon in Dialog Box to be
- // resolved correctly.
- bundle.putParcelable(ChooserTargetActionsDialogFragment.USER_HANDLE_KEY,
- getResolveInfoUserHandle(
- targetInfo.getResolveInfo(),
- mChooserMultiProfilePagerAdapter.getCurrentUserHandle()));
- bundle.putParcelableArrayList(ChooserTargetActionsDialogFragment.TARGET_INFOS_KEY,
- targetList);
- fragment.setArguments(bundle);
-
- fragment.show(getFragmentManager(), TARGET_DETAILS_FRAGMENT_TAG);
- }
-
private void modifyTargetIntent(Intent in) {
if (isSendAction(in)) {
in.addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT |
@@ -2544,10 +2495,7 @@
@Override
public boolean isComponentPinned(ComponentName name) {
- if (Flags.legacyChooserPinningRemoval()) {
- return false;
- }
- return mPinnedSharedPrefs.getBoolean(name.flattenToString(), false);
+ return false;
}
@Override
@@ -3135,34 +3083,10 @@
if (isClickable) {
itemView.setOnClickListener(v -> startSelected(mListPosition,
false/* always */, true/* filterd */));
-
- itemView.setOnLongClickListener(v -> {
- final TargetInfo ti = mChooserMultiProfilePagerAdapter.getActiveListAdapter()
- .targetInfoForPosition(mListPosition, /* filtered */ true);
-
- // This should always be the case for ItemViewHolder, check for validity
- if (ti instanceof DisplayResolveInfo && shouldShowTargetDetails(ti)) {
- showTargetDetails((DisplayResolveInfo) ti);
- }
- return true;
- });
}
}
}
- private boolean shouldShowTargetDetails(TargetInfo ti) {
- if (Flags.legacyChooserPinningRemoval()) {
- // Never show the long press menu if we've removed pinning.
- return false;
- }
- ComponentName nearbyShare = getNearbySharingComponent();
- // Suppress target details for nearby share to hide pin/unpin action
- boolean isNearbyShare = nearbyShare != null && nearbyShare.equals(
- ti.getResolvedComponentName()) && shouldNearbyShareBeFirstInRankedRow();
- return ti instanceof SelectableTargetInfo
- || (ti instanceof DisplayResolveInfo && !isNearbyShare);
- }
-
/**
* Add a footer to the list, to support scrolling behavior below the navbar.
*/
@@ -3517,16 +3441,6 @@
}
});
- // Show menu for both direct share and app share targets after long click.
- v.setOnLongClickListener(v1 -> {
- TargetInfo ti = mChooserListAdapter.targetInfoForPosition(
- holder.getItemIndex(column), true);
- if (shouldShowTargetDetails(ti)) {
- showTargetDetails(ti);
- }
- return true;
- });
-
holder.addView(i, v);
// Force Direct Share to be 2 lines and auto-wrap to second line via hoz scroll =
diff --git a/core/java/com/android/internal/app/ResolverActivity.java b/core/java/com/android/internal/app/ResolverActivity.java
index 78f06b6..84715aa 100644
--- a/core/java/com/android/internal/app/ResolverActivity.java
+++ b/core/java/com/android/internal/app/ResolverActivity.java
@@ -217,6 +217,12 @@
public static final String EXTRA_IS_AUDIO_CAPTURE_DEVICE = "is_audio_capture_device";
/**
+ * Boolean extra to indicate if Resolver Sheet needs to be started in single user mode.
+ */
+ protected static final String EXTRA_RESTRICT_TO_SINGLE_USER =
+ "com.android.internal.app.ResolverActivity.EXTRA_RESTRICT_TO_SINGLE_USER";
+
+ /**
* Integer extra to indicate which profile should be automatically selected.
* <p>Can only be used if there is a work profile.
* <p>Possible values can be either {@link #PROFILE_PERSONAL} or {@link #PROFILE_WORK}.
@@ -750,8 +756,10 @@
}
protected UserHandle getPersonalProfileUserHandle() {
- if (privateSpaceEnabled() && isLaunchedAsPrivateProfile()){
- return mPrivateProfileUserHandle;
+ // When launched in single user mode, only personal tab is populated, so we use
+ // tabOwnerUserHandleForLaunch as personal tab's user handle.
+ if (privateSpaceEnabled() && isLaunchedInSingleUserMode()) {
+ return getTabOwnerUserHandleForLaunch();
}
return mPersonalProfileUserHandle;
}
@@ -822,11 +830,11 @@
// If we are in work or private profile's process, return WorkProfile/PrivateProfile user
// as owner, otherwise we always return PersonalProfile user as owner
if (UserHandle.of(UserHandle.myUserId()).equals(getWorkProfileUserHandle())) {
- return getWorkProfileUserHandle();
+ return mWorkProfileUserHandle;
} else if (privateSpaceEnabled() && isLaunchedAsPrivateProfile()) {
- return getPrivateProfileUserHandle();
+ return mPrivateProfileUserHandle;
}
- return getPersonalProfileUserHandle();
+ return mPersonalProfileUserHandle;
}
private boolean hasWorkProfile() {
@@ -847,8 +855,18 @@
&& (UserHandle.myUserId() == getPrivateProfileUserHandle().getIdentifier());
}
+ protected final boolean isLaunchedInSingleUserMode() {
+ // When launched from Private Profile, return true
+ if (isLaunchedAsPrivateProfile()) {
+ return true;
+ }
+ return getIntent()
+ .getBooleanExtra(EXTRA_RESTRICT_TO_SINGLE_USER, /* defaultValue = */ false);
+ }
+
protected boolean shouldShowTabs() {
- if (privateSpaceEnabled() && isLaunchedAsPrivateProfile()) {
+ // No Tabs are shown when launched in single user mode.
+ if (privateSpaceEnabled() && isLaunchedInSingleUserMode()) {
return false;
}
return hasWorkProfile() && ENABLE_TABBED_VIEW;
diff --git a/core/java/com/android/internal/app/SuspendedAppActivity.java b/core/java/com/android/internal/app/SuspendedAppActivity.java
index 467cd49..751368f 100644
--- a/core/java/com/android/internal/app/SuspendedAppActivity.java
+++ b/core/java/com/android/internal/app/SuspendedAppActivity.java
@@ -16,6 +16,7 @@
package com.android.internal.app;
+import static android.app.admin.flags.Flags.crossUserSuspensionEnabled;
import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_AWARE;
import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_UNAWARE;
import static android.content.pm.SuspendDialogInfo.BUTTON_ACTION_MORE_DETAILS;
@@ -59,6 +60,7 @@
public static final String EXTRA_SUSPENDED_PACKAGE = PACKAGE_NAME + ".extra.SUSPENDED_PACKAGE";
public static final String EXTRA_SUSPENDING_PACKAGE =
PACKAGE_NAME + ".extra.SUSPENDING_PACKAGE";
+ public static final String EXTRA_SUSPENDING_USER = PACKAGE_NAME + ".extra.SUSPENDING_USER";
public static final String EXTRA_DIALOG_INFO = PACKAGE_NAME + ".extra.DIALOG_INFO";
public static final String EXTRA_ACTIVITY_OPTIONS = PACKAGE_NAME + ".extra.ACTIVITY_OPTIONS";
public static final String EXTRA_UNSUSPEND_INTENT = PACKAGE_NAME + ".extra.UNSUSPEND_INTENT";
@@ -67,6 +69,7 @@
private IntentSender mOnUnsuspend;
private String mSuspendedPackage;
private String mSuspendingPackage;
+ private int mSuspendingUserId;
private int mNeutralButtonAction;
private int mUserId;
private PackageManager mPm;
@@ -117,7 +120,7 @@
.setPackage(mSuspendingPackage);
final String requiredPermission = Manifest.permission.SEND_SHOW_SUSPENDED_APP_DETAILS;
final ResolveInfo resolvedInfo = mPm.resolveActivityAsUser(moreDetailsIntent,
- MATCH_DIRECT_BOOT_UNAWARE | MATCH_DIRECT_BOOT_AWARE, mUserId);
+ MATCH_DIRECT_BOOT_UNAWARE | MATCH_DIRECT_BOOT_AWARE, mSuspendingUserId);
if (resolvedInfo != null && resolvedInfo.activityInfo != null
&& requiredPermission.equals(resolvedInfo.activityInfo.permission)) {
moreDetailsIntent.putExtra(Intent.EXTRA_PACKAGE_NAME, mSuspendedPackage)
@@ -231,12 +234,17 @@
}
mSuspendedPackage = intent.getStringExtra(EXTRA_SUSPENDED_PACKAGE);
mSuspendingPackage = intent.getStringExtra(EXTRA_SUSPENDING_PACKAGE);
+ if (crossUserSuspensionEnabled()) {
+ mSuspendingUserId = intent.getIntExtra(EXTRA_SUSPENDING_USER, mUserId);
+ } else {
+ mSuspendingUserId = mUserId;
+ }
mSuppliedDialogInfo = intent.getParcelableExtra(EXTRA_DIALOG_INFO, android.content.pm.SuspendDialogInfo.class);
mOnUnsuspend = intent.getParcelableExtra(EXTRA_UNSUSPEND_INTENT, android.content.IntentSender.class);
if (mSuppliedDialogInfo != null) {
try {
mSuspendingAppResources = createContextAsUser(
- UserHandle.of(mUserId), /* flags */ 0).getPackageManager()
+ UserHandle.of(mSuspendingUserId), /* flags */ 0).getPackageManager()
.getResourcesForApplication(mSuspendingPackage);
} catch (PackageManager.NameNotFoundException ne) {
Slog.e(TAG, "Could not find resources for " + mSuspendingPackage, ne);
@@ -299,7 +307,7 @@
case BUTTON_ACTION_MORE_DETAILS:
if (mMoreDetailsIntent != null) {
startActivityAsUser(mMoreDetailsIntent, mOptions,
- UserHandle.of(mUserId));
+ UserHandle.of(mSuspendingUserId));
} else {
Slog.wtf(TAG, "Neutral button should not have existed!");
}
@@ -324,7 +332,7 @@
.putExtra(Intent.EXTRA_PACKAGE_NAME, mSuspendedPackage)
.setPackage(mSuspendingPackage)
.addFlags(Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND);
- sendBroadcastAsUser(reportUnsuspend, UserHandle.of(mUserId));
+ sendBroadcastAsUser(reportUnsuspend, UserHandle.of(mSuspendingUserId));
if (mOnUnsuspend != null) {
Bundle activityOptions =
@@ -365,6 +373,9 @@
.putExtra(Intent.EXTRA_USER_ID, userId)
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
| Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
+ if (crossUserSuspensionEnabled()) {
+ intent.putExtra(EXTRA_SUSPENDING_USER, suspendingPackage.userId);
+ }
return intent;
}
}
diff --git a/core/java/com/android/internal/compat/compat_logging_flags.aconfig b/core/java/com/android/internal/compat/compat_logging_flags.aconfig
index fab3856..a5c31ed 100644
--- a/core/java/com/android/internal/compat/compat_logging_flags.aconfig
+++ b/core/java/com/android/internal/compat/compat_logging_flags.aconfig
@@ -2,7 +2,7 @@
flag {
name: "skip_old_and_disabled_compat_logging"
- namespace: "platform_compat"
+ namespace: "app_compat"
description: "Feature flag for skipping debug logging for changes that do not target the latest sdk or are disabled"
bug: "323949942"
is_fixed_read_only: true
diff --git a/core/java/com/android/internal/jank/Cuj.java b/core/java/com/android/internal/jank/Cuj.java
index 3662d69..d2a533c 100644
--- a/core/java/com/android/internal/jank/Cuj.java
+++ b/core/java/com/android/internal/jank/Cuj.java
@@ -124,10 +124,13 @@
public static final int CUJ_BACK_PANEL_ARROW = 88;
public static final int CUJ_LAUNCHER_CLOSE_ALL_APPS_BACK = 89;
public static final int CUJ_LAUNCHER_SEARCH_QSB_WEB_SEARCH = 90;
+ public static final int CUJ_LAUNCHER_LAUNCH_APP_PAIR_FROM_WORKSPACE = 91;
+ public static final int CUJ_LAUNCHER_LAUNCH_APP_PAIR_FROM_TASKBAR = 92;
+ public static final int CUJ_LAUNCHER_SAVE_APP_PAIR = 93;
// When adding a CUJ, update this and make sure to also update CUJ_TO_STATSD_INTERACTION_TYPE.
@VisibleForTesting
- static final int LAST_CUJ = CUJ_LAUNCHER_SEARCH_QSB_WEB_SEARCH;
+ static final int LAST_CUJ = CUJ_LAUNCHER_SAVE_APP_PAIR;
/** @hide */
@IntDef({
@@ -212,6 +215,9 @@
CUJ_BACK_PANEL_ARROW,
CUJ_LAUNCHER_CLOSE_ALL_APPS_BACK,
CUJ_LAUNCHER_SEARCH_QSB_WEB_SEARCH,
+ CUJ_LAUNCHER_LAUNCH_APP_PAIR_FROM_WORKSPACE,
+ CUJ_LAUNCHER_LAUNCH_APP_PAIR_FROM_TASKBAR,
+ CUJ_LAUNCHER_SAVE_APP_PAIR
})
@Retention(RetentionPolicy.SOURCE)
public @interface CujType {
@@ -306,6 +312,9 @@
CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_BACK_PANEL_ARROW] = FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__BACK_PANEL_ARROW;
CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_LAUNCHER_CLOSE_ALL_APPS_BACK] = FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LAUNCHER_CLOSE_ALL_APPS_BACK;
CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_LAUNCHER_SEARCH_QSB_WEB_SEARCH] = FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LAUNCHER_SEARCH_QSB_WEB_SEARCH;
+ CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_LAUNCHER_LAUNCH_APP_PAIR_FROM_WORKSPACE] = FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LAUNCHER_LAUNCH_APP_PAIR_FROM_WORKSPACE;
+ CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_LAUNCHER_LAUNCH_APP_PAIR_FROM_TASKBAR] = FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LAUNCHER_LAUNCH_APP_PAIR_FROM_TASKBAR;
+ CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_LAUNCHER_SAVE_APP_PAIR] = FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LAUNCHER_SAVE_APP_PAIR;
}
private Cuj() {
@@ -484,6 +493,12 @@
return "LAUNCHER_CLOSE_ALL_APPS_BACK";
case CUJ_LAUNCHER_SEARCH_QSB_WEB_SEARCH:
return "LAUNCHER_SEARCH_QSB_WEB_SEARCH";
+ case CUJ_LAUNCHER_LAUNCH_APP_PAIR_FROM_WORKSPACE:
+ return "LAUNCHER_LAUNCH_APP_PAIR_FROM_WORKSPACE";
+ case CUJ_LAUNCHER_LAUNCH_APP_PAIR_FROM_TASKBAR:
+ return "LAUNCHER_LAUNCH_APP_PAIR_FROM_TASKBAR";
+ case CUJ_LAUNCHER_SAVE_APP_PAIR:
+ return "LAUNCHER_SAVE_APP_PAIR";
}
return "UNKNOWN";
}
diff --git a/core/java/com/android/internal/jank/InteractionJankMonitor.java b/core/java/com/android/internal/jank/InteractionJankMonitor.java
index 0ec8b74..a288fb7 100644
--- a/core/java/com/android/internal/jank/InteractionJankMonitor.java
+++ b/core/java/com/android/internal/jank/InteractionJankMonitor.java
@@ -165,6 +165,9 @@
@Deprecated public static final int CUJ_PREDICTIVE_BACK_CROSS_ACTIVITY = Cuj.CUJ_PREDICTIVE_BACK_CROSS_ACTIVITY;
@Deprecated public static final int CUJ_PREDICTIVE_BACK_CROSS_TASK = Cuj.CUJ_PREDICTIVE_BACK_CROSS_TASK;
@Deprecated public static final int CUJ_PREDICTIVE_BACK_HOME = Cuj.CUJ_PREDICTIVE_BACK_HOME;
+ @Deprecated public static final int CUJ_LAUNCHER_LAUNCH_APP_PAIR_FROM_WORKSPACE = Cuj.CUJ_LAUNCHER_LAUNCH_APP_PAIR_FROM_WORKSPACE;
+ @Deprecated public static final int CUJ_LAUNCHER_LAUNCH_APP_PAIR_FROM_TASKBAR = Cuj.CUJ_LAUNCHER_LAUNCH_APP_PAIR_FROM_TASKBAR;
+ @Deprecated public static final int CUJ_LAUNCHER_SAVE_APP_PAIR = Cuj.CUJ_LAUNCHER_SAVE_APP_PAIR;
private static class InstanceHolder {
public static final InteractionJankMonitor INSTANCE =
diff --git a/core/java/com/android/internal/net/ConnectivityBlobStore.java b/core/java/com/android/internal/net/ConnectivityBlobStore.java
new file mode 100644
index 0000000..1b18485
--- /dev/null
+++ b/core/java/com/android/internal/net/ConnectivityBlobStore.java
@@ -0,0 +1,173 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.net;
+
+import android.annotation.NonNull;
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.database.SQLException;
+import android.database.sqlite.SQLiteDatabase;
+import android.os.Binder;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Database for storing blobs with a key of name strings.
+ * @hide
+ */
+public class ConnectivityBlobStore {
+ private static final String TAG = ConnectivityBlobStore.class.getSimpleName();
+ private static final String TABLENAME = "blob_table";
+ private static final String ROOT_DIR = "/data/misc/connectivityblobdb/";
+
+ private static final String CREATE_TABLE =
+ "CREATE TABLE IF NOT EXISTS " + TABLENAME + " ("
+ + "owner INTEGER,"
+ + "name BLOB,"
+ + "blob BLOB,"
+ + "UNIQUE(owner, name));";
+
+ private final SQLiteDatabase mDb;
+
+ /**
+ * Construct a ConnectivityBlobStore object.
+ *
+ * @param dbName the filename of the database to create/access.
+ */
+ public ConnectivityBlobStore(String dbName) {
+ this(new File(ROOT_DIR + dbName));
+ }
+
+ @VisibleForTesting
+ public ConnectivityBlobStore(File file) {
+ final SQLiteDatabase.OpenParams params = new SQLiteDatabase.OpenParams.Builder()
+ .addOpenFlags(SQLiteDatabase.CREATE_IF_NECESSARY)
+ .addOpenFlags(SQLiteDatabase.ENABLE_WRITE_AHEAD_LOGGING)
+ .build();
+ mDb = SQLiteDatabase.openDatabase(file, params);
+ mDb.execSQL(CREATE_TABLE);
+ }
+
+ /**
+ * Stores the blob under the name in the database. Existing blobs by the same name will be
+ * replaced.
+ *
+ * @param name The name of the blob
+ * @param blob The blob.
+ * @return true if the blob was successfully added. False otherwise.
+ * @hide
+ */
+ public boolean put(@NonNull String name, @NonNull byte[] blob) {
+ final int ownerUid = Binder.getCallingUid();
+ final ContentValues values = new ContentValues();
+ values.put("owner", ownerUid);
+ values.put("name", name);
+ values.put("blob", blob);
+
+ // No need for try-catch since it is done within db.replace
+ // nullColumnHack is for the case where values may be empty since SQL does not allow
+ // inserting a completely empty row. Since values is never empty, set this to null.
+ final long res = mDb.replace(TABLENAME, null /* nullColumnHack */, values);
+ return res > 0;
+ }
+
+ /**
+ * Retrieves a blob by the name from the database.
+ *
+ * @param name Name of the blob to retrieve.
+ * @return The unstructured blob, that is the blob that was stored using
+ * {@link com.android.internal.net.ConnectivityBlobStore#put}.
+ * Returns null if no blob was found.
+ * @hide
+ */
+ public byte[] get(@NonNull String name) {
+ final int ownerUid = Binder.getCallingUid();
+ try (Cursor cursor = mDb.query(TABLENAME,
+ new String[] {"blob"} /* columns */,
+ "owner=? AND name=?" /* selection */,
+ new String[] {Integer.toString(ownerUid), name} /* selectionArgs */,
+ null /* groupBy */,
+ null /* having */,
+ null /* orderBy */)) {
+ if (cursor.moveToFirst()) {
+ return cursor.getBlob(0);
+ }
+ } catch (SQLException e) {
+ Log.e(TAG, "Error in getting " + name + ": " + e);
+ }
+
+ return null;
+ }
+
+ /**
+ * Removes a blob by the name from the database.
+ *
+ * @param name Name of the blob to be removed.
+ * @return True if a blob was removed. False if no such name was found.
+ * @hide
+ */
+ public boolean remove(@NonNull String name) {
+ final int ownerUid = Binder.getCallingUid();
+ try {
+ final int res = mDb.delete(TABLENAME,
+ "owner=? AND name=?" /* whereClause */,
+ new String[] {Integer.toString(ownerUid), name} /* whereArgs */);
+ return res > 0;
+ } catch (SQLException e) {
+ Log.e(TAG, "Error in removing " + name + ": " + e);
+ return false;
+ }
+ }
+
+ /**
+ * Lists the name suffixes stored in the database matching the given prefix, sorted in
+ * ascending order.
+ *
+ * @param prefix String of prefix to list from the stored names.
+ * @return An array of strings representing the name suffixes stored in the database
+ * matching the given prefix, sorted in ascending order.
+ * The return value may be empty but never null.
+ * @hide
+ */
+ public String[] list(@NonNull String prefix) {
+ final int ownerUid = Binder.getCallingUid();
+ final List<String> names = new ArrayList<String>();
+ try (Cursor cursor = mDb.query(TABLENAME,
+ new String[] {"name"} /* columns */,
+ "owner=? AND name LIKE ?" /* selection */,
+ new String[] {Integer.toString(ownerUid), prefix + "%"} /* selectionArgs */,
+ null /* groupBy */,
+ null /* having */,
+ "name ASC" /* orderBy */)) {
+ if (cursor.moveToFirst()) {
+ do {
+ final String name = cursor.getString(0);
+ names.add(name.substring(prefix.length()));
+ } while (cursor.moveToNext());
+ }
+ } catch (SQLException e) {
+ Log.e(TAG, "Error in listing " + prefix + ": " + e);
+ }
+
+ return names.toArray(new String[names.size()]);
+ }
+}
diff --git a/core/java/com/android/internal/policy/DecorView.java b/core/java/com/android/internal/policy/DecorView.java
index 0f1f7e9..a65a1bb 100644
--- a/core/java/com/android/internal/policy/DecorView.java
+++ b/core/java/com/android/internal/policy/DecorView.java
@@ -137,7 +137,7 @@
private static final int SCRIM_LIGHT = 0xe6ffffff; // 90% white
- private static final int SCRIM_ALPHA = 0xcc0000; // 80% alpha
+ private static final int SCRIM_ALPHA = 0xcc000000; // 80% alpha
public static final ColorViewAttributes STATUS_BAR_COLOR_VIEW_ATTRIBUTES =
new ColorViewAttributes(FLAG_TRANSLUCENT_STATUS,
diff --git a/core/java/com/android/internal/statusbar/IStatusBar.aidl b/core/java/com/android/internal/statusbar/IStatusBar.aidl
index a22232a..f5b1a47 100644
--- a/core/java/com/android/internal/statusbar/IStatusBar.aidl
+++ b/core/java/com/android/internal/statusbar/IStatusBar.aidl
@@ -388,9 +388,9 @@
*/
void showMediaOutputSwitcher(String packageName);
- /** Enters desktop mode.
+ /** Enters desktop mode from the current focused app.
*
* @param displayId the id of the current display.
*/
- void enterDesktop(int displayId);
+ void moveFocusedTaskToDesktop(int displayId);
}
diff --git a/core/java/com/android/internal/view/BaseIWindow.java b/core/java/com/android/internal/view/BaseIWindow.java
index 600058e..e33704b 100644
--- a/core/java/com/android/internal/view/BaseIWindow.java
+++ b/core/java/com/android/internal/view/BaseIWindow.java
@@ -33,6 +33,7 @@
import android.view.ScrollCaptureResponse;
import android.view.WindowInsets.Type.InsetsType;
import android.view.inputmethod.ImeTracker;
+import android.window.ActivityWindowInfo;
import android.window.ClientWindowFrames;
import com.android.internal.os.IResultReceiver;
@@ -53,7 +54,8 @@
@Override
public void resized(ClientWindowFrames frames, boolean reportDraw,
MergedConfiguration mergedConfiguration, InsetsState insetsState, boolean forceLayout,
- boolean alwaysConsumeSystemBars, int displayId, int seqId, boolean dragResizing) {
+ boolean alwaysConsumeSystemBars, int displayId, int seqId, boolean dragResizing,
+ @Nullable ActivityWindowInfo activityWindowInfo) {
if (reportDraw) {
try {
mSession.finishDrawing(this, null /* postDrawTransaction */, seqId);
diff --git a/core/java/com/android/internal/widget/ConversationAvatarData.java b/core/java/com/android/internal/widget/ConversationAvatarData.java
index e04772f..bc9cd40 100644
--- a/core/java/com/android/internal/widget/ConversationAvatarData.java
+++ b/core/java/com/android/internal/widget/ConversationAvatarData.java
@@ -21,9 +21,9 @@
/**
* @hide
*/
-interface ConversationAvatarData {
+public interface ConversationAvatarData {
final class OneToOneConversationAvatarData implements ConversationAvatarData {
- final Drawable mDrawable;
+ public final Drawable mDrawable;
OneToOneConversationAvatarData(Drawable drawable) {
mDrawable = drawable;
diff --git a/core/java/com/android/internal/widget/ConversationHeaderData.java b/core/java/com/android/internal/widget/ConversationHeaderData.java
index 0953b39..ea92155 100644
--- a/core/java/com/android/internal/widget/ConversationHeaderData.java
+++ b/core/java/com/android/internal/widget/ConversationHeaderData.java
@@ -21,7 +21,7 @@
/**
* @hide
*/
-final class ConversationHeaderData {
+public final class ConversationHeaderData {
private final CharSequence mConversationText;
private final ConversationAvatarData mConversationAvatarData;
@@ -38,7 +38,7 @@
}
@Nullable
- ConversationAvatarData getConversationAvatar() {
+ public ConversationAvatarData getConversationAvatar() {
return mConversationAvatarData;
}
}
diff --git a/core/java/com/android/internal/widget/ConversationLayout.java b/core/java/com/android/internal/widget/ConversationLayout.java
index 6d5a96a..b6066ba 100644
--- a/core/java/com/android/internal/widget/ConversationLayout.java
+++ b/core/java/com/android/internal/widget/ConversationLayout.java
@@ -162,6 +162,8 @@
private TouchDelegateComposite mTouchDelegate = new TouchDelegateComposite(this);
private ArrayList<MessagingLinearLayout.MessagingChild> mToRecycle = new ArrayList<>();
private boolean mPrecomputedTextEnabled = false;
+ @Nullable
+ private ConversationHeaderData mConversationHeaderData;
public ConversationLayout(@NonNull Context context) {
super(context);
@@ -651,6 +653,7 @@
private void setConversationAvatarAndNameFromData(
ConversationHeaderData conversationHeaderData) {
+ mConversationHeaderData = conversationHeaderData;
final OneToOneConversationAvatarData oneToOneConversationDrawable;
final GroupConversationAvatarData groupConversationAvatarData;
final ConversationAvatarData conversationAvatar =
@@ -804,7 +807,10 @@
bottomBackground.setLayoutParams(layoutParams);
}
- private void bindFacePileWithDrawable(ImageView bottomBackground, ImageView bottomView,
+ /**
+ * Binds group avatar drawables to face pile.
+ */
+ public void bindFacePileWithDrawable(ImageView bottomBackground, ImageView bottomView,
ImageView topView, GroupConversationAvatarData groupConversationAvatarData) {
applyNotificationBackgroundColor(bottomBackground);
bottomView.setImageDrawable(groupConversationAvatarData.mLastIcon);
@@ -1573,6 +1579,11 @@
return mConversationIcon;
}
+ @Nullable
+ public ConversationHeaderData getConversationHeaderData() {
+ return mConversationHeaderData;
+ }
+
private static class TouchDelegateComposite extends TouchDelegate {
private final ArrayList<TouchDelegate> mDelegates = new ArrayList<>();
diff --git a/core/java/com/android/internal/widget/EmphasizedNotificationButton.java b/core/java/com/android/internal/widget/EmphasizedNotificationButton.java
index 01b4569..c07e624 100644
--- a/core/java/com/android/internal/widget/EmphasizedNotificationButton.java
+++ b/core/java/com/android/internal/widget/EmphasizedNotificationButton.java
@@ -278,11 +278,6 @@
// be ready to glue. This can only happen if the button is initialized and displayed and
// *then* someone calls glueIcon or glueLabel.
- if (mIconToGlue == null) {
- Log.w(TAG, "glueIconAndLabelIfNeeded: label glued without icon; doing nothing");
- return;
- }
-
if (mLabelToGlue == null) {
Log.w(TAG, "glueIconAndLabelIfNeeded: icon glued without label; doing nothing");
return;
@@ -318,6 +313,14 @@
private static final String POP_DIRECTIONAL_ISOLATE = "\u2069";
private void glueIconAndLabel(int layoutDirection) {
+ if (mIconToGlue == null) {
+ if (DEBUG_NEW_ACTION_LAYOUT) {
+ Log.d(TAG, "glueIconAndLabel: null icon, setting text to label");
+ }
+ setText(mLabelToGlue);
+ return;
+ }
+
final boolean rtlLayout = layoutDirection == LAYOUT_DIRECTION_RTL;
if (DEBUG_NEW_ACTION_LAYOUT) {
diff --git a/core/java/com/android/internal/widget/ImageFloatingTextView.java b/core/java/com/android/internal/widget/ImageFloatingTextView.java
index 5da6435..352e6d8 100644
--- a/core/java/com/android/internal/widget/ImageFloatingTextView.java
+++ b/core/java/com/android/internal/widget/ImageFloatingTextView.java
@@ -31,6 +31,8 @@
import android.widget.RemoteViews;
import android.widget.TextView;
+import com.android.internal.R;
+
/**
* A TextView that can float around an image on the end.
*
@@ -49,6 +51,7 @@
private int mMaxLinesForHeight = -1;
private int mLayoutMaxLines = -1;
private int mImageEndMargin;
+ private final int mMaxLineUpperLimit;
private int mStaticLayoutCreationCountInOnMeasure = 0;
@@ -71,6 +74,8 @@
super(context, attrs, defStyleAttr, defStyleRes);
setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_FULL_FAST);
setBreakStrategy(Layout.BREAK_STRATEGY_HIGH_QUALITY);
+ mMaxLineUpperLimit =
+ getResources().getInteger(R.integer.config_notificationLongTextMaxLineCount);
}
@Override
@@ -102,6 +107,11 @@
} else {
maxLines = getMaxLines() >= 0 ? getMaxLines() : Integer.MAX_VALUE;
}
+
+ if (mMaxLineUpperLimit > 0) {
+ maxLines = Math.min(maxLines, mMaxLineUpperLimit);
+ }
+
builder.setMaxLines(maxLines);
mLayoutMaxLines = maxLines;
if (shouldEllipsize) {
diff --git a/core/jni/OWNERS b/core/jni/OWNERS
index 3aca751..2a4f062 100644
--- a/core/jni/OWNERS
+++ b/core/jni/OWNERS
@@ -27,6 +27,7 @@
# WindowManager
per-file android_graphics_BLASTBufferQueue.cpp = file:/services/core/java/com/android/server/wm/OWNERS
per-file android_view_Surface* = file:/services/core/java/com/android/server/wm/OWNERS
+per-file android_view_WindowManagerGlobal.cpp = file:/services/core/java/com/android/server/wm/OWNERS
per-file android_window_* = file:/services/core/java/com/android/server/wm/OWNERS
# Resources
diff --git a/core/jni/android_os_Parcel.cpp b/core/jni/android_os_Parcel.cpp
index 3539476..584ebaa 100644
--- a/core/jni/android_os_Parcel.cpp
+++ b/core/jni/android_os_Parcel.cpp
@@ -661,6 +661,35 @@
return;
}
+static jboolean android_os_Parcel_hasBinders(JNIEnv* env, jclass clazz, jlong nativePtr) {
+ Parcel* parcel = reinterpret_cast<Parcel*>(nativePtr);
+ if (parcel != NULL) {
+ bool result;
+ status_t err = parcel->hasBinders(&result);
+ if (err != NO_ERROR) {
+ signalExceptionForError(env, clazz, err);
+ return JNI_FALSE;
+ }
+ return result ? JNI_TRUE : JNI_FALSE;
+ }
+ return JNI_FALSE;
+}
+
+static jboolean android_os_Parcel_hasBindersInRange(JNIEnv* env, jclass clazz, jlong nativePtr,
+ jint offset, jint length) {
+ Parcel* parcel = reinterpret_cast<Parcel*>(nativePtr);
+ if (parcel != NULL) {
+ bool result;
+ status_t err = parcel->hasBindersInRange(offset, length, &result);
+ if (err != NO_ERROR) {
+ signalExceptionForError(env, clazz, err);
+ return JNI_FALSE;
+ }
+ return result ? JNI_TRUE : JNI_FALSE;
+ }
+ return JNI_FALSE;
+}
+
static jboolean android_os_Parcel_hasFileDescriptors(jlong nativePtr)
{
jboolean ret = JNI_FALSE;
@@ -806,7 +835,7 @@
}
// ----------------------------------------------------------------------------
-
+// clang-format off
static const JNINativeMethod gParcelMethods[] = {
// @CriticalNative
{"nativeMarkSensitive", "(J)V", (void*)android_os_Parcel_markSensitive},
@@ -886,6 +915,9 @@
// @CriticalNative
{"nativeHasFileDescriptors", "(J)Z", (void*)android_os_Parcel_hasFileDescriptors},
{"nativeHasFileDescriptorsInRange", "(JII)Z", (void*)android_os_Parcel_hasFileDescriptorsInRange},
+
+ {"nativeHasBinders", "(J)Z", (void*)android_os_Parcel_hasBinders},
+ {"nativeHasBindersInRange", "(JII)Z", (void*)android_os_Parcel_hasBindersInRange},
{"nativeWriteInterfaceToken", "(JLjava/lang/String;)V", (void*)android_os_Parcel_writeInterfaceToken},
{"nativeEnforceInterface", "(JLjava/lang/String;)V", (void*)android_os_Parcel_enforceInterface},
@@ -900,6 +932,7 @@
// @CriticalNative
{"nativeReplaceCallingWorkSourceUid", "(JI)Z", (void*)android_os_Parcel_replaceCallingWorkSourceUid},
};
+// clang-format on
const char* const kParcelPathName = "android/os/Parcel";
diff --git a/core/jni/android_os_Trace.cpp b/core/jni/android_os_Trace.cpp
index 422bc1e..4387a4c 100644
--- a/core/jni/android_os_Trace.cpp
+++ b/core/jni/android_os_Trace.cpp
@@ -124,7 +124,7 @@
});
}
-static jboolean android_os_Trace_nativeIsTagEnabled(JNIEnv* env, jlong tag) {
+static jboolean android_os_Trace_nativeIsTagEnabled(jlong tag) {
return tracing_perfetto::isTagEnabled(tag);
}
diff --git a/core/jni/android_util_Process.cpp b/core/jni/android_util_Process.cpp
index d2e58bb..982189e 100644
--- a/core/jni/android_util_Process.cpp
+++ b/core/jni/android_util_Process.cpp
@@ -1137,6 +1137,41 @@
}
}
+void android_os_Process_sendSignalThrows(JNIEnv* env, jobject clazz, jint pid, jint sig) {
+ if (pid <= 0) {
+ jniThrowExceptionFmt(env, "java/lang/IllegalArgumentException", "Invalid argument: pid(%d)",
+ pid);
+ return;
+ }
+ int ret = kill(pid, sig);
+ if (ret < 0) {
+ if (errno == ESRCH) {
+ jniThrowExceptionFmt(env, "java/util/NoSuchElementException",
+ "Process with pid %d not found", pid);
+ } else {
+ signalExceptionForError(env, errno, pid);
+ }
+ }
+}
+
+void android_os_Process_sendTgSignalThrows(JNIEnv* env, jobject clazz, jint tgid, jint tid,
+ jint sig) {
+ if (tgid <= 0 || tid <= 0) {
+ jniThrowExceptionFmt(env, "java/lang/IllegalArgumentException",
+ "Invalid argument: tgid(%d), tid(%d)", tid, tgid);
+ return;
+ }
+ int ret = tgkill(tgid, tid, sig);
+ if (ret < 0) {
+ if (errno == ESRCH) {
+ jniThrowExceptionFmt(env, "java/util/NoSuchElementException",
+ "Process with tid %d and tgid %d not found", tid, tgid);
+ } else {
+ signalExceptionForError(env, errno, tid);
+ }
+ }
+}
+
static jlong android_os_Process_getElapsedCpuTime(JNIEnv* env, jobject clazz)
{
struct timespec ts;
@@ -1357,6 +1392,8 @@
{"setGid", "(I)I", (void*)android_os_Process_setGid},
{"sendSignal", "(II)V", (void*)android_os_Process_sendSignal},
{"sendSignalQuiet", "(II)V", (void*)android_os_Process_sendSignalQuiet},
+ {"sendSignalThrows", "(II)V", (void*)android_os_Process_sendSignalThrows},
+ {"sendTgSignalThrows", "(III)V", (void*)android_os_Process_sendTgSignalThrows},
{"setProcessFrozen", "(IIZ)V", (void*)android_os_Process_setProcessFrozen},
{"getFreeMemory", "()J", (void*)android_os_Process_getFreeMemory},
{"getTotalMemory", "()J", (void*)android_os_Process_getTotalMemory},
diff --git a/core/jni/android_view_WindowManagerGlobal.cpp b/core/jni/android_view_WindowManagerGlobal.cpp
index b03ac88..abc621d 100644
--- a/core/jni/android_view_WindowManagerGlobal.cpp
+++ b/core/jni/android_view_WindowManagerGlobal.cpp
@@ -48,7 +48,7 @@
surfaceControlObj(env,
android_view_SurfaceControl_getJavaSurfaceControl(env,
surfaceControl));
- jobject clientTokenObj = javaObjectForIBinder(env, clientToken);
+ ScopedLocalRef<jobject> clientTokenObj(env, javaObjectForIBinder(env, clientToken));
ScopedLocalRef<jobject> clientInputTransferTokenObj(
env,
android_window_InputTransferToken_getJavaInputTransferToken(env,
@@ -57,7 +57,7 @@
inputChannelObj(env,
env->CallStaticObjectMethod(gWindowManagerGlobal.clazz,
gWindowManagerGlobal.createInputChannel,
- clientTokenObj,
+ clientTokenObj.get(),
hostInputTransferTokenObj.get(),
surfaceControlObj.get(),
clientInputTransferTokenObj.get()));
@@ -68,9 +68,9 @@
void removeInputChannel(const sp<IBinder>& clientToken) {
JNIEnv* env = AndroidRuntime::getJNIEnv();
- jobject clientTokenObj(javaObjectForIBinder(env, clientToken));
+ ScopedLocalRef<jobject> clientTokenObj(env, javaObjectForIBinder(env, clientToken));
env->CallStaticObjectMethod(gWindowManagerGlobal.clazz, gWindowManagerGlobal.removeInputChannel,
- clientTokenObj);
+ clientTokenObj.get());
}
int register_android_view_WindowManagerGlobal(JNIEnv* env) {
diff --git a/core/jni/android_window_InputTransferToken.cpp b/core/jni/android_window_InputTransferToken.cpp
index 8fb668d..5bcea9b 100644
--- a/core/jni/android_window_InputTransferToken.cpp
+++ b/core/jni/android_window_InputTransferToken.cpp
@@ -70,6 +70,11 @@
return javaObjectForIBinder(env, inputTransferToken->mToken);
}
+static jlong nativeGetBinderTokenRef(JNIEnv*, jclass, jlong nativeObj) {
+ sp<InputTransferToken> inputTransferToken = reinterpret_cast<InputTransferToken*>(nativeObj);
+ return reinterpret_cast<jlong>(inputTransferToken->mToken.get());
+}
+
InputTransferToken* android_window_InputTransferToken_getNativeInputTransferToken(
JNIEnv* env, jobject inputTransferTokenObj) {
if (inputTransferTokenObj != nullptr &&
@@ -114,6 +119,7 @@
{"nativeWriteToParcel", "(JLandroid/os/Parcel;)V", (void*)nativeWriteToParcel},
{"nativeReadFromParcel", "(Landroid/os/Parcel;)J", (void*)nativeReadFromParcel},
{"nativeGetBinderToken", "(J)Landroid/os/IBinder;", (void*)nativeGetBinderToken},
+ {"nativeGetBinderTokenRef", "(J)J", (void*)nativeGetBinderTokenRef},
{"nativeGetNativeInputTransferTokenFinalizer", "()J", (void*)nativeGetNativeInputTransferTokenFinalizer},
{"nativeEquals", "(JJ)Z", (void*) nativeEquals},
// clang-format on
diff --git a/core/proto/android/providers/settings/secure.proto b/core/proto/android/providers/settings/secure.proto
index 763d9ce..6b0c2d2 100644
--- a/core/proto/android/providers/settings/secure.proto
+++ b/core/proto/android/providers/settings/secure.proto
@@ -143,9 +143,11 @@
optional SettingProto gesture_setup_complete = 9 [ (android.privacy).dest = DEST_AUTOMATIC ];
optional SettingProto touch_gesture_enabled = 10 [ (android.privacy).dest = DEST_AUTOMATIC ];
optional SettingProto long_press_home_enabled = 11 [ (android.privacy).dest = DEST_AUTOMATIC ];
- optional SettingProto search_press_hold_nav_handle_enabled = 12 [ (android.privacy).dest = DEST_AUTOMATIC ];
- optional SettingProto search_long_press_home_enabled = 13 [ (android.privacy).dest = DEST_AUTOMATIC ];
+ // Deprecated - use search_all_entrypoints_enabled instead
+ optional SettingProto search_press_hold_nav_handle_enabled = 12 [ (android.privacy).dest = DEST_AUTOMATIC, deprecated = true ];
+ optional SettingProto search_long_press_home_enabled = 13 [ (android.privacy).dest = DEST_AUTOMATIC, deprecated = true ];
optional SettingProto visual_query_accessibility_detection_enabled = 14 [ (android.privacy).dest = DEST_AUTOMATIC ];
+ optional SettingProto search_all_entrypoints_enabled = 15 [ (android.privacy).dest = DEST_AUTOMATIC ];
}
optional Assist assist = 7;
diff --git a/core/proto/android/service/package.proto b/core/proto/android/service/package.proto
index 068f4dd..d30f195 100644
--- a/core/proto/android/service/package.proto
+++ b/core/proto/android/service/package.proto
@@ -142,6 +142,7 @@
// UTC timestamp of first install for the user
optional int32 first_install_time_ms = 11;
optional ArchiveState archive_state = 12;
+ repeated int32 suspending_user = 13;
}
message InstallSourceProto {
diff --git a/core/res/res/drawable/activity_embedding_divider_handle.xml b/core/res/res/drawable/activity_embedding_divider_handle.xml
new file mode 100644
index 0000000..d9f363c
--- /dev/null
+++ b/core/res/res/drawable/activity_embedding_divider_handle.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ 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.
+ -->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:state_pressed="true"
+ android:drawable="@drawable/activity_embedding_divider_handle_pressed" />
+ <item android:drawable="@drawable/activity_embedding_divider_handle_default" />
+</selector>
\ No newline at end of file
diff --git a/core/res/res/drawable/activity_embedding_divider_handle_default.xml b/core/res/res/drawable/activity_embedding_divider_handle_default.xml
new file mode 100644
index 0000000..565f671
--- /dev/null
+++ b/core/res/res/drawable/activity_embedding_divider_handle_default.xml
@@ -0,0 +1,23 @@
+<!--
+ 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.
+ -->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+ <corners android:radius="@dimen/activity_embedding_divider_handle_radius" />
+ <size
+ android:width="@dimen/activity_embedding_divider_handle_width"
+ android:height="@dimen/activity_embedding_divider_handle_height" />
+ <solid android:color="@color/activity_embedding_divider_color" />
+</shape>
\ No newline at end of file
diff --git a/core/res/res/drawable/activity_embedding_divider_handle_pressed.xml b/core/res/res/drawable/activity_embedding_divider_handle_pressed.xml
new file mode 100644
index 0000000..e5cca239
--- /dev/null
+++ b/core/res/res/drawable/activity_embedding_divider_handle_pressed.xml
@@ -0,0 +1,23 @@
+<!--
+ 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.
+ -->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+ <corners android:radius="@dimen/activity_embedding_divider_handle_radius_pressed" />
+ <size
+ android:width="@dimen/activity_embedding_divider_handle_width_pressed"
+ android:height="@dimen/activity_embedding_divider_handle_height_pressed" />
+ <solid android:color="@color/activity_embedding_divider_color_pressed" />
+</shape>
\ No newline at end of file
diff --git a/core/res/res/drawable/autofill_dataset_picker_background.xml b/core/res/res/drawable/autofill_dataset_picker_background.xml
index d574970..6c4ef11 100644
--- a/core/res/res/drawable/autofill_dataset_picker_background.xml
+++ b/core/res/res/drawable/autofill_dataset_picker_background.xml
@@ -16,7 +16,7 @@
<inset xmlns:android="http://schemas.android.com/apk/res/android">
<shape android:shape="rectangle">
- <corners android:radius="@dimen/config_bottomDialogCornerRadius" />
+ <corners android:radius="@dimen/config_buttonCornerRadius" />
<solid android:color="?attr/colorBackground" />
</shape>
</inset>
diff --git a/core/res/res/layout/transient_notification_with_icon.xml b/core/res/res/layout/transient_notification_with_icon.xml
index 0dfb3ad..04518b2 100644
--- a/core/res/res/layout/transient_notification_with_icon.xml
+++ b/core/res/res/layout/transient_notification_with_icon.xml
@@ -22,7 +22,7 @@
android:orientation="horizontal"
android:gravity="center_vertical"
android:maxWidth="@dimen/toast_width"
- android:background="?android:attr/colorBackground"
+ android:background="@android:drawable/toast_frame"
android:elevation="@dimen/toast_elevation"
android:layout_marginEnd="16dp"
android:layout_marginStart="16dp"
@@ -31,8 +31,11 @@
<ImageView
android:id="@android:id/icon"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content" />
+ android:layout_width="24dp"
+ android:layout_height="24dp"
+ android:layout_marginTop="10dp"
+ android:layout_marginBottom="10dp"
+ android:layout_marginEnd="10dp" />
<TextView
android:id="@android:id/message"
diff --git a/core/res/res/values/colors.xml b/core/res/res/values/colors.xml
index 417c6df..e671919 100644
--- a/core/res/res/values/colors.xml
+++ b/core/res/res/values/colors.xml
@@ -593,6 +593,10 @@
<color name="accessibility_magnification_thumbnail_container_background_color">#99000000</color>
<color name="accessibility_magnification_thumbnail_container_stroke_color">#FFFFFF</color>
+ <!-- Activity Embedding divider -->
+ <color name="activity_embedding_divider_color">#8e918f</color>
+ <color name="activity_embedding_divider_color_pressed">#e3e3e3</color>
+
<!-- Lily Language Picker language item view colors -->
<color name="language_picker_item_text_color">#202124</color>
<color name="language_picker_item_text_color_secondary">#5F6368</color>
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index e3f1cb6..89ac81e 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -1483,6 +1483,11 @@
<!-- Number of notifications to keep in the notification service historical archive -->
<integer name="config_notificationServiceArchiveSize">100</integer>
+ <!-- Upper limit imposed for long text content for BigTextStyle, MessagingStyle and
+ ConversationStyle notifications for performance reasons, and that line count is also
+ capped by vertical space available. It is only enabled when the value is positive int.-->
+ <integer name="config_notificationLongTextMaxLineCount">10</integer>
+
<!-- Allow the menu hard key to be disabled in LockScreen on some devices -->
<bool name="config_disableMenuKeyInLockScreen">false</bool>
@@ -6414,10 +6419,8 @@
<!-- Default value for Settings.ASSIST_TOUCH_GESTURE_ENABLED -->
<bool name="config_assistTouchGestureEnabledDefault">true</bool>
- <!-- Default value for Settings.SEARCH_PRESS_HOLD_NAV_HANDLE_ENABLED -->
- <bool name="config_searchPressHoldNavHandleEnabledDefault">true</bool>
- <!-- Default value for Settings.ASSIST_LONG_PRESS_HOME_ENABLED for search overlay -->
- <bool name="config_searchLongPressHomeEnabledDefault">true</bool>
+ <!-- Default value for Settings.SEARCH_ALL_ENTRYPOINTS_ENABLED -->
+ <bool name="config_searchAllEntrypointsEnabledDefault">true</bool>
<!-- The maximum byte size of the information contained in the bundle of
HotwordDetectedResult. -->
diff --git a/core/res/res/values/dimens.xml b/core/res/res/values/dimens.xml
index 291a593..4aa741d 100644
--- a/core/res/res/values/dimens.xml
+++ b/core/res/res/values/dimens.xml
@@ -1028,6 +1028,16 @@
<dimen name="popup_enter_animation_from_y_delta">20dp</dimen>
<dimen name="popup_exit_animation_to_y_delta">-10dp</dimen>
+ <!-- Dimensions for the activity embedding divider. -->
+ <dimen name="activity_embedding_divider_handle_width">4dp</dimen>
+ <dimen name="activity_embedding_divider_handle_height">48dp</dimen>
+ <dimen name="activity_embedding_divider_handle_radius">2dp</dimen>
+ <dimen name="activity_embedding_divider_handle_width_pressed">12dp</dimen>
+ <dimen name="activity_embedding_divider_handle_height_pressed">53dp</dimen>
+ <dimen name="activity_embedding_divider_handle_radius_pressed">6dp</dimen>
+ <dimen name="activity_embedding_divider_touch_target_width">24dp</dimen>
+ <dimen name="activity_embedding_divider_touch_target_height">64dp</dimen>
+
<!-- Default handwriting bounds offsets for editors. -->
<dimen name="handwriting_bounds_offset_left">10dp</dimen>
<dimen name="handwriting_bounds_offset_top">40dp</dimen>
diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml
index f915f03..a3dba48 100644
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -231,8 +231,10 @@
<string name="NetworkPreferenceSwitchSummary">Try changing preferred network. Tap to change.</string>
<!-- Displayed to tell the user that emergency calls might not be available. -->
<string name="EmergencyCallWarningTitle">Emergency calling unavailable</string>
- <!-- Displayed to tell the user that emergency calls might not be available. -->
- <string name="EmergencyCallWarningSummary">Can\u2019t make emergency calls over Wi\u2011Fi</string>
+ <!-- Displayed to tell the user that emergency calls might not be available; this is shown to
+ the user when only WiFi calling is available and the carrier does not support emergency
+ calls over WiFi calling. -->
+ <string name="EmergencyCallWarningSummary">Emergency calls require a mobile network</string>
<!-- Telephony notification channel name for a channel containing network alert notifications. -->
<string name="notification_channel_network_alert">Alerts</string>
@@ -3247,6 +3249,12 @@
<!-- Title for EditText context menu [CHAR LIMIT=20] -->
<string name="editTextMenuTitle">Text actions</string>
+ <!-- Error shown when a user uses a stylus to try handwriting on a text field which doesn't support stylus handwriting. [CHAR LIMIT=TOAST] -->
+ <string name="error_handwriting_unsupported">Handwriting is not supported in this field</string>
+
+ <!-- Error shown when a user uses a stylus to try handwriting on a password text field which doesn't support stylus handwriting. [CHAR LIMIT=TOAST] -->
+ <string name="error_handwriting_unsupported_password">Handwriting is not supported in password fields</string>
+
<!-- Content description of the back button for accessibility (not shown on the screen). [CHAR LIMIT=NONE] -->
<string name="input_method_nav_back_button_desc">Back</string>
<!-- Content description of the switch input method button for accessibility (not shown on the screen). [CHAR LIMIT=NONE] -->
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index f4b42f6..2e029b2 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -2078,6 +2078,7 @@
<java-symbol type="integer" name="config_notificationsBatteryMediumARGB" />
<java-symbol type="integer" name="config_notificationsBatteryNearlyFullLevel" />
<java-symbol type="integer" name="config_notificationServiceArchiveSize" />
+ <java-symbol type="integer" name="config_notificationLongTextMaxLineCount" />
<java-symbol type="dimen" name="config_rotaryEncoderAxisScrollTickInterval" />
<java-symbol type="integer" name="config_recentVibrationsDumpSizeLimit" />
<java-symbol type="integer" name="config_previousVibrationsDumpSizeLimit" />
@@ -3121,6 +3122,8 @@
<!-- TextView -->
<java-symbol type="bool" name="config_textShareSupported" />
<java-symbol type="string" name="failed_to_copy_to_clipboard" />
+ <java-symbol type="string" name="error_handwriting_unsupported" />
+ <java-symbol type="string" name="error_handwriting_unsupported_password" />
<java-symbol type="id" name="notification_material_reply_container" />
<java-symbol type="id" name="notification_material_reply_text_1" />
@@ -5016,8 +5019,7 @@
<java-symbol type="bool" name="config_assistLongPressHomeEnabledDefault" />
<java-symbol type="bool" name="config_assistTouchGestureEnabledDefault" />
- <java-symbol type="bool" name="config_searchPressHoldNavHandleEnabledDefault" />
- <java-symbol type="bool" name="config_searchLongPressHomeEnabledDefault" />
+ <java-symbol type="bool" name="config_searchAllEntrypointsEnabledDefault" />
<java-symbol type="integer" name="config_hotwordDetectedResultMaxBundleSize" />
@@ -5335,6 +5337,11 @@
<java-symbol type="raw" name="default_ringtone_vibration_effect" />
+ <!-- For activity embedding divider -->
+ <java-symbol type="drawable" name="activity_embedding_divider_handle" />
+ <java-symbol type="dimen" name="activity_embedding_divider_touch_target_width" />
+ <java-symbol type="dimen" name="activity_embedding_divider_touch_target_height" />
+
<!-- Whether we order unlocking and waking -->
<java-symbol type="bool" name="config_orderUnlockAndWake" />
diff --git a/core/res/res/xml/sms_short_codes.xml b/core/res/res/xml/sms_short_codes.xml
index 7d740ef..c8625b9 100644
--- a/core/res/res/xml/sms_short_codes.xml
+++ b/core/res/res/xml/sms_short_codes.xml
@@ -42,8 +42,8 @@
<!-- Argentina: 5 digits, known short codes listed -->
<shortcode country="ar" pattern="\\d{5}" free="11711|28291|44077|78887" />
- <!-- Armenia: 3-4 digits, emergency numbers 10[123] -->
- <shortcode country="am" pattern="\\d{3,4}" premium="11[2456]1|3024" free="10[123]" />
+ <!-- Armenia: 3-5 digits, emergency numbers 10[123] -->
+ <shortcode country="am" pattern="\\d{3,5}" premium="11[2456]1|3024" free="10[123]|71522|71512|71502" />
<!-- Austria: 10 digits, premium prefix 09xx, plus EU -->
<shortcode country="at" pattern="11\\d{4}" premium="09.*" free="116\\d{3}" />
@@ -111,7 +111,7 @@
<shortcode country="do" pattern="\\d{1,6}" free="912892" />
<!-- Ecuador: 1-6 digits (standard system default, not country specific) -->
- <shortcode country="ec" pattern="\\d{1,6}" free="466453" />
+ <shortcode country="ec" pattern="\\d{1,6}" free="466453|18512" />
<!-- Estonia: short codes 3-5 digits starting with 1, plus premium 7 digit numbers starting with 90, plus EU.
http://www.tja.ee/public/documents/Elektrooniline_side/Oigusaktid/ENG/Estonian_Numbering_Plan_annex_06_09_2010.mht -->
@@ -137,11 +137,11 @@
visual voicemail code for EE: 887 -->
<shortcode country="gb" pattern="\\d{4,6}" premium="[5-8]\\d{4}" free="116\\d{3}|2020|35890|61002|61202|887|83669|34664|40406|60174|7726|37726|88555|9017|9018" />
- <!-- Georgia: 4 digits, known premium codes listed -->
- <shortcode country="ge" pattern="\\d{4}" premium="801[234]|888[239]" />
+ <!-- Georgia: 1-5 digits, known premium codes listed -->
+ <shortcode country="ge" pattern="\\d{1,5}" premium="801[234]|888[239]" free="95201|95202|95203" />
<!-- Ghana: 4 digits, known premium codes listed -->
- <shortcode country="gh" pattern="\\d{4}" free="5041" />
+ <shortcode country="gh" pattern="\\d{4}" free="5041|3777" />
<!-- Greece: 5 digits (54xxx, 19yxx, x=0-9, y=0-5): http://www.cmtelecom.com/premium-sms/greece -->
<shortcode country="gr" pattern="\\d{5}" premium="54\\d{3}|19[0-5]\\d{2}" free="116\\d{3}|12115" />
@@ -210,6 +210,9 @@
<!-- Macedonia: 1-6 digits (not confirmed), known premium codes listed -->
<shortcode country="mk" pattern="\\d{1,6}" free="129005|122" />
+ <!-- Mongolia : 1-6 digits (standard system default, not country specific) -->
+ <shortcode country="mn" pattern="\\d{1,6}" free="44444|45678|445566" />
+
<!-- Malawi: 1-5 digits (standard system default, not country specific) -->
<shortcode country="mw" pattern="\\d{1,5}" free="4276" />
@@ -247,7 +250,7 @@
<shortcode country="ph" pattern="\\d{1,5}" free="2147|5495|5496" />
<!-- Pakistan -->
- <shortcode country="pk" pattern="\\d{1,5}" free="2057|9092" />
+ <shortcode country="pk" pattern="\\d{1,6}" free="2057|9092|909203" />
<!-- Palestine: 5 digits, known premium codes listed -->
<shortcode country="ps" pattern="\\d{1,5}" free="37477|6681" />
@@ -291,7 +294,7 @@
<shortcode country="sk" premium="\\d{4}" free="116\\d{3}|8000" />
<!-- Senegal(SN): 1-5 digits (standard system default, not country specific) -->
- <shortcode country="sn" pattern="\\d{1,5}" free="21215" />
+ <shortcode country="sn" pattern="\\d{1,5}" free="21215|21098" />
<!-- El Salvador(SV): 1-5 digits (standard system default, not country specific) -->
<shortcode country="sv" pattern="\\d{4,6}" free="466453" />
@@ -321,14 +324,17 @@
visual voicemail code for T-Mobile: 122 -->
<shortcode country="us" pattern="\\d{5,6}" premium="20433|21(?:344|472)|22715|23(?:333|847)|24(?:15|28)0|25209|27(?:449|606|663)|28498|305(?:00|83)|32(?:340|941)|33(?:166|786|849)|34746|35(?:182|564)|37975|38(?:135|146|254)|41(?:366|463)|42335|43(?:355|500)|44(?:578|711|811)|45814|46(?:157|173|327)|46666|47553|48(?:221|277|669)|50(?:844|920)|51(?:062|368)|52944|54(?:723|892)|55928|56483|57370|59(?:182|187|252|342)|60339|61(?:266|982)|62478|64(?:219|898)|65(?:108|500)|69(?:208|388)|70877|71851|72(?:078|087|465)|73(?:288|588|882|909|997)|74(?:034|332|815)|76426|79213|81946|83177|84(?:103|685)|85797|86(?:234|236|666)|89616|90(?:715|842|938)|91(?:362|958)|94719|95297|96(?:040|666|835|969)|97(?:142|294|688)|99(?:689|796|807)" standard="44567|244444" free="122|87902|21696|24614|28003|30356|33669|40196|41064|41270|43753|44034|46645|52413|56139|57969|61785|66975|75136|76227|81398|83952|85140|86566|86799|95737|96684|99245|611611|96831" />
+ <!--Uruguay : 1-5 digits (standard system default, not country specific) -->
+ <shortcode country="uy" pattern="\\d{1,5}" free="55002" />
+
<!-- Vietnam: 1-5 digits (standard system default, not country specific) -->
- <shortcode country="vn" pattern="\\d{1,5}" free="5001|9055" />
+ <shortcode country="vn" pattern="\\d{1,5}" free="5001|9055|8079" />
<!-- Mayotte (French Territory): 1-5 digits (not confirmed) -->
<shortcode country="yt" pattern="\\d{1,5}" free="38600,36300,36303,959" />
<!-- South Africa -->
- <shortcode country="za" pattern="\\d{1,5}" free="44136|30791|36056" />
+ <shortcode country="za" pattern="\\d{1,5}" free="44136|30791|36056|33009" />
<!-- Zimbabwe -->
<shortcode country="zw" pattern="\\d{1,5}" free="33679" />
diff --git a/core/tests/bugreports/OWNERS b/core/tests/bugreports/OWNERS
new file mode 100644
index 0000000..dbd767c
--- /dev/null
+++ b/core/tests/bugreports/OWNERS
@@ -0,0 +1,2 @@
+# Bug component: 153446
+file:/platform/frameworks/native:/cmds/dumpstate/OWNERS
diff --git a/core/tests/coretests/src/android/app/servertransaction/ClientTransactionItemTest.java b/core/tests/coretests/src/android/app/servertransaction/ClientTransactionItemTest.java
index 9907397..2ce7a7d 100644
--- a/core/tests/coretests/src/android/app/servertransaction/ClientTransactionItemTest.java
+++ b/core/tests/coretests/src/android/app/servertransaction/ClientTransactionItemTest.java
@@ -88,6 +88,7 @@
private InsetsState mInsetsState;
private ClientWindowFrames mFrames;
private MergedConfiguration mMergedConfiguration;
+ private ActivityWindowInfo mActivityWindowInfo;
@Before
public void setup() {
@@ -99,6 +100,7 @@
mInsetsState = new InsetsState();
mFrames = new ClientWindowFrames();
mMergedConfiguration = new MergedConfiguration(mGlobalConfig, mConfiguration);
+ mActivityWindowInfo = new ActivityWindowInfo();
doReturn(mActivity).when(mHandler).getActivity(mActivityToken);
doReturn(mActivitiesToBeDestroyed).when(mHandler).getActivitiesToBeDestroyed();
@@ -107,7 +109,7 @@
@Test
public void testActivityConfigurationChangeItem_getContextToUpdate() {
final ActivityConfigurationChangeItem item = ActivityConfigurationChangeItem
- .obtain(mActivityToken, mConfiguration, new ActivityWindowInfo());
+ .obtain(mActivityToken, mConfiguration, mActivityWindowInfo);
final Context context = item.getContextToUpdate(mHandler);
assertEquals(mActivity, context);
@@ -118,7 +120,7 @@
final ActivityRelaunchItem item = ActivityRelaunchItem
.obtain(mActivityToken, null /* pendingResults */, null /* pendingNewIntents */,
0 /* configChange */, mMergedConfiguration, false /* preserveWindow */,
- new ActivityWindowInfo());
+ mActivityWindowInfo);
final Context context = item.getContextToUpdate(mHandler);
assertEquals(mActivity, context);
@@ -177,7 +179,7 @@
@Test
public void testMoveToDisplayItem_getContextToUpdate() {
final MoveToDisplayItem item = MoveToDisplayItem
- .obtain(mActivityToken, DEFAULT_DISPLAY, mConfiguration, new ActivityWindowInfo());
+ .obtain(mActivityToken, DEFAULT_DISPLAY, mConfiguration, mActivityWindowInfo);
final Context context = item.getContextToUpdate(mHandler);
assertEquals(mActivity, context);
@@ -218,13 +220,13 @@
final WindowStateResizeItem item = WindowStateResizeItem.obtain(mWindow, mFrames,
true /* reportDraw */, mMergedConfiguration, mInsetsState, true /* forceLayout */,
true /* alwaysConsumeSystemBars */, 123 /* displayId */, 321 /* syncSeqId */,
- true /* dragResizing */);
+ true /* dragResizing */, mActivityToken, mActivityWindowInfo);
item.execute(mHandler, mPendingActions);
verify(mWindow).resized(mFrames,
true /* reportDraw */, mMergedConfiguration, mInsetsState, true /* forceLayout */,
true /* alwaysConsumeSystemBars */, 123 /* displayId */, 321 /* syncSeqId */,
- true /* dragResizing */);
+ true /* dragResizing */, mActivityWindowInfo);
}
@Test
@@ -232,7 +234,7 @@
final WindowStateResizeItem item = WindowStateResizeItem.obtain(mWindow, mFrames,
true /* reportDraw */, mMergedConfiguration, mInsetsState, true /* forceLayout */,
true /* alwaysConsumeSystemBars */, 123 /* displayId */, 321 /* syncSeqId */,
- true /* dragResizing */);
+ true /* dragResizing */, mActivityToken, mActivityWindowInfo);
final Context context = item.getContextToUpdate(mHandler);
assertEquals(ActivityThread.currentApplication(), context);
diff --git a/core/tests/coretests/src/android/os/BundleTest.java b/core/tests/coretests/src/android/os/BundleTest.java
index 93c2e0e..40e79ad 100644
--- a/core/tests/coretests/src/android/os/BundleTest.java
+++ b/core/tests/coretests/src/android/os/BundleTest.java
@@ -24,6 +24,7 @@
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
+import android.platform.test.annotations.DisabledOnRavenwood;
import android.platform.test.annotations.IgnoreUnderRavenwood;
import android.platform.test.annotations.Presubmit;
import android.platform.test.ravenwood.RavenwoodRule;
@@ -445,6 +446,42 @@
assertThat(bundle.size()).isEqualTo(0);
}
+ @Test
+ @DisabledOnRavenwood(blockedBy = Parcel.class)
+ public void parcelledBundleWithBinder_shouldReturnHasBindersTrue() throws Exception {
+ Bundle bundle = new Bundle();
+ bundle.putParcelable("test", new CustomParcelable(13, "Tiramisu"));
+ bundle.putBinder("test_binder",
+ new IBinderWorkSourceNestedService.Stub() {
+
+ public int[] nestedCallWithWorkSourceToSet(int uidToBlame) {
+ return new int[0];
+ }
+
+ public int[] nestedCall() {
+ return new int[0];
+ }
+ });
+ Bundle bundle2 = new Bundle(getParcelledBundle(bundle));
+ assertEquals(bundle2.hasBinders(), Bundle.STATUS_BINDERS_PRESENT);
+
+ bundle2.putParcelable("test2", new CustomParcelable(13, "Tiramisu"));
+ assertEquals(bundle2.hasBinders(), Bundle.STATUS_BINDERS_UNKNOWN);
+ }
+
+ @Test
+ @DisabledOnRavenwood(blockedBy = Parcel.class)
+ public void parcelledBundleWithoutBinder_shouldReturnHasBindersFalse() throws Exception {
+ Bundle bundle = new Bundle();
+ bundle.putParcelable("test", new CustomParcelable(13, "Tiramisu"));
+ Bundle bundle2 = new Bundle(getParcelledBundle(bundle));
+ //Should fail to load with framework classloader.
+ assertEquals(bundle2.hasBinders(), Bundle.STATUS_BINDERS_NOT_PRESENT);
+
+ bundle2.putParcelable("test2", new CustomParcelable(13, "Tiramisu"));
+ assertEquals(bundle2.hasBinders(), Bundle.STATUS_BINDERS_UNKNOWN);
+ }
+
private Bundle getMalformedBundle() {
Parcel p = Parcel.obtain();
p.writeInt(BaseBundle.BUNDLE_MAGIC);
@@ -520,6 +557,7 @@
public CustomParcelable createFromParcel(Parcel in) {
return new CustomParcelable(in);
}
+
@Override
public CustomParcelable[] newArray(int size) {
return new CustomParcelable[size];
diff --git a/core/tests/coretests/src/android/os/ParcelTest.java b/core/tests/coretests/src/android/os/ParcelTest.java
index 26f6d69..442394e3 100644
--- a/core/tests/coretests/src/android/os/ParcelTest.java
+++ b/core/tests/coretests/src/android/os/ParcelTest.java
@@ -347,4 +347,30 @@
p.recycle();
Binder.setIsDirectlyHandlingTransactionOverride(false);
}
+
+ @Test
+ @IgnoreUnderRavenwood(blockedBy = Parcel.class)
+ public void testHasBinders_AfterWritingBinderToParcel() {
+ Binder binder = new Binder();
+ Parcel pA = Parcel.obtain();
+ int iA = pA.dataPosition();
+ pA.writeInt(13);
+ assertFalse(pA.hasBinders());
+ pA.writeStrongBinder(binder);
+ assertTrue(pA.hasBinders());
+ }
+
+
+ @Test
+ @IgnoreUnderRavenwood(blockedBy = Parcel.class)
+ public void testHasBindersInRange_AfterWritingBinderToParcel() {
+ Binder binder = new Binder();
+ Parcel pA = Parcel.obtain();
+ pA.writeInt(13);
+
+ int binderStartPos = pA.dataPosition();
+ pA.writeStrongBinder(binder);
+ int binderEndPos = pA.dataPosition();
+ assertTrue(pA.hasBinders(binderStartPos, binderEndPos - binderStartPos));
+ }
}
diff --git a/core/tests/coretests/src/android/view/ViewRootImplTest.java b/core/tests/coretests/src/android/view/ViewRootImplTest.java
index 652011b..41b67ce 100644
--- a/core/tests/coretests/src/android/view/ViewRootImplTest.java
+++ b/core/tests/coretests/src/android/view/ViewRootImplTest.java
@@ -81,6 +81,7 @@
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
+import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -462,6 +463,7 @@
*/
@UiThreadTest
@Test
+ @Ignore("Can be enabled only after b/330596920 is ready")
@RequiresFlagsEnabled(FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY)
public void votePreferredFrameRate_getDefaultValues() {
ViewRootImpl viewRootImpl = new ViewRootImpl(sContext,
@@ -478,6 +480,7 @@
* Also, mIsFrameRateBoosting should be true when the visibility becomes visible
*/
@Test
+ @Ignore("Can be enabled only after b/330596920 is ready")
@RequiresFlagsEnabled({FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY,
FLAG_TOOLKIT_FRAME_RATE_BY_SIZE_READ_ONLY})
public void votePreferredFrameRate_voteFrameRateCategory_visibility_bySize() {
@@ -511,6 +514,7 @@
* <7%: FRAME_RATE_CATEGORY_LOW
*/
@Test
+ @Ignore("Can be enabled only after b/330596920 is ready")
@RequiresFlagsEnabled({FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY,
FLAG_TOOLKIT_FRAME_RATE_BY_SIZE_READ_ONLY})
public void votePreferredFrameRate_voteFrameRateCategory_smallSize_bySize() {
@@ -539,6 +543,7 @@
* >=7% : FRAME_RATE_CATEGORY_NORMAL
*/
@Test
+ @Ignore("Can be enabled only after b/330596920 is ready")
@RequiresFlagsEnabled({FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY,
FLAG_TOOLKIT_FRAME_RATE_BY_SIZE_READ_ONLY})
public void votePreferredFrameRate_voteFrameRateCategory_normalSize_bySize() {
@@ -571,6 +576,7 @@
* Also, mIsFrameRateBoosting should be true when the visibility becomes visible
*/
@Test
+ @Ignore("Can be enabled only after b/330596920 is ready")
@RequiresFlagsEnabled(FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY)
public void votePreferredFrameRate_voteFrameRateCategory_visibility_defaultHigh() {
View view = new View(sContext);
@@ -603,6 +609,7 @@
* <7%: FRAME_RATE_CATEGORY_NORMAL
*/
@Test
+ @Ignore("Can be enabled only after b/330596920 is ready")
@RequiresFlagsEnabled(FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY)
public void votePreferredFrameRate_voteFrameRateCategory_smallSize_defaultHigh() {
View view = new View(sContext);
@@ -630,6 +637,7 @@
* >=7% : FRAME_RATE_CATEGORY_HIGH
*/
@Test
+ @Ignore("Can be enabled only after b/330596920 is ready")
@RequiresFlagsEnabled(FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY)
public void votePreferredFrameRate_voteFrameRateCategory_normalSize_defaultHigh() {
View view = new View(sContext);
@@ -659,6 +667,7 @@
* It should take the max value among all of the voted categories per frame.
*/
@Test
+ @Ignore("Can be enabled only after b/330596920 is ready")
@RequiresFlagsEnabled(FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY)
public void votePreferredFrameRate_voteFrameRateCategory_aggregate() {
View view = new View(sContext);
@@ -704,6 +713,7 @@
* prioritize 60Hz..
*/
@Test
+ @Ignore("Can be enabled only after b/330596920 is ready")
@RequiresFlagsEnabled(FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY)
public void votePreferredFrameRate_voteFrameRate_aggregate() {
View view = new View(sContext);
@@ -762,6 +772,7 @@
* submit your preferred choice to the ViewRootImpl.
*/
@Test
+ @Ignore("Can be enabled only after b/330596920 is ready")
@RequiresFlagsEnabled(FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY)
public void votePreferredFrameRate_voteFrameRate_category() {
View view = new View(sContext);
@@ -801,6 +812,7 @@
* Also, we shouldn't call setFrameRate.
*/
@Test
+ @Ignore("Can be enabled only after b/330596920 is ready")
@RequiresFlagsEnabled({FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY, FLAG_VIEW_VELOCITY_API})
public void votePreferredFrameRate_voteFrameRateCategory_velocityToHigh() {
View view = new View(sContext);
@@ -832,6 +844,7 @@
* We should boost the frame rate if the value of mInsetsAnimationRunning is true.
*/
@Test
+ @Ignore("Can be enabled only after b/330596920 is ready")
@RequiresFlagsEnabled(FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY)
public void votePreferredFrameRate_insetsAnimation() {
View view = new View(sContext);
@@ -868,6 +881,7 @@
* Test FrameRateBoostOnTouchEnabled API
*/
@Test
+ @Ignore("Can be enabled only after b/330596920 is ready")
@RequiresFlagsEnabled(FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY)
public void votePreferredFrameRate_frameRateBoostOnTouch() {
View view = new View(sContext);
@@ -900,6 +914,7 @@
* mPreferredFrameRate should be set to 0.
*/
@Test
+ @Ignore("Can be enabled only after b/330596920 is ready")
@RequiresFlagsEnabled(FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY)
public void votePreferredFrameRate_voteFrameRateTimeOut() throws InterruptedException {
final long delay = 200L;
@@ -937,6 +952,7 @@
* A View should either vote a frame rate or a frame rate category instead of both.
*/
@Test
+ @Ignore("Can be enabled only after b/330596920 is ready")
@RequiresFlagsEnabled(FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY)
public void votePreferredFrameRate_voteFrameRateOnly() {
View view = new View(sContext);
@@ -979,6 +995,7 @@
* - otherwise, use the previous category value.
*/
@Test
+ @Ignore("Can be enabled only after b/330596920 is ready")
@RequiresFlagsEnabled(FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY)
public void votePreferredFrameRate_infrequentLayer_defaultHigh() throws InterruptedException {
final long delay = 200L;
@@ -1039,6 +1056,7 @@
*/
@UiThreadTest
@Test
+ @Ignore("Can be enabled only after b/330596920 is ready")
@RequiresFlagsEnabled(FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY)
public void votePreferredFrameRate_isFrameRatePowerSavingsBalanced() {
ViewRootImpl viewRootImpl = new ViewRootImpl(sContext,
@@ -1056,6 +1074,7 @@
* 2. If FT2-FT1 > 15ms && FT3-FT2 > 15ms -> vote for NORMAL category
*/
@Test
+ @Ignore("Can be enabled only after b/330596920 is ready")
@RequiresFlagsEnabled(FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY)
public void votePreferredFrameRate_applyTextureViewHeuristic() throws InterruptedException {
final long delay = 30L;
diff --git a/core/tests/coretests/src/android/view/stylus/HandwritingInitiatorTest.java b/core/tests/coretests/src/android/view/stylus/HandwritingInitiatorTest.java
index a5c9624..faad472 100644
--- a/core/tests/coretests/src/android/view/stylus/HandwritingInitiatorTest.java
+++ b/core/tests/coretests/src/android/view/stylus/HandwritingInitiatorTest.java
@@ -52,9 +52,11 @@
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
+import android.view.inputmethod.Flags;
import android.view.inputmethod.InputMethodManager;
import android.widget.EditText;
+import androidx.test.annotation.UiThreadTest;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
import androidx.test.platform.app.InstrumentationRegistry;
@@ -72,6 +74,7 @@
*/
@Presubmit
@SmallTest
+@UiThreadTest
@RunWith(AndroidJUnit4.class)
public class HandwritingInitiatorTest {
private static final long TIMEOUT = ViewConfiguration.getLongPressTimeout();
@@ -133,7 +136,7 @@
when(mTestView1.getOffsetForPosition(anyFloat(), anyFloat())).thenReturn(4);
when(mTestView1.getLineAtCoordinate(anyFloat())).thenReturn(0);
- mHandwritingInitiator.onInputConnectionCreated(mTestView1);
+ onEditorFocusedOrConnectionCreated(mTestView1);
final int x1 = (sHwArea1.left + sHwArea1.right) / 2;
final int y1 = (sHwArea1.top + sHwArea1.bottom) / 2;
MotionEvent stylusEvent1 = createStylusEvent(ACTION_DOWN, x1, y1, 0);
@@ -170,7 +173,7 @@
when(mTestView1.getOffsetForPosition(anyFloat(), anyFloat())).thenReturn(4);
when(mTestView1.getLineAtCoordinate(anyFloat())).thenReturn(2);
- mHandwritingInitiator.onInputConnectionCreated(mTestView1);
+ onEditorFocusedOrConnectionCreated(mTestView1);
final int x1 = (sHwArea1.left + sHwArea1.right) / 2;
final int y1 = (sHwArea1.top + sHwArea1.bottom) / 2;
MotionEvent stylusEvent1 = createStylusEvent(ACTION_DOWN, x1, y1, 0);
@@ -200,7 +203,7 @@
@Test
public void onTouchEvent_startHandwritingOnce_when_stylusMoveMultiTimes_withinHWArea() {
- mHandwritingInitiator.onInputConnectionCreated(mTestView1);
+ onEditorFocusedOrConnectionCreated(mTestView1);
final int x1 = (sHwArea1.left + sHwArea1.right) / 2;
final int y1 = (sHwArea1.top + sHwArea1.bottom) / 2;
MotionEvent stylusEvent1 = createStylusEvent(ACTION_DOWN, x1, y1, 0);
@@ -244,9 +247,7 @@
when(mTestView1.getOffsetForPosition(anyFloat(), anyFloat())).thenReturn(4);
when(mTestView1.getLineAtCoordinate(anyFloat())).thenReturn(0);
- if (!mInitiateWithoutConnection) {
- mHandwritingInitiator.onInputConnectionCreated(mTestView1);
- }
+ onEditorFocusedOrConnectionCreated(mTestView1);
final int x1 = sHwArea1.left - HW_BOUNDS_OFFSETS_LEFT_PX / 2;
final int y1 = sHwArea1.top - HW_BOUNDS_OFFSETS_TOP_PX / 2;
MotionEvent stylusEvent1 = createStylusEvent(ACTION_DOWN, x1, y1, 0);
@@ -282,13 +283,7 @@
MotionEvent stylusEvent2 = createStylusEvent(ACTION_MOVE, x2, y2, 0);
mHandwritingInitiator.onTouchEvent(stylusEvent2);
- if (mInitiateWithoutConnection) {
- // Focus is changed after stylus movement.
- mHandwritingInitiator.updateFocusedView(mTestView1, /*fromTouchEvent*/ true);
- } else {
- // InputConnection is created after stylus movement.
- mHandwritingInitiator.onInputConnectionCreated(mTestView1);
- }
+ onEditorFocusedOrConnectionCreated(mTestView1);
verify(mHandwritingInitiator, times(1)).startHandwriting(mTestView1);
}
@@ -310,24 +305,11 @@
final int y2 = y1;
MotionEvent stylusEvent2 = createStylusEvent(ACTION_MOVE, x2, y2, 0);
mHandwritingInitiator.onTouchEvent(stylusEvent2);
-
- if (!mInitiateWithoutConnection) {
- // First create InputConnection for mTestView2 and verify that handwriting is not
- // started.
- mHandwritingInitiator.onInputConnectionCreated(mTestView2);
- }
-
+ onEditorFocusedOrConnectionCreated(mTestView2);
// Note: mTestView2 receives focus when initiationWithoutInputConnection() is enabled.
// verify that handwriting is not started.
verify(mHandwritingInitiator, never()).startHandwriting(mTestView2);
- if (mInitiateWithoutConnection) {
- // Focus is changed after stylus movement.
- mHandwritingInitiator.updateFocusedView(mTestView1, /*fromTouchEvent*/ true);
- } else {
- // Next create InputConnection for mTextView1. Handwriting is started for this view
- // since the stylus down point is closest to this view.
- mHandwritingInitiator.onInputConnectionCreated(mTestView1);
- }
+ onEditorFocusedOrConnectionCreated(mTestView1);
// Handwriting is started for this view since the stylus down point is closest to this
// view.
verify(mHandwritingInitiator).startHandwriting(mTestView1);
@@ -349,7 +331,7 @@
delegateView.setIsHandwritingDelegate(true);
mTestView1.setHandwritingDelegatorCallback(
- () -> mHandwritingInitiator.onInputConnectionCreated(delegateView));
+ () -> onEditorFocusedOrConnectionCreated(delegateView));
final int x1 = (sHwArea1.left + sHwArea1.right) / 2;
final int y1 = (sHwArea1.top + sHwArea1.bottom) / 2;
@@ -369,17 +351,15 @@
public void onTouchEvent_tryAcceptDelegation_delegatorCallbackFocusesDelegate() {
View delegateView = new EditText(mContext);
delegateView.setIsHandwritingDelegate(true);
+ if (mInitiateWithoutConnection) {
+ mHandwritingInitiator.onEditorFocused(delegateView);
+ }
mHandwritingInitiator.onInputConnectionCreated(delegateView);
reset(mHandwritingInitiator);
- if (mInitiateWithoutConnection) {
- mTestView1.setHandwritingDelegatorCallback(
- () -> mHandwritingInitiator.updateFocusedView(
- delegateView, /*fromTouchEvent*/ false));
- } else {
- mTestView1.setHandwritingDelegatorCallback(
- () -> mHandwritingInitiator.onDelegateViewFocused(delegateView));
- }
+
+ mTestView1.setHandwritingDelegatorCallback(
+ () -> mHandwritingInitiator.onDelegateViewFocused(delegateView));
final int x1 = (sHwArea1.left + sHwArea1.right) / 2;
final int y1 = (sHwArea1.top + sHwArea1.bottom) / 2;
@@ -391,7 +371,7 @@
MotionEvent stylusEvent2 = createStylusEvent(ACTION_MOVE, x2, y2, 0);
mHandwritingInitiator.onTouchEvent(stylusEvent2);
- verify(mHandwritingInitiator, times(1)).tryAcceptStylusHandwritingDelegation(delegateView);
+ verify(mHandwritingInitiator, times(1)).tryAcceptStylusHandwritingDelegation(any());
}
@Test
@@ -429,14 +409,6 @@
assertThat(onTouchEventResult4).isTrue();
}
- private void callOnInputConnectionOrUpdateViewFocus(View view) {
- if (mInitiateWithoutConnection) {
- mHandwritingInitiator.updateFocusedView(view, /*fromTouchEvent*/ true);
- } else {
- mHandwritingInitiator.onInputConnectionCreated(view);
- }
- }
-
@Test
public void onTouchEvent_notStartHandwriting_whenHandwritingNotAvailable() {
final Rect rect = new Rect(600, 600, 900, 900);
@@ -444,7 +416,7 @@
false /* isStylusHandwritingAvailable */);
mHandwritingInitiator.updateHandwritingAreasForView(testView);
- callOnInputConnectionOrUpdateViewFocus(testView);
+ onEditorFocusedOrConnectionCreated(testView);
final int x1 = (rect.left + rect.right) / 2;
final int y1 = (rect.top + rect.bottom) / 2;
MotionEvent stylusEvent1 = createStylusEvent(ACTION_DOWN, x1, y1, 0);
@@ -463,7 +435,7 @@
@Test
public void onTouchEvent_notStartHandwriting_when_stylusTap_withinHWArea() {
- callOnInputConnectionOrUpdateViewFocus(mTestView1);
+ onEditorFocusedOrConnectionCreated(mTestView1);
final int x1 = 200;
final int y1 = 200;
MotionEvent stylusEvent1 = createStylusEvent(ACTION_DOWN, x1, y1, 0);
@@ -479,7 +451,7 @@
@Test
public void onTouchEvent_notStartHandwriting_when_stylusMove_outOfHWArea() {
- callOnInputConnectionOrUpdateViewFocus(mTestView1);
+ onEditorFocusedOrConnectionCreated(mTestView1);
final int x1 = 10;
final int y1 = 10;
MotionEvent stylusEvent1 = createStylusEvent(ACTION_DOWN, x1, y1, 0);
@@ -495,7 +467,7 @@
@Test
public void onTouchEvent_notStartHandwriting_when_stylusMove_afterTimeOut() {
- callOnInputConnectionOrUpdateViewFocus(mTestView1);
+ onEditorFocusedOrConnectionCreated(mTestView1);
final int x1 = 10;
final int y1 = 10;
final long time1 = 10L;
@@ -551,9 +523,7 @@
@Test
public void onTouchEvent_focusView_inputConnectionAlreadyBuilt_stylusMoveOnce_withinHWArea() {
- if (!mInitiateWithoutConnection) {
- mHandwritingInitiator.onInputConnectionCreated(mTestView1);
- }
+ onEditorFocusedOrConnectionCreated(mTestView1);
final int x1 = (sHwArea1.left + sHwArea1.right) / 2;
final int y1 = (sHwArea1.top + sHwArea1.bottom) / 2;
MotionEvent stylusEvent1 = createStylusEvent(ACTION_DOWN, x1, y1, 0);
@@ -606,14 +576,14 @@
verify(mTestView2, times(1)).requestFocus();
- callOnInputConnectionOrUpdateViewFocus(mTestView2);
+ onEditorFocusedOrConnectionCreated(mTestView2);
verify(mHandwritingInitiator, times(1)).startHandwriting(mTestView2);
}
@Test
public void onTouchEvent_handwritingAreaOverlapped_focusedViewHasPriority() {
// Simulate the case where mTestView1 is focused.
- callOnInputConnectionOrUpdateViewFocus(mTestView1);
+ onEditorFocusedOrConnectionCreated(mTestView1);
// The ACTION_DOWN location is within the handwriting bounds of both mTestView1 and
// mTestView2. Although it's closer to mTestView2's handwriting bounds, handwriting is
// initiated for mTestView1 because it's focused.
@@ -651,7 +621,7 @@
@Test
public void onResolvePointerIcon_afterHandwriting_hidePointerIconForConnectedView() {
// simulate the case where sTestView1 is focused.
- mHandwritingInitiator.onInputConnectionCreated(mTestView1);
+ onEditorFocusedOrConnectionCreated(mTestView1);
injectStylusEvent(mHandwritingInitiator, sHwArea1.centerX(), sHwArea1.centerY(),
/* exceedsHWSlop */ true);
// Verify that handwriting started for sTestView1.
@@ -677,15 +647,14 @@
public void onResolvePointerIcon_afterHandwriting_hidePointerIconForDelegatorView() {
// Set mTextView2 to be the delegate of mTestView1.
mTestView2.setIsHandwritingDelegate(true);
+ mTestView1.setHandwritingDelegatorCallback(
+ () -> {
+ if (mInitiateWithoutConnection) {
+ mHandwritingInitiator.updateFocusedView(mTestView2);
+ }
+ mHandwritingInitiator.onInputConnectionCreated(mTestView2);
+ });
- if (mInitiateWithoutConnection) {
- mTestView1.setHandwritingDelegatorCallback(
- () -> mHandwritingInitiator.updateFocusedView(
- mTestView2, /*fromTouchEvent*/ false));
- } else {
- mTestView1.setHandwritingDelegatorCallback(
- () -> mHandwritingInitiator.onInputConnectionCreated(mTestView2));
- }
injectStylusEvent(mHandwritingInitiator, sHwArea1.centerX(), sHwArea1.centerY(),
/* exceedsHWSlop */ true);
// Prerequisite check, verify that handwriting started for delegateView.
@@ -700,7 +669,7 @@
@Test
public void onResolvePointerIcon_showHoverIconAfterTap() {
// Simulate the case where sTestView1 is focused.
- mHandwritingInitiator.onInputConnectionCreated(mTestView1);
+ onEditorFocusedOrConnectionCreated(mTestView1);
injectStylusEvent(mHandwritingInitiator, sHwArea1.centerX(), sHwArea1.centerY(),
/* exceedsHWSlop */ true);
// Verify that handwriting started for sTestView1.
@@ -722,7 +691,7 @@
@Test
public void onResolvePointerIcon_showHoverIconAfterFocusChange() {
// Simulate the case where sTestView1 is focused.
- mHandwritingInitiator.onInputConnectionCreated(mTestView1);
+ onEditorFocusedOrConnectionCreated(mTestView1);
injectStylusEvent(mHandwritingInitiator, sHwArea1.centerX(), sHwArea1.centerY(),
/* exceedsHWSlop */ true);
// Verify that handwriting started for sTestView1.
@@ -733,14 +702,8 @@
// After handwriting is initiated for the connected view, hide the hover icon.
assertThat(icon1).isNull();
- // Simulate that focus is switched to mTestView2 first and then switched back.
- if (mInitiateWithoutConnection) {
- mHandwritingInitiator.updateFocusedView(mTestView2, /*fromTouchEvent*/ true);
- mHandwritingInitiator.updateFocusedView(mTestView1, /*fromTouchEvent*/ true);
- } else {
- mHandwritingInitiator.onInputConnectionCreated(mTestView2);
- mHandwritingInitiator.onInputConnectionCreated(mTestView1);
- }
+ onEditorFocusedOrConnectionCreated(mTestView2);
+ onEditorFocusedOrConnectionCreated(mTestView1);
PointerIcon icon2 = mHandwritingInitiator.onResolvePointerIcon(mContext, hoverEvent1);
// After the change of focus, hover icon shows again.
@@ -752,11 +715,11 @@
if (mInitiateWithoutConnection) {
mTestView1.setAutoHandwritingEnabled(false);
mTestView1.setHandwritingDelegatorCallback(null);
- mHandwritingInitiator.updateFocusedView(mTestView1, /*fromTouchEvent*/ true);
+ onEditorFocusedOrConnectionCreated(mTestView1);
} else {
View mockView = createView(sHwArea1, false /* autoHandwritingEnabled */,
true /* isStylusHandwritingAvailable */);
- mHandwritingInitiator.onInputConnectionCreated(mockView);
+ onEditorFocusedOrConnectionCreated(mockView);
}
final int x1 = (sHwArea1.left + sHwArea1.right) / 2;
final int y1 = (sHwArea1.top + sHwArea1.bottom) / 2;
@@ -972,4 +935,12 @@
1 /* yPrecision */, 0 /* deviceId */, 0 /* edgeFlags */,
InputDevice.SOURCE_STYLUS, 0 /* flags */);
}
+
+ private void onEditorFocusedOrConnectionCreated(View testView) {
+ if (Flags.initiationWithoutInputConnection()) {
+ mHandwritingInitiator.onEditorFocused(testView);
+ } else {
+ mHandwritingInitiator.onInputConnectionCreated(testView);
+ }
+ }
}
diff --git a/core/tests/coretests/src/com/android/internal/accessibility/AccessibilityShortcutChooserActivityTest.java b/core/tests/coretests/src/com/android/internal/accessibility/AccessibilityShortcutChooserActivityTest.java
index 60a436e..745390d 100644
--- a/core/tests/coretests/src/com/android/internal/accessibility/AccessibilityShortcutChooserActivityTest.java
+++ b/core/tests/coretests/src/com/android/internal/accessibility/AccessibilityShortcutChooserActivityTest.java
@@ -25,7 +25,6 @@
import static androidx.test.espresso.matcher.RootMatchers.isDialog;
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
import static androidx.test.espresso.matcher.ViewMatchers.withClassName;
-import static androidx.test.espresso.matcher.ViewMatchers.withId;
import static androidx.test.espresso.matcher.ViewMatchers.withText;
import static com.google.common.truth.Truth.assertThat;
@@ -54,7 +53,6 @@
import android.content.pm.ServiceInfo;
import android.os.Bundle;
import android.os.Handler;
-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;
@@ -176,21 +174,6 @@
}
@Test
- @RequiresFlagsDisabled(Flags.FLAG_CLEANUP_ACCESSIBILITY_WARNING_DIALOG)
- public void selectTestService_oldPermissionDialog_deny_dialogIsHidden() {
- launchActivity();
- openShortcutsList();
-
- mDevice.findObject(By.text(TEST_LABEL)).clickAndWait(Until.newWindow(), UI_TIMEOUT_MS);
- onView(withText(DENY_LABEL)).perform(scrollTo(), click());
- InstrumentationRegistry.getInstrumentation().waitForIdleSync();
-
- onView(withId(R.id.accessibility_permissionDialog_title)).inRoot(isDialog()).check(
- doesNotExist());
- }
-
- @Test
- @RequiresFlagsEnabled(Flags.FLAG_CLEANUP_ACCESSIBILITY_WARNING_DIALOG)
public void selectTestService_permissionDialog_allow_rowChecked() {
launchActivity();
openShortcutsList();
@@ -202,7 +185,6 @@
}
@Test
- @RequiresFlagsEnabled(Flags.FLAG_CLEANUP_ACCESSIBILITY_WARNING_DIALOG)
public void selectTestService_permissionDialog_deny_rowNotChecked() {
launchActivity();
openShortcutsList();
@@ -214,7 +196,6 @@
}
@Test
- @RequiresFlagsEnabled(Flags.FLAG_CLEANUP_ACCESSIBILITY_WARNING_DIALOG)
public void selectTestService_permissionDialog_uninstall_callsUninstaller_rowRemoved() {
launchActivity();
openShortcutsList();
@@ -228,7 +209,6 @@
}
@Test
- @RequiresFlagsEnabled(Flags.FLAG_CLEANUP_ACCESSIBILITY_WARNING_DIALOG)
public void selectTestService_permissionDialog_notShownWhenNotRequired() throws Exception {
when(mAccessibilityManagerService.isAccessibilityServiceWarningRequired(any()))
.thenReturn(false);
@@ -243,7 +223,6 @@
}
@Test
- @RequiresFlagsEnabled(Flags.FLAG_CLEANUP_ACCESSIBILITY_WARNING_DIALOG)
public void selectTestService_notPermittedByAdmin_blockedEvenIfNoWarningRequired()
throws Exception {
when(mAccessibilityManagerService.isAccessibilityServiceWarningRequired(any()))
@@ -380,11 +359,9 @@
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
- if (Flags.cleanupAccessibilityWarningDialog()) {
- // Setting the Theme is necessary here for the dialog to use the proper style
- // resources as designated in its layout XML.
- setTheme(R.style.Theme_DeviceDefault_DayNight);
- }
+ // Setting the Theme is necessary here for the dialog to use the proper style
+ // resources as designated in its layout XML.
+ setTheme(R.style.Theme_DeviceDefault_DayNight);
}
@Override
diff --git a/core/tests/coretests/src/com/android/internal/accessibility/dialog/AccessibilityServiceWarningTest.java b/core/tests/coretests/src/com/android/internal/accessibility/dialog/AccessibilityServiceWarningTest.java
index 24aab61..362eeea 100644
--- a/core/tests/coretests/src/com/android/internal/accessibility/dialog/AccessibilityServiceWarningTest.java
+++ b/core/tests/coretests/src/com/android/internal/accessibility/dialog/AccessibilityServiceWarningTest.java
@@ -25,7 +25,6 @@
import android.app.AlertDialog;
import android.content.Context;
import android.os.RemoteException;
-import android.platform.test.annotations.RequiresFlagsEnabled;
import android.platform.test.flag.junit.CheckFlagsRule;
import android.platform.test.flag.junit.DeviceFlagsValueProvider;
import android.testing.AndroidTestingRunner;
@@ -57,8 +56,6 @@
*/
@RunWith(AndroidTestingRunner.class)
@TestableLooper.RunWithLooper
-@RequiresFlagsEnabled(
- android.view.accessibility.Flags.FLAG_CLEANUP_ACCESSIBILITY_WARNING_DIALOG)
public class AccessibilityServiceWarningTest {
private static final String A11Y_SERVICE_PACKAGE_LABEL = "TestA11yService";
private static final String A11Y_SERVICE_SUMMARY = "TestA11yService summary";
diff --git a/core/tests/coretests/src/com/android/internal/app/ResolverActivityTest.java b/core/tests/coretests/src/com/android/internal/app/ResolverActivityTest.java
index cb8754a..488f017 100644
--- a/core/tests/coretests/src/com/android/internal/app/ResolverActivityTest.java
+++ b/core/tests/coretests/src/com/android/internal/app/ResolverActivityTest.java
@@ -27,6 +27,7 @@
import static androidx.test.espresso.matcher.ViewMatchers.withText;
import static com.android.internal.app.MatcherUtils.first;
+import static com.android.internal.app.ResolverActivity.EXTRA_RESTRICT_TO_SINGLE_USER;
import static com.android.internal.app.ResolverDataProvider.createPackageManagerMockedInfo;
import static com.android.internal.app.ResolverWrapperActivity.sOverrides;
@@ -1254,6 +1255,51 @@
}
}
+ @Test
+ public void testTriggerFromMainProfile_inSingleUserMode_withWorkProfilePresent() {
+ mSetFlagsRule.enableFlags(android.os.Flags.FLAG_ALLOW_PRIVATE_PROFILE,
+ android.multiuser.Flags.FLAG_ALLOW_RESOLVER_SHEET_FOR_PRIVATE_SPACE);
+ markWorkProfileUserAvailable();
+ setTabOwnerUserHandleForLaunch(PERSONAL_USER_HANDLE);
+ Intent sendIntent = createSendImageIntent();
+ sendIntent.putExtra(EXTRA_RESTRICT_TO_SINGLE_USER, true);
+ List<ResolvedComponentInfo> personalResolvedComponentInfos =
+ createResolvedComponentsForTestWithOtherProfile(3, PERSONAL_USER_HANDLE);
+ List<ResolvedComponentInfo> workResolvedComponentInfos = createResolvedComponentsForTest(4,
+ sOverrides.workProfileUserHandle);
+ setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos);
+ final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent);
+ waitForIdle();
+ assertThat(activity.getPersonalListAdapter().getCount(), is(2));
+ onView(withId(R.id.tabs)).check(matches(not(isDisplayed())));
+ assertEquals(activity.getMultiProfilePagerAdapterCount(), 1);
+ for (ResolvedComponentInfo resolvedInfo : personalResolvedComponentInfos) {
+ assertEquals(resolvedInfo.getResolveInfoAt(0).userHandle, PERSONAL_USER_HANDLE);
+ }
+ }
+
+ @Test
+ public void testTriggerFromWorkProfile_inSingleUserMode() {
+ mSetFlagsRule.enableFlags(android.os.Flags.FLAG_ALLOW_PRIVATE_PROFILE,
+ android.multiuser.Flags.FLAG_ALLOW_RESOLVER_SHEET_FOR_PRIVATE_SPACE);
+ markWorkProfileUserAvailable();
+ setTabOwnerUserHandleForLaunch(sOverrides.workProfileUserHandle);
+ Intent sendIntent = createSendImageIntent();
+ sendIntent.putExtra(EXTRA_RESTRICT_TO_SINGLE_USER, true);
+ List<ResolvedComponentInfo> personalResolvedComponentInfos =
+ createResolvedComponentsForTest(3, sOverrides.workProfileUserHandle);
+ setupResolverControllers(personalResolvedComponentInfos);
+ final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent);
+ waitForIdle();
+ assertThat(activity.getPersonalListAdapter().getCount(), is(3));
+ onView(withId(R.id.tabs)).check(matches(not(isDisplayed())));
+ assertEquals(activity.getMultiProfilePagerAdapterCount(), 1);
+ for (ResolvedComponentInfo resolvedInfo : personalResolvedComponentInfos) {
+ assertEquals(resolvedInfo.getResolveInfoAt(0).userHandle,
+ sOverrides.workProfileUserHandle);
+ }
+ }
+
private Intent createSendImageIntent() {
Intent sendIntent = new Intent();
sendIntent.setAction(Intent.ACTION_SEND);
@@ -1339,6 +1385,10 @@
ResolverWrapperActivity.sOverrides.privateProfileUserHandle = UserHandle.of(12);
}
+ private void setTabOwnerUserHandleForLaunch(UserHandle tabOwnerUserHandleForLaunch) {
+ sOverrides.tabOwnerUserHandleForLaunch = tabOwnerUserHandleForLaunch;
+ }
+
private void setupResolverControllers(
List<ResolvedComponentInfo> personalResolvedComponentInfos,
List<ResolvedComponentInfo> workResolvedComponentInfos) {
diff --git a/core/tests/coretests/src/com/android/internal/app/ResolverWrapperActivity.java b/core/tests/coretests/src/com/android/internal/app/ResolverWrapperActivity.java
index 862cbd5..4604b01 100644
--- a/core/tests/coretests/src/com/android/internal/app/ResolverWrapperActivity.java
+++ b/core/tests/coretests/src/com/android/internal/app/ResolverWrapperActivity.java
@@ -116,6 +116,10 @@
when(sOverrides.resolverListController.getUserHandle()).thenReturn(UserHandle.SYSTEM);
return sOverrides.resolverListController;
}
+ if (isLaunchedInSingleUserMode()) {
+ when(sOverrides.resolverListController.getUserHandle()).thenReturn(userHandle);
+ return sOverrides.resolverListController;
+ }
when(sOverrides.workResolverListController.getUserHandle()).thenReturn(userHandle);
return sOverrides.workResolverListController;
}
diff --git a/core/tests/coretests/src/com/android/internal/net/ConnectivityBlobStoreTest.java b/core/tests/coretests/src/com/android/internal/net/ConnectivityBlobStoreTest.java
new file mode 100644
index 0000000..68545cf
--- /dev/null
+++ b/core/tests/coretests/src/com/android/internal/net/ConnectivityBlobStoreTest.java
@@ -0,0 +1,156 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.net;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import android.content.Context;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.File;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class ConnectivityBlobStoreTest {
+ private static final String DATABASE_FILENAME = "ConnectivityBlobStore.db";
+ private static final String TEST_NAME = "TEST_NAME";
+ private static final byte[] TEST_BLOB = new byte[] {(byte) 10, (byte) 90, (byte) 45, (byte) 12};
+
+ private Context mContext;
+ private File mFile;
+
+ private ConnectivityBlobStore createConnectivityBlobStore() {
+ return new ConnectivityBlobStore(mFile);
+ }
+
+ @Before
+ public void setUp() throws Exception {
+ mContext = InstrumentationRegistry.getContext();
+ mFile = mContext.getDatabasePath(DATABASE_FILENAME);
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ mContext.deleteDatabase(DATABASE_FILENAME);
+ }
+
+ @Test
+ public void testFileCreateDelete() {
+ assertFalse(mFile.exists());
+ createConnectivityBlobStore();
+ assertTrue(mFile.exists());
+
+ assertTrue(mContext.deleteDatabase(DATABASE_FILENAME));
+ assertFalse(mFile.exists());
+ }
+
+ @Test
+ public void testPutAndGet() throws Exception {
+ final ConnectivityBlobStore connectivityBlobStore = createConnectivityBlobStore();
+ assertNull(connectivityBlobStore.get(TEST_NAME));
+
+ assertTrue(connectivityBlobStore.put(TEST_NAME, TEST_BLOB));
+ assertArrayEquals(TEST_BLOB, connectivityBlobStore.get(TEST_NAME));
+
+ // Test replacement
+ final byte[] newBlob = new byte[] {(byte) 15, (byte) 20};
+ assertTrue(connectivityBlobStore.put(TEST_NAME, newBlob));
+ assertArrayEquals(newBlob, connectivityBlobStore.get(TEST_NAME));
+ }
+
+ @Test
+ public void testRemove() throws Exception {
+ final ConnectivityBlobStore connectivityBlobStore = createConnectivityBlobStore();
+ assertNull(connectivityBlobStore.get(TEST_NAME));
+ assertFalse(connectivityBlobStore.remove(TEST_NAME));
+
+ assertTrue(connectivityBlobStore.put(TEST_NAME, TEST_BLOB));
+ assertArrayEquals(TEST_BLOB, connectivityBlobStore.get(TEST_NAME));
+
+ assertTrue(connectivityBlobStore.remove(TEST_NAME));
+ assertNull(connectivityBlobStore.get(TEST_NAME));
+
+ // Removing again returns false
+ assertFalse(connectivityBlobStore.remove(TEST_NAME));
+ }
+
+ @Test
+ public void testMultipleNames() throws Exception {
+ final String name1 = TEST_NAME + "1";
+ final String name2 = TEST_NAME + "2";
+ final ConnectivityBlobStore connectivityBlobStore = createConnectivityBlobStore();
+
+ assertNull(connectivityBlobStore.get(name1));
+ assertNull(connectivityBlobStore.get(name2));
+ assertFalse(connectivityBlobStore.remove(name1));
+ assertFalse(connectivityBlobStore.remove(name2));
+
+ assertTrue(connectivityBlobStore.put(name1, TEST_BLOB));
+ assertTrue(connectivityBlobStore.put(name2, TEST_BLOB));
+ assertArrayEquals(TEST_BLOB, connectivityBlobStore.get(name1));
+ assertArrayEquals(TEST_BLOB, connectivityBlobStore.get(name2));
+
+ // Replace the blob for name1 only.
+ final byte[] newBlob = new byte[] {(byte) 16, (byte) 21};
+ assertTrue(connectivityBlobStore.put(name1, newBlob));
+ assertArrayEquals(newBlob, connectivityBlobStore.get(name1));
+
+ assertTrue(connectivityBlobStore.remove(name1));
+ assertNull(connectivityBlobStore.get(name1));
+ assertArrayEquals(TEST_BLOB, connectivityBlobStore.get(name2));
+
+ assertFalse(connectivityBlobStore.remove(name1));
+ assertTrue(connectivityBlobStore.remove(name2));
+ assertNull(connectivityBlobStore.get(name2));
+ assertFalse(connectivityBlobStore.remove(name2));
+ }
+
+ @Test
+ public void testList() throws Exception {
+ final String[] unsortedNames = new String[] {
+ TEST_NAME + "1",
+ TEST_NAME + "2",
+ TEST_NAME + "0",
+ "NON_MATCHING_PREFIX",
+ "MATCHING_SUFFIX_" + TEST_NAME
+ };
+ // Expected to match and discard the prefix and be in increasing sorted order.
+ final String[] expected = new String[] {
+ "0",
+ "1",
+ "2"
+ };
+ final ConnectivityBlobStore connectivityBlobStore = createConnectivityBlobStore();
+
+ for (int i = 0; i < unsortedNames.length; i++) {
+ assertTrue(connectivityBlobStore.put(unsortedNames[i], TEST_BLOB));
+ }
+ final String[] actual = connectivityBlobStore.list(TEST_NAME /* prefix */);
+ assertArrayEquals(expected, actual);
+ }
+}
diff --git a/core/tests/coretests/src/com/android/internal/net/OWNERS b/core/tests/coretests/src/com/android/internal/net/OWNERS
new file mode 100644
index 0000000..f51ba47
--- /dev/null
+++ b/core/tests/coretests/src/com/android/internal/net/OWNERS
@@ -0,0 +1 @@
+include /core/java/com/android/internal/net/OWNERS
diff --git a/data/etc/com.android.settings.xml b/data/etc/com.android.settings.xml
index fbe1b8e..6bdd291 100644
--- a/data/etc/com.android.settings.xml
+++ b/data/etc/com.android.settings.xml
@@ -49,6 +49,7 @@
<permission name="android.permission.READ_SEARCH_INDEXABLES"/>
<permission name="android.permission.REBOOT"/>
<permission name="android.permission.RECOVERY"/>
+ <permission name="android.permission.SCHEDULE_EXACT_ALARM"/>
<permission name="android.permission.STATUS_BAR"/>
<permission name="android.permission.SUGGEST_MANUAL_TIME_AND_ZONE"/>
<permission name="android.permission.TETHER_PRIVILEGED"/>
diff --git a/data/etc/privapp-permissions-platform.xml b/data/etc/privapp-permissions-platform.xml
index 9c1c7006..ea3235b 100644
--- a/data/etc/privapp-permissions-platform.xml
+++ b/data/etc/privapp-permissions-platform.xml
@@ -588,6 +588,8 @@
<permission name="android.permission.OVERRIDE_SYSTEM_KEY_BEHAVIOR_IN_FOCUSED_WINDOW" />
<!-- Permission required for CTS test - PackageManagerShellCommandInstallTest -->
<permission name="android.permission.EMERGENCY_INSTALL_PACKAGES" />
+ <!-- Permission required for Cts test - CtsSettingsTestCases -->
+ <permission name="android.permission.PREPARE_FACTORY_RESET" />
</privapp-permissions>
<privapp-permissions package="com.android.statementservice">
diff --git a/data/keyboards/Vendor_054c_Product_05c4.idc b/data/keyboards/Vendor_054c_Product_05c4.idc
index 2cb3f7b..2da6227 100644
--- a/data/keyboards/Vendor_054c_Product_05c4.idc
+++ b/data/keyboards/Vendor_054c_Product_05c4.idc
@@ -13,9 +13,11 @@
# limitations under the License.
#
-# Sony DS4 motion sensor configuration file.
+# Sony Playstation(R) DualShock 4 Controller
#
+## Motion sensor ##
+
# reporting mode 0 - continuous
sensor.accelerometer.reportingMode = 0
# The delay between sensor events corresponding to the lowest frequency in microsecond
@@ -33,3 +35,28 @@
sensor.gyroscope.minDelay = 5000
# The power in mA used by this sensor while in use
sensor.gyroscope.power = 0.8
+
+## Touchpad ##
+
+# After the DualShock 4 has been connected over Bluetooth for a minute or so,
+# its reports start bunching up in time, meaning that we receive 2–4 reports
+# within a millisecond followed by a >10ms wait until the next batch.
+#
+# This uneven timing causes the apparent speed of a finger (calculated using
+# time deltas between received reports) to vary dramatically even if it's
+# actually moving smoothly across the touchpad, triggering the touchpad stack's
+# drumroll detection logic. For moving fingers, the drumroll detection logic
+# splits the finger's single movement into many small movements of consecutive
+# touches, which are then inhibited by the click wiggle filter. For tapping
+# fingers, it prevents tapping to click because it thinks the finger's moving
+# too fast.
+#
+# Since this touchpad doesn't seem to have to drumroll issues, we can safely
+# disable drumroll detection.
+gestureProp.Drumroll_Suppression_Enable = 0
+
+# Because of the way this touchpad is positioned, touches around the edges are
+# no more likely to be palms than ones in the middle, so remove the edge zones
+# from the palm classifier to increase the usable area of the pad.
+gestureProp.Palm_Edge_Zone_Width = 0
+gestureProp.Tap_Exclusion_Border_Width = 0
diff --git a/data/keyboards/Vendor_054c_Product_09cc.idc b/data/keyboards/Vendor_054c_Product_09cc.idc
index 2cb3f7b..2a1a4fc 100644
--- a/data/keyboards/Vendor_054c_Product_09cc.idc
+++ b/data/keyboards/Vendor_054c_Product_09cc.idc
@@ -13,9 +13,11 @@
# limitations under the License.
#
-# Sony DS4 motion sensor configuration file.
+# Sony Playstation(R) DualShock 4 Controller
#
+## Motion sensor ##
+
# reporting mode 0 - continuous
sensor.accelerometer.reportingMode = 0
# The delay between sensor events corresponding to the lowest frequency in microsecond
@@ -33,3 +35,28 @@
sensor.gyroscope.minDelay = 5000
# The power in mA used by this sensor while in use
sensor.gyroscope.power = 0.8
+
+## Touchpad ##
+
+# After the DualShock 4 has been connected over Bluetooth for a minute or so,
+# its reports start bunching up in time, meaning that we receive 2–4 reports
+# within a millisecond followed by a >10ms wait until the next batch.
+#
+# This uneven timing causes the apparent speed of a finger (calculated using
+# time deltas between received reports) to vary dramatically even if it's
+# actually moving smoothly across the touchpad, triggering the touchpad stack's
+# drumroll detection logic. For moving fingers, the drumroll detection logic
+# splits the finger's single movement into many small movements of consecutive
+# touches, which are then inhibited by the click wiggle filter. For tapping
+# fingers, it prevents tapping to click because it thinks the finger's moving
+# too fast.
+#
+# Since this touchpad doesn't seem to have drumroll issues, we can safely
+# disable drumroll detection.
+gestureProp.Drumroll_Suppression_Enable = 0
+
+# Because of the way this touchpad is positioned, touches around the edges are
+# no more likely to be palms than ones in the middle, so remove the edge zones
+# from the palm classifier to increase the usable area of the pad.
+gestureProp.Palm_Edge_Zone_Width = 0
+gestureProp.Tap_Exclusion_Border_Width = 0
diff --git a/graphics/java/android/framework_graphics.aconfig b/graphics/java/android/framework_graphics.aconfig
index 6c81a60..1e41b4d 100644
--- a/graphics/java/android/framework_graphics.aconfig
+++ b/graphics/java/android/framework_graphics.aconfig
@@ -2,6 +2,7 @@
flag {
name: "exact_compute_bounds"
+ is_exported: true
namespace: "core_graphics"
description: "Add a function without unused exact param for computeBounds."
bug: "304478551"
@@ -9,6 +10,7 @@
flag {
name: "yuv_image_compress_to_ultra_hdr"
+ is_exported: true
namespace: "core_graphics"
description: "Feature flag for YUV image compress to Ultra HDR."
bug: "308978825"
diff --git a/keystore/java/android/security/AndroidKeyStoreMaintenance.java b/keystore/java/android/security/AndroidKeyStoreMaintenance.java
index 2430e8d..efbbfc2 100644
--- a/keystore/java/android/security/AndroidKeyStoreMaintenance.java
+++ b/keystore/java/android/security/AndroidKeyStoreMaintenance.java
@@ -175,20 +175,6 @@
}
/**
- * Informs Keystore 2.0 that an off body event was detected.
- */
- public static void onDeviceOffBody() {
- StrictMode.noteDiskWrite();
- try {
- getService().onDeviceOffBody();
- } catch (Exception e) {
- // TODO This fails open. This is not a regression with respect to keystore1 but it
- // should get fixed.
- Log.e(TAG, "Error while reporting device off body event.", e);
- }
- }
-
- /**
* Migrates a key given by the source descriptor to the location designated by the destination
* descriptor.
*
diff --git a/keystore/java/android/security/KeyStore.java b/keystore/java/android/security/KeyStore.java
index bd9abec..f105072 100644
--- a/keystore/java/android/security/KeyStore.java
+++ b/keystore/java/android/security/KeyStore.java
@@ -56,11 +56,4 @@
return Authorization.addAuthToken(authToken);
}
-
- /**
- * Notify keystore that the device went off-body.
- */
- public void onDeviceOffBody() {
- AndroidKeyStoreMaintenance.onDeviceOffBody();
- }
}
diff --git a/keystore/java/android/security/keystore/KeyGenParameterSpec.java b/keystore/java/android/security/keystore/KeyGenParameterSpec.java
index 7aecfd8..d359a90 100644
--- a/keystore/java/android/security/keystore/KeyGenParameterSpec.java
+++ b/keystore/java/android/security/keystore/KeyGenParameterSpec.java
@@ -880,9 +880,7 @@
}
/**
- * Returns {@code true} if the screen must be unlocked for this key to be used for decryption or
- * signing. Encryption and signature verification will still be available when the screen is
- * locked.
+ * Returns {@code true} if the key is authorized to be used only while the device is unlocked.
*
* @see Builder#setUnlockedDeviceRequired(boolean)
*/
@@ -1672,16 +1670,16 @@
* {@link #setUserAuthenticationValidityDurationSeconds} and
* {@link #setUserAuthenticationRequired}). Once the device has been removed from the
* user's body, the key will be considered unauthorized and the user will need to
- * re-authenticate to use it. For keys without an authentication validity period this
- * parameter has no effect.
+ * re-authenticate to use it. If the device does not have an on-body sensor or the key does
+ * not have an authentication validity period, this parameter has no effect.
+ * <p>
+ * Since Android 12 (API level 31), this parameter has no effect even on devices that have
+ * an on-body sensor. A future version of Android may restore enforcement of this parameter.
+ * Meanwhile, it is recommended to not use it.
*
- * <p>Similarly, on devices that do not have an on-body sensor, this parameter will have no
- * effect; the device will always be considered to be "on-body" and the key will therefore
- * remain authorized until the validity period ends.
- *
- * @param remainsValid if {@code true}, and if the device supports on-body detection, key
- * will be invalidated when the device is removed from the user's body or when the
- * authentication validity expires, whichever occurs first.
+ * @param remainsValid if {@code true}, and if the device supports enforcement of this
+ * parameter, the key will be invalidated when the device is removed from the user's body or
+ * when the authentication validity expires, whichever occurs first.
*/
@NonNull
public Builder setUserAuthenticationValidWhileOnBody(boolean remainsValid) {
@@ -1723,11 +1721,49 @@
}
/**
- * Sets whether the keystore requires the screen to be unlocked before allowing decryption
- * using this key. If this is set to {@code true}, any attempt to decrypt or sign using this
- * key while the screen is locked will fail. A locked device requires a PIN, password,
- * biometric, or other trusted factor to access. While the screen is locked, any associated
- * public key can still be used (e.g for signature verification).
+ * Sets whether this key is authorized to be used only while the device is unlocked.
+ * <p>
+ * The device is considered to be locked for a user when the user's apps are currently
+ * inaccessible and some form of lock screen authentication is required to regain access to
+ * them. For the full definition, see {@link KeyguardManager#isDeviceLocked()}.
+ * <p>
+ * Public key operations aren't restricted by {@code setUnlockedDeviceRequired(true)} and
+ * may be performed even while the device is locked. In Android 11 (API level 30) and lower,
+ * encryption and verification operations with symmetric keys weren't restricted either.
+ * <p>
+ * Keys that use {@code setUnlockedDeviceRequired(true)} can be imported and generated even
+ * while the device is locked, as long as the device has been unlocked at least once since
+ * the last reboot. However, such keys cannot be used (except for the unrestricted
+ * operations mentioned above) until the device is unlocked. Apps that need to encrypt data
+ * while the device is locked such that it can only be decrypted while the device is
+ * unlocked can generate a key and encrypt the data in software, import the key into
+ * Keystore using {@code setUnlockedDeviceRequired(true)}, and zeroize the original key.
+ * <p>
+ * {@code setUnlockedDeviceRequired(true)} is related to but distinct from
+ * {@link #setUserAuthenticationRequired(boolean) setUserAuthenticationRequired(true)}.
+ * {@code setUnlockedDeviceRequired(true)} requires that the device be unlocked, whereas
+ * {@code setUserAuthenticationRequired(true)} requires that a specific type of strong
+ * authentication has happened within a specific time period. They may be used together or
+ * separately; there are cases in which one requirement can be satisfied but not the other.
+ * <p>
+ * <b>Warning:</b> Be careful using {@code setUnlockedDeviceRequired(true)} on Android 14
+ * (API level 34) and lower, since the following bugs existed in Android 12 through 14:
+ * <ul>
+ * <li>When the user didn't have a secure lock screen, unlocked-device-required keys
+ * couldn't be generated, imported, or used.</li>
+ * <li>When the user's secure lock screen was removed, all of that user's
+ * unlocked-device-required keys were automatically deleted.</li>
+ * <li>Unlocking the device with a non-strong biometric, such as face on many devices,
+ * didn't re-authorize the use of unlocked-device-required keys.</li>
+ * <li>Unlocking the device with a biometric didn't re-authorize the use of
+ * unlocked-device-required keys in profiles that share their parent user's lock.</li>
+ * </ul>
+ * These issues are fixed in Android 15, so apps can avoid them by using
+ * {@code setUnlockedDeviceRequired(true)} only on Android 15 and higher.
+ * Apps that use both {@code setUnlockedDeviceRequired(true)} and
+ * {@link #setUserAuthenticationRequired(boolean) setUserAuthenticationRequired(true)}
+ * are unaffected by the first two issues, since the first two issues describe expected
+ * behavior for {@code setUserAuthenticationRequired(true)}.
*/
@NonNull
public Builder setUnlockedDeviceRequired(boolean unlockedDeviceRequired) {
diff --git a/keystore/java/android/security/keystore/KeyInfo.java b/keystore/java/android/security/keystore/KeyInfo.java
index 5cffe46..2163ca2 100644
--- a/keystore/java/android/security/keystore/KeyInfo.java
+++ b/keystore/java/android/security/keystore/KeyInfo.java
@@ -279,7 +279,7 @@
}
/**
- * Returns {@code true} if the key is authorized to be used only when the device is unlocked.
+ * Returns {@code true} if the key is authorized to be used only while the device is unlocked.
*
* <p>This authorization applies only to secret key and private key operations. Public key
* operations are not restricted.
diff --git a/keystore/java/android/security/keystore/KeyProtection.java b/keystore/java/android/security/keystore/KeyProtection.java
index 31b4a5e..8e5ac45 100644
--- a/keystore/java/android/security/keystore/KeyProtection.java
+++ b/keystore/java/android/security/keystore/KeyProtection.java
@@ -577,9 +577,7 @@
}
/**
- * Returns {@code true} if the screen must be unlocked for this key to be used for decryption or
- * signing. Encryption and signature verification will still be available when the screen is
- * locked.
+ * Returns {@code true} if the key is authorized to be used only while the device is unlocked.
*
* @see Builder#setUnlockedDeviceRequired(boolean)
*/
@@ -1039,16 +1037,16 @@
* {@link #setUserAuthenticationValidityDurationSeconds} and
* {@link #setUserAuthenticationRequired}). Once the device has been removed from the
* user's body, the key will be considered unauthorized and the user will need to
- * re-authenticate to use it. For keys without an authentication validity period this
- * parameter has no effect.
+ * re-authenticate to use it. If the device does not have an on-body sensor or the key does
+ * not have an authentication validity period, this parameter has no effect.
+ * <p>
+ * Since Android 12 (API level 31), this parameter has no effect even on devices that have
+ * an on-body sensor. A future version of Android may restore enforcement of this parameter.
+ * Meanwhile, it is recommended to not use it.
*
- * <p>Similarly, on devices that do not have an on-body sensor, this parameter will have no
- * effect; the device will always be considered to be "on-body" and the key will therefore
- * remain authorized until the validity period ends.
- *
- * @param remainsValid if {@code true}, and if the device supports on-body detection, key
- * will be invalidated when the device is removed from the user's body or when the
- * authentication validity expires, whichever occurs first.
+ * @param remainsValid if {@code true}, and if the device supports enforcement of this
+ * parameter, the key will be invalidated when the device is removed from the user's body or
+ * when the authentication validity expires, whichever occurs first.
*/
@NonNull
public Builder setUserAuthenticationValidWhileOnBody(boolean remainsValid) {
@@ -1117,11 +1115,49 @@
}
/**
- * Sets whether the keystore requires the screen to be unlocked before allowing decryption
- * using this key. If this is set to {@code true}, any attempt to decrypt or sign using this
- * key while the screen is locked will fail. A locked device requires a PIN, password,
- * biometric, or other trusted factor to access. While the screen is locked, the key can
- * still be used for encryption or signature verification.
+ * Sets whether this key is authorized to be used only while the device is unlocked.
+ * <p>
+ * The device is considered to be locked for a user when the user's apps are currently
+ * inaccessible and some form of lock screen authentication is required to regain access to
+ * them. For the full definition, see {@link KeyguardManager#isDeviceLocked()}.
+ * <p>
+ * Public key operations aren't restricted by {@code setUnlockedDeviceRequired(true)} and
+ * may be performed even while the device is locked. In Android 11 (API level 30) and lower,
+ * encryption and verification operations with symmetric keys weren't restricted either.
+ * <p>
+ * Keys that use {@code setUnlockedDeviceRequired(true)} can be imported and generated even
+ * while the device is locked, as long as the device has been unlocked at least once since
+ * the last reboot. However, such keys cannot be used (except for the unrestricted
+ * operations mentioned above) until the device is unlocked. Apps that need to encrypt data
+ * while the device is locked such that it can only be decrypted while the device is
+ * unlocked can generate a key and encrypt the data in software, import the key into
+ * Keystore using {@code setUnlockedDeviceRequired(true)}, and zeroize the original key.
+ * <p>
+ * {@code setUnlockedDeviceRequired(true)} is related to but distinct from
+ * {@link #setUserAuthenticationRequired(boolean) setUserAuthenticationRequired(true)}.
+ * {@code setUnlockedDeviceRequired(true)} requires that the device be unlocked, whereas
+ * {@code setUserAuthenticationRequired(true)} requires that a specific type of strong
+ * authentication has happened within a specific time period. They may be used together or
+ * separately; there are cases in which one requirement can be satisfied but not the other.
+ * <p>
+ * <b>Warning:</b> Be careful using {@code setUnlockedDeviceRequired(true)} on Android 14
+ * (API level 34) and lower, since the following bugs existed in Android 12 through 14:
+ * <ul>
+ * <li>When the user didn't have a secure lock screen, unlocked-device-required keys
+ * couldn't be generated, imported, or used.</li>
+ * <li>When the user's secure lock screen was removed, all of that user's
+ * unlocked-device-required keys were automatically deleted.</li>
+ * <li>Unlocking the device with a non-strong biometric, such as face on many devices,
+ * didn't re-authorize the use of unlocked-device-required keys.</li>
+ * <li>Unlocking the device with a biometric didn't re-authorize the use of
+ * unlocked-device-required keys in profiles that share their parent user's lock.</li>
+ * </ul>
+ * These issues are fixed in Android 15, so apps can avoid them by using
+ * {@code setUnlockedDeviceRequired(true)} only on Android 15 and higher.
+ * Apps that use both {@code setUnlockedDeviceRequired(true)} and
+ * {@link #setUserAuthenticationRequired(boolean) setUserAuthenticationRequired(true)}
+ * are unaffected by the first two issues, since the first two issues describe expected
+ * behavior for {@code setUserAuthenticationRequired(true)}.
*/
@NonNull
public Builder setUnlockedDeviceRequired(boolean unlockedDeviceRequired) {
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsImpl.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsImpl.java
index 9756278..16c77d0 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsImpl.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsImpl.java
@@ -53,7 +53,7 @@
* The min version of the WM Extensions that must be supported in the current platform version.
*/
@VisibleForTesting
- static final int EXTENSIONS_VERSION_CURRENT_PLATFORM = 5;
+ static final int EXTENSIONS_VERSION_CURRENT_PLATFORM = 6;
private final Object mLock = new Object();
private volatile DeviceStateManagerFoldingFeatureProducer mFoldingFeatureProducer;
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/DividerPresenter.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/DividerPresenter.java
index 100185b..cae232e 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/DividerPresenter.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/DividerPresenter.java
@@ -17,6 +17,12 @@
package androidx.window.extensions.embedding;
import static android.util.TypedValue.COMPLEX_UNIT_DIP;
+import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
+import static android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL;
+import static android.view.WindowManager.LayoutParams.FLAG_SLIPPERY;
+import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_PANEL;
+import static android.window.TaskFragmentOperation.OP_TYPE_CREATE_OR_MOVE_TASK_FRAGMENT_DECOR_SURFACE;
+import static android.window.TaskFragmentOperation.OP_TYPE_REMOVE_TASK_FRAGMENT_DECOR_SURFACE;
import static androidx.window.extensions.embedding.DividerAttributes.RATIO_UNSET;
import static androidx.window.extensions.embedding.DividerAttributes.WIDTH_UNSET;
@@ -28,34 +34,253 @@
import android.annotation.Nullable;
import android.app.ActivityThread;
import android.content.Context;
+import android.content.pm.ActivityInfo;
+import android.content.res.Configuration;
+import android.graphics.Color;
+import android.graphics.PixelFormat;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.RotateDrawable;
+import android.hardware.display.DisplayManager;
+import android.os.IBinder;
import android.util.TypedValue;
+import android.view.Gravity;
+import android.view.SurfaceControl;
+import android.view.SurfaceControlViewHost;
+import android.view.WindowManager;
+import android.view.WindowlessWindowManager;
+import android.widget.FrameLayout;
+import android.widget.ImageButton;
+import android.window.InputTransferToken;
+import android.window.TaskFragmentOperation;
+import android.window.TaskFragmentParentInfo;
+import android.window.WindowContainerTransaction;
+import androidx.annotation.IdRes;
import androidx.annotation.NonNull;
+import com.android.internal.R;
import com.android.internal.annotations.VisibleForTesting;
import com.android.window.flags.Flags;
+import java.util.Objects;
+
/**
* Manages the rendering and interaction of the divider.
*/
class DividerPresenter {
+ private static final String WINDOW_NAME = "AE Divider";
+
// TODO(b/327067596) Update based on UX guidance.
- @VisibleForTesting static final float DEFAULT_MIN_RATIO = 0.35f;
- @VisibleForTesting static final float DEFAULT_MAX_RATIO = 0.65f;
- @VisibleForTesting static final int DEFAULT_DIVIDER_WIDTH_DP = 24;
+ private static final Color DEFAULT_DIVIDER_COLOR = Color.valueOf(Color.BLACK);
+ @VisibleForTesting
+ static final float DEFAULT_MIN_RATIO = 0.35f;
+ @VisibleForTesting
+ static final float DEFAULT_MAX_RATIO = 0.65f;
+ @VisibleForTesting
+ static final int DEFAULT_DIVIDER_WIDTH_DP = 24;
- static int getDividerWidthPx(@NonNull DividerAttributes dividerAttributes) {
+ /**
+ * The {@link Properties} of the divider. This field is {@code null} when no divider should be
+ * drawn, e.g. when the split doesn't have {@link DividerAttributes} or when the decor surface
+ * is not available.
+ */
+ @Nullable
+ @VisibleForTesting
+ Properties mProperties;
+
+ /**
+ * The {@link Renderer} of the divider. This field is {@code null} when no divider should be
+ * drawn, i.e. when {@link #mProperties} is {@code null}. The {@link Renderer} is recreated or
+ * updated when {@link #mProperties} is changed.
+ */
+ @Nullable
+ @VisibleForTesting
+ Renderer mRenderer;
+
+ /**
+ * The owner TaskFragment token of the decor surface. The decor surface is placed right above
+ * the owner TaskFragment surface and is removed if the owner TaskFragment is destroyed.
+ */
+ @Nullable
+ @VisibleForTesting
+ IBinder mDecorSurfaceOwner;
+
+ /** Updates the divider when external conditions are changed. */
+ void updateDivider(
+ @NonNull WindowContainerTransaction wct,
+ @NonNull TaskFragmentParentInfo parentInfo,
+ @Nullable SplitContainer topSplitContainer) {
+ if (!Flags.activityEmbeddingInteractiveDividerFlag()) {
+ return;
+ }
+
+ // Clean up the decor surface if top SplitContainer is null.
+ if (topSplitContainer == null) {
+ removeDecorSurfaceAndDivider(wct);
+ return;
+ }
+
+ // Clean up the decor surface if DividerAttributes is null.
+ final DividerAttributes dividerAttributes =
+ topSplitContainer.getCurrentSplitAttributes().getDividerAttributes();
+ if (dividerAttributes == null) {
+ removeDecorSurfaceAndDivider(wct);
+ return;
+ }
+
+ if (topSplitContainer.getCurrentSplitAttributes().getSplitType()
+ instanceof SplitAttributes.SplitType.ExpandContainersSplitType) {
+ // No divider is needed for ExpandContainersSplitType.
+ removeDivider();
+ return;
+ }
+
+ // Skip updating when the TFs have not been updated to match the SplitAttributes.
+ if (topSplitContainer.getPrimaryContainer().getLastRequestedBounds().isEmpty()
+ || topSplitContainer.getSecondaryContainer().getLastRequestedBounds().isEmpty()) {
+ return;
+ }
+
+ final SurfaceControl decorSurface = parentInfo.getDecorSurface();
+ if (decorSurface == null) {
+ // Clean up when the decor surface is currently unavailable.
+ removeDivider();
+ // Request to create the decor surface
+ createOrMoveDecorSurface(wct, topSplitContainer.getPrimaryContainer());
+ return;
+ }
+
+ // make the top primary container the owner of the decor surface.
+ if (!Objects.equals(mDecorSurfaceOwner,
+ topSplitContainer.getPrimaryContainer().getTaskFragmentToken())) {
+ createOrMoveDecorSurface(wct, topSplitContainer.getPrimaryContainer());
+ }
+
+ updateProperties(
+ new Properties(
+ parentInfo.getConfiguration(),
+ dividerAttributes,
+ decorSurface,
+ getInitialDividerPosition(topSplitContainer),
+ isVerticalSplit(topSplitContainer),
+ parentInfo.getDisplayId()));
+ }
+
+ private void updateProperties(@NonNull Properties properties) {
+ if (Properties.equalsForDivider(mProperties, properties)) {
+ return;
+ }
+ final Properties previousProperties = mProperties;
+ mProperties = properties;
+
+ if (mRenderer == null) {
+ // Create a new renderer when a renderer doesn't exist yet.
+ mRenderer = new Renderer();
+ } else if (!Properties.areSameSurfaces(
+ previousProperties.mDecorSurface, mProperties.mDecorSurface)
+ || previousProperties.mDisplayId != mProperties.mDisplayId) {
+ // Release and recreate the renderer if the decor surface or the display has changed.
+ mRenderer.release();
+ mRenderer = new Renderer();
+ } else {
+ // Otherwise, update the renderer for the new properties.
+ mRenderer.update();
+ }
+ }
+
+ /**
+ * Creates a decor surface for the TaskFragment if no decor surface exists, or changes the owner
+ * of the existing decor surface to be the specified TaskFragment.
+ *
+ * See {@link TaskFragmentOperation#OP_TYPE_CREATE_OR_MOVE_TASK_FRAGMENT_DECOR_SURFACE}.
+ */
+ private void createOrMoveDecorSurface(
+ @NonNull WindowContainerTransaction wct, @NonNull TaskFragmentContainer container) {
+ final TaskFragmentOperation operation = new TaskFragmentOperation.Builder(
+ OP_TYPE_CREATE_OR_MOVE_TASK_FRAGMENT_DECOR_SURFACE)
+ .build();
+ wct.addTaskFragmentOperation(container.getTaskFragmentToken(), operation);
+ mDecorSurfaceOwner = container.getTaskFragmentToken();
+ }
+
+ private void removeDecorSurfaceAndDivider(@NonNull WindowContainerTransaction wct) {
+ if (mDecorSurfaceOwner != null) {
+ final TaskFragmentOperation operation = new TaskFragmentOperation.Builder(
+ OP_TYPE_REMOVE_TASK_FRAGMENT_DECOR_SURFACE)
+ .build();
+ wct.addTaskFragmentOperation(mDecorSurfaceOwner, operation);
+ mDecorSurfaceOwner = null;
+ }
+ removeDivider();
+ }
+
+ private void removeDivider() {
+ if (mRenderer != null) {
+ mRenderer.release();
+ }
+ mProperties = null;
+ mRenderer = null;
+ }
+
+ @VisibleForTesting
+ static int getInitialDividerPosition(@NonNull SplitContainer splitContainer) {
+ final Rect primaryBounds =
+ splitContainer.getPrimaryContainer().getLastRequestedBounds();
+ final Rect secondaryBounds =
+ splitContainer.getSecondaryContainer().getLastRequestedBounds();
+ if (isVerticalSplit(splitContainer)) {
+ return Math.min(primaryBounds.right, secondaryBounds.right);
+ } else {
+ return Math.min(primaryBounds.bottom, secondaryBounds.bottom);
+ }
+ }
+
+ private static boolean isVerticalSplit(@NonNull SplitContainer splitContainer) {
+ final int layoutDirection = splitContainer.getCurrentSplitAttributes().getLayoutDirection();
+ switch(layoutDirection) {
+ case SplitAttributes.LayoutDirection.LEFT_TO_RIGHT:
+ case SplitAttributes.LayoutDirection.RIGHT_TO_LEFT:
+ case SplitAttributes.LayoutDirection.LOCALE:
+ return true;
+ case SplitAttributes.LayoutDirection.TOP_TO_BOTTOM:
+ case SplitAttributes.LayoutDirection.BOTTOM_TO_TOP:
+ return false;
+ default:
+ throw new IllegalArgumentException("Invalid layout direction:" + layoutDirection);
+ }
+ }
+
+ private static void safeReleaseSurfaceControl(@Nullable SurfaceControl sc) {
+ if (sc != null) {
+ sc.release();
+ }
+ }
+
+ private static int getDividerWidthPx(@NonNull DividerAttributes dividerAttributes) {
int dividerWidthDp = dividerAttributes.getWidthDp();
+ return convertDpToPixel(dividerWidthDp);
+ }
+ private static int convertDpToPixel(int dp) {
// TODO(b/329193115) support divider on secondary display
final Context applicationContext = ActivityThread.currentActivityThread().getApplication();
return (int) TypedValue.applyDimension(
COMPLEX_UNIT_DIP,
- dividerWidthDp,
+ dp,
applicationContext.getResources().getDisplayMetrics());
}
+ private static int getDimensionDp(@IdRes int resId) {
+ final Context context = ActivityThread.currentActivityThread().getApplication();
+ final int px = context.getResources().getDimensionPixelSize(resId);
+ return (int) TypedValue.convertPixelsToDimension(
+ COMPLEX_UNIT_DIP,
+ px,
+ context.getResources().getDisplayMetrics());
+ }
+
/**
* Returns the container bound offset that is a result of the presence of a divider.
*
@@ -140,6 +365,12 @@
widthDp = DEFAULT_DIVIDER_WIDTH_DP;
}
+ if (dividerAttributes.getDividerType() == DividerAttributes.DIVIDER_TYPE_DRAGGABLE) {
+ // Draggable divider width must be larger than the drag handle size.
+ widthDp = Math.max(widthDp,
+ getDimensionDp(R.dimen.activity_embedding_divider_touch_target_width));
+ }
+
float minRatio = dividerAttributes.getPrimaryMinRatio();
if (minRatio == RATIO_UNSET) {
minRatio = DEFAULT_MIN_RATIO;
@@ -156,4 +387,231 @@
.setPrimaryMaxRatio(maxRatio)
.build();
}
+
+ /**
+ * Properties for the {@link DividerPresenter}. The rendering of the divider solely depends on
+ * these properties. When any value is updated, the divider is re-rendered. The Properties
+ * instance is created only when all the pre-conditions of drawing a divider are met.
+ */
+ @VisibleForTesting
+ static class Properties {
+ private static final int CONFIGURATION_MASK_FOR_DIVIDER =
+ ActivityInfo.CONFIG_DENSITY | ActivityInfo.CONFIG_WINDOW_CONFIGURATION;
+ @NonNull
+ private final Configuration mConfiguration;
+ @NonNull
+ private final DividerAttributes mDividerAttributes;
+ @NonNull
+ private final SurfaceControl mDecorSurface;
+
+ /** The initial position of the divider calculated based on container bounds. */
+ private final int mInitialDividerPosition;
+
+ /** Whether the split is vertical, such as left-to-right or right-to-left split. */
+ private final boolean mIsVerticalSplit;
+
+ private final int mDisplayId;
+
+ @VisibleForTesting
+ Properties(
+ @NonNull Configuration configuration,
+ @NonNull DividerAttributes dividerAttributes,
+ @NonNull SurfaceControl decorSurface,
+ int initialDividerPosition,
+ boolean isVerticalSplit,
+ int displayId) {
+ mConfiguration = configuration;
+ mDividerAttributes = dividerAttributes;
+ mDecorSurface = decorSurface;
+ mInitialDividerPosition = initialDividerPosition;
+ mIsVerticalSplit = isVerticalSplit;
+ mDisplayId = displayId;
+ }
+
+ /**
+ * Compares whether two Properties objects are equal for rendering the divider. The
+ * Configuration is checked for rendering related fields, and other fields are checked for
+ * regular equality.
+ */
+ private static boolean equalsForDivider(@Nullable Properties a, @Nullable Properties b) {
+ if (a == b) {
+ return true;
+ }
+ if (a == null || b == null) {
+ return false;
+ }
+ return areSameSurfaces(a.mDecorSurface, b.mDecorSurface)
+ && Objects.equals(a.mDividerAttributes, b.mDividerAttributes)
+ && areConfigurationsEqualForDivider(a.mConfiguration, b.mConfiguration)
+ && a.mInitialDividerPosition == b.mInitialDividerPosition
+ && a.mIsVerticalSplit == b.mIsVerticalSplit
+ && a.mDisplayId == b.mDisplayId;
+ }
+
+ private static boolean areSameSurfaces(
+ @Nullable SurfaceControl sc1, @Nullable SurfaceControl sc2) {
+ if (sc1 == sc2) {
+ // If both are null or both refer to the same object.
+ return true;
+ }
+ if (sc1 == null || sc2 == null) {
+ return false;
+ }
+ return sc1.isSameSurface(sc2);
+ }
+
+ private static boolean areConfigurationsEqualForDivider(
+ @NonNull Configuration a, @NonNull Configuration b) {
+ final int diff = a.diff(b);
+ return (diff & CONFIGURATION_MASK_FOR_DIVIDER) == 0;
+ }
+ }
+
+ /**
+ * Handles the rendering of the divider. When the decor surface is updated, the renderer is
+ * recreated. When other fields in the Properties are changed, the renderer is updated.
+ */
+ @VisibleForTesting
+ class Renderer {
+ @NonNull
+ private final SurfaceControl mDividerSurface;
+ @NonNull
+ private final WindowlessWindowManager mWindowlessWindowManager;
+ @NonNull
+ private final SurfaceControlViewHost mViewHost;
+ @NonNull
+ private final FrameLayout mDividerLayout;
+ private final int mDividerWidthPx;
+
+ private Renderer() {
+ mDividerWidthPx = getDividerWidthPx(mProperties.mDividerAttributes);
+
+ mDividerSurface = createChildSurface("DividerSurface", true /* visible */);
+ mWindowlessWindowManager = new WindowlessWindowManager(
+ mProperties.mConfiguration,
+ mDividerSurface,
+ new InputTransferToken());
+
+ final Context context = ActivityThread.currentActivityThread().getApplication();
+ final DisplayManager displayManager = context.getSystemService(DisplayManager.class);
+ mViewHost = new SurfaceControlViewHost(
+ context, displayManager.getDisplay(mProperties.mDisplayId),
+ mWindowlessWindowManager, "DividerContainer");
+ mDividerLayout = new FrameLayout(context);
+
+ update();
+ }
+
+ /** Updates the divider when properties are changed */
+ @VisibleForTesting
+ void update() {
+ mWindowlessWindowManager.setConfiguration(mProperties.mConfiguration);
+ updateSurface();
+ updateLayout();
+ updateDivider();
+ }
+
+ @VisibleForTesting
+ void release() {
+ mViewHost.release();
+ // TODO handle synchronization between surface transactions and WCT.
+ new SurfaceControl.Transaction().remove(mDividerSurface).apply();
+ safeReleaseSurfaceControl(mDividerSurface);
+ }
+
+ private void updateSurface() {
+ final Rect taskBounds = mProperties.mConfiguration.windowConfiguration.getBounds();
+ // TODO handle synchronization between surface transactions and WCT.
+ final SurfaceControl.Transaction t = new SurfaceControl.Transaction();
+ if (mProperties.mIsVerticalSplit) {
+ t.setPosition(mDividerSurface, mProperties.mInitialDividerPosition, 0.0f);
+ t.setWindowCrop(mDividerSurface, mDividerWidthPx, taskBounds.height());
+ } else {
+ t.setPosition(mDividerSurface, 0.0f, mProperties.mInitialDividerPosition);
+ t.setWindowCrop(mDividerSurface, taskBounds.width(), mDividerWidthPx);
+ }
+ t.apply();
+ }
+
+ private void updateLayout() {
+ final Rect taskBounds = mProperties.mConfiguration.windowConfiguration.getBounds();
+ final WindowManager.LayoutParams lp = mProperties.mIsVerticalSplit
+ ? new WindowManager.LayoutParams(
+ mDividerWidthPx,
+ taskBounds.height(),
+ TYPE_APPLICATION_PANEL,
+ FLAG_NOT_FOCUSABLE | FLAG_NOT_TOUCH_MODAL | FLAG_SLIPPERY,
+ PixelFormat.TRANSLUCENT)
+ : new WindowManager.LayoutParams(
+ taskBounds.width(),
+ mDividerWidthPx,
+ TYPE_APPLICATION_PANEL,
+ FLAG_NOT_FOCUSABLE | FLAG_NOT_TOUCH_MODAL | FLAG_SLIPPERY,
+ PixelFormat.TRANSLUCENT);
+ lp.setTitle(WINDOW_NAME);
+ mViewHost.setView(mDividerLayout, lp);
+ }
+
+ private void updateDivider() {
+ mDividerLayout.removeAllViews();
+ mDividerLayout.setBackgroundColor(DEFAULT_DIVIDER_COLOR.toArgb());
+ if (mProperties.mDividerAttributes.getDividerType()
+ == DividerAttributes.DIVIDER_TYPE_DRAGGABLE) {
+ drawDragHandle();
+ }
+ mViewHost.getView().invalidate();
+ }
+
+ private void drawDragHandle() {
+ final Context context = mDividerLayout.getContext();
+ final ImageButton button = new ImageButton(context);
+ final FrameLayout.LayoutParams params = mProperties.mIsVerticalSplit
+ ? new FrameLayout.LayoutParams(
+ context.getResources().getDimensionPixelSize(
+ R.dimen.activity_embedding_divider_touch_target_width),
+ context.getResources().getDimensionPixelSize(
+ R.dimen.activity_embedding_divider_touch_target_height))
+ : new FrameLayout.LayoutParams(
+ context.getResources().getDimensionPixelSize(
+ R.dimen.activity_embedding_divider_touch_target_height),
+ context.getResources().getDimensionPixelSize(
+ R.dimen.activity_embedding_divider_touch_target_width));
+ params.gravity = Gravity.CENTER;
+ button.setLayoutParams(params);
+ button.setBackgroundColor(R.color.transparent);
+
+ final Drawable handle = context.getResources().getDrawable(
+ R.drawable.activity_embedding_divider_handle, context.getTheme());
+ if (mProperties.mIsVerticalSplit) {
+ button.setImageDrawable(handle);
+ } else {
+ // Rotate the handle drawable
+ RotateDrawable rotatedHandle = new RotateDrawable();
+ rotatedHandle.setFromDegrees(90f);
+ rotatedHandle.setToDegrees(90f);
+ rotatedHandle.setPivotXRelative(true);
+ rotatedHandle.setPivotYRelative(true);
+ rotatedHandle.setPivotX(0.5f);
+ rotatedHandle.setPivotY(0.5f);
+ rotatedHandle.setLevel(1);
+ rotatedHandle.setDrawable(handle);
+
+ button.setImageDrawable(rotatedHandle);
+ }
+ mDividerLayout.addView(button);
+ }
+
+ @NonNull
+ private SurfaceControl createChildSurface(@NonNull String name, boolean visible) {
+ final Rect bounds = mProperties.mConfiguration.windowConfiguration.getBounds();
+ return new SurfaceControl.Builder()
+ .setParent(mProperties.mDecorSurface)
+ .setName(name)
+ .setHidden(!visible)
+ .setCallsite("DividerManager.createChildSurface")
+ .setBufferSize(bounds.width(), bounds.height())
+ .setColorLayer()
+ .build();
+ }
+ }
}
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java
index 80afb16d..3f4dddf 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java
@@ -168,11 +168,14 @@
* @param fragmentToken token of an existing TaskFragment.
*/
void expandTaskFragment(@NonNull WindowContainerTransaction wct,
- @NonNull IBinder fragmentToken) {
+ @NonNull TaskFragmentContainer container) {
+ final IBinder fragmentToken = container.getTaskFragmentToken();
resizeTaskFragment(wct, fragmentToken, new Rect());
clearAdjacentTaskFragments(wct, fragmentToken);
updateWindowingMode(wct, fragmentToken, WINDOWING_MODE_UNDEFINED);
updateAnimationParams(wct, fragmentToken, TaskFragmentAnimationParams.DEFAULT);
+
+ container.getTaskContainer().updateDivider(wct);
}
/**
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
index 0cc4b1f..1bc8264 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
@@ -844,6 +844,7 @@
// Checks if container should be updated before apply new parentInfo.
final boolean shouldUpdateContainer = taskContainer.shouldUpdateContainer(parentInfo);
taskContainer.updateTaskFragmentParentInfo(parentInfo);
+ taskContainer.updateDivider(wct);
// If the last direct activity of the host task is dismissed and the overlay container is
// the only taskFragment, the overlay container should also be dismissed.
@@ -1224,7 +1225,7 @@
final TaskFragmentContainer container = getContainerWithActivity(activity);
if (shouldContainerBeExpanded(container)) {
// Make sure that the existing container is expanded.
- mPresenter.expandTaskFragment(wct, container.getTaskFragmentToken());
+ mPresenter.expandTaskFragment(wct, container);
} else {
// Put activity into a new expanded container.
final TaskFragmentContainer newContainer = newContainer(activity, getTaskId(activity));
@@ -1928,7 +1929,7 @@
}
if (shouldContainerBeExpanded(container)) {
if (container.getInfo() != null) {
- mPresenter.expandTaskFragment(wct, container.getTaskFragmentToken());
+ mPresenter.expandTaskFragment(wct, container);
}
// If the info is not available yet the task fragment will be expanded when it's ready
return;
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java
index f680694..20bc820 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java
@@ -368,6 +368,7 @@
updateTaskFragmentWindowingModeIfRegistered(wct, secondaryContainer, windowingMode);
updateAnimationParams(wct, primaryContainer.getTaskFragmentToken(), splitAttributes);
updateAnimationParams(wct, secondaryContainer.getTaskFragmentToken(), splitAttributes);
+ taskContainer.updateDivider(wct);
}
private void setAdjacentTaskFragments(@NonNull WindowContainerTransaction wct,
@@ -686,8 +687,8 @@
splitContainer.getPrimaryContainer().getTaskFragmentToken();
final IBinder secondaryToken =
splitContainer.getSecondaryContainer().getTaskFragmentToken();
- expandTaskFragment(wct, primaryToken);
- expandTaskFragment(wct, secondaryToken);
+ expandTaskFragment(wct, splitContainer.getPrimaryContainer());
+ expandTaskFragment(wct, splitContainer.getSecondaryContainer());
// Set the companion TaskFragment when the two containers stacked.
setCompanionTaskFragment(wct, primaryToken, secondaryToken,
splitContainer.getSplitRule(), true /* isStacked */);
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java
index 73109e2..e75a317 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java
@@ -77,6 +77,9 @@
private boolean mHasDirectActivity;
+ @Nullable
+ private TaskFragmentParentInfo mTaskFragmentParentInfo;
+
/**
* TaskFragments that the organizer has requested to be closed. They should be removed when
* the organizer receives
@@ -85,14 +88,17 @@
*/
final Set<IBinder> mFinishedContainer = new ArraySet<>();
+ // TODO(b/293654166): move DividerPresenter to SplitController.
+ @NonNull
+ final DividerPresenter mDividerPresenter;
+
/**
* The {@link TaskContainer} constructor
*
- * @param taskId The ID of the Task, which must match {@link Activity#getTaskId()} with
- * {@code activityInTask}.
+ * @param taskId The ID of the Task, which must match {@link Activity#getTaskId()} with
+ * {@code activityInTask}.
* @param activityInTask The {@link Activity} in the Task with {@code taskId}. It is used to
* initialize the {@link TaskContainer} properties.
- *
*/
TaskContainer(int taskId, @NonNull Activity activityInTask) {
if (taskId == INVALID_TASK_ID) {
@@ -107,6 +113,7 @@
// the host task is visible and has an activity in the task.
mIsVisible = true;
mHasDirectActivity = true;
+ mDividerPresenter = new DividerPresenter();
}
int getTaskId() {
@@ -136,10 +143,12 @@
}
void updateTaskFragmentParentInfo(@NonNull TaskFragmentParentInfo info) {
+ // TODO(b/293654166): cache the TaskFragmentParentInfo and remove these fields.
mConfiguration.setTo(info.getConfiguration());
mDisplayId = info.getDisplayId();
mIsVisible = info.isVisible();
mHasDirectActivity = info.hasDirectActivity();
+ mTaskFragmentParentInfo = info;
}
/**
@@ -161,8 +170,8 @@
* Returns the windowing mode for the TaskFragments below this Task, which should be split with
* other TaskFragments.
*
- * @param taskFragmentBounds Requested bounds for the TaskFragment. It will be empty when
- * the pair of TaskFragments are stacked due to the limited space.
+ * @param taskFragmentBounds Requested bounds for the TaskFragment. It will be empty when
+ * the pair of TaskFragments are stacked due to the limited space.
*/
@WindowingMode
int getWindowingModeForTaskFragment(@Nullable Rect taskFragmentBounds) {
@@ -228,7 +237,7 @@
@Nullable
TaskFragmentContainer getTopNonFinishingTaskFragmentContainer(boolean includePin,
- boolean includeOverlay) {
+ boolean includeOverlay) {
for (int i = mContainers.size() - 1; i >= 0; i--) {
final TaskFragmentContainer container = mContainers.get(i);
if (!includePin && isTaskFragmentContainerPinned(container)) {
@@ -283,7 +292,7 @@
return mContainers.indexOf(child);
}
- /** Whether the Task is in an intermediate state waiting for the server update.*/
+ /** Whether the Task is in an intermediate state waiting for the server update. */
boolean isInIntermediateState() {
for (TaskFragmentContainer container : mContainers) {
if (container.isInIntermediateState()) {
@@ -389,6 +398,26 @@
return mContainers;
}
+ void updateDivider(@NonNull WindowContainerTransaction wct) {
+ if (mTaskFragmentParentInfo != null) {
+ // Update divider only if TaskFragmentParentInfo is available.
+ mDividerPresenter.updateDivider(
+ wct, mTaskFragmentParentInfo, getTopNonFinishingSplitContainer());
+ }
+ }
+
+ @Nullable
+ private SplitContainer getTopNonFinishingSplitContainer() {
+ for (int i = mSplitContainers.size() - 1; i >= 0; i--) {
+ final SplitContainer splitContainer = mSplitContainers.get(i);
+ if (!splitContainer.getPrimaryContainer().isFinished()
+ && !splitContainer.getSecondaryContainer().isFinished()) {
+ return splitContainer;
+ }
+ }
+ return null;
+ }
+
private void onTaskFragmentContainerUpdated() {
// TODO(b/300211704): Find a better mechanism to handle the z-order in case we introduce
// another special container that should also be on top in the future.
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java
index a6bf99d..e20a3e0 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java
@@ -748,6 +748,10 @@
}
}
+ @NonNull Rect getLastRequestedBounds() {
+ return mLastRequestedBounds;
+ }
+
/**
* Checks if last requested windowing mode is equal to the provided value.
* @see WindowContainerTransaction#setWindowingMode
diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/DividerPresenterTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/DividerPresenterTest.java
index 2a277f4..4d1d807 100644
--- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/DividerPresenterTest.java
+++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/DividerPresenterTest.java
@@ -16,22 +16,49 @@
package androidx.window.extensions.embedding;
+import static android.window.TaskFragmentOperation.OP_TYPE_CREATE_OR_MOVE_TASK_FRAGMENT_DECOR_SURFACE;
+import static android.window.TaskFragmentOperation.OP_TYPE_REMOVE_TASK_FRAGMENT_DECOR_SURFACE;
+
import static androidx.window.extensions.embedding.DividerPresenter.getBoundsOffsetForDivider;
+import static androidx.window.extensions.embedding.DividerPresenter.getInitialDividerPosition;
import static androidx.window.extensions.embedding.SplitPresenter.CONTAINER_POSITION_BOTTOM;
import static androidx.window.extensions.embedding.SplitPresenter.CONTAINER_POSITION_LEFT;
import static androidx.window.extensions.embedding.SplitPresenter.CONTAINER_POSITION_RIGHT;
import static androidx.window.extensions.embedding.SplitPresenter.CONTAINER_POSITION_TOP;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNull;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import android.content.res.Configuration;
+import android.graphics.Rect;
+import android.os.Binder;
+import android.os.IBinder;
import android.platform.test.annotations.Presubmit;
+import android.platform.test.flag.junit.SetFlagsRule;
+import android.view.Display;
+import android.view.SurfaceControl;
+import android.window.TaskFragmentOperation;
+import android.window.TaskFragmentParentInfo;
+import android.window.WindowContainerTransaction;
import androidx.annotation.NonNull;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
+import com.android.window.flags.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;
/**
* Test class for {@link DividerPresenter}.
@@ -43,6 +70,167 @@
@SmallTest
@RunWith(AndroidJUnit4.class)
public class DividerPresenterTest {
+ @Rule
+ public final SetFlagsRule mSetFlagRule = new SetFlagsRule();
+
+ @Mock
+ private DividerPresenter.Renderer mRenderer;
+
+ @Mock
+ private WindowContainerTransaction mTransaction;
+
+ @Mock
+ private TaskFragmentParentInfo mParentInfo;
+
+ @Mock
+ private SplitContainer mSplitContainer;
+
+ @Mock
+ private SurfaceControl mSurfaceControl;
+
+ private DividerPresenter mDividerPresenter;
+
+ private final IBinder mPrimaryContainerToken = new Binder();
+
+ private final IBinder mSecondaryContainerToken = new Binder();
+
+ private final IBinder mAnotherContainerToken = new Binder();
+
+ private DividerPresenter.Properties mProperties;
+
+ private static final DividerAttributes DEFAULT_DIVIDER_ATTRIBUTES =
+ new DividerAttributes.Builder(DividerAttributes.DIVIDER_TYPE_DRAGGABLE).build();
+
+ private static final DividerAttributes ANOTHER_DIVIDER_ATTRIBUTES =
+ new DividerAttributes.Builder(DividerAttributes.DIVIDER_TYPE_DRAGGABLE)
+ .setWidthDp(10).build();
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ mSetFlagRule.enableFlags(Flags.FLAG_ACTIVITY_EMBEDDING_INTERACTIVE_DIVIDER_FLAG);
+
+ when(mParentInfo.getDisplayId()).thenReturn(Display.DEFAULT_DISPLAY);
+ when(mParentInfo.getConfiguration()).thenReturn(new Configuration());
+ when(mParentInfo.getDecorSurface()).thenReturn(mSurfaceControl);
+
+ when(mSplitContainer.getCurrentSplitAttributes()).thenReturn(
+ new SplitAttributes.Builder()
+ .setDividerAttributes(DEFAULT_DIVIDER_ATTRIBUTES)
+ .build());
+ final TaskFragmentContainer mockPrimaryContainer =
+ createMockTaskFragmentContainer(
+ mPrimaryContainerToken, new Rect(0, 0, 950, 1000));
+ final TaskFragmentContainer mockSecondaryContainer =
+ createMockTaskFragmentContainer(
+ mSecondaryContainerToken, new Rect(1000, 0, 2000, 1000));
+ when(mSplitContainer.getPrimaryContainer()).thenReturn(mockPrimaryContainer);
+ when(mSplitContainer.getSecondaryContainer()).thenReturn(mockSecondaryContainer);
+
+ mProperties = new DividerPresenter.Properties(
+ new Configuration(),
+ DEFAULT_DIVIDER_ATTRIBUTES,
+ mSurfaceControl,
+ getInitialDividerPosition(mSplitContainer),
+ true /* isVerticalSplit */,
+ Display.DEFAULT_DISPLAY);
+
+ mDividerPresenter = new DividerPresenter();
+ mDividerPresenter.mProperties = mProperties;
+ mDividerPresenter.mRenderer = mRenderer;
+ mDividerPresenter.mDecorSurfaceOwner = mPrimaryContainerToken;
+ }
+
+ @Test
+ public void testUpdateDivider() {
+ when(mSplitContainer.getCurrentSplitAttributes()).thenReturn(
+ new SplitAttributes.Builder()
+ .setDividerAttributes(ANOTHER_DIVIDER_ATTRIBUTES)
+ .build());
+ mDividerPresenter.updateDivider(
+ mTransaction,
+ mParentInfo,
+ mSplitContainer);
+
+ assertNotEquals(mProperties, mDividerPresenter.mProperties);
+ verify(mRenderer).update();
+ verify(mTransaction, never()).addTaskFragmentOperation(any(), any());
+ }
+
+ @Test
+ public void testUpdateDivider_updateDecorSurfaceOwnerIfPrimaryContainerChanged() {
+ final TaskFragmentContainer mockPrimaryContainer =
+ createMockTaskFragmentContainer(
+ mAnotherContainerToken, new Rect(0, 0, 750, 1000));
+ final TaskFragmentContainer mockSecondaryContainer =
+ createMockTaskFragmentContainer(
+ mSecondaryContainerToken, new Rect(800, 0, 2000, 1000));
+ when(mSplitContainer.getPrimaryContainer()).thenReturn(mockPrimaryContainer);
+ when(mSplitContainer.getSecondaryContainer()).thenReturn(mockSecondaryContainer);
+ mDividerPresenter.updateDivider(
+ mTransaction,
+ mParentInfo,
+ mSplitContainer);
+
+ assertNotEquals(mProperties, mDividerPresenter.mProperties);
+ verify(mRenderer).update();
+ final TaskFragmentOperation operation = new TaskFragmentOperation.Builder(
+ OP_TYPE_CREATE_OR_MOVE_TASK_FRAGMENT_DECOR_SURFACE)
+ .build();
+ assertEquals(mAnotherContainerToken, mDividerPresenter.mDecorSurfaceOwner);
+ verify(mTransaction).addTaskFragmentOperation(mAnotherContainerToken, operation);
+ }
+
+ @Test
+ public void testUpdateDivider_noChangeIfPropertiesIdentical() {
+ mDividerPresenter.updateDivider(
+ mTransaction,
+ mParentInfo,
+ mSplitContainer);
+
+ assertEquals(mProperties, mDividerPresenter.mProperties);
+ verify(mRenderer, never()).update();
+ verify(mTransaction, never()).addTaskFragmentOperation(any(), any());
+ }
+
+ @Test
+ public void testUpdateDivider_dividerRemovedWhenSplitContainerIsNull() {
+ mDividerPresenter.updateDivider(
+ mTransaction,
+ mParentInfo,
+ null /* splitContainer */);
+ final TaskFragmentOperation taskFragmentOperation = new TaskFragmentOperation.Builder(
+ OP_TYPE_REMOVE_TASK_FRAGMENT_DECOR_SURFACE)
+ .build();
+
+ verify(mTransaction).addTaskFragmentOperation(
+ mPrimaryContainerToken, taskFragmentOperation);
+ verify(mRenderer).release();
+ assertNull(mDividerPresenter.mRenderer);
+ assertNull(mDividerPresenter.mProperties);
+ assertNull(mDividerPresenter.mDecorSurfaceOwner);
+ }
+
+ @Test
+ public void testUpdateDivider_dividerRemovedWhenDividerAttributesIsNull() {
+ when(mSplitContainer.getCurrentSplitAttributes()).thenReturn(
+ new SplitAttributes.Builder().setDividerAttributes(null).build());
+ mDividerPresenter.updateDivider(
+ mTransaction,
+ mParentInfo,
+ mSplitContainer);
+ final TaskFragmentOperation taskFragmentOperation = new TaskFragmentOperation.Builder(
+ OP_TYPE_REMOVE_TASK_FRAGMENT_DECOR_SURFACE)
+ .build();
+
+ verify(mTransaction).addTaskFragmentOperation(
+ mPrimaryContainerToken, taskFragmentOperation);
+ verify(mRenderer).release();
+ assertNull(mDividerPresenter.mRenderer);
+ assertNull(mDividerPresenter.mProperties);
+ assertNull(mDividerPresenter.mDecorSurfaceOwner);
+ }
+
@Test
public void testSanitizeDividerAttributes_setDefaultValues() {
DividerAttributes attributes =
@@ -61,7 +249,7 @@
public void testSanitizeDividerAttributes_notChangingValidValues() {
DividerAttributes attributes =
new DividerAttributes.Builder(DividerAttributes.DIVIDER_TYPE_DRAGGABLE)
- .setWidthDp(10)
+ .setWidthDp(24)
.setPrimaryMinRatio(0.3f)
.setPrimaryMaxRatio(0.7f)
.build();
@@ -123,6 +311,14 @@
dividerWidthPx, splitType, expectedTopLeftOffset, expectedBottomRightOffset);
}
+ private TaskFragmentContainer createMockTaskFragmentContainer(
+ @NonNull IBinder token, @NonNull Rect bounds) {
+ final TaskFragmentContainer container = mock(TaskFragmentContainer.class);
+ when(container.getTaskFragmentToken()).thenReturn(token);
+ when(container.getLastRequestedBounds()).thenReturn(bounds);
+ return container;
+ }
+
private void assertDividerOffsetEquals(
int dividerWidthPx,
@NonNull SplitAttributes.SplitType splitType,
diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizerTest.java
index dd087e8..6f37e9c 100644
--- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizerTest.java
+++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizerTest.java
@@ -107,7 +107,7 @@
mOrganizer.mFragmentInfos.put(container.getTaskFragmentToken(), info);
container.setInfo(mTransaction, info);
- mOrganizer.expandTaskFragment(mTransaction, container.getTaskFragmentToken());
+ mOrganizer.expandTaskFragment(mTransaction, container);
verify(mTransaction).setWindowingMode(container.getInfo().getToken(),
WINDOWING_MODE_UNDEFINED);
diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java
index cdb37ac..c246a19 100644
--- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java
+++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java
@@ -642,7 +642,7 @@
false /* isOnReparent */);
assertTrue(result);
- verify(mSplitPresenter).expandTaskFragment(mTransaction, container.getTaskFragmentToken());
+ verify(mSplitPresenter).expandTaskFragment(mTransaction, container);
}
@Test
diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitPresenterTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitPresenterTest.java
index 941b4e1..62d8aa3 100644
--- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitPresenterTest.java
+++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitPresenterTest.java
@@ -665,8 +665,8 @@
assertEquals(RESULT_EXPANDED, mPresenter.expandSplitContainerIfNeeded(mTransaction,
splitContainer, mActivity, secondaryActivity, null /* secondaryIntent */));
- verify(mPresenter).expandTaskFragment(mTransaction, primaryTf.getTaskFragmentToken());
- verify(mPresenter).expandTaskFragment(mTransaction, secondaryTf.getTaskFragmentToken());
+ verify(mPresenter).expandTaskFragment(mTransaction, primaryTf);
+ verify(mPresenter).expandTaskFragment(mTransaction, secondaryTf);
splitContainer.updateCurrentSplitAttributes(SPLIT_ATTRIBUTES);
clearInvocations(mPresenter);
@@ -675,8 +675,8 @@
splitContainer, mActivity, null /* secondaryActivity */,
new Intent(ApplicationProvider.getApplicationContext(),
MinimumDimensionActivity.class)));
- verify(mPresenter).expandTaskFragment(mTransaction, primaryTf.getTaskFragmentToken());
- verify(mPresenter).expandTaskFragment(mTransaction, secondaryTf.getTaskFragmentToken());
+ verify(mPresenter).expandTaskFragment(mTransaction, primaryTf);
+ verify(mPresenter).expandTaskFragment(mTransaction, secondaryTf);
}
@Test
diff --git a/libs/WindowManager/Shell/multivalentTests/Android.bp b/libs/WindowManager/Shell/multivalentTests/Android.bp
index 1686d0d..1ad19c9 100644
--- a/libs/WindowManager/Shell/multivalentTests/Android.bp
+++ b/libs/WindowManager/Shell/multivalentTests/Android.bp
@@ -46,6 +46,7 @@
exclude_srcs: ["src/com/android/wm/shell/bubbles/BubbleStackViewTest.kt"],
static_libs: [
"junit",
+ "androidx.core_core-animation-testing",
"androidx.test.runner",
"androidx.test.rules",
"androidx.test.ext.junit",
@@ -64,6 +65,7 @@
static_libs: [
"WindowManager-Shell",
"junit",
+ "androidx.core_core-animation-testing",
"androidx.test.runner",
"androidx.test.rules",
"androidx.test.ext.junit",
diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubblePositionerTest.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubblePositionerTest.kt
index e422198..e73d880 100644
--- a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubblePositionerTest.kt
+++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubblePositionerTest.kt
@@ -26,6 +26,7 @@
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
+import com.android.internal.protolog.common.ProtoLog
import com.android.wm.shell.R
import com.android.wm.shell.bubbles.BubblePositioner.MAX_HEIGHT
import com.android.wm.shell.common.bubbles.BubbleBarLocation
@@ -54,6 +55,7 @@
@Before
fun setUp() {
+ ProtoLog.REQUIRE_PROTOLOGTOOL = false
val windowManager = context.getSystemService(WindowManager::class.java)
positioner = BubblePositioner(context, windowManager)
}
@@ -167,8 +169,9 @@
@Test
fun testGetRestingPosition_afterBoundsChange() {
- positioner.update(defaultDeviceConfig.copy(isLargeScreen = true,
- windowBounds = Rect(0, 0, 2000, 1600)))
+ positioner.update(
+ defaultDeviceConfig.copy(isLargeScreen = true, windowBounds = Rect(0, 0, 2000, 1600))
+ )
// Set the resting position to the right side
var allowableStackRegion = positioner.getAllowableStackPositionRegion(1 /* bubbleCount */)
@@ -176,8 +179,9 @@
positioner.restingPosition = restingPosition
// Now make the device smaller
- positioner.update(defaultDeviceConfig.copy(isLargeScreen = false,
- windowBounds = Rect(0, 0, 1000, 1600)))
+ positioner.update(
+ defaultDeviceConfig.copy(isLargeScreen = false, windowBounds = Rect(0, 0, 1000, 1600))
+ )
// Check the resting position is on the correct side
allowableStackRegion = positioner.getAllowableStackPositionRegion(1 /* bubbleCount */)
@@ -236,7 +240,8 @@
0 /* taskId */,
null /* locus */,
true /* isDismissable */,
- directExecutor()) {}
+ directExecutor()
+ ) {}
// Ensure the height is the same as the desired value
assertThat(positioner.getExpandedViewHeight(bubble))
@@ -263,7 +268,8 @@
0 /* taskId */,
null /* locus */,
true /* isDismissable */,
- directExecutor()) {}
+ directExecutor()
+ ) {}
// Ensure the height is the same as the desired value
val minHeight =
@@ -471,20 +477,20 @@
fun testGetTaskViewContentWidth_onLeft() {
positioner.update(defaultDeviceConfig.copy(insets = Insets.of(100, 0, 200, 0)))
val taskViewWidth = positioner.getTaskViewContentWidth(true /* onLeft */)
- val paddings = positioner.getExpandedViewContainerPadding(true /* onLeft */,
- false /* isOverflow */)
- assertThat(taskViewWidth).isEqualTo(
- positioner.screenRect.width() - paddings[0] - paddings[2])
+ val paddings =
+ positioner.getExpandedViewContainerPadding(true /* onLeft */, false /* isOverflow */)
+ assertThat(taskViewWidth)
+ .isEqualTo(positioner.screenRect.width() - paddings[0] - paddings[2])
}
@Test
fun testGetTaskViewContentWidth_onRight() {
positioner.update(defaultDeviceConfig.copy(insets = Insets.of(100, 0, 200, 0)))
val taskViewWidth = positioner.getTaskViewContentWidth(false /* onLeft */)
- val paddings = positioner.getExpandedViewContainerPadding(false /* onLeft */,
- false /* isOverflow */)
- assertThat(taskViewWidth).isEqualTo(
- positioner.screenRect.width() - paddings[0] - paddings[2])
+ val paddings =
+ positioner.getExpandedViewContainerPadding(false /* onLeft */, false /* isOverflow */)
+ assertThat(taskViewWidth)
+ .isEqualTo(positioner.screenRect.width() - paddings[0] - paddings[2])
}
@Test
@@ -513,6 +519,66 @@
assertThat(positioner.isBubbleBarOnLeft).isFalse()
}
+ @Test
+ fun testGetBubbleBarExpandedViewBounds_onLeft() {
+ testGetBubbleBarExpandedViewBounds(onLeft = true, isOverflow = false)
+ }
+
+ @Test
+ fun testGetBubbleBarExpandedViewBounds_onRight() {
+ testGetBubbleBarExpandedViewBounds(onLeft = false, isOverflow = false)
+ }
+
+ @Test
+ fun testGetBubbleBarExpandedViewBounds_isOverflow_onLeft() {
+ testGetBubbleBarExpandedViewBounds(onLeft = true, isOverflow = true)
+ }
+
+ @Test
+ fun testGetBubbleBarExpandedViewBounds_isOverflow_onRight() {
+ testGetBubbleBarExpandedViewBounds(onLeft = false, isOverflow = true)
+ }
+
+ private fun testGetBubbleBarExpandedViewBounds(onLeft: Boolean, isOverflow: Boolean) {
+ positioner.setShowingInBubbleBar(true)
+ val deviceConfig =
+ defaultDeviceConfig.copy(
+ isLargeScreen = true,
+ isLandscape = true,
+ insets = Insets.of(10, 20, 5, 15),
+ windowBounds = Rect(0, 0, 2000, 2600)
+ )
+ positioner.update(deviceConfig)
+
+ positioner.bubbleBarBounds = getBubbleBarBounds(onLeft, deviceConfig)
+
+ val expandedViewPadding =
+ context.resources.getDimensionPixelSize(R.dimen.bubble_expanded_view_padding)
+
+ val left: Int
+ val right: Int
+ if (onLeft) {
+ // Pin to the left, calculate right
+ left = deviceConfig.insets.left + expandedViewPadding
+ right = left + positioner.getExpandedViewWidthForBubbleBar(isOverflow)
+ } else {
+ // Pin to the right, calculate left
+ right =
+ deviceConfig.windowBounds.right - deviceConfig.insets.right - expandedViewPadding
+ left = right - positioner.getExpandedViewWidthForBubbleBar(isOverflow)
+ }
+ // Above the bubble bar
+ val bottom = positioner.bubbleBarBounds.top - expandedViewPadding
+ // Calculate right and top based on size
+ val top = bottom - positioner.getExpandedViewHeightForBubbleBar(isOverflow)
+ val expectedBounds = Rect(left, top, right, bottom)
+
+ val bounds = Rect()
+ positioner.getBubbleBarExpandedViewBounds(onLeft, isOverflow, bounds)
+
+ assertThat(bounds).isEqualTo(expectedBounds)
+ }
+
private val defaultYPosition: Float
/**
* Calculates the Y position bubbles should be placed based on the config. Based on the
@@ -544,4 +610,21 @@
positioner.getAllowableStackPositionRegion(1 /* bubbleCount */)
return allowableStackRegion.top + allowableStackRegion.height() * offsetPercent
}
+
+ private fun getBubbleBarBounds(onLeft: Boolean, deviceConfig: DeviceConfig): Rect {
+ val width = 200
+ val height = 100
+ val bottom = deviceConfig.windowBounds.bottom - deviceConfig.insets.bottom
+ val top = bottom - height
+ val left: Int
+ val right: Int
+ if (onLeft) {
+ left = deviceConfig.insets.left
+ right = left + width
+ } else {
+ right = deviceConfig.windowBounds.right - deviceConfig.insets.right
+ left = right - width
+ }
+ return Rect(left, top, right, bottom)
+ }
}
diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarDropTargetControllerTest.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarDropTargetControllerTest.kt
new file mode 100644
index 0000000..2ac7791
--- /dev/null
+++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarDropTargetControllerTest.kt
@@ -0,0 +1,180 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.wm.shell.bubbles.bar
+
+import android.content.Context
+import android.graphics.Insets
+import android.graphics.Rect
+import android.view.View
+import android.view.WindowManager
+import android.widget.FrameLayout
+import androidx.core.animation.AnimatorTestRule
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.internal.protolog.common.ProtoLog
+import com.android.wm.shell.R
+import com.android.wm.shell.bubbles.BubblePositioner
+import com.android.wm.shell.bubbles.DeviceConfig
+import com.android.wm.shell.bubbles.bar.BubbleBarDropTargetController.Companion.DROP_TARGET_ALPHA_IN_DURATION
+import com.android.wm.shell.bubbles.bar.BubbleBarDropTargetController.Companion.DROP_TARGET_ALPHA_OUT_DURATION
+import com.android.wm.shell.bubbles.bar.BubbleBarDropTargetController.Companion.DROP_TARGET_SCALE
+import com.android.wm.shell.common.bubbles.BubbleBarLocation
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.ClassRule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/** Tests for [BubbleBarDropTargetController] */
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class BubbleBarDropTargetControllerTest {
+
+ companion object {
+ @JvmField @ClassRule val animatorTestRule: AnimatorTestRule = AnimatorTestRule()
+ }
+
+ private val context = ApplicationProvider.getApplicationContext<Context>()
+ private lateinit var controller: BubbleBarDropTargetController
+ private lateinit var positioner: BubblePositioner
+ private lateinit var container: FrameLayout
+
+ @Before
+ fun setUp() {
+ ProtoLog.REQUIRE_PROTOLOGTOOL = false
+ container = FrameLayout(context)
+ val windowManager = context.getSystemService(WindowManager::class.java)
+ positioner = BubblePositioner(context, windowManager)
+ positioner.setShowingInBubbleBar(true)
+ val deviceConfig =
+ DeviceConfig(
+ windowBounds = Rect(0, 0, 2000, 2600),
+ isLargeScreen = true,
+ isSmallTablet = false,
+ isLandscape = true,
+ isRtl = false,
+ insets = Insets.of(10, 20, 30, 40)
+ )
+ positioner.update(deviceConfig)
+ positioner.bubbleBarBounds = Rect(1800, 2400, 1970, 2560)
+
+ controller = BubbleBarDropTargetController(context, container, positioner)
+ }
+
+ @Test
+ fun show_moveLeftToRight_isVisibleWithExpectedBounds() {
+ val expectedBoundsOnLeft = getExpectedDropTargetBounds(onLeft = true)
+ val expectedBoundsOnRight = getExpectedDropTargetBounds(onLeft = false)
+
+ runOnMainSync { controller.show(BubbleBarLocation.LEFT) }
+ waitForAnimateIn()
+ val viewOnLeft = getDropTargetView()
+ assertThat(viewOnLeft).isNotNull()
+ assertThat(viewOnLeft!!.alpha).isEqualTo(1f)
+ assertThat(viewOnLeft.layoutParams.width).isEqualTo(expectedBoundsOnLeft.width())
+ assertThat(viewOnLeft.layoutParams.height).isEqualTo(expectedBoundsOnLeft.height())
+ assertThat(viewOnLeft.x).isEqualTo(expectedBoundsOnLeft.left)
+ assertThat(viewOnLeft.y).isEqualTo(expectedBoundsOnLeft.top)
+
+ runOnMainSync { controller.show(BubbleBarLocation.RIGHT) }
+ waitForAnimateOut()
+ waitForAnimateIn()
+ val viewOnRight = getDropTargetView()
+ assertThat(viewOnRight).isNotNull()
+ assertThat(viewOnRight!!.alpha).isEqualTo(1f)
+ assertThat(viewOnRight.layoutParams.width).isEqualTo(expectedBoundsOnRight.width())
+ assertThat(viewOnRight.layoutParams.height).isEqualTo(expectedBoundsOnRight.height())
+ assertThat(viewOnRight.x).isEqualTo(expectedBoundsOnRight.left)
+ assertThat(viewOnRight.y).isEqualTo(expectedBoundsOnRight.top)
+ }
+
+ @Test
+ fun toggleSetHidden_dropTargetShown_updatesAlpha() {
+ runOnMainSync { controller.show(BubbleBarLocation.RIGHT) }
+ waitForAnimateIn()
+ val view = getDropTargetView()
+ assertThat(view).isNotNull()
+ assertThat(view!!.alpha).isEqualTo(1f)
+
+ runOnMainSync { controller.setHidden(true) }
+ waitForAnimateOut()
+ val hiddenView = getDropTargetView()
+ assertThat(hiddenView).isNotNull()
+ assertThat(hiddenView!!.alpha).isEqualTo(0f)
+
+ runOnMainSync { controller.setHidden(false) }
+ waitForAnimateIn()
+ val shownView = getDropTargetView()
+ assertThat(shownView).isNotNull()
+ assertThat(shownView!!.alpha).isEqualTo(1f)
+ }
+
+ @Test
+ fun toggleSetHidden_dropTargetNotShown_viewNotCreated() {
+ runOnMainSync { controller.setHidden(true) }
+ waitForAnimateOut()
+ assertThat(getDropTargetView()).isNull()
+ runOnMainSync { controller.setHidden(false) }
+ waitForAnimateIn()
+ assertThat(getDropTargetView()).isNull()
+ }
+
+ @Test
+ fun dismiss_dropTargetShown_viewRemoved() {
+ runOnMainSync { controller.show(BubbleBarLocation.LEFT) }
+ waitForAnimateIn()
+ assertThat(getDropTargetView()).isNotNull()
+ runOnMainSync { controller.dismiss() }
+ waitForAnimateOut()
+ assertThat(getDropTargetView()).isNull()
+ }
+
+ @Test
+ fun dismiss_dropTargetNotShown_doesNothing() {
+ runOnMainSync { controller.dismiss() }
+ waitForAnimateOut()
+ assertThat(getDropTargetView()).isNull()
+ }
+
+ private fun getDropTargetView(): View? = container.findViewById(R.id.bubble_bar_drop_target)
+
+ private fun getExpectedDropTargetBounds(onLeft: Boolean): Rect {
+ val rect = Rect()
+ positioner.getBubbleBarExpandedViewBounds(onLeft, false /* isOveflowExpanded */, rect)
+ // Scale the rect to expected size, but keep the center point the same
+ val centerX = rect.centerX()
+ val centerY = rect.centerY()
+ rect.scale(DROP_TARGET_SCALE)
+ rect.offset(centerX - rect.centerX(), centerY - rect.centerY())
+ return rect
+ }
+
+ private fun runOnMainSync(runnable: Runnable) {
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(runnable)
+ }
+
+ private fun waitForAnimateIn() {
+ // Advance animator for on-device test
+ runOnMainSync { animatorTestRule.advanceTimeBy(DROP_TARGET_ALPHA_IN_DURATION) }
+ }
+
+ private fun waitForAnimateOut() {
+ // Advance animator for on-device test
+ runOnMainSync { animatorTestRule.advanceTimeBy(DROP_TARGET_ALPHA_OUT_DURATION) }
+ }
+}
diff --git a/libs/WindowManager/Shell/res/color/bubble_drop_target_background_color.xml b/libs/WindowManager/Shell/res/color/bubble_drop_target_background_color.xml
new file mode 100644
index 0000000..ab1ab98
--- /dev/null
+++ b/libs/WindowManager/Shell/res/color/bubble_drop_target_background_color.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ ~ Copyright (C) 2024 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android">
+ <item android:alpha="0.35" android:color="?androidprv:attr/materialColorPrimaryContainer" />
+</selector>
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/res/drawable/bubble_drop_target_background.xml b/libs/WindowManager/Shell/res/drawable/bubble_drop_target_background.xml
index 468b5c2..9dcde3b 100644
--- a/libs/WindowManager/Shell/res/drawable/bubble_drop_target_background.xml
+++ b/libs/WindowManager/Shell/res/drawable/bubble_drop_target_background.xml
@@ -1,5 +1,4 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
+<?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");
@@ -14,9 +13,12 @@
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
-<shape android:shape="rectangle"
- xmlns:android="http://schemas.android.com/apk/res/android">
- <solid android:color="#bf309fb5" />
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+ android:shape="rectangle">
<corners android:radius="@dimen/bubble_bar_expanded_view_corner_radius" />
- <stroke android:width="1dp" android:color="#A00080FF"/>
+ <solid android:color="@color/bubble_drop_target_background_color" />
+ <stroke
+ android:width="1dp"
+ android:color="?androidprv:attr/materialColorPrimaryContainer" />
</shape>
diff --git a/libs/WindowManager/Shell/res/values/dimen.xml b/libs/WindowManager/Shell/res/values/dimen.xml
index 00fb298..c032a81 100644
--- a/libs/WindowManager/Shell/res/values/dimen.xml
+++ b/libs/WindowManager/Shell/res/values/dimen.xml
@@ -213,7 +213,7 @@
<dimen name="bubble_swap_animation_offset">15dp</dimen>
<!-- How far offscreen the bubble stack rests. There's some padding around the bubble so
add 3dp to the desired overhang. -->
- <dimen name="bubble_stack_offscreen">3dp</dimen>
+ <dimen name="bubble_stack_offscreen">2.5dp</dimen>
<!-- How far down the screen the stack starts. -->
<dimen name="bubble_stack_starting_offset_y">120dp</dimen>
<!-- Space between the pointer triangle and the bubble expanded view -->
@@ -535,5 +535,7 @@
<!-- The vertical margin that needs to be preserved between the scaled window bounds and the
original window bounds (once the surface is scaled enough to do so) -->
<dimen name="cross_task_back_vertical_margin">8dp</dimen>
+ <!-- The offset from the left edge of the entering page for the cross-activity animation -->
+ <dimen name="cross_activity_back_entering_start_offset">96dp</dimen>
</resources>
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityBackAnimation.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityBackAnimation.java
deleted file mode 100644
index d6f7c36..0000000
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityBackAnimation.java
+++ /dev/null
@@ -1,455 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.wm.shell.back;
-
-import static android.view.RemoteAnimationTarget.MODE_CLOSING;
-import static android.view.RemoteAnimationTarget.MODE_OPENING;
-import static android.window.BackEvent.EDGE_RIGHT;
-
-import static com.android.internal.jank.InteractionJankMonitor.CUJ_PREDICTIVE_BACK_CROSS_ACTIVITY;
-import static com.android.wm.shell.back.BackAnimationConstants.UPDATE_SYSUI_FLAGS_THRESHOLD;
-import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_BACK_PREVIEW;
-
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.animation.ValueAnimator;
-import android.annotation.NonNull;
-import android.content.Context;
-import android.graphics.Matrix;
-import android.graphics.PointF;
-import android.graphics.Rect;
-import android.graphics.RectF;
-import android.os.RemoteException;
-import android.util.FloatProperty;
-import android.util.TypedValue;
-import android.view.IRemoteAnimationFinishedCallback;
-import android.view.IRemoteAnimationRunner;
-import android.view.RemoteAnimationTarget;
-import android.view.SurfaceControl;
-import android.view.animation.Interpolator;
-import android.window.BackEvent;
-import android.window.BackMotionEvent;
-import android.window.BackProgressAnimator;
-import android.window.IOnBackInvokedCallback;
-
-import com.android.internal.dynamicanimation.animation.SpringAnimation;
-import com.android.internal.dynamicanimation.animation.SpringForce;
-import com.android.internal.policy.ScreenDecorationsUtils;
-import com.android.internal.protolog.common.ProtoLog;
-import com.android.wm.shell.animation.Interpolators;
-import com.android.wm.shell.common.annotations.ShellMainThread;
-
-import javax.inject.Inject;
-
-/** Class that defines cross-activity animation. */
-@ShellMainThread
-public class CrossActivityBackAnimation extends ShellBackAnimation {
- /**
- * Minimum scale of the entering/closing window.
- */
- private static final float MIN_WINDOW_SCALE = 0.9f;
-
- /** Duration of post animation after gesture committed. */
- private static final int POST_ANIMATION_DURATION = 350;
- private static final Interpolator INTERPOLATOR = Interpolators.STANDARD_DECELERATE;
- private static final FloatProperty<CrossActivityBackAnimation> ENTER_PROGRESS_PROP =
- new FloatProperty<>("enter-alpha") {
- @Override
- public void setValue(CrossActivityBackAnimation anim, float value) {
- anim.setEnteringProgress(value);
- }
-
- @Override
- public Float get(CrossActivityBackAnimation object) {
- return object.getEnteringProgress();
- }
- };
- private static final FloatProperty<CrossActivityBackAnimation> LEAVE_PROGRESS_PROP =
- new FloatProperty<>("leave-alpha") {
- @Override
- public void setValue(CrossActivityBackAnimation anim, float value) {
- anim.setLeavingProgress(value);
- }
-
- @Override
- public Float get(CrossActivityBackAnimation object) {
- return object.getLeavingProgress();
- }
- };
- private static final float MIN_WINDOW_ALPHA = 0.01f;
- private static final float WINDOW_X_SHIFT_DP = 48;
- private static final int SCALE_FACTOR = 100;
- // TODO(b/264710590): Use the progress commit threshold from ViewConfiguration once it exists.
- private static final float TARGET_COMMIT_PROGRESS = 0.5f;
- private static final float ENTER_ALPHA_THRESHOLD = 0.22f;
-
- private final Rect mStartTaskRect = new Rect();
- private final float mCornerRadius;
-
- // The closing window properties.
- private final RectF mClosingRect = new RectF();
-
- // The entering window properties.
- private final Rect mEnteringStartRect = new Rect();
- private final RectF mEnteringRect = new RectF();
- private final SpringAnimation mEnteringProgressSpring;
- private final SpringAnimation mLeavingProgressSpring;
- // Max window x-shift in pixels.
- private final float mWindowXShift;
- private final BackAnimationRunner mBackAnimationRunner;
-
- private float mEnteringProgress = 0f;
- private float mLeavingProgress = 0f;
-
- private final PointF mInitialTouchPos = new PointF();
-
- private final Matrix mTransformMatrix = new Matrix();
-
- private final float[] mTmpFloat9 = new float[9];
-
- private RemoteAnimationTarget mEnteringTarget;
- private RemoteAnimationTarget mClosingTarget;
- private SurfaceControl.Transaction mTransaction = new SurfaceControl.Transaction();
-
- private boolean mBackInProgress = false;
- private boolean mIsRightEdge;
- private boolean mTriggerBack = false;
-
- private PointF mTouchPos = new PointF();
- private IRemoteAnimationFinishedCallback mFinishCallback;
-
- private final BackProgressAnimator mProgressAnimator = new BackProgressAnimator();
-
- private final BackAnimationBackground mBackground;
-
- @Inject
- public CrossActivityBackAnimation(Context context, BackAnimationBackground background) {
- mCornerRadius = ScreenDecorationsUtils.getWindowCornerRadius(context);
- mBackAnimationRunner = new BackAnimationRunner(
- new Callback(), new Runner(), context, CUJ_PREDICTIVE_BACK_CROSS_ACTIVITY);
- mBackground = background;
- mEnteringProgressSpring = new SpringAnimation(this, ENTER_PROGRESS_PROP);
- mEnteringProgressSpring.setSpring(new SpringForce()
- .setStiffness(SpringForce.STIFFNESS_MEDIUM)
- .setDampingRatio(SpringForce.DAMPING_RATIO_NO_BOUNCY));
- mLeavingProgressSpring = new SpringAnimation(this, LEAVE_PROGRESS_PROP);
- mLeavingProgressSpring.setSpring(new SpringForce()
- .setStiffness(SpringForce.STIFFNESS_MEDIUM)
- .setDampingRatio(SpringForce.DAMPING_RATIO_NO_BOUNCY));
- mWindowXShift = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, WINDOW_X_SHIFT_DP,
- context.getResources().getDisplayMetrics());
- }
-
- /**
- * Returns 1 if x >= edge1, 0 if x <= edge0, and a smoothed value between the two.
- * From https://en.wikipedia.org/wiki/Smoothstep
- */
- private static float smoothstep(float edge0, float edge1, float x) {
- if (x < edge0) return 0;
- if (x >= edge1) return 1;
-
- x = (x - edge0) / (edge1 - edge0);
- return x * x * (3 - 2 * x);
- }
-
- /**
- * Linearly map x from range (a1, a2) to range (b1, b2).
- */
- private static float mapLinear(float x, float a1, float a2, float b1, float b2) {
- return b1 + (x - a1) * (b2 - b1) / (a2 - a1);
- }
-
- /**
- * Linearly map a normalized value from (0, 1) to (min, max).
- */
- private static float mapRange(float value, float min, float max) {
- return min + (value * (max - min));
- }
-
- private void startBackAnimation() {
- if (mEnteringTarget == null || mClosingTarget == null) {
- ProtoLog.d(WM_SHELL_BACK_PREVIEW, "Entering target or closing target is null.");
- return;
- }
- mTransaction.setAnimationTransaction();
-
- // Offset start rectangle to align task bounds.
- mStartTaskRect.set(mClosingTarget.windowConfiguration.getBounds());
- mStartTaskRect.offsetTo(0, 0);
-
- // Draw background with task background color.
- mBackground.ensureBackground(mClosingTarget.windowConfiguration.getBounds(),
- mEnteringTarget.taskInfo.taskDescription.getBackgroundColor(), mTransaction);
- setEnteringProgress(0);
- setLeavingProgress(0);
- }
-
- private void applyTransform(SurfaceControl leash, RectF targetRect, float targetAlpha) {
- if (leash == null || !leash.isValid()) {
- return;
- }
-
- final float scale = targetRect.width() / mStartTaskRect.width();
- mTransformMatrix.reset();
- mTransformMatrix.setScale(scale, scale);
- mTransformMatrix.postTranslate(targetRect.left, targetRect.top);
- mTransaction.setAlpha(leash, targetAlpha)
- .setMatrix(leash, mTransformMatrix, mTmpFloat9)
- .setWindowCrop(leash, mStartTaskRect)
- .setCornerRadius(leash, mCornerRadius);
- }
-
- private void finishAnimation() {
- if (mEnteringTarget != null) {
- if (mEnteringTarget.leash != null && mEnteringTarget.leash.isValid()) {
- mTransaction.setCornerRadius(mEnteringTarget.leash, 0);
- mEnteringTarget.leash.release();
- }
- mEnteringTarget = null;
- }
- if (mClosingTarget != null) {
- if (mClosingTarget.leash != null) {
- mClosingTarget.leash.release();
- }
- mClosingTarget = null;
- }
- if (mBackground != null) {
- mBackground.removeBackground(mTransaction);
- }
-
- mTransaction.apply();
- mBackInProgress = false;
- mTransformMatrix.reset();
- mInitialTouchPos.set(0, 0);
-
- if (mFinishCallback != null) {
- try {
- mFinishCallback.onAnimationFinished();
- } catch (RemoteException e) {
- e.printStackTrace();
- }
- mFinishCallback = null;
- }
- mEnteringProgressSpring.animateToFinalPosition(0);
- mEnteringProgressSpring.skipToEnd();
- mLeavingProgressSpring.animateToFinalPosition(0);
- mLeavingProgressSpring.skipToEnd();
- }
-
- private void onGestureProgress(@NonNull BackEvent backEvent) {
- if (!mBackInProgress) {
- mIsRightEdge = backEvent.getSwipeEdge() == EDGE_RIGHT;
- mInitialTouchPos.set(backEvent.getTouchX(), backEvent.getTouchY());
- mBackInProgress = true;
- }
- mTouchPos.set(backEvent.getTouchX(), backEvent.getTouchY());
-
- float progress = backEvent.getProgress();
- float springProgress = (mTriggerBack
- ? mapLinear(progress, 0f, 1, TARGET_COMMIT_PROGRESS, 1)
- : mapLinear(progress, 0, 1f, 0, TARGET_COMMIT_PROGRESS)) * SCALE_FACTOR;
- mLeavingProgressSpring.animateToFinalPosition(springProgress);
- mEnteringProgressSpring.animateToFinalPosition(springProgress);
- mBackground.onBackProgressed(progress);
- }
-
- private void onGestureCommitted() {
- if (mEnteringTarget == null || mClosingTarget == null || mClosingTarget.leash == null
- || mEnteringTarget.leash == null || !mEnteringTarget.leash.isValid()
- || !mClosingTarget.leash.isValid()) {
- finishAnimation();
- return;
- }
- // End the fade animations
- mLeavingProgressSpring.cancel();
- mEnteringProgressSpring.cancel();
-
- // We enter phase 2 of the animation, the starting coordinates for phase 2 are the current
- // coordinate of the gesture driven phase.
- mEnteringRect.round(mEnteringStartRect);
- mTransaction.hide(mClosingTarget.leash);
-
- ValueAnimator valueAnimator =
- ValueAnimator.ofFloat(1f, 0f).setDuration(POST_ANIMATION_DURATION);
- valueAnimator.setInterpolator(INTERPOLATOR);
- valueAnimator.addUpdateListener(animation -> {
- float progress = animation.getAnimatedFraction();
- updatePostCommitEnteringAnimation(progress);
- if (progress > 1 - UPDATE_SYSUI_FLAGS_THRESHOLD) {
- mBackground.resetStatusBarCustomization();
- }
- mTransaction.apply();
- });
-
- valueAnimator.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(Animator animation) {
- mBackground.resetStatusBarCustomization();
- finishAnimation();
- }
- });
- valueAnimator.start();
- }
-
- private void updatePostCommitEnteringAnimation(float progress) {
- float left = mapRange(progress, mEnteringStartRect.left, mStartTaskRect.left);
- float top = mapRange(progress, mEnteringStartRect.top, mStartTaskRect.top);
- float width = mapRange(progress, mEnteringStartRect.width(), mStartTaskRect.width());
- float height = mapRange(progress, mEnteringStartRect.height(), mStartTaskRect.height());
- float alpha = mapRange(progress, getPreCommitEnteringAlpha(), 1.0f);
- mEnteringRect.set(left, top, left + width, top + height);
- applyTransform(mEnteringTarget.leash, mEnteringRect, alpha);
- }
-
- private float getPreCommitEnteringAlpha() {
- return Math.max(smoothstep(ENTER_ALPHA_THRESHOLD, 0.7f, mEnteringProgress),
- MIN_WINDOW_ALPHA);
- }
-
- private float getEnteringProgress() {
- return mEnteringProgress * SCALE_FACTOR;
- }
-
- private void setEnteringProgress(float value) {
- mEnteringProgress = value / SCALE_FACTOR;
- if (mEnteringTarget != null && mEnteringTarget.leash != null) {
- transformWithProgress(
- mEnteringProgress,
- getPreCommitEnteringAlpha(),
- mEnteringTarget.leash,
- mEnteringRect,
- -mWindowXShift,
- 0
- );
- }
- }
-
- private float getPreCommitLeavingAlpha() {
- return Math.max(1 - smoothstep(0, ENTER_ALPHA_THRESHOLD, mLeavingProgress),
- MIN_WINDOW_ALPHA);
- }
-
- private float getLeavingProgress() {
- return mLeavingProgress * SCALE_FACTOR;
- }
-
- private void setLeavingProgress(float value) {
- mLeavingProgress = value / SCALE_FACTOR;
- if (mClosingTarget != null && mClosingTarget.leash != null) {
- transformWithProgress(
- mLeavingProgress,
- getPreCommitLeavingAlpha(),
- mClosingTarget.leash,
- mClosingRect,
- 0,
- mIsRightEdge ? 0 : mWindowXShift
- );
- }
- }
-
- private void transformWithProgress(float progress, float alpha, SurfaceControl surface,
- RectF targetRect, float deltaXMin, float deltaXMax) {
-
- final int width = mStartTaskRect.width();
- final int height = mStartTaskRect.height();
-
- final float interpolatedProgress = INTERPOLATOR.getInterpolation(progress);
- final float closingScale = MIN_WINDOW_SCALE
- + (1 - interpolatedProgress) * (1 - MIN_WINDOW_SCALE);
- final float closingWidth = closingScale * width;
- final float closingHeight = (float) height / width * closingWidth;
-
- // Move the window along the X axis.
- float closingLeft = mStartTaskRect.left + (width - closingWidth) / 2;
- closingLeft += mapRange(interpolatedProgress, deltaXMin, deltaXMax);
-
- // Move the window along the Y axis.
- final float closingTop = (height - closingHeight) * 0.5f;
- targetRect.set(
- closingLeft, closingTop, closingLeft + closingWidth, closingTop + closingHeight);
-
- applyTransform(surface, targetRect, Math.max(alpha, MIN_WINDOW_ALPHA));
- mTransaction.apply();
- }
-
- @Override
- public BackAnimationRunner getRunner() {
- return mBackAnimationRunner;
- }
-
- private final class Callback extends IOnBackInvokedCallback.Default {
- @Override
- public void onBackStarted(BackMotionEvent backEvent) {
- mTriggerBack = backEvent.getTriggerBack();
- mProgressAnimator.onBackStarted(backEvent,
- CrossActivityBackAnimation.this::onGestureProgress);
- }
-
- @Override
- public void onBackProgressed(@NonNull BackMotionEvent backEvent) {
- mTriggerBack = backEvent.getTriggerBack();
- mProgressAnimator.onBackProgressed(backEvent);
- }
-
- @Override
- public void onBackCancelled() {
- mProgressAnimator.onBackCancelled(() -> {
- // mProgressAnimator can reach finish stage earlier than mLeavingProgressSpring,
- // and if we release all animation leash first, the leavingProgressSpring won't
- // able to update the animation anymore, which cause flicker.
- // Here should force update the closing animation target to the final stage before
- // release it.
- setLeavingProgress(0);
- finishAnimation();
- });
- }
-
- @Override
- public void onBackInvoked() {
- mProgressAnimator.reset();
- onGestureCommitted();
- }
- }
-
- private final class Runner extends IRemoteAnimationRunner.Default {
- @Override
- public void onAnimationStart(
- int transit,
- RemoteAnimationTarget[] apps,
- RemoteAnimationTarget[] wallpapers,
- RemoteAnimationTarget[] nonApps,
- IRemoteAnimationFinishedCallback finishedCallback) {
- ProtoLog.d(WM_SHELL_BACK_PREVIEW, "Start back to activity animation.");
- for (RemoteAnimationTarget a : apps) {
- if (a.mode == MODE_CLOSING) {
- mClosingTarget = a;
- }
- if (a.mode == MODE_OPENING) {
- mEnteringTarget = a;
- }
- }
-
- startBackAnimation();
- mFinishCallback = finishedCallback;
- }
-
- @Override
- public void onAnimationCancelled() {
- finishAnimation();
- }
- }
-}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityBackAnimation.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityBackAnimation.kt
new file mode 100644
index 0000000..edf29dd
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityBackAnimation.kt
@@ -0,0 +1,367 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.wm.shell.back
+
+import android.animation.Animator
+import android.animation.AnimatorListenerAdapter
+import android.animation.ValueAnimator
+import android.content.Context
+import android.content.res.Configuration
+import android.graphics.Matrix
+import android.graphics.PointF
+import android.graphics.Rect
+import android.graphics.RectF
+import android.os.RemoteException
+import android.view.Display
+import android.view.IRemoteAnimationFinishedCallback
+import android.view.IRemoteAnimationRunner
+import android.view.RemoteAnimationTarget
+import android.view.SurfaceControl
+import android.view.animation.DecelerateInterpolator
+import android.view.animation.Interpolator
+import android.window.BackEvent
+import android.window.BackMotionEvent
+import android.window.BackProgressAnimator
+import android.window.IOnBackInvokedCallback
+import com.android.internal.jank.Cuj
+import com.android.internal.policy.ScreenDecorationsUtils
+import com.android.internal.protolog.common.ProtoLog
+import com.android.wm.shell.R
+import com.android.wm.shell.RootTaskDisplayAreaOrganizer
+import com.android.wm.shell.animation.Interpolators
+import com.android.wm.shell.common.annotations.ShellMainThread
+import com.android.wm.shell.protolog.ShellProtoLogGroup
+import javax.inject.Inject
+import kotlin.math.abs
+import kotlin.math.max
+import kotlin.math.min
+
+/** Class that defines cross-activity animation. */
+@ShellMainThread
+class CrossActivityBackAnimation @Inject constructor(
+ private val context: Context,
+ private val background: BackAnimationBackground,
+ private val rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer
+) : ShellBackAnimation() {
+
+ private val startClosingRect = RectF()
+ private val targetClosingRect = RectF()
+ private val currentClosingRect = RectF()
+
+ private val startEnteringRect = RectF()
+ private val targetEnteringRect = RectF()
+ private val currentEnteringRect = RectF()
+
+ private val taskBoundsRect = Rect()
+
+ private val cornerRadius = ScreenDecorationsUtils.getWindowCornerRadius(context)
+
+ private val backAnimationRunner = BackAnimationRunner(
+ Callback(), Runner(), context, Cuj.CUJ_PREDICTIVE_BACK_CROSS_ACTIVITY
+ )
+ private val initialTouchPos = PointF()
+ private val transformMatrix = Matrix()
+ private val tmpFloat9 = FloatArray(9)
+ private var enteringTarget: RemoteAnimationTarget? = null
+ private var closingTarget: RemoteAnimationTarget? = null
+ private val transaction = SurfaceControl.Transaction()
+ private var triggerBack = false
+ private var finishCallback: IRemoteAnimationFinishedCallback? = null
+ private val progressAnimator = BackProgressAnimator()
+ private val displayBoundsMargin =
+ context.resources.getDimension(R.dimen.cross_task_back_vertical_margin)
+ private val enteringStartOffset =
+ context.resources.getDimension(R.dimen.cross_activity_back_entering_start_offset)
+
+ private val gestureInterpolator = Interpolators.STANDARD_DECELERATE
+ private val postCommitInterpolator = Interpolators.FAST_OUT_SLOW_IN
+ private val verticalMoveInterpolator: Interpolator = DecelerateInterpolator()
+
+ private var scrimLayer: SurfaceControl? = null
+ private var maxScrimAlpha: Float = 0f
+
+ override fun getRunner() = backAnimationRunner
+
+ private fun startBackAnimation(backMotionEvent: BackMotionEvent) {
+ if (enteringTarget == null || closingTarget == null) {
+ ProtoLog.d(
+ ShellProtoLogGroup.WM_SHELL_BACK_PREVIEW,
+ "Entering target or closing target is null."
+ )
+ return
+ }
+ triggerBack = backMotionEvent.triggerBack
+ initialTouchPos.set(backMotionEvent.touchX, backMotionEvent.touchY)
+
+ transaction.setAnimationTransaction()
+
+ // Offset start rectangle to align task bounds.
+ taskBoundsRect.set(closingTarget!!.windowConfiguration.bounds)
+ taskBoundsRect.offsetTo(0, 0)
+
+ startClosingRect.set(taskBoundsRect)
+
+ // scale closing target into the middle for rhs and to the right for lhs
+ targetClosingRect.set(startClosingRect)
+ targetClosingRect.scaleCentered(MAX_SCALE)
+ if (backMotionEvent.swipeEdge != BackEvent.EDGE_RIGHT) {
+ targetClosingRect.offset(
+ startClosingRect.right - targetClosingRect.right - displayBoundsMargin, 0f
+ )
+ }
+
+ // the entering target starts 96dp to the left of the screen edge...
+ startEnteringRect.set(startClosingRect)
+ startEnteringRect.offset(-enteringStartOffset, 0f)
+
+ // ...and gets scaled in sync with the closing target
+ targetEnteringRect.set(startEnteringRect)
+ targetEnteringRect.scaleCentered(MAX_SCALE)
+
+ // Draw background with task background color.
+ background.ensureBackground(
+ closingTarget!!.windowConfiguration.bounds,
+ enteringTarget!!.taskInfo.taskDescription!!.backgroundColor, transaction
+ )
+ ensureScrimLayer()
+ transaction.apply()
+ }
+
+ private fun onGestureProgress(backEvent: BackEvent) {
+ val progress = gestureInterpolator.getInterpolation(backEvent.progress)
+ background.onBackProgressed(progress)
+ currentClosingRect.setInterpolatedRectF(startClosingRect, targetClosingRect, progress)
+ val yOffset = getYOffset(currentClosingRect, backEvent.touchY)
+ currentClosingRect.offset(0f, yOffset)
+ applyTransform(closingTarget?.leash, currentClosingRect, 1f)
+ currentEnteringRect.setInterpolatedRectF(startEnteringRect, targetEnteringRect, progress)
+ currentEnteringRect.offset(0f, yOffset)
+ applyTransform(enteringTarget?.leash, currentEnteringRect, 1f)
+ transaction.apply()
+ }
+
+ private fun getYOffset(centeredRect: RectF, touchY: Float): Float {
+ val screenHeight = taskBoundsRect.height()
+ // Base the window movement in the Y axis on the touch movement in the Y axis.
+ val rawYDelta = touchY - initialTouchPos.y
+ val yDirection = (if (rawYDelta < 0) -1 else 1)
+ // limit yDelta interpretation to 1/2 of screen height in either direction
+ val deltaYRatio = min(screenHeight / 2f, abs(rawYDelta)) / (screenHeight / 2f)
+ val interpolatedYRatio: Float = verticalMoveInterpolator.getInterpolation(deltaYRatio)
+ // limit y-shift so surface never passes 8dp screen margin
+ val deltaY = yDirection * interpolatedYRatio * max(
+ 0f, (screenHeight - centeredRect.height()) / 2f - displayBoundsMargin
+ )
+ return deltaY
+ }
+
+ private fun onGestureCommitted() {
+ if (closingTarget?.leash == null || enteringTarget?.leash == null ||
+ !enteringTarget!!.leash.isValid || !closingTarget!!.leash.isValid
+ ) {
+ finishAnimation()
+ return
+ }
+
+ // We enter phase 2 of the animation, the starting coordinates for phase 2 are the current
+ // coordinate of the gesture driven phase. Let's update the start and target rects and kick
+ // off the animator
+ startClosingRect.set(currentClosingRect)
+ startEnteringRect.set(currentEnteringRect)
+ targetEnteringRect.set(taskBoundsRect)
+ targetClosingRect.set(taskBoundsRect)
+ targetClosingRect.offset(currentClosingRect.left + enteringStartOffset, 0f)
+
+ val valueAnimator = ValueAnimator.ofFloat(1f, 0f).setDuration(POST_ANIMATION_DURATION)
+ valueAnimator.addUpdateListener { animation: ValueAnimator ->
+ val progress = animation.animatedFraction
+ onPostCommitProgress(progress)
+ if (progress > 1 - BackAnimationConstants.UPDATE_SYSUI_FLAGS_THRESHOLD) {
+ background.resetStatusBarCustomization()
+ }
+ }
+ valueAnimator.addListener(object : AnimatorListenerAdapter() {
+ override fun onAnimationEnd(animation: Animator) {
+ background.resetStatusBarCustomization()
+ finishAnimation()
+ }
+ })
+ valueAnimator.start()
+ }
+
+ private fun onPostCommitProgress(linearProgress: Float) {
+ val closingAlpha = max(1f - linearProgress * 2, 0f)
+ val progress = postCommitInterpolator.getInterpolation(linearProgress)
+ scrimLayer?.let { transaction.setAlpha(it, maxScrimAlpha * (1f - linearProgress)) }
+ currentClosingRect.setInterpolatedRectF(startClosingRect, targetClosingRect, progress)
+ applyTransform(closingTarget?.leash, currentClosingRect, closingAlpha)
+ currentEnteringRect.setInterpolatedRectF(startEnteringRect, targetEnteringRect, progress)
+ applyTransform(enteringTarget?.leash, currentEnteringRect, 1f)
+ transaction.apply()
+ }
+
+ private fun finishAnimation() {
+ enteringTarget?.let {
+ if (it.leash != null && it.leash.isValid) {
+ transaction.setCornerRadius(it.leash, 0f)
+ it.leash.release()
+ }
+ enteringTarget = null
+ }
+
+ closingTarget?.leash?.release()
+ closingTarget = null
+
+ background.removeBackground(transaction)
+ transaction.apply()
+ transformMatrix.reset()
+ initialTouchPos.set(0f, 0f)
+ try {
+ finishCallback?.onAnimationFinished()
+ } catch (e: RemoteException) {
+ e.printStackTrace()
+ }
+ finishCallback = null
+ removeScrimLayer()
+ }
+
+ private fun applyTransform(leash: SurfaceControl?, rect: RectF, alpha: Float) {
+ if (leash == null || !leash.isValid) return
+ val scale = rect.width() / taskBoundsRect.width()
+ transformMatrix.reset()
+ transformMatrix.setScale(scale, scale)
+ transformMatrix.postTranslate(rect.left, rect.top)
+ transaction.setAlpha(leash, alpha)
+ .setMatrix(leash, transformMatrix, tmpFloat9)
+ .setCrop(leash, taskBoundsRect)
+ .setCornerRadius(leash, cornerRadius)
+ }
+
+ private fun ensureScrimLayer() {
+ if (scrimLayer != null) return
+ val isDarkTheme: Boolean = isDarkMode(context)
+ val scrimBuilder = SurfaceControl.Builder()
+ .setName("Cross-Activity back animation scrim")
+ .setCallsite("CrossActivityBackAnimation")
+ .setColorLayer()
+ .setOpaque(false)
+ .setHidden(false)
+
+ rootTaskDisplayAreaOrganizer.attachToDisplayArea(Display.DEFAULT_DISPLAY, scrimBuilder)
+ scrimLayer = scrimBuilder.build()
+ val colorComponents = floatArrayOf(0f, 0f, 0f)
+ maxScrimAlpha = if (isDarkTheme) MAX_SCRIM_ALPHA_DARK else MAX_SCRIM_ALPHA_LIGHT
+ transaction
+ .setColor(scrimLayer, colorComponents)
+ .setAlpha(scrimLayer!!, maxScrimAlpha)
+ .setRelativeLayer(scrimLayer!!, closingTarget!!.leash, -1)
+ .show(scrimLayer)
+ }
+
+ private fun removeScrimLayer() {
+ scrimLayer?.let {
+ if (it.isValid) {
+ transaction.remove(it).apply()
+ }
+ }
+ scrimLayer = null
+ }
+
+
+ private inner class Callback : IOnBackInvokedCallback.Default() {
+ override fun onBackStarted(backMotionEvent: BackMotionEvent) {
+ startBackAnimation(backMotionEvent)
+ progressAnimator.onBackStarted(backMotionEvent) { backEvent: BackEvent ->
+ onGestureProgress(backEvent)
+ }
+ }
+
+ override fun onBackProgressed(backEvent: BackMotionEvent) {
+ triggerBack = backEvent.triggerBack
+ progressAnimator.onBackProgressed(backEvent)
+ }
+
+ override fun onBackCancelled() {
+ progressAnimator.onBackCancelled {
+ finishAnimation()
+ }
+ }
+
+ override fun onBackInvoked() {
+ progressAnimator.reset()
+ onGestureCommitted()
+ }
+ }
+
+ private inner class Runner : IRemoteAnimationRunner.Default() {
+ override fun onAnimationStart(
+ transit: Int,
+ apps: Array<RemoteAnimationTarget>,
+ wallpapers: Array<RemoteAnimationTarget>?,
+ nonApps: Array<RemoteAnimationTarget>?,
+ finishedCallback: IRemoteAnimationFinishedCallback
+ ) {
+ ProtoLog.d(
+ ShellProtoLogGroup.WM_SHELL_BACK_PREVIEW, "Start back to activity animation."
+ )
+ for (a in apps) {
+ when (a.mode) {
+ RemoteAnimationTarget.MODE_CLOSING -> closingTarget = a
+ RemoteAnimationTarget.MODE_OPENING -> enteringTarget = a
+ }
+ }
+ finishCallback = finishedCallback
+ }
+
+ override fun onAnimationCancelled() {
+ finishAnimation()
+ }
+ }
+
+ companion object {
+ /** Max scale of the entering/closing window.*/
+ private const val MAX_SCALE = 0.9f
+
+ /** Duration of post animation after gesture committed. */
+ private const val POST_ANIMATION_DURATION = 300L
+
+ private const val MAX_SCRIM_ALPHA_DARK = 0.8f
+ private const val MAX_SCRIM_ALPHA_LIGHT = 0.2f
+ }
+}
+
+private fun isDarkMode(context: Context): Boolean {
+ return context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK ==
+ Configuration.UI_MODE_NIGHT_YES
+}
+
+private fun RectF.setInterpolatedRectF(start: RectF, target: RectF, progress: Float) {
+ require(!(progress < 0 || progress > 1)) { "Progress value must be between 0 and 1" }
+ left = start.left + (target.left - start.left) * progress
+ top = start.top + (target.top - start.top) * progress
+ right = start.right + (target.right - start.right) * progress
+ bottom = start.bottom + (target.bottom - start.bottom) * progress
+}
+
+private fun RectF.scaleCentered(
+ scale: Float,
+ pivotX: Float = left + width() / 2,
+ pivotY: Float = top + height() / 2
+) {
+ offset(-pivotX, -pivotY) // move pivot to origin
+ scale(scale)
+ offset(pivotX, pivotY) // Move back to the original position
+}
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 f4a401c..4d5e516 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
@@ -870,7 +870,7 @@
if (onLeft) {
left = getInsets().left + padding;
} else {
- left = getAvailableRect().width() - width - padding;
+ left = getAvailableRect().right - width - padding;
}
int top = getExpandedViewBottomForBubbleBar() - height;
out.offsetTo(left, top);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarDropTargetController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarDropTargetController.kt
index 55ec6cd..f6b4653 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarDropTargetController.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarDropTargetController.kt
@@ -21,6 +21,10 @@
import android.view.View
import android.widget.FrameLayout
import android.widget.FrameLayout.LayoutParams
+import androidx.annotation.VisibleForTesting
+import androidx.core.animation.Animator
+import androidx.core.animation.AnimatorListenerAdapter
+import androidx.core.animation.ObjectAnimator
import com.android.wm.shell.R
import com.android.wm.shell.bubbles.BubblePositioner
import com.android.wm.shell.common.bubbles.BubbleBarLocation
@@ -33,6 +37,7 @@
) {
private var dropTargetView: View? = null
+ private var animator: ObjectAnimator? = null
private val tempRect: Rect by lazy(LazyThreadSafetyMode.NONE) { Rect() }
/**
@@ -57,7 +62,8 @@
/**
* Set the view hidden or not
*
- * Requires the drop target to be first shown by calling [show]. Otherwise does not do anything.
+ * Requires the drop target to be first shown by calling [animateIn]. Otherwise does not do
+ * anything.
*/
fun setHidden(hidden: Boolean) {
val targetView = dropTargetView ?: return
@@ -106,20 +112,40 @@
}
private fun View.animateIn() {
- animate().alpha(1f).setDuration(DROP_TARGET_ALPHA_IN_DURATION).start()
+ animator?.cancel()
+ animator =
+ ObjectAnimator.ofFloat(this, View.ALPHA, 1f)
+ .setDuration(DROP_TARGET_ALPHA_IN_DURATION)
+ .addEndAction { animator = null }
+ animator?.start()
}
private fun View.animateOut(endAction: Runnable? = null) {
- animate()
- .alpha(0f)
- .setDuration(DROP_TARGET_ALPHA_OUT_DURATION)
- .withEndAction(endAction)
- .start()
+ animator?.cancel()
+ animator =
+ ObjectAnimator.ofFloat(this, View.ALPHA, 0f)
+ .setDuration(DROP_TARGET_ALPHA_OUT_DURATION)
+ .addEndAction {
+ endAction?.run()
+ animator = null
+ }
+ animator?.start()
+ }
+
+ private fun <T : Animator> T.addEndAction(runnable: Runnable): T {
+ addListener(
+ object : AnimatorListenerAdapter() {
+ override fun onAnimationEnd(animation: Animator) {
+ runnable.run()
+ }
+ }
+ )
+ return this
}
companion object {
- private const val DROP_TARGET_ALPHA_IN_DURATION = 150L
- private const val DROP_TARGET_ALPHA_OUT_DURATION = 100L
- private const val DROP_TARGET_SCALE = 0.9f
+ @VisibleForTesting const val DROP_TARGET_ALPHA_IN_DURATION = 150L
+ @VisibleForTesting const val DROP_TARGET_ALPHA_OUT_DURATION = 100L
+ @VisibleForTesting const val DROP_TARGET_SCALE = 0.9f
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/MultiInstanceHelper.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/common/MultiInstanceHelper.kt
index 4c34971..9e8dfb5 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/MultiInstanceHelper.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/MultiInstanceHelper.kt
@@ -21,11 +21,9 @@
import android.content.pm.LauncherApps
import android.content.pm.PackageManager
import android.os.UserHandle
-import android.view.WindowManager
import android.view.WindowManager.PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI
import com.android.internal.annotations.VisibleForTesting
import com.android.wm.shell.R
-import com.android.wm.shell.protolog.ShellProtoLogGroup
import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL
import com.android.wm.shell.util.KtProtoLog
import java.util.Arrays
@@ -37,7 +35,8 @@
private val context: Context,
private val packageManager: PackageManager,
private val staticAppsSupportingMultiInstance: Array<String> = context.resources
- .getStringArray(R.array.config_appsSupportMultiInstancesSplit)) {
+ .getStringArray(R.array.config_appsSupportMultiInstancesSplit),
+ private val supportsMultiInstanceProperty: Boolean) {
/**
* Returns whether a specific component desires to be launched in multiple instances.
@@ -59,6 +58,11 @@
}
}
+ if (!supportsMultiInstanceProperty) {
+ // If not checking the multi-instance properties, then return early
+ return false;
+ }
+
// Check the activity property first
try {
val activityProp = packageManager.getProperty(
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/SystemWindows.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/SystemWindows.java
index e4cf6d1..98dccbb 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/SystemWindows.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/SystemWindows.java
@@ -48,6 +48,7 @@
import android.view.WindowManager;
import android.view.WindowlessWindowManager;
import android.view.inputmethod.ImeTracker;
+import android.window.ActivityWindowInfo;
import android.window.ClientWindowFrames;
import android.window.InputTransferToken;
@@ -348,7 +349,7 @@
public void resized(ClientWindowFrames frames, boolean reportDraw,
MergedConfiguration newMergedConfiguration, InsetsState insetsState,
boolean forceLayout, boolean alwaysConsumeSystemBars, int displayId, int syncSeqId,
- boolean dragResizing) {}
+ boolean dragResizing, @Nullable ActivityWindowInfo activityWindowInfo) {}
@Override
public void insetsControlChanged(InsetsState insetsState,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
index 8d489e1..5122114 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
@@ -29,6 +29,7 @@
import com.android.internal.logging.UiEventLogger;
import com.android.launcher3.icons.IconProvider;
+import com.android.window.flags.Flags;
import com.android.wm.shell.ProtoLogController;
import com.android.wm.shell.R;
import com.android.wm.shell.RootDisplayAreaOrganizer;
@@ -326,7 +327,8 @@
@WMSingleton
@Provides
static MultiInstanceHelper provideMultiInstanceHelper(Context context) {
- return new MultiInstanceHelper(context, context.getPackageManager());
+ return new MultiInstanceHelper(context, context.getPackageManager(),
+ Flags.supportsMultiInstanceSystemUi());
}
//
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMode.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMode.java
index 838603f..5889da1 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMode.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMode.java
@@ -49,7 +49,7 @@
/** Called when requested to go to desktop mode from the current focused app. */
- void enterDesktop(int displayId);
+ void moveFocusedTaskToDesktop(int displayId);
/** Called when requested to go to fullscreen from the current focused desktop app. */
void moveFocusedTaskToFullscreen(int displayId);
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 992e5ae..cdef4fd 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
@@ -263,7 +263,7 @@
}
/** Enter desktop by using the focused task in given `displayId` */
- fun enterDesktop(displayId: Int) {
+ fun moveFocusedTaskToDesktop(displayId: Int) {
val allFocusedTasks =
shellTaskOrganizer.getRunningTasks(displayId).filter { taskInfo ->
taskInfo.isFocused &&
@@ -1212,9 +1212,9 @@
}
}
- override fun enterDesktop(displayId: Int) {
+ override fun moveFocusedTaskToDesktop(displayId: Int) {
mainExecutor.execute {
- this@DesktopTasksController.enterDesktop(displayId)
+ this@DesktopTasksController.moveFocusedTaskToDesktop(displayId)
}
}
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 af26e29..b830a41 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
@@ -15,6 +15,7 @@
import android.content.Intent
import android.content.Intent.FILL_IN_COMPONENT
import android.graphics.Rect
+import android.os.Bundle
import android.os.IBinder
import android.os.SystemClock
import android.view.SurfaceControl
@@ -124,7 +125,7 @@
options.toBundle()
)
val wct = WindowContainerTransaction()
- wct.sendPendingIntent(pendingIntent, launchHomeIntent, options.toBundle())
+ wct.sendPendingIntent(pendingIntent, launchHomeIntent, Bundle())
val startTransitionToken = transitions
.startTransition(TRANSIT_DESKTOP_MODE_START_DRAG_TO_DESKTOP, wct, this)
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/TaskSnapshotWindow.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/TaskSnapshotWindow.java
index 1a0c011..ceac40d 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/TaskSnapshotWindow.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/TaskSnapshotWindow.java
@@ -23,6 +23,7 @@
import android.annotation.BinderThread;
import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.app.ActivityManager;
import android.app.ActivityManager.TaskDescription;
import android.graphics.Paint;
@@ -42,6 +43,7 @@
import android.view.View;
import android.view.WindowManager;
import android.view.WindowManagerGlobal;
+import android.window.ActivityWindowInfo;
import android.window.ClientWindowFrames;
import android.window.SnapshotDrawerUtils;
import android.window.StartingWindowInfo;
@@ -214,7 +216,7 @@
public void resized(ClientWindowFrames frames, boolean reportDraw,
MergedConfiguration mergedConfiguration, InsetsState insetsState,
boolean forceLayout, boolean alwaysConsumeSystemBars, int displayId, int seqId,
- boolean dragResizing) {
+ boolean dragResizing, @Nullable ActivityWindowInfo activityWindowInfo) {
final TaskSnapshotWindow snapshot = mOuter.get();
if (snapshot == null) {
return;
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 9130edf..74e85f8 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
@@ -334,6 +334,7 @@
boolean isDisplayRotationAnimationStarted = false;
final boolean isDreamTransition = isDreamTransition(info);
final boolean isOnlyTranslucent = isOnlyTranslucent(info);
+ final boolean isActivityLevel = isActivityLevelOnly(info);
for (int i = info.getChanges().size() - 1; i >= 0; --i) {
final TransitionInfo.Change change = info.getChanges().get(i);
@@ -502,8 +503,35 @@
: new Rect(change.getEndAbsBounds());
clipRect.offsetTo(0, 0);
+ final TransitionInfo.Root animRoot = TransitionUtil.getRootFor(change, info);
+ final Point animRelOffset = new Point(
+ change.getEndAbsBounds().left - animRoot.getOffset().x,
+ change.getEndAbsBounds().top - animRoot.getOffset().y);
+ if (change.getActivityComponent() != null && !isActivityLevel) {
+ // At this point, this is an independent activity change in a non-activity
+ // transition. This means that an activity transition got erroneously combined
+ // with another ongoing transition. This then means that the animation root may
+ // not tightly fit the activities, so we have to put them in a separate crop.
+ final int layer = Transitions.calculateAnimLayer(change, i,
+ info.getChanges().size(), info.getType());
+ final SurfaceControl leash = new SurfaceControl.Builder()
+ .setName("Transition ActivityWrap: "
+ + change.getActivityComponent().toShortString())
+ .setParent(animRoot.getLeash())
+ .setContainerLayer().build();
+ startTransaction.setCrop(leash, clipRect);
+ startTransaction.setPosition(leash, animRelOffset.x, animRelOffset.y);
+ startTransaction.setLayer(leash, layer);
+ startTransaction.show(leash);
+ startTransaction.reparent(change.getLeash(), leash);
+ startTransaction.setPosition(change.getLeash(), 0, 0);
+ animRelOffset.set(0, 0);
+ finishTransaction.reparent(leash, null);
+ leash.release();
+ }
+
buildSurfaceAnimation(animations, a, change.getLeash(), onAnimFinish,
- mTransactionPool, mMainExecutor, change.getEndRelOffset(), cornerRadius,
+ mTransactionPool, mMainExecutor, animRelOffset, cornerRadius,
clipRect);
if (info.getAnimationOptions() != null) {
@@ -612,6 +640,18 @@
return (translucentOpen + translucentClose) > 0;
}
+ /**
+ * Does `info` only contain activity-level changes? This kinda assumes that if so, they are
+ * all in one task.
+ */
+ private static boolean isActivityLevelOnly(@NonNull TransitionInfo info) {
+ for (int i = info.getChanges().size() - 1; i >= 0; --i) {
+ final TransitionInfo.Change change = info.getChanges().get(i);
+ if (change.getActivityComponent() == null) return false;
+ }
+ return true;
+ }
+
@Override
public void mergeAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info,
@NonNull SurfaceControl.Transaction t, @NonNull IBinder mergeTarget,
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 ccd0b2d..a77602b 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
@@ -31,7 +31,6 @@
import static android.window.TransitionInfo.FLAG_BACK_GESTURE_ANIMATED;
import static android.window.TransitionInfo.FLAG_IS_BEHIND_STARTING_WINDOW;
import static android.window.TransitionInfo.FLAG_IS_OCCLUDED;
-import static android.window.TransitionInfo.FLAG_IS_WALLPAPER;
import static android.window.TransitionInfo.FLAG_MOVED_TO_TOP;
import static android.window.TransitionInfo.FLAG_NO_ANIMATION;
import static android.window.TransitionInfo.FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT;
@@ -496,6 +495,7 @@
if (mode == TRANSIT_TO_FRONT) {
// When the window is moved to front, make sure the crop is updated to prevent it
// from using the old crop.
+ t.setPosition(leash, change.getEndRelOffset().x, change.getEndRelOffset().y);
t.setWindowCrop(leash, change.getEndAbsBounds().width(),
change.getEndAbsBounds().height());
}
@@ -507,6 +507,8 @@
t.setMatrix(leash, 1, 0, 0, 1);
t.setAlpha(leash, 1.f);
t.setPosition(leash, change.getEndRelOffset().x, change.getEndRelOffset().y);
+ t.setWindowCrop(leash, change.getEndAbsBounds().width(),
+ change.getEndAbsBounds().height());
}
continue;
}
@@ -530,6 +532,44 @@
}
}
+ static int calculateAnimLayer(@NonNull TransitionInfo.Change change, int i,
+ int numChanges, @WindowManager.TransitionType int transitType) {
+ // Put animating stuff above this line and put static stuff below it.
+ final int zSplitLine = numChanges + 1;
+ final boolean isOpening = isOpeningType(transitType);
+ final boolean isClosing = isClosingType(transitType);
+ final int mode = change.getMode();
+ // Put all the OPEN/SHOW on top
+ if (mode == TRANSIT_OPEN || mode == TRANSIT_TO_FRONT) {
+ if (isOpening
+ // This is for when an activity launches while a different transition is
+ // collecting.
+ || change.hasFlags(FLAG_MOVED_TO_TOP)) {
+ // put on top
+ return zSplitLine + numChanges - i;
+ } else {
+ // put on bottom
+ return zSplitLine - i;
+ }
+ } else if (mode == TRANSIT_CLOSE || mode == TRANSIT_TO_BACK) {
+ if (isOpening) {
+ // put on bottom and leave visible
+ return zSplitLine - i;
+ } else {
+ // put on top
+ return zSplitLine + numChanges - i;
+ }
+ } else { // CHANGE or other
+ if (isClosing || TransitionUtil.isOrderOnly(change)) {
+ // Put below CLOSE mode (in the "static" section).
+ return zSplitLine - i;
+ } else {
+ // Put above CLOSE mode.
+ return zSplitLine + numChanges - i;
+ }
+ }
+ }
+
/**
* Reparents all participants into a shared parent and orders them based on: the global transit
* type, their transit mode, and their destination z-order.
@@ -537,19 +577,14 @@
private static void setupAnimHierarchy(@NonNull TransitionInfo info,
@NonNull SurfaceControl.Transaction t, @NonNull SurfaceControl.Transaction finishT) {
final int type = info.getType();
- final boolean isOpening = isOpeningType(type);
- final boolean isClosing = isClosingType(type);
for (int i = 0; i < info.getRootCount(); ++i) {
t.show(info.getRoot(i).getLeash());
}
final int numChanges = info.getChanges().size();
- // Put animating stuff above this line and put static stuff below it.
- final int zSplitLine = numChanges + 1;
// changes should be ordered top-to-bottom in z
for (int i = numChanges - 1; i >= 0; --i) {
final TransitionInfo.Change change = info.getChanges().get(i);
final SurfaceControl leash = change.getLeash();
- final int mode = change.getMode();
// Don't reparent anything that isn't independent within its parents
if (!TransitionInfo.isIndependent(change, info)) {
@@ -558,50 +593,14 @@
boolean hasParent = change.getParent() != null;
- final int rootIdx = TransitionUtil.rootIndexFor(change, info);
+ final TransitionInfo.Root root = TransitionUtil.getRootFor(change, info);
if (!hasParent) {
- t.reparent(leash, info.getRoot(rootIdx).getLeash());
+ t.reparent(leash, root.getLeash());
t.setPosition(leash,
- change.getStartAbsBounds().left - info.getRoot(rootIdx).getOffset().x,
- change.getStartAbsBounds().top - info.getRoot(rootIdx).getOffset().y);
+ change.getStartAbsBounds().left - root.getOffset().x,
+ change.getStartAbsBounds().top - root.getOffset().y);
}
- final int layer;
- // Put all the OPEN/SHOW on top
- if ((change.getFlags() & FLAG_IS_WALLPAPER) != 0) {
- // Wallpaper is always at the bottom, opening wallpaper on top of closing one.
- if (mode == TRANSIT_OPEN || mode == TRANSIT_TO_FRONT) {
- layer = -zSplitLine + numChanges - i;
- } else {
- layer = -zSplitLine - i;
- }
- } else if (mode == TRANSIT_OPEN || mode == TRANSIT_TO_FRONT) {
- if (isOpening
- // This is for when an activity launches while a different transition is
- // collecting.
- || change.hasFlags(FLAG_MOVED_TO_TOP)) {
- // put on top
- layer = zSplitLine + numChanges - i;
- } else {
- // put on bottom
- layer = zSplitLine - i;
- }
- } else if (mode == TRANSIT_CLOSE || mode == TRANSIT_TO_BACK) {
- if (isOpening) {
- // put on bottom and leave visible
- layer = zSplitLine - i;
- } else {
- // put on top
- layer = zSplitLine + numChanges - i;
- }
- } else { // CHANGE or other
- if (isClosing || TransitionUtil.isOrderOnly(change)) {
- // Put below CLOSE mode (in the "static" section).
- layer = zSplitLine - i;
- } else {
- // Put above CLOSE mode.
- layer = zSplitLine + numChanges - i;
- }
- }
+ final int layer = calculateAnimLayer(change, i, numChanges, type);
t.setLayer(leash, layer);
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositioner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositioner.java
index 6f8b3d5..76096b0 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositioner.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositioner.java
@@ -18,6 +18,7 @@
import static android.view.WindowManager.TRANSIT_CHANGE;
+import android.graphics.Point;
import android.graphics.PointF;
import android.graphics.Rect;
import android.os.IBinder;
@@ -178,10 +179,11 @@
for (TransitionInfo.Change change: info.getChanges()) {
final SurfaceControl sc = change.getLeash();
final Rect endBounds = change.getEndAbsBounds();
+ final Point endPosition = change.getEndRelOffset();
startTransaction.setWindowCrop(sc, endBounds.width(), endBounds.height())
- .setPosition(sc, endBounds.left, endBounds.top);
+ .setPosition(sc, endPosition.x, endPosition.y);
finishTransaction.setWindowCrop(sc, endBounds.width(), endBounds.height())
- .setPosition(sc, endBounds.left, endBounds.top);
+ .setPosition(sc, endPosition.x, endPosition.y);
}
startTransaction.apply();
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositioner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositioner.java
index c12a93e..5fce5d2 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositioner.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositioner.java
@@ -18,6 +18,7 @@
import static android.view.WindowManager.TRANSIT_CHANGE;
+import android.graphics.Point;
import android.graphics.PointF;
import android.graphics.Rect;
import android.os.IBinder;
@@ -179,10 +180,11 @@
for (TransitionInfo.Change change: info.getChanges()) {
final SurfaceControl sc = change.getLeash();
final Rect endBounds = change.getEndAbsBounds();
+ final Point endPosition = change.getEndRelOffset();
startTransaction.setWindowCrop(sc, endBounds.width(), endBounds.height())
- .setPosition(sc, endBounds.left, endBounds.top);
+ .setPosition(sc, endPosition.x, endPosition.y);
finishTransaction.setWindowCrop(sc, endBounds.width(), endBounds.height())
- .setPosition(sc, endBounds.left, endBounds.top);
+ .setPosition(sc, endPosition.x, endPosition.y);
}
startTransaction.apply();
diff --git a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/FromSplitScreenEnterPipOnUserLeaveHintTest.kt b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/FromSplitScreenEnterPipOnUserLeaveHintTest.kt
index 1ccc7d8..5f25d70 100644
--- a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/FromSplitScreenEnterPipOnUserLeaveHintTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/FromSplitScreenEnterPipOnUserLeaveHintTest.kt
@@ -24,6 +24,7 @@
import android.tools.flicker.legacy.LegacyFlickerTestFactory
import android.tools.helpers.WindowUtils
import android.tools.traces.parsers.toFlickerComponent
+import androidx.test.filters.FlakyTest
import androidx.test.filters.RequiresDevice
import com.android.server.wm.flicker.helpers.SimpleAppHelper
import com.android.server.wm.flicker.testapp.ActivityOptions
@@ -181,6 +182,12 @@
}
}
+ /** {@inheritDoc} */
+ @FlakyTest(bugId = 312446524)
+ @Test
+ override fun visibleLayersShownMoreThanOneConsecutiveEntry() =
+ super.visibleLayersShownMoreThanOneConsecutiveEntry()
+
companion object {
@Parameterized.Parameters(name = "{0}")
@JvmStatic
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java
index 9ded6ea..703eb19 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java
@@ -61,6 +61,7 @@
import androidx.test.filters.SmallTest;
import com.android.internal.util.test.FakeSettingsProvider;
+import com.android.wm.shell.RootTaskDisplayAreaOrganizer;
import com.android.wm.shell.ShellTestCase;
import com.android.wm.shell.TestShellExecutor;
import com.android.wm.shell.sysui.ShellCommandHandler;
@@ -113,6 +114,8 @@
private InputManager mInputManager;
@Mock
private ShellCommandHandler mShellCommandHandler;
+ @Mock
+ private RootTaskDisplayAreaOrganizer mRootTaskDisplayAreaOrganizer;
private BackAnimationController mController;
private TestableContentResolver mContentResolver;
@@ -133,7 +136,8 @@
mShellInit = spy(new ShellInit(mShellExecutor));
mShellBackAnimationRegistry =
new ShellBackAnimationRegistry(
- new CrossActivityBackAnimation(mContext, mAnimationBackground),
+ new CrossActivityBackAnimation(
+ mContext, mAnimationBackground, mRootTaskDisplayAreaOrganizer),
new CrossTaskBackAnimation(mContext, mAnimationBackground),
/* dialogCloseAnimation= */ null,
new CustomizeActivityAnimation(mContext, mAnimationBackground),
@@ -528,8 +532,8 @@
@Test
public void testBackToActivity() throws RemoteException {
- final CrossActivityBackAnimation animation = new CrossActivityBackAnimation(mContext,
- mAnimationBackground);
+ final CrossActivityBackAnimation animation = new CrossActivityBackAnimation(
+ mContext, mAnimationBackground, mRootTaskDisplayAreaOrganizer);
verifySystemBackBehavior(BackNavigationInfo.TYPE_CROSS_ACTIVITY, animation.getRunner());
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/MultiInstanceHelperTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/MultiInstanceHelperTest.kt
index 2f5fe11..bec91e9 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/MultiInstanceHelperTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/MultiInstanceHelperTest.kt
@@ -32,9 +32,12 @@
import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers
import org.mockito.ArgumentMatchers.eq
+import org.mockito.kotlin.any
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.doThrow
import org.mockito.kotlin.mock
+import org.mockito.kotlin.never
+import org.mockito.kotlin.verify
import org.mockito.kotlin.whenever
@RunWith(AndroidJUnit4::class)
@@ -77,7 +80,7 @@
@Test
fun supportsMultiInstanceSplit_inStaticAllowList() {
val allowList = arrayOf(TEST_PACKAGE)
- val helper = MultiInstanceHelper(mContext, context.packageManager, allowList)
+ val helper = MultiInstanceHelper(mContext, context.packageManager, allowList, true)
val component = ComponentName(TEST_PACKAGE, TEST_ACTIVITY)
assertEquals(true, helper.supportsMultiInstanceSplit(component))
}
@@ -85,7 +88,7 @@
@Test
fun supportsMultiInstanceSplit_notInStaticAllowList() {
val allowList = arrayOf(TEST_PACKAGE)
- val helper = MultiInstanceHelper(mContext, context.packageManager, allowList)
+ val helper = MultiInstanceHelper(mContext, context.packageManager, allowList, true)
val component = ComponentName(TEST_NOT_ALLOWED_PACKAGE, TEST_ACTIVITY)
assertEquals(false, helper.supportsMultiInstanceSplit(component))
}
@@ -104,7 +107,7 @@
eq(component.packageName)))
.thenReturn(appProp)
- val helper = MultiInstanceHelper(mContext, pm, emptyArray())
+ val helper = MultiInstanceHelper(mContext, pm, emptyArray(), true)
// Expect activity property to override application property
assertEquals(true, helper.supportsMultiInstanceSplit(component))
}
@@ -123,7 +126,7 @@
eq(component.packageName)))
.thenReturn(appProp)
- val helper = MultiInstanceHelper(mContext, pm, emptyArray())
+ val helper = MultiInstanceHelper(mContext, pm, emptyArray(), true)
// Expect activity property to override application property
assertEquals(false, helper.supportsMultiInstanceSplit(component))
}
@@ -141,7 +144,7 @@
eq(component.packageName)))
.thenReturn(appProp)
- val helper = MultiInstanceHelper(mContext, pm, emptyArray())
+ val helper = MultiInstanceHelper(mContext, pm, emptyArray(), true)
// Expect fall through to app property
assertEquals(true, helper.supportsMultiInstanceSplit(component))
}
@@ -158,10 +161,30 @@
eq(component.packageName)))
.thenThrow(PackageManager.NameNotFoundException())
- val helper = MultiInstanceHelper(mContext, pm, emptyArray())
+ val helper = MultiInstanceHelper(mContext, pm, emptyArray(), true)
assertEquals(false, helper.supportsMultiInstanceSplit(component))
}
+ @Test
+ @Throws(PackageManager.NameNotFoundException::class)
+ fun checkNoMultiInstancePropertyFlag_ignoreProperty() {
+ val component = ComponentName(TEST_PACKAGE, TEST_ACTIVITY)
+ val pm = mock<PackageManager>()
+ val activityProp = PackageManager.Property("", true, "", "")
+ whenever(pm.getProperty(eq(PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI),
+ eq(component)))
+ .thenReturn(activityProp)
+ val appProp = PackageManager.Property("", true, "", "")
+ whenever(pm.getProperty(eq(PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI),
+ eq(component.packageName)))
+ .thenReturn(appProp)
+
+ val helper = MultiInstanceHelper(mContext, pm, emptyArray(), false)
+ // Expect we only check the static list and not the property
+ assertEquals(false, helper.supportsMultiInstanceSplit(component))
+ verify(pm, never()).getProperty(any(), any<ComponentName>())
+ }
+
companion object {
val TEST_PACKAGE = "com.android.wm.shell.common"
val TEST_NOT_ALLOWED_PACKAGE = "com.android.wm.shell.common.fake";
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
index 254bf7d..4fbf2bd 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
@@ -833,7 +833,7 @@
verify(launchAdjacentController).launchAdjacentEnabled = true
}
@Test
- fun enterDesktop_fullscreenTaskIsMovedToDesktop() {
+ fun moveFocusedTaskToDesktop_fullscreenTaskIsMovedToDesktop() {
val task1 = setUpFullscreenTask()
val task2 = setUpFullscreenTask()
val task3 = setUpFullscreenTask()
@@ -842,7 +842,7 @@
task2.isFocused = false
task3.isFocused = false
- controller.enterDesktop(DEFAULT_DISPLAY)
+ controller.moveFocusedTaskToDesktop(DEFAULT_DISPLAY)
val wct = getLatestMoveToDesktopWct()
assertThat(wct.changes[task1.token.asBinder()]?.windowingMode)
@@ -850,7 +850,7 @@
}
@Test
- fun enterDesktop_splitScreenTaskIsMovedToDesktop() {
+ fun moveFocusedTaskToDesktop_splitScreenTaskIsMovedToDesktop() {
val task1 = setUpSplitScreenTask()
val task2 = setUpFullscreenTask()
val task3 = setUpFullscreenTask()
@@ -863,7 +863,7 @@
task4.parentTaskId = task1.taskId
- controller.enterDesktop(DEFAULT_DISPLAY)
+ controller.moveFocusedTaskToDesktop(DEFAULT_DISPLAY)
val wct = getLatestMoveToDesktopWct()
assertThat(wct.changes[task4.token.asBinder()]?.windowingMode)
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositionerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositionerTest.kt
index ce7b633..9174556 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositionerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositionerTest.kt
@@ -2,6 +2,7 @@
import android.app.ActivityManager
import android.app.WindowConfiguration
+import android.graphics.Point
import android.graphics.Rect
import android.os.IBinder
import android.testing.AndroidTestingRunner
@@ -11,6 +12,7 @@
import android.view.Surface.ROTATION_90
import android.view.SurfaceControl
import android.view.WindowManager
+import android.window.TransitionInfo
import android.window.WindowContainerToken
import android.window.WindowContainerTransaction
import android.window.WindowContainerTransaction.Change.CHANGE_DRAG_RESIZING
@@ -41,6 +43,8 @@
import org.mockito.MockitoAnnotations
import org.mockito.kotlin.doReturn
import java.util.function.Supplier
+import org.mockito.Mockito.eq
+import org.mockito.Mockito.mock
import org.mockito.Mockito.`when` as whenever
/**
@@ -575,6 +579,32 @@
})
}
+ @Test
+ fun testStartAnimation_useEndRelOffset() {
+ val mockTransitionInfo = mock(TransitionInfo::class.java)
+ val changeMock = mock(TransitionInfo.Change::class.java)
+ val startTransaction = mock(SurfaceControl.Transaction::class.java)
+ val finishTransaction = mock(SurfaceControl.Transaction::class.java)
+ val point = Point(10, 20)
+ val bounds = Rect(1, 2, 3, 4)
+ `when`(changeMock.endRelOffset).thenReturn(point)
+ `when`(changeMock.endAbsBounds).thenReturn(bounds)
+ `when`(mockTransitionInfo.changes).thenReturn(listOf(changeMock))
+ `when`(startTransaction.setWindowCrop(any(),
+ eq(bounds.width()),
+ eq(bounds.height()))).thenReturn(startTransaction)
+ `when`(finishTransaction.setWindowCrop(any(),
+ eq(bounds.width()),
+ eq(bounds.height()))).thenReturn(finishTransaction)
+
+ taskPositioner.startAnimation(mockTransitionBinder, mockTransitionInfo, startTransaction,
+ finishTransaction, { _ -> })
+
+ verify(startTransaction).setPosition(any(), eq(point.x.toFloat()), eq(point.y.toFloat()))
+ verify(finishTransaction).setPosition(any(), eq(point.x.toFloat()), eq(point.y.toFloat()))
+ verify(changeMock).endRelOffset
+ }
+
private fun WindowContainerTransaction.Change.ofBounds(bounds: Rect): Boolean {
return ((windowSetMask and WindowConfiguration.WINDOW_CONFIG_BOUNDS) != 0) &&
bounds == configuration.windowConfiguration.bounds
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositionerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositionerTest.kt
index 7f6e538..a9f4492 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositionerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositionerTest.kt
@@ -17,6 +17,7 @@
import android.app.ActivityManager
import android.app.WindowConfiguration
+import android.graphics.Point
import android.graphics.Rect
import android.os.IBinder
import android.testing.AndroidTestingRunner
@@ -25,6 +26,7 @@
import android.view.Surface.ROTATION_270
import android.view.Surface.ROTATION_90
import android.view.SurfaceControl
+import android.view.SurfaceControl.Transaction
import android.view.WindowManager.TRANSIT_CHANGE
import android.window.TransitionInfo
import android.window.WindowContainerToken
@@ -39,6 +41,7 @@
import com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_RIGHT
import com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_TOP
import com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_UNDEFINED
+import java.util.function.Supplier
import junit.framework.Assert
import org.junit.Before
import org.junit.Test
@@ -47,13 +50,13 @@
import org.mockito.Mockito.any
import org.mockito.Mockito.argThat
import org.mockito.Mockito.eq
+import org.mockito.Mockito.mock
import org.mockito.Mockito.never
import org.mockito.Mockito.times
import org.mockito.Mockito.verify
import org.mockito.Mockito.`when`
-import org.mockito.MockitoAnnotations
-import java.util.function.Supplier
import org.mockito.Mockito.`when` as whenever
+import org.mockito.MockitoAnnotations
/**
* Tests for [VeiledResizeTaskPositioner].
@@ -439,6 +442,40 @@
Assert.assertFalse(taskPositioner.isResizingOrAnimating)
}
+ @Test
+ fun testStartAnimation_useEndRelOffset() {
+ val changeMock = mock(TransitionInfo.Change::class.java)
+ val startTransaction = mock(Transaction::class.java)
+ val finishTransaction = mock(Transaction::class.java)
+ val point = Point(10, 20)
+ val bounds = Rect(1, 2, 3, 4)
+ `when`(changeMock.endRelOffset).thenReturn(point)
+ `when`(changeMock.endAbsBounds).thenReturn(bounds)
+ `when`(mockTransitionInfo.changes).thenReturn(listOf(changeMock))
+ `when`(startTransaction.setWindowCrop(
+ any(),
+ eq(bounds.width()),
+ eq(bounds.height())
+ )).thenReturn(startTransaction)
+ `when`(finishTransaction.setWindowCrop(
+ any(),
+ eq(bounds.width()),
+ eq(bounds.height())
+ )).thenReturn(finishTransaction)
+
+ taskPositioner.startAnimation(
+ mockTransitionBinder,
+ mockTransitionInfo,
+ startTransaction,
+ finishTransaction,
+ mockFinishCallback
+ )
+
+ verify(startTransaction).setPosition(any(), eq(point.x.toFloat()), eq(point.y.toFloat()))
+ verify(finishTransaction).setPosition(any(), eq(point.x.toFloat()), eq(point.y.toFloat()))
+ verify(changeMock).endRelOffset
+ }
+
private fun performDrag(
startX: Float,
startY: Float,
diff --git a/libs/hwui/aconfig/hwui_flags.aconfig b/libs/hwui/aconfig/hwui_flags.aconfig
index 76a0a64..659bcdc 100644
--- a/libs/hwui/aconfig/hwui_flags.aconfig
+++ b/libs/hwui/aconfig/hwui_flags.aconfig
@@ -2,6 +2,7 @@
flag {
name: "clip_shader"
+ is_exported: true
namespace: "core_graphics"
description: "API for canvas shader clipping operations"
bug: "280116960"
@@ -9,6 +10,7 @@
flag {
name: "matrix_44"
+ is_exported: true
namespace: "core_graphics"
description: "API for 4x4 matrix and related canvas functions"
bug: "280116960"
@@ -16,6 +18,7 @@
flag {
name: "limited_hdr"
+ is_exported: true
namespace: "core_graphics"
description: "API to enable apps to restrict the amount of HDR headroom that is used"
bug: "234181960"
@@ -44,6 +47,7 @@
flag {
name: "gainmap_animations"
+ is_exported: true
namespace: "core_graphics"
description: "APIs to help enable animations involving gainmaps"
bug: "296482289"
@@ -51,6 +55,7 @@
flag {
name: "gainmap_constructor_with_metadata"
+ is_exported: true
namespace: "core_graphics"
description: "APIs to create a new gainmap with a bitmap for metadata."
bug: "304478551"
@@ -65,6 +70,7 @@
flag {
name: "requested_formats_v"
+ is_exported: true
namespace: "core_graphics"
description: "Enable r_8, r_16_uint, rg_1616_uint, and rgba_10101010 in the SDK"
bug: "292545615"
diff --git a/location/java/android/location/flags/location.aconfig b/location/java/android/location/flags/location.aconfig
index f33bcb7..ce689ac 100644
--- a/location/java/android/location/flags/location.aconfig
+++ b/location/java/android/location/flags/location.aconfig
@@ -9,6 +9,7 @@
flag {
name: "location_bypass"
+ is_exported: true
namespace: "location"
description: "Enable location bypass appops behavior"
bug: "329151785"
diff --git a/media/java/android/media/MediaCas.java b/media/java/android/media/MediaCas.java
index ab7c27f..2d7db5e 100644
--- a/media/java/android/media/MediaCas.java
+++ b/media/java/android/media/MediaCas.java
@@ -35,6 +35,7 @@
import android.os.Bundle;
import android.os.Handler;
import android.os.HandlerThread;
+import android.os.IBinder;
import android.os.IHwBinder;
import android.os.Looper;
import android.os.Message;
@@ -43,7 +44,6 @@
import android.os.ServiceManager;
import android.os.ServiceSpecificException;
import android.util.Log;
-import android.util.Singleton;
import com.android.internal.util.FrameworkStatsLog;
@@ -264,71 +264,107 @@
public static final int PLUGIN_STATUS_SESSION_NUMBER_CHANGED =
android.hardware.cas.StatusEvent.PLUGIN_SESSION_NUMBER_CHANGED;
- private static final Singleton<IMediaCasService> sService =
- new Singleton<IMediaCasService>() {
+ private static IMediaCasService sService = null;
+ private static Object sAidlLock = new Object();
+
+ /** DeathListener for AIDL service */
+ private static IBinder.DeathRecipient sDeathListener =
+ new IBinder.DeathRecipient() {
@Override
- protected IMediaCasService create() {
- try {
- Log.d(TAG, "Trying to get AIDL service");
- IMediaCasService serviceAidl =
- IMediaCasService.Stub.asInterface(
- ServiceManager.waitForDeclaredService(
- IMediaCasService.DESCRIPTOR + "/default"));
- if (serviceAidl != null) {
- return serviceAidl;
- }
- } catch (Exception eAidl) {
- Log.d(TAG, "Failed to get cas AIDL service");
+ public void binderDied() {
+ synchronized (sAidlLock) {
+ Log.d(TAG, "The service is dead");
+ sService.asBinder().unlinkToDeath(sDeathListener, 0);
+ sService = null;
}
- return null;
- }
- };
-
- private static final Singleton<android.hardware.cas.V1_0.IMediaCasService> sServiceHidl =
- new Singleton<android.hardware.cas.V1_0.IMediaCasService>() {
- @Override
- protected android.hardware.cas.V1_0.IMediaCasService create() {
- try {
- Log.d(TAG, "Trying to get cas@1.2 service");
- android.hardware.cas.V1_2.IMediaCasService serviceV12 =
- android.hardware.cas.V1_2.IMediaCasService.getService(
- true /*wait*/);
- if (serviceV12 != null) {
- return serviceV12;
- }
- } catch (Exception eV1_2) {
- Log.d(TAG, "Failed to get cas@1.2 service");
- }
-
- try {
- Log.d(TAG, "Trying to get cas@1.1 service");
- android.hardware.cas.V1_1.IMediaCasService serviceV11 =
- android.hardware.cas.V1_1.IMediaCasService.getService(
- true /*wait*/);
- if (serviceV11 != null) {
- return serviceV11;
- }
- } catch (Exception eV1_1) {
- Log.d(TAG, "Failed to get cas@1.1 service");
- }
-
- try {
- Log.d(TAG, "Trying to get cas@1.0 service");
- return android.hardware.cas.V1_0.IMediaCasService.getService(true /*wait*/);
- } catch (Exception eV1_0) {
- Log.d(TAG, "Failed to get cas@1.0 service");
- }
-
- return null;
}
};
static IMediaCasService getService() {
- return sService.get();
+ synchronized (sAidlLock) {
+ if (sService == null || !sService.asBinder().isBinderAlive()) {
+ try {
+ Log.d(TAG, "Trying to get AIDL service");
+ sService =
+ IMediaCasService.Stub.asInterface(
+ ServiceManager.waitForDeclaredService(
+ IMediaCasService.DESCRIPTOR + "/default"));
+ if (sService != null) {
+ sService.asBinder().linkToDeath(sDeathListener, 0);
+ }
+ } catch (Exception eAidl) {
+ Log.d(TAG, "Failed to get cas AIDL service");
+ }
+ }
+ return sService;
+ }
}
+ private static android.hardware.cas.V1_0.IMediaCasService sServiceHidl = null;
+ private static Object sHidlLock = new Object();
+
+ /** Used to indicate the right end-point to handle the serviceDied method */
+ private static final long MEDIA_CAS_HIDL_COOKIE = 394;
+
+ /** DeathListener for HIDL service */
+ private static IHwBinder.DeathRecipient sDeathListenerHidl =
+ new IHwBinder.DeathRecipient() {
+ @Override
+ public void serviceDied(long cookie) {
+ if (cookie == MEDIA_CAS_HIDL_COOKIE) {
+ synchronized (sHidlLock) {
+ sServiceHidl = null;
+ }
+ }
+ }
+ };
+
static android.hardware.cas.V1_0.IMediaCasService getServiceHidl() {
- return sServiceHidl.get();
+ synchronized (sHidlLock) {
+ if (sServiceHidl != null) {
+ return sServiceHidl;
+ } else {
+ try {
+ Log.d(TAG, "Trying to get cas@1.2 service");
+ android.hardware.cas.V1_2.IMediaCasService serviceV12 =
+ android.hardware.cas.V1_2.IMediaCasService.getService(true /*wait*/);
+ if (serviceV12 != null) {
+ sServiceHidl = serviceV12;
+ sServiceHidl.linkToDeath(sDeathListenerHidl, MEDIA_CAS_HIDL_COOKIE);
+ return sServiceHidl;
+ }
+ } catch (Exception eV1_2) {
+ Log.d(TAG, "Failed to get cas@1.2 service");
+ }
+
+ try {
+ Log.d(TAG, "Trying to get cas@1.1 service");
+ android.hardware.cas.V1_1.IMediaCasService serviceV11 =
+ android.hardware.cas.V1_1.IMediaCasService.getService(true /*wait*/);
+ if (serviceV11 != null) {
+ sServiceHidl = serviceV11;
+ sServiceHidl.linkToDeath(sDeathListenerHidl, MEDIA_CAS_HIDL_COOKIE);
+ return sServiceHidl;
+ }
+ } catch (Exception eV1_1) {
+ Log.d(TAG, "Failed to get cas@1.1 service");
+ }
+
+ try {
+ Log.d(TAG, "Trying to get cas@1.0 service");
+ sServiceHidl =
+ android.hardware.cas.V1_0.IMediaCasService.getService(true /*wait*/);
+ if (sServiceHidl != null) {
+ sServiceHidl.linkToDeath(sDeathListenerHidl, MEDIA_CAS_HIDL_COOKIE);
+ }
+ return sServiceHidl;
+ } catch (Exception eV1_0) {
+ Log.d(TAG, "Failed to get cas@1.0 service");
+ }
+ }
+ }
+ // Couldn't find an HIDL service, returning null.
+ return null;
}
private void validateInternalStates() {
@@ -756,7 +792,7 @@
* @return Whether the specified CA system is supported on this device.
*/
public static boolean isSystemIdSupported(int CA_system_id) {
- IMediaCasService service = sService.get();
+ IMediaCasService service = getService();
if (service != null) {
try {
return service.isSystemIdSupported(CA_system_id);
@@ -765,7 +801,7 @@
}
}
- android.hardware.cas.V1_0.IMediaCasService serviceHidl = sServiceHidl.get();
+ android.hardware.cas.V1_0.IMediaCasService serviceHidl = getServiceHidl();
if (serviceHidl != null) {
try {
return serviceHidl.isSystemIdSupported(CA_system_id);
@@ -781,7 +817,7 @@
* @return an array of descriptors for the available CA plugins.
*/
public static PluginDescriptor[] enumeratePlugins() {
- IMediaCasService service = sService.get();
+ IMediaCasService service = getService();
if (service != null) {
try {
AidlCasPluginDescriptor[] descriptors = service.enumeratePlugins();
@@ -794,10 +830,11 @@
}
return results;
} catch (RemoteException e) {
+ Log.e(TAG, "Some exception while enumerating plugins");
}
}
- android.hardware.cas.V1_0.IMediaCasService serviceHidl = sServiceHidl.get();
+ android.hardware.cas.V1_0.IMediaCasService serviceHidl = getServiceHidl();
if (serviceHidl != null) {
try {
ArrayList<HidlCasPluginDescriptor> descriptors = serviceHidl.enumeratePlugins();
diff --git a/media/java/android/media/flags/editing.aconfig b/media/java/android/media/flags/editing.aconfig
index c3997e9..5bf1b4e 100644
--- a/media/java/android/media/flags/editing.aconfig
+++ b/media/java/android/media/flags/editing.aconfig
@@ -2,6 +2,7 @@
flag {
name: "add_media_metrics_editing"
+ is_exported: true
namespace: "media_solutions"
description: "Add media metrics for transcoding/editing events."
bug: "297487694"
diff --git a/media/java/android/media/flags/media_better_together.aconfig b/media/java/android/media/flags/media_better_together.aconfig
index bf39425..40929f7 100644
--- a/media/java/android/media/flags/media_better_together.aconfig
+++ b/media/java/android/media/flags/media_better_together.aconfig
@@ -2,6 +2,7 @@
flag {
name: "enable_rlp_callbacks_in_media_router2"
+ is_exported: true
namespace: "media_solutions"
description: "Make RouteListingPreference getter and callbacks public in MediaRouter2."
bug: "281067101"
@@ -16,6 +17,7 @@
flag {
name: "enable_audio_policies_device_and_bluetooth_controller"
+ is_exported: true
namespace: "media_solutions"
description: "Use Audio Policies implementation for device and Bluetooth route controllers."
bug: "280576228"
@@ -44,6 +46,7 @@
flag {
name: "enable_new_media_route_2_info_types"
+ is_exported: true
namespace: "media_solutions"
description: "Enables the following type constants in MediaRoute2Info: CAR, COMPUTER, GAME_CONSOLE, SMARTPHONE, SMARTWATCH, TABLET, TABLET_DOCKED. Note that this doesn't gate any behavior. It only guards some API int symbols."
bug: "301713440"
@@ -51,6 +54,7 @@
flag {
name: "enable_privileged_routing_for_media_routing_control"
+ is_exported: true
namespace: "media_solutions"
description: "Allow access to privileged routing capabilities to MEDIA_ROUTING_CONTROL holders."
bug: "305919655"
@@ -58,6 +62,7 @@
flag {
name: "enable_cross_user_routing_in_media_router2"
+ is_exported: true
namespace: "media_solutions"
description: "Allows clients of privileged MediaRouter2 that hold INTERACT_ACROSS_USERS_FULL to control routing across users."
bug: "288580225"
@@ -72,6 +77,7 @@
flag {
name: "enable_built_in_speaker_route_suitability_statuses"
+ is_exported: true
namespace: "media_solutions"
description: "Make MediaRoute2Info provide information about routes suitability for transfer."
bug: "279555229"
@@ -79,6 +85,7 @@
flag {
name: "enable_notifying_activity_manager_with_media_session_status_change"
+ is_exported: true
namespace: "media_solutions"
description: "Notify ActivityManager with the changes in playback state of the media session."
bug: "295518668"
@@ -86,6 +93,7 @@
flag {
name: "enable_get_transferable_routes"
+ is_exported: true
namespace: "media_solutions"
description: "Exposes RoutingController#getTransferableRoutes() (previously hidden) to the public API."
bug: "323154573"
@@ -100,6 +108,7 @@
flag {
name: "enable_screen_off_scanning"
+ is_exported: true
namespace: "media_solutions"
description: "Enable new MediaRouter2 API to enable watch companion apps to scan while the phone screen is off."
bug: "281072508"
diff --git a/media/java/android/media/tv/flags/media_tv.aconfig b/media/java/android/media/tv/flags/media_tv.aconfig
index f110705..1731e5e 100644
--- a/media/java/android/media/tv/flags/media_tv.aconfig
+++ b/media/java/android/media/tv/flags/media_tv.aconfig
@@ -2,6 +2,7 @@
flag {
name: "broadcast_visibility_types"
+ is_exported: true
namespace: "media_tv"
description: "Constants for standardizing broadcast visibility types."
bug: "222402395"
@@ -9,6 +10,7 @@
flag {
name: "enable_ad_service_fw"
+ is_exported: true
namespace: "media_tv"
description: "Enable the TV client-side AD framework."
bug: "303506816"
@@ -16,6 +18,7 @@
flag {
name: "tiaf_v_apis"
+ is_exported: true
namespace: "media_tv"
description: "TIAF V3.0 APIs for Android V"
bug: "303323657"
diff --git a/native/android/OWNERS b/native/android/OWNERS
index 0b86909..9a3527d 100644
--- a/native/android/OWNERS
+++ b/native/android/OWNERS
@@ -16,6 +16,8 @@
per-file native_window_jni.cpp = file:/services/core/java/com/android/server/wm/OWNERS
per-file native_activity.cpp = file:/services/core/java/com/android/server/wm/OWNERS
per-file surface_control.cpp = file:/services/core/java/com/android/server/wm/OWNERS
+per-file surface_control_input_receiver.cpp = file:/services/core/java/com/android/server/wm/OWNERS
+per-file input_transfer_token.cpp = file:/services/core/java/com/android/server/wm/OWNERS
# Graphics
per-file choreographer.cpp = file:/graphics/java/android/graphics/OWNERS
diff --git a/native/android/surface_control_input_receiver.cpp b/native/android/surface_control_input_receiver.cpp
index da0defd..a84ec73 100644
--- a/native/android/surface_control_input_receiver.cpp
+++ b/native/android/surface_control_input_receiver.cpp
@@ -45,6 +45,8 @@
mClientToken(clientToken),
mInputTransferToken(inputTransferToken) {}
+ // The InputConsumer does not keep the InputReceiver alive so the receiver is cleared once the
+ // owner releases it.
~InputReceiver() {
remove();
}
@@ -190,7 +192,9 @@
void AInputReceiver_release(AInputReceiver* aInputReceiver) {
InputReceiver* inputReceiver = AInputReceiver_to_InputReceiver(aInputReceiver);
- inputReceiver->remove();
+ if (inputReceiver != nullptr) {
+ inputReceiver->remove();
+ }
delete inputReceiver;
}
diff --git a/nfc/api/current.txt b/nfc/api/current.txt
index da292a81..80b2be2 100644
--- a/nfc/api/current.txt
+++ b/nfc/api/current.txt
@@ -268,10 +268,9 @@
}
@FlaggedApi("android.nfc.nfc_read_polling_loop") public final class PollingFrame implements android.os.Parcelable {
- ctor public PollingFrame(int, @Nullable byte[], int, int, boolean);
method public int describeContents();
method @NonNull public byte[] getData();
- method public int getTimestamp();
+ method public long getTimestamp();
method public boolean getTriggeredAutoTransact();
method public int getType();
method public int getVendorSpecificGain();
diff --git a/nfc/java/android/nfc/cardemulation/ApduServiceInfo.java b/nfc/java/android/nfc/cardemulation/ApduServiceInfo.java
index be3c248..a353df7 100644
--- a/nfc/java/android/nfc/cardemulation/ApduServiceInfo.java
+++ b/nfc/java/android/nfc/cardemulation/ApduServiceInfo.java
@@ -723,6 +723,7 @@
* delivered to {@link HostApduService#processPollingFrames(List)}. Adding a key with this
* multiple times will cause the value to be overwritten each time.
* @param pollingLoopFilter the polling loop filter to add, must be a valid hexadecimal string
+ * @param autoTransact whether Observe Mode should be disabled when this filter matches or not
*/
@FlaggedApi(Flags.FLAG_NFC_READ_POLLING_LOOP)
public void addPollingLoopFilter(@NonNull String pollingLoopFilter,
@@ -747,6 +748,7 @@
* multiple times will cause the value to be overwritten each time.
* @param pollingLoopPatternFilter the polling loop pattern filter to add, must be a valid
* regex to match a hexadecimal string
+ * @param autoTransact whether Observe Mode should be disabled when this filter matches or not
*/
@FlaggedApi(Flags.FLAG_NFC_READ_POLLING_LOOP)
public void addPollingLoopPatternFilter(@NonNull String pollingLoopPatternFilter,
diff --git a/nfc/java/android/nfc/cardemulation/PollingFrame.java b/nfc/java/android/nfc/cardemulation/PollingFrame.java
index af63a6e..654e8cc 100644
--- a/nfc/java/android/nfc/cardemulation/PollingFrame.java
+++ b/nfc/java/android/nfc/cardemulation/PollingFrame.java
@@ -16,6 +16,7 @@
package android.nfc.cardemulation;
+import android.annotation.DurationMillisLong;
import android.annotation.FlaggedApi;
import android.annotation.IntDef;
import android.annotation.NonNull;
@@ -148,7 +149,8 @@
private final int mType;
private final byte[] mData;
private final int mGain;
- private final int mTimestamp;
+ @DurationMillisLong
+ private final long mTimestamp;
private final boolean mTriggeredAutoTransact;
public static final @NonNull Parcelable.Creator<PollingFrame> CREATOR =
@@ -180,16 +182,18 @@
* @param type the type of the frame
* @param data a byte array of the data contained in the frame
* @param gain the vendor-specific gain of the field
- * @param timestamp the timestamp in millisecones
+ * @param timestampMillis the timestamp in millisecones
* @param triggeredAutoTransact whether or not this frame triggered the device to start a
* transaction automatically
+ *
+ * @hide
*/
public PollingFrame(@PollingFrameType int type, @Nullable byte[] data,
- int gain, int timestamp, boolean triggeredAutoTransact) {
+ int gain, @DurationMillisLong long timestampMillis, boolean triggeredAutoTransact) {
mType = type;
mData = data == null ? new byte[0] : data;
mGain = gain;
- mTimestamp = timestamp;
+ mTimestamp = timestampMillis;
mTriggeredAutoTransact = triggeredAutoTransact;
}
@@ -230,7 +234,7 @@
* frames relative to each other.
* @return the timestamp in milliseconds
*/
- public int getTimestamp() {
+ public @DurationMillisLong long getTimestamp() {
return mTimestamp;
}
@@ -264,7 +268,7 @@
frame.putInt(KEY_POLLING_LOOP_GAIN, (byte) getVendorSpecificGain());
}
frame.putByteArray(KEY_POLLING_LOOP_DATA, getData());
- frame.putInt(KEY_POLLING_LOOP_TIMESTAMP, getTimestamp());
+ frame.putLong(KEY_POLLING_LOOP_TIMESTAMP, getTimestamp());
frame.putBoolean(KEY_POLLING_LOOP_TRIGGERED_AUTOTRANSACT, getTriggeredAutoTransact());
return frame;
}
@@ -273,7 +277,7 @@
public String toString() {
return "PollingFrame { Type: " + (char) getType()
+ ", gain: " + getVendorSpecificGain()
- + ", timestamp: " + Integer.toUnsignedString(getTimestamp())
+ + ", timestamp: " + Long.toUnsignedString(getTimestamp())
+ ", data: [" + HexFormat.ofDelimiter(" ").formatHex(getData()) + "] }";
}
}
diff --git a/nfc/java/android/nfc/flags.aconfig b/nfc/java/android/nfc/flags.aconfig
index ba084c0..6d4a17c 100644
--- a/nfc/java/android/nfc/flags.aconfig
+++ b/nfc/java/android/nfc/flags.aconfig
@@ -63,6 +63,7 @@
flag {
name: "enable_nfc_charging"
+ is_exported: true
namespace: "nfc"
description: "Flag for NFC charging changes"
bug: "292143899"
diff --git a/packages/CrashRecovery/aconfig/flags.aconfig b/packages/CrashRecovery/aconfig/flags.aconfig
index 572a669..8627eac 100644
--- a/packages/CrashRecovery/aconfig/flags.aconfig
+++ b/packages/CrashRecovery/aconfig/flags.aconfig
@@ -10,6 +10,7 @@
flag {
name: "enable_crashrecovery"
+ is_exported: true
namespace: "crashrecovery"
description: "Enables various dependencies of crashrecovery module"
bug: "289203818"
diff --git a/packages/CrashRecovery/services/java/com/android/server/PackageWatchdog.java b/packages/CrashRecovery/services/java/com/android/server/PackageWatchdog.java
index 37b5d40..a8d8f9a 100644
--- a/packages/CrashRecovery/services/java/com/android/server/PackageWatchdog.java
+++ b/packages/CrashRecovery/services/java/com/android/server/PackageWatchdog.java
@@ -26,6 +26,7 @@
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.VersionedPackage;
+import android.crashrecovery.flags.Flags;
import android.net.ConnectivityModuleConnector;
import android.os.Environment;
import android.os.Handler;
@@ -57,16 +58,20 @@
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
+import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.Collections;
+import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
@@ -130,8 +135,25 @@
@VisibleForTesting
static final int DEFAULT_BOOT_LOOP_TRIGGER_COUNT = 5;
- @VisibleForTesting
+
static final long DEFAULT_BOOT_LOOP_TRIGGER_WINDOW_MS = TimeUnit.MINUTES.toMillis(10);
+ // Boot loop at which packageWatchdog starts first mitigation
+ private static final String BOOT_LOOP_THRESHOLD =
+ "persist.device_config.configuration.boot_loop_threshold";
+ @VisibleForTesting
+ static final int DEFAULT_BOOT_LOOP_THRESHOLD = 15;
+ // Once boot_loop_threshold is surpassed next mitigation would be triggered after
+ // specified number of reboots.
+ private static final String BOOT_LOOP_MITIGATION_INCREMENT =
+ "persist.device_config.configuration..boot_loop_mitigation_increment";
+ @VisibleForTesting
+ static final int DEFAULT_BOOT_LOOP_MITIGATION_INCREMENT = 2;
+
+ // Threshold level at which or above user might experience significant disruption.
+ private static final String MAJOR_USER_IMPACT_LEVEL_THRESHOLD =
+ "persist.device_config.configuration.major_user_impact_level_threshold";
+ private static final int DEFAULT_MAJOR_USER_IMPACT_LEVEL_THRESHOLD =
+ PackageHealthObserverImpact.USER_IMPACT_LEVEL_71;
private long mNumberOfNativeCrashPollsRemaining;
@@ -145,6 +167,7 @@
private static final String ATTR_EXPLICIT_HEALTH_CHECK_DURATION = "health-check-duration";
private static final String ATTR_PASSED_HEALTH_CHECK = "passed-health-check";
private static final String ATTR_MITIGATION_CALLS = "mitigation-calls";
+ private static final String ATTR_MITIGATION_COUNT = "mitigation-count";
// A file containing information about the current mitigation count in the case of a boot loop.
// This allows boot loop information to persist in the case of an fs-checkpoint being
@@ -230,8 +253,16 @@
mConnectivityModuleConnector = connectivityModuleConnector;
mSystemClock = clock;
mNumberOfNativeCrashPollsRemaining = NUMBER_OF_NATIVE_CRASH_POLLS;
- mBootThreshold = new BootThreshold(DEFAULT_BOOT_LOOP_TRIGGER_COUNT,
- DEFAULT_BOOT_LOOP_TRIGGER_WINDOW_MS);
+ if (Flags.recoverabilityDetection()) {
+ mBootThreshold = new BootThreshold(DEFAULT_BOOT_LOOP_TRIGGER_COUNT,
+ DEFAULT_BOOT_LOOP_TRIGGER_WINDOW_MS,
+ SystemProperties.getInt(BOOT_LOOP_MITIGATION_INCREMENT,
+ DEFAULT_BOOT_LOOP_MITIGATION_INCREMENT));
+ } else {
+ mBootThreshold = new BootThreshold(DEFAULT_BOOT_LOOP_TRIGGER_COUNT,
+ DEFAULT_BOOT_LOOP_TRIGGER_WINDOW_MS);
+ }
+
loadFromFile();
sPackageWatchdog = this;
}
@@ -436,8 +467,13 @@
mitigationCount =
currentMonitoredPackage.getMitigationCountLocked();
}
- currentObserverToNotify.execute(versionedPackage,
- failureReason, mitigationCount);
+ if (Flags.recoverabilityDetection()) {
+ maybeExecute(currentObserverToNotify, versionedPackage,
+ failureReason, currentObserverImpact, mitigationCount);
+ } else {
+ currentObserverToNotify.execute(versionedPackage,
+ failureReason, mitigationCount);
+ }
}
}
}
@@ -467,37 +503,76 @@
}
}
if (currentObserverToNotify != null) {
- currentObserverToNotify.execute(failingPackage, failureReason, 1);
+ if (Flags.recoverabilityDetection()) {
+ maybeExecute(currentObserverToNotify, failingPackage, failureReason,
+ currentObserverImpact, /*mitigationCount=*/ 1);
+ } else {
+ currentObserverToNotify.execute(failingPackage, failureReason, 1);
+ }
}
}
+ private void maybeExecute(PackageHealthObserver currentObserverToNotify,
+ VersionedPackage versionedPackage,
+ @FailureReasons int failureReason,
+ int currentObserverImpact,
+ int mitigationCount) {
+ if (currentObserverImpact < getUserImpactLevelLimit()) {
+ currentObserverToNotify.execute(versionedPackage, failureReason, mitigationCount);
+ }
+ }
+
+
/**
* Called when the system server boots. If the system server is detected to be in a boot loop,
* query each observer and perform the mitigation action with the lowest user impact.
*/
+ @SuppressWarnings("GuardedBy")
public void noteBoot() {
synchronized (mLock) {
- if (mBootThreshold.incrementAndTest()) {
- mBootThreshold.reset();
+ boolean mitigate = mBootThreshold.incrementAndTest();
+ if (mitigate) {
+ if (!Flags.recoverabilityDetection()) {
+ mBootThreshold.reset();
+ }
int mitigationCount = mBootThreshold.getMitigationCount() + 1;
PackageHealthObserver currentObserverToNotify = null;
+ ObserverInternal currentObserverInternal = null;
int currentObserverImpact = Integer.MAX_VALUE;
for (int i = 0; i < mAllObservers.size(); i++) {
final ObserverInternal observer = mAllObservers.valueAt(i);
PackageHealthObserver registeredObserver = observer.registeredObserver;
if (registeredObserver != null) {
- int impact = registeredObserver.onBootLoop(mitigationCount);
+ int impact = Flags.recoverabilityDetection()
+ ? registeredObserver.onBootLoop(
+ observer.getBootMitigationCount() + 1)
+ : registeredObserver.onBootLoop(mitigationCount);
if (impact != PackageHealthObserverImpact.USER_IMPACT_LEVEL_0
&& impact < currentObserverImpact) {
currentObserverToNotify = registeredObserver;
+ currentObserverInternal = observer;
currentObserverImpact = impact;
}
}
}
if (currentObserverToNotify != null) {
- mBootThreshold.setMitigationCount(mitigationCount);
- mBootThreshold.saveMitigationCountToMetadata();
- currentObserverToNotify.executeBootLoopMitigation(mitigationCount);
+ if (Flags.recoverabilityDetection()) {
+ if (currentObserverImpact < getUserImpactLevelLimit()
+ || (currentObserverImpact >= getUserImpactLevelLimit()
+ && mBootThreshold.getCount() >= getBootLoopThreshold())) {
+ int currentObserverMitigationCount =
+ currentObserverInternal.getBootMitigationCount() + 1;
+ currentObserverInternal.setBootMitigationCount(
+ currentObserverMitigationCount);
+ saveAllObserversBootMitigationCountToMetadata(METADATA_FILE);
+ currentObserverToNotify.executeBootLoopMitigation(
+ currentObserverMitigationCount);
+ }
+ } else {
+ mBootThreshold.setMitigationCount(mitigationCount);
+ mBootThreshold.saveMitigationCountToMetadata();
+ currentObserverToNotify.executeBootLoopMitigation(mitigationCount);
+ }
}
}
}
@@ -567,13 +642,27 @@
mShortTaskHandler.post(()->checkAndMitigateNativeCrashes());
}
+ private int getUserImpactLevelLimit() {
+ return SystemProperties.getInt(MAJOR_USER_IMPACT_LEVEL_THRESHOLD,
+ DEFAULT_MAJOR_USER_IMPACT_LEVEL_THRESHOLD);
+ }
+
+ private int getBootLoopThreshold() {
+ return SystemProperties.getInt(BOOT_LOOP_THRESHOLD,
+ DEFAULT_BOOT_LOOP_THRESHOLD);
+ }
+
/** Possible severity values of the user impact of a {@link PackageHealthObserver#execute}. */
@Retention(SOURCE)
@IntDef(value = {PackageHealthObserverImpact.USER_IMPACT_LEVEL_0,
PackageHealthObserverImpact.USER_IMPACT_LEVEL_10,
+ PackageHealthObserverImpact.USER_IMPACT_LEVEL_20,
PackageHealthObserverImpact.USER_IMPACT_LEVEL_30,
PackageHealthObserverImpact.USER_IMPACT_LEVEL_50,
PackageHealthObserverImpact.USER_IMPACT_LEVEL_70,
+ PackageHealthObserverImpact.USER_IMPACT_LEVEL_71,
+ PackageHealthObserverImpact.USER_IMPACT_LEVEL_75,
+ PackageHealthObserverImpact.USER_IMPACT_LEVEL_80,
PackageHealthObserverImpact.USER_IMPACT_LEVEL_90,
PackageHealthObserverImpact.USER_IMPACT_LEVEL_100})
public @interface PackageHealthObserverImpact {
@@ -582,11 +671,15 @@
/* Action has low user impact, user of a device will barely notice. */
int USER_IMPACT_LEVEL_10 = 10;
/* Actions having medium user impact, user of a device will likely notice. */
+ int USER_IMPACT_LEVEL_20 = 20;
int USER_IMPACT_LEVEL_30 = 30;
int USER_IMPACT_LEVEL_50 = 50;
int USER_IMPACT_LEVEL_70 = 70;
- int USER_IMPACT_LEVEL_90 = 90;
/* Action has high user impact, a last resort, user of a device will be very frustrated. */
+ int USER_IMPACT_LEVEL_71 = 71;
+ int USER_IMPACT_LEVEL_75 = 75;
+ int USER_IMPACT_LEVEL_80 = 80;
+ int USER_IMPACT_LEVEL_90 = 90;
int USER_IMPACT_LEVEL_100 = 100;
}
@@ -1144,6 +1237,12 @@
}
}
+ @VisibleForTesting
+ @GuardedBy("mLock")
+ void registerObserverInternal(ObserverInternal observerInternal) {
+ mAllObservers.put(observerInternal.name, observerInternal);
+ }
+
/**
* Represents an observer monitoring a set of packages along with the failure thresholds for
* each package.
@@ -1151,17 +1250,23 @@
* <p> Note, the PackageWatchdog#mLock must always be held when reading or writing
* instances of this class.
*/
- private static class ObserverInternal {
+ static class ObserverInternal {
public final String name;
@GuardedBy("mLock")
private final ArrayMap<String, MonitoredPackage> mPackages = new ArrayMap<>();
@Nullable
@GuardedBy("mLock")
public PackageHealthObserver registeredObserver;
+ private int mMitigationCount;
ObserverInternal(String name, List<MonitoredPackage> packages) {
+ this(name, packages, /*mitigationCount=*/ 0);
+ }
+
+ ObserverInternal(String name, List<MonitoredPackage> packages, int mitigationCount) {
this.name = name;
updatePackagesLocked(packages);
+ this.mMitigationCount = mitigationCount;
}
/**
@@ -1173,6 +1278,9 @@
try {
out.startTag(null, TAG_OBSERVER);
out.attribute(null, ATTR_NAME, name);
+ if (Flags.recoverabilityDetection()) {
+ out.attributeInt(null, ATTR_MITIGATION_COUNT, mMitigationCount);
+ }
for (int i = 0; i < mPackages.size(); i++) {
MonitoredPackage p = mPackages.valueAt(i);
p.writeLocked(out);
@@ -1185,6 +1293,14 @@
}
}
+ public int getBootMitigationCount() {
+ return mMitigationCount;
+ }
+
+ public void setBootMitigationCount(int mitigationCount) {
+ mMitigationCount = mitigationCount;
+ }
+
@GuardedBy("mLock")
public void updatePackagesLocked(List<MonitoredPackage> packages) {
for (int pIndex = 0; pIndex < packages.size(); pIndex++) {
@@ -1289,6 +1405,7 @@
**/
public static ObserverInternal read(TypedXmlPullParser parser, PackageWatchdog watchdog) {
String observerName = null;
+ int observerMitigationCount = 0;
if (TAG_OBSERVER.equals(parser.getName())) {
observerName = parser.getAttributeValue(null, ATTR_NAME);
if (TextUtils.isEmpty(observerName)) {
@@ -1299,6 +1416,9 @@
List<MonitoredPackage> packages = new ArrayList<>();
int innerDepth = parser.getDepth();
try {
+ if (Flags.recoverabilityDetection()) {
+ observerMitigationCount = parser.getAttributeInt(null, ATTR_MITIGATION_COUNT);
+ }
while (XmlUtils.nextElementWithin(parser, innerDepth)) {
if (TAG_PACKAGE.equals(parser.getName())) {
try {
@@ -1319,7 +1439,7 @@
if (packages.isEmpty()) {
return null;
}
- return new ObserverInternal(observerName, packages);
+ return new ObserverInternal(observerName, packages, observerMitigationCount);
}
/** Dumps information about this observer and the packages it watches. */
@@ -1679,6 +1799,27 @@
}
}
+ @GuardedBy("mLock")
+ @SuppressWarnings("GuardedBy")
+ void saveAllObserversBootMitigationCountToMetadata(String filePath) {
+ HashMap<String, Integer> bootMitigationCounts = new HashMap<>();
+ for (int i = 0; i < mAllObservers.size(); i++) {
+ final ObserverInternal observer = mAllObservers.valueAt(i);
+ bootMitigationCounts.put(observer.name, observer.getBootMitigationCount());
+ }
+
+ try {
+ FileOutputStream fileStream = new FileOutputStream(new File(filePath));
+ ObjectOutputStream objectStream = new ObjectOutputStream(fileStream);
+ objectStream.writeObject(bootMitigationCounts);
+ objectStream.flush();
+ objectStream.close();
+ fileStream.close();
+ } catch (Exception e) {
+ Slog.i(TAG, "Could not save observers metadata to file: " + e);
+ }
+ }
+
/**
* Handles the thresholding logic for system server boots.
*/
@@ -1686,10 +1827,16 @@
private final int mBootTriggerCount;
private final long mTriggerWindow;
+ private final int mBootMitigationIncrement;
BootThreshold(int bootTriggerCount, long triggerWindow) {
+ this(bootTriggerCount, triggerWindow, /*bootMitigationIncrement=*/ 1);
+ }
+
+ BootThreshold(int bootTriggerCount, long triggerWindow, int bootMitigationIncrement) {
this.mBootTriggerCount = bootTriggerCount;
this.mTriggerWindow = triggerWindow;
+ this.mBootMitigationIncrement = bootMitigationIncrement;
}
public void reset() {
@@ -1761,8 +1908,13 @@
/** Increments the boot counter, and returns whether the device is bootlooping. */
+ @GuardedBy("mLock")
public boolean incrementAndTest() {
- readMitigationCountFromMetadataIfNecessary();
+ if (Flags.recoverabilityDetection()) {
+ readAllObserversBootMitigationCountIfNecessary(METADATA_FILE);
+ } else {
+ readMitigationCountFromMetadataIfNecessary();
+ }
final long now = mSystemClock.uptimeMillis();
if (now - getStart() < 0) {
Slog.e(TAG, "Window was less than zero. Resetting start to current time.");
@@ -1770,8 +1922,12 @@
setMitigationStart(now);
}
if (now - getMitigationStart() > DEFAULT_DEESCALATION_WINDOW_MS) {
- setMitigationCount(0);
setMitigationStart(now);
+ if (Flags.recoverabilityDetection()) {
+ resetAllObserversBootMitigationCount();
+ } else {
+ setMitigationCount(0);
+ }
}
final long window = now - getStart();
if (window >= mTriggerWindow) {
@@ -1782,9 +1938,48 @@
int count = getCount() + 1;
setCount(count);
EventLogTags.writeRescueNote(Process.ROOT_UID, count, window);
+ if (Flags.recoverabilityDetection()) {
+ boolean mitigate = (count >= mBootTriggerCount)
+ && (count - mBootTriggerCount) % mBootMitigationIncrement == 0;
+ return mitigate;
+ }
return count >= mBootTriggerCount;
}
}
+ @GuardedBy("mLock")
+ private void resetAllObserversBootMitigationCount() {
+ for (int i = 0; i < mAllObservers.size(); i++) {
+ final ObserverInternal observer = mAllObservers.valueAt(i);
+ observer.setBootMitigationCount(0);
+ }
+ }
+
+ @GuardedBy("mLock")
+ @SuppressWarnings("GuardedBy")
+ void readAllObserversBootMitigationCountIfNecessary(String filePath) {
+ File metadataFile = new File(filePath);
+ if (metadataFile.exists()) {
+ try {
+ FileInputStream fileStream = new FileInputStream(metadataFile);
+ ObjectInputStream objectStream = new ObjectInputStream(fileStream);
+ HashMap<String, Integer> bootMitigationCounts =
+ (HashMap<String, Integer>) objectStream.readObject();
+ objectStream.close();
+ fileStream.close();
+
+ for (int i = 0; i < mAllObservers.size(); i++) {
+ final ObserverInternal observer = mAllObservers.valueAt(i);
+ if (bootMitigationCounts.containsKey(observer.name)) {
+ observer.setBootMitigationCount(
+ bootMitigationCounts.get(observer.name));
+ }
+ }
+ } catch (Exception e) {
+ Slog.i(TAG, "Could not read observer metadata file: " + e);
+ }
+ }
+ }
+
}
}
diff --git a/packages/CrashRecovery/services/java/com/android/server/RescueParty.java b/packages/CrashRecovery/services/java/com/android/server/RescueParty.java
index 7bdc1a0..7093ba4 100644
--- a/packages/CrashRecovery/services/java/com/android/server/RescueParty.java
+++ b/packages/CrashRecovery/services/java/com/android/server/RescueParty.java
@@ -20,6 +20,7 @@
import static com.android.server.pm.PackageManagerServiceUtils.logCriticalInfo;
+import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.ContentResolver;
@@ -27,6 +28,7 @@
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.VersionedPackage;
+import android.crashrecovery.flags.Flags;
import android.os.Build;
import android.os.Environment;
import android.os.PowerManager;
@@ -53,6 +55,8 @@
import com.android.server.crashrecovery.proto.CrashRecoveryStatsLog;
import java.io.File;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
@@ -89,6 +93,40 @@
@VisibleForTesting
static final int LEVEL_FACTORY_RESET = 5;
@VisibleForTesting
+ static final int RESCUE_LEVEL_NONE = 0;
+ @VisibleForTesting
+ static final int RESCUE_LEVEL_SCOPED_DEVICE_CONFIG_RESET = 1;
+ @VisibleForTesting
+ static final int RESCUE_LEVEL_ALL_DEVICE_CONFIG_RESET = 2;
+ @VisibleForTesting
+ static final int RESCUE_LEVEL_WARM_REBOOT = 3;
+ @VisibleForTesting
+ static final int RESCUE_LEVEL_RESET_SETTINGS_UNTRUSTED_DEFAULTS = 4;
+ @VisibleForTesting
+ static final int RESCUE_LEVEL_RESET_SETTINGS_UNTRUSTED_CHANGES = 5;
+ @VisibleForTesting
+ static final int RESCUE_LEVEL_RESET_SETTINGS_TRUSTED_DEFAULTS = 6;
+ @VisibleForTesting
+ static final int RESCUE_LEVEL_FACTORY_RESET = 7;
+
+ @IntDef(prefix = { "RESCUE_LEVEL_" }, value = {
+ RESCUE_LEVEL_NONE,
+ RESCUE_LEVEL_SCOPED_DEVICE_CONFIG_RESET,
+ RESCUE_LEVEL_ALL_DEVICE_CONFIG_RESET,
+ RESCUE_LEVEL_WARM_REBOOT,
+ RESCUE_LEVEL_RESET_SETTINGS_UNTRUSTED_DEFAULTS,
+ RESCUE_LEVEL_RESET_SETTINGS_UNTRUSTED_CHANGES,
+ RESCUE_LEVEL_RESET_SETTINGS_TRUSTED_DEFAULTS,
+ RESCUE_LEVEL_FACTORY_RESET
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ @interface RescueLevels {}
+
+ @VisibleForTesting
+ static final String RESCUE_NON_REBOOT_LEVEL_LIMIT = "persist.sys.rescue_non_reboot_level_limit";
+ @VisibleForTesting
+ static final int DEFAULT_RESCUE_NON_REBOOT_LEVEL_LIMIT = RESCUE_LEVEL_WARM_REBOOT - 1;
+ @VisibleForTesting
static final String TAG = "RescueParty";
@VisibleForTesting
static final long DEFAULT_OBSERVING_DURATION_MS = TimeUnit.DAYS.toMillis(2);
@@ -347,11 +385,20 @@
}
private static int getMaxRescueLevel(boolean mayPerformReboot) {
- if (!mayPerformReboot
- || SystemProperties.getBoolean(PROP_DISABLE_FACTORY_RESET_FLAG, false)) {
- return LEVEL_RESET_SETTINGS_TRUSTED_DEFAULTS;
+ if (Flags.recoverabilityDetection()) {
+ if (!mayPerformReboot
+ || SystemProperties.getBoolean(PROP_DISABLE_FACTORY_RESET_FLAG, false)) {
+ return SystemProperties.getInt(RESCUE_NON_REBOOT_LEVEL_LIMIT,
+ DEFAULT_RESCUE_NON_REBOOT_LEVEL_LIMIT);
+ }
+ return RESCUE_LEVEL_FACTORY_RESET;
+ } else {
+ if (!mayPerformReboot
+ || SystemProperties.getBoolean(PROP_DISABLE_FACTORY_RESET_FLAG, false)) {
+ return LEVEL_RESET_SETTINGS_TRUSTED_DEFAULTS;
+ }
+ return LEVEL_FACTORY_RESET;
}
- return LEVEL_FACTORY_RESET;
}
/**
@@ -379,6 +426,46 @@
}
}
+ /**
+ * Get the rescue level to perform if this is the n-th attempt at mitigating failure.
+ * When failedPackage is null then 1st and 2nd mitigation counts are redundant (scoped and
+ * all device config reset). Behaves as if one mitigation attempt was already done.
+ *
+ * @param mitigationCount the mitigation attempt number (1 = first attempt etc.).
+ * @param mayPerformReboot whether or not a reboot and factory reset may be performed
+ * for the given failure.
+ * @param failedPackage in case of bootloop this is null.
+ * @return the rescue level for the n-th mitigation attempt.
+ */
+ private static @RescueLevels int getRescueLevel(int mitigationCount, boolean mayPerformReboot,
+ @Nullable VersionedPackage failedPackage) {
+ // Skipping RESCUE_LEVEL_SCOPED_DEVICE_CONFIG_RESET since it's not defined without a failed
+ // package.
+ if (failedPackage == null && mitigationCount > 0) {
+ mitigationCount += 1;
+ }
+ if (mitigationCount == 1) {
+ return RESCUE_LEVEL_SCOPED_DEVICE_CONFIG_RESET;
+ } else if (mitigationCount == 2) {
+ return RESCUE_LEVEL_ALL_DEVICE_CONFIG_RESET;
+ } else if (mitigationCount == 3) {
+ return Math.min(getMaxRescueLevel(mayPerformReboot), RESCUE_LEVEL_WARM_REBOOT);
+ } else if (mitigationCount == 4) {
+ return Math.min(getMaxRescueLevel(mayPerformReboot),
+ RESCUE_LEVEL_RESET_SETTINGS_UNTRUSTED_DEFAULTS);
+ } else if (mitigationCount == 5) {
+ return Math.min(getMaxRescueLevel(mayPerformReboot),
+ RESCUE_LEVEL_RESET_SETTINGS_UNTRUSTED_CHANGES);
+ } else if (mitigationCount == 6) {
+ return Math.min(getMaxRescueLevel(mayPerformReboot),
+ RESCUE_LEVEL_RESET_SETTINGS_TRUSTED_DEFAULTS);
+ } else if (mitigationCount >= 7) {
+ return Math.min(getMaxRescueLevel(mayPerformReboot), RESCUE_LEVEL_FACTORY_RESET);
+ } else {
+ return RESCUE_LEVEL_NONE;
+ }
+ }
+
private static void executeRescueLevel(Context context, @Nullable String failedPackage,
int level) {
Slog.w(TAG, "Attempting rescue level " + levelToString(level));
@@ -397,6 +484,15 @@
private static void executeRescueLevelInternal(Context context, int level, @Nullable
String failedPackage) throws Exception {
+ if (Flags.recoverabilityDetection()) {
+ executeRescueLevelInternalNew(context, level, failedPackage);
+ } else {
+ executeRescueLevelInternalOld(context, level, failedPackage);
+ }
+ }
+
+ private static void executeRescueLevelInternalOld(Context context, int level, @Nullable
+ String failedPackage) throws Exception {
if (level <= LEVEL_RESET_SETTINGS_TRUSTED_DEFAULTS) {
// Disabling flag resets on master branch for trunk stable launch.
@@ -410,8 +506,6 @@
// Try our best to reset all settings possible, and once finished
// rethrow any exception that we encountered
Exception res = null;
- Runnable runnable;
- Thread thread;
switch (level) {
case LEVEL_RESET_SETTINGS_UNTRUSTED_DEFAULTS:
try {
@@ -453,21 +547,7 @@
}
break;
case LEVEL_WARM_REBOOT:
- // Request the reboot from a separate thread to avoid deadlock on PackageWatchdog
- // when device shutting down.
- setRebootProperty(true);
- runnable = () -> {
- try {
- PowerManager pm = context.getSystemService(PowerManager.class);
- if (pm != null) {
- pm.reboot(TAG);
- }
- } catch (Throwable t) {
- logRescueException(level, failedPackage, t);
- }
- };
- thread = new Thread(runnable);
- thread.start();
+ executeWarmReboot(context, level, failedPackage);
break;
case LEVEL_FACTORY_RESET:
// Before the completion of Reboot, if any crash happens then PackageWatchdog
@@ -475,23 +555,9 @@
// Adding a check to prevent factory reset to execute before above reboot completes.
// Note: this reboot property is not persistent resets after reboot is completed.
if (isRebootPropertySet()) {
- break;
+ return;
}
- setFactoryResetProperty(true);
- long now = System.currentTimeMillis();
- setLastFactoryResetTimeMs(now);
- runnable = new Runnable() {
- @Override
- public void run() {
- try {
- RecoverySystem.rebootPromptAndWipeUserData(context, TAG);
- } catch (Throwable t) {
- logRescueException(level, failedPackage, t);
- }
- }
- };
- thread = new Thread(runnable);
- thread.start();
+ executeFactoryReset(context, level, failedPackage);
break;
}
@@ -500,6 +566,83 @@
}
}
+ private static void executeRescueLevelInternalNew(Context context, @RescueLevels int level,
+ @Nullable String failedPackage) throws Exception {
+ CrashRecoveryStatsLog.write(CrashRecoveryStatsLog.RESCUE_PARTY_RESET_REPORTED,
+ level, levelToString(level));
+ switch (level) {
+ case RESCUE_LEVEL_SCOPED_DEVICE_CONFIG_RESET:
+ // Temporary disable deviceConfig reset
+ // resetDeviceConfig(context, /*isScoped=*/true, failedPackage);
+ break;
+ case RESCUE_LEVEL_ALL_DEVICE_CONFIG_RESET:
+ // Temporary disable deviceConfig reset
+ // resetDeviceConfig(context, /*isScoped=*/false, failedPackage);
+ break;
+ case RESCUE_LEVEL_WARM_REBOOT:
+ executeWarmReboot(context, level, failedPackage);
+ break;
+ case RESCUE_LEVEL_RESET_SETTINGS_UNTRUSTED_DEFAULTS:
+ resetAllSettingsIfNecessary(context, Settings.RESET_MODE_UNTRUSTED_DEFAULTS, level);
+ break;
+ case RESCUE_LEVEL_RESET_SETTINGS_UNTRUSTED_CHANGES:
+ resetAllSettingsIfNecessary(context, Settings.RESET_MODE_UNTRUSTED_CHANGES, level);
+ break;
+ case RESCUE_LEVEL_RESET_SETTINGS_TRUSTED_DEFAULTS:
+ resetAllSettingsIfNecessary(context, Settings.RESET_MODE_TRUSTED_DEFAULTS, level);
+ break;
+ case RESCUE_LEVEL_FACTORY_RESET:
+ // Before the completion of Reboot, if any crash happens then PackageWatchdog
+ // escalates to next level i.e. factory reset, as they happen in separate threads.
+ // Adding a check to prevent factory reset to execute before above reboot completes.
+ // Note: this reboot property is not persistent resets after reboot is completed.
+ if (isRebootPropertySet()) {
+ return;
+ }
+ executeFactoryReset(context, level, failedPackage);
+ break;
+ }
+ }
+
+ private static void executeWarmReboot(Context context, int level,
+ @Nullable String failedPackage) {
+ // Request the reboot from a separate thread to avoid deadlock on PackageWatchdog
+ // when device shutting down.
+ setRebootProperty(true);
+ Runnable runnable = () -> {
+ try {
+ PowerManager pm = context.getSystemService(PowerManager.class);
+ if (pm != null) {
+ pm.reboot(TAG);
+ }
+ } catch (Throwable t) {
+ logRescueException(level, failedPackage, t);
+ }
+ };
+ Thread thread = new Thread(runnable);
+ thread.start();
+ }
+
+ private static void executeFactoryReset(Context context, int level,
+ @Nullable String failedPackage) {
+ setFactoryResetProperty(true);
+ long now = System.currentTimeMillis();
+ setLastFactoryResetTimeMs(now);
+ Runnable runnable = new Runnable() {
+ @Override
+ public void run() {
+ try {
+ RecoverySystem.rebootPromptAndWipeUserData(context, TAG);
+ } catch (Throwable t) {
+ logRescueException(level, failedPackage, t);
+ }
+ }
+ };
+ Thread thread = new Thread(runnable);
+ thread.start();
+ }
+
+
private static String getCompleteMessage(Throwable t) {
final StringBuilder builder = new StringBuilder();
builder.append(t.getMessage());
@@ -521,17 +664,38 @@
}
private static int mapRescueLevelToUserImpact(int rescueLevel) {
- switch(rescueLevel) {
- case LEVEL_RESET_SETTINGS_UNTRUSTED_DEFAULTS:
- case LEVEL_RESET_SETTINGS_UNTRUSTED_CHANGES:
- return PackageHealthObserverImpact.USER_IMPACT_LEVEL_10;
- case LEVEL_RESET_SETTINGS_TRUSTED_DEFAULTS:
- case LEVEL_WARM_REBOOT:
- return PackageHealthObserverImpact.USER_IMPACT_LEVEL_50;
- case LEVEL_FACTORY_RESET:
- return PackageHealthObserverImpact.USER_IMPACT_LEVEL_100;
- default:
- return PackageHealthObserverImpact.USER_IMPACT_LEVEL_0;
+ if (Flags.recoverabilityDetection()) {
+ switch (rescueLevel) {
+ case RESCUE_LEVEL_SCOPED_DEVICE_CONFIG_RESET:
+ return PackageHealthObserverImpact.USER_IMPACT_LEVEL_10;
+ case RESCUE_LEVEL_ALL_DEVICE_CONFIG_RESET:
+ return PackageHealthObserverImpact.USER_IMPACT_LEVEL_20;
+ case RESCUE_LEVEL_WARM_REBOOT:
+ return PackageHealthObserverImpact.USER_IMPACT_LEVEL_50;
+ case RESCUE_LEVEL_RESET_SETTINGS_UNTRUSTED_DEFAULTS:
+ return PackageHealthObserverImpact.USER_IMPACT_LEVEL_71;
+ case RESCUE_LEVEL_RESET_SETTINGS_UNTRUSTED_CHANGES:
+ return PackageHealthObserverImpact.USER_IMPACT_LEVEL_75;
+ case RESCUE_LEVEL_RESET_SETTINGS_TRUSTED_DEFAULTS:
+ return PackageHealthObserverImpact.USER_IMPACT_LEVEL_80;
+ case RESCUE_LEVEL_FACTORY_RESET:
+ return PackageHealthObserverImpact.USER_IMPACT_LEVEL_100;
+ default:
+ return PackageHealthObserverImpact.USER_IMPACT_LEVEL_0;
+ }
+ } else {
+ switch (rescueLevel) {
+ case LEVEL_RESET_SETTINGS_UNTRUSTED_DEFAULTS:
+ case LEVEL_RESET_SETTINGS_UNTRUSTED_CHANGES:
+ return PackageHealthObserverImpact.USER_IMPACT_LEVEL_10;
+ case LEVEL_RESET_SETTINGS_TRUSTED_DEFAULTS:
+ case LEVEL_WARM_REBOOT:
+ return PackageHealthObserverImpact.USER_IMPACT_LEVEL_50;
+ case LEVEL_FACTORY_RESET:
+ return PackageHealthObserverImpact.USER_IMPACT_LEVEL_100;
+ default:
+ return PackageHealthObserverImpact.USER_IMPACT_LEVEL_0;
+ }
}
}
@@ -548,7 +712,7 @@
final ContentResolver resolver = context.getContentResolver();
try {
Settings.Global.resetToDefaultsAsUser(resolver, null, mode,
- UserHandle.SYSTEM.getIdentifier());
+ UserHandle.SYSTEM.getIdentifier());
} catch (Exception e) {
res = new RuntimeException("Failed to reset global settings", e);
}
@@ -667,8 +831,13 @@
@FailureReasons int failureReason, int mitigationCount) {
if (!isDisabled() && (failureReason == PackageWatchdog.FAILURE_REASON_APP_CRASH
|| failureReason == PackageWatchdog.FAILURE_REASON_APP_NOT_RESPONDING)) {
- return mapRescueLevelToUserImpact(getRescueLevel(mitigationCount,
+ if (Flags.recoverabilityDetection()) {
+ return mapRescueLevelToUserImpact(getRescueLevel(mitigationCount,
+ mayPerformReboot(failedPackage), failedPackage));
+ } else {
+ return mapRescueLevelToUserImpact(getRescueLevel(mitigationCount,
mayPerformReboot(failedPackage)));
+ }
} else {
return PackageHealthObserverImpact.USER_IMPACT_LEVEL_0;
}
@@ -682,8 +851,10 @@
}
if (failureReason == PackageWatchdog.FAILURE_REASON_APP_CRASH
|| failureReason == PackageWatchdog.FAILURE_REASON_APP_NOT_RESPONDING) {
- final int level = getRescueLevel(mitigationCount,
- mayPerformReboot(failedPackage));
+ final int level = Flags.recoverabilityDetection() ? getRescueLevel(mitigationCount,
+ mayPerformReboot(failedPackage), failedPackage)
+ : getRescueLevel(mitigationCount,
+ mayPerformReboot(failedPackage));
executeRescueLevel(mContext,
failedPackage == null ? null : failedPackage.getPackageName(), level);
return true;
@@ -716,7 +887,12 @@
if (isDisabled()) {
return PackageHealthObserverImpact.USER_IMPACT_LEVEL_0;
}
- return mapRescueLevelToUserImpact(getRescueLevel(mitigationCount, true));
+ if (Flags.recoverabilityDetection()) {
+ return mapRescueLevelToUserImpact(getRescueLevel(mitigationCount,
+ true, /*failedPackage=*/ null));
+ } else {
+ return mapRescueLevelToUserImpact(getRescueLevel(mitigationCount, true));
+ }
}
@Override
@@ -725,8 +901,10 @@
return false;
}
boolean mayPerformReboot = !shouldThrottleReboot();
- executeRescueLevel(mContext, /*failedPackage=*/ null,
- getRescueLevel(mitigationCount, mayPerformReboot));
+ final int level = Flags.recoverabilityDetection() ? getRescueLevel(mitigationCount,
+ mayPerformReboot, /*failedPackage=*/ null)
+ : getRescueLevel(mitigationCount, mayPerformReboot);
+ executeRescueLevel(mContext, /*failedPackage=*/ null, level);
return true;
}
@@ -843,14 +1021,44 @@
}
private static String levelToString(int level) {
- switch (level) {
- case LEVEL_NONE: return "NONE";
- case LEVEL_RESET_SETTINGS_UNTRUSTED_DEFAULTS: return "RESET_SETTINGS_UNTRUSTED_DEFAULTS";
- case LEVEL_RESET_SETTINGS_UNTRUSTED_CHANGES: return "RESET_SETTINGS_UNTRUSTED_CHANGES";
- case LEVEL_RESET_SETTINGS_TRUSTED_DEFAULTS: return "RESET_SETTINGS_TRUSTED_DEFAULTS";
- case LEVEL_WARM_REBOOT: return "WARM_REBOOT";
- case LEVEL_FACTORY_RESET: return "FACTORY_RESET";
- default: return Integer.toString(level);
+ if (Flags.recoverabilityDetection()) {
+ switch (level) {
+ case RESCUE_LEVEL_NONE:
+ return "NONE";
+ case RESCUE_LEVEL_SCOPED_DEVICE_CONFIG_RESET:
+ return "SCOPED_DEVICE_CONFIG_RESET";
+ case RESCUE_LEVEL_ALL_DEVICE_CONFIG_RESET:
+ return "ALL_DEVICE_CONFIG_RESET";
+ case RESCUE_LEVEL_WARM_REBOOT:
+ return "WARM_REBOOT";
+ case RESCUE_LEVEL_RESET_SETTINGS_UNTRUSTED_DEFAULTS:
+ return "RESET_SETTINGS_UNTRUSTED_DEFAULTS";
+ case RESCUE_LEVEL_RESET_SETTINGS_UNTRUSTED_CHANGES:
+ return "RESET_SETTINGS_UNTRUSTED_CHANGES";
+ case RESCUE_LEVEL_RESET_SETTINGS_TRUSTED_DEFAULTS:
+ return "RESET_SETTINGS_TRUSTED_DEFAULTS";
+ case RESCUE_LEVEL_FACTORY_RESET:
+ return "FACTORY_RESET";
+ default:
+ return Integer.toString(level);
+ }
+ } else {
+ switch (level) {
+ case LEVEL_NONE:
+ return "NONE";
+ case LEVEL_RESET_SETTINGS_UNTRUSTED_DEFAULTS:
+ return "RESET_SETTINGS_UNTRUSTED_DEFAULTS";
+ case LEVEL_RESET_SETTINGS_UNTRUSTED_CHANGES:
+ return "RESET_SETTINGS_UNTRUSTED_CHANGES";
+ case LEVEL_RESET_SETTINGS_TRUSTED_DEFAULTS:
+ return "RESET_SETTINGS_TRUSTED_DEFAULTS";
+ case LEVEL_WARM_REBOOT:
+ return "WARM_REBOOT";
+ case LEVEL_FACTORY_RESET:
+ return "FACTORY_RESET";
+ default:
+ return Integer.toString(level);
+ }
}
}
}
diff --git a/packages/CrashRecovery/services/java/com/android/server/rollback/RollbackPackageHealthObserver.java b/packages/CrashRecovery/services/java/com/android/server/rollback/RollbackPackageHealthObserver.java
index 0fb9327..93f26ae 100644
--- a/packages/CrashRecovery/services/java/com/android/server/rollback/RollbackPackageHealthObserver.java
+++ b/packages/CrashRecovery/services/java/com/android/server/rollback/RollbackPackageHealthObserver.java
@@ -69,7 +69,7 @@
*
* @hide
*/
-final class RollbackPackageHealthObserver implements PackageHealthObserver {
+public final class RollbackPackageHealthObserver implements PackageHealthObserver {
private static final String TAG = "RollbackPackageHealthObserver";
private static final String NAME = "rollback-observer";
private static final int PERSISTENT_MASK = ApplicationInfo.FLAG_PERSISTENT
@@ -89,7 +89,7 @@
private boolean mTwoPhaseRollbackEnabled;
@VisibleForTesting
- RollbackPackageHealthObserver(Context context, ApexManager apexManager) {
+ public RollbackPackageHealthObserver(Context context, ApexManager apexManager) {
mContext = context;
HandlerThread handlerThread = new HandlerThread("RollbackPackageHealthObserver");
handlerThread.start();
diff --git a/packages/CredentialManager/res/layout/credman_dropdown_bottom_sheet.xml b/packages/CredentialManager/res/layout/credman_dropdown_bottom_sheet.xml
index 7f09dd5..914987a 100644
--- a/packages/CredentialManager/res/layout/credman_dropdown_bottom_sheet.xml
+++ b/packages/CredentialManager/res/layout/credman_dropdown_bottom_sheet.xml
@@ -33,10 +33,9 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
- android:paddingLeft="@dimen/autofill_view_left_padding"
+ android:paddingStart="@dimen/autofill_view_left_padding"
android:src="@drawable/more_horiz_24px"
android:tint="?androidprv:attr/materialColorOnSurface"
- android:layout_alignParentStart="true"
android:contentDescription="@string/more_options_content_description"
android:background="@null"/>
@@ -44,8 +43,8 @@
android:id="@+id/text_container"
android:layout_width="@dimen/autofill_dropdown_textview_max_width"
android:layout_height="wrap_content"
- android:paddingLeft="@dimen/autofill_view_left_padding"
- android:paddingRight="@dimen/autofill_view_right_padding"
+ android:paddingStart="@dimen/autofill_view_left_padding"
+ android:paddingEnd="@dimen/autofill_view_right_padding"
android:paddingTop="@dimen/more_options_item_vertical_padding"
android:paddingBottom="@dimen/more_options_item_vertical_padding"
android:orientation="vertical">
@@ -54,9 +53,7 @@
android:id="@android:id/text1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:layout_alignParentTop="true"
android:textColor="?androidprv:attr/materialColorOnSurface"
- android:layout_toEndOf="@android:id/icon1"
style="@style/autofill.TextTitle"/>
</LinearLayout>
diff --git a/packages/CredentialManager/res/layout/credman_dropdown_presentation_layout.xml b/packages/CredentialManager/res/layout/credman_dropdown_presentation_layout.xml
index 08948d7..e998fe8 100644
--- a/packages/CredentialManager/res/layout/credman_dropdown_presentation_layout.xml
+++ b/packages/CredentialManager/res/layout/credman_dropdown_presentation_layout.xml
@@ -42,8 +42,8 @@
android:id="@+id/text_container"
android:layout_width="@dimen/autofill_dropdown_textview_max_width"
android:layout_height="wrap_content"
- android:paddingLeft="@dimen/autofill_view_left_padding"
- android:paddingRight="@dimen/autofill_view_right_padding"
+ android:paddingStart="@dimen/autofill_view_left_padding"
+ android:paddingEnd="@dimen/autofill_view_right_padding"
android:paddingTop="@dimen/autofill_view_top_padding"
android:paddingBottom="@dimen/autofill_view_bottom_padding"
android:orientation="vertical">
@@ -52,8 +52,6 @@
android:id="@android:id/text1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:layout_alignParentTop="true"
- android:layout_toEndOf="@android:id/icon1"
android:textColor="?androidprv:attr/materialColorOnSurface"
style="@style/autofill.TextTitle"/>
@@ -61,8 +59,6 @@
android:id="@android:id/text2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:layout_below="@android:id/text1"
- android:layout_toEndOf="@android:id/icon1"
android:textColor="?androidprv:attr/materialColorOnSurfaceVariant"
style="@style/autofill.TextSubtitle"/>
diff --git a/packages/CredentialManager/shared/AndroidManifest.xml b/packages/CredentialManager/shared/AndroidManifest.xml
index a460887..51c7fb6 100644
--- a/packages/CredentialManager/shared/AndroidManifest.xml
+++ b/packages/CredentialManager/shared/AndroidManifest.xml
@@ -17,6 +17,6 @@
*/
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
- package="com.android.credentialmanager">
+ package="com.android.credentialmanager.shared">
</manifest>
diff --git a/packages/CredentialManager/shared/src/com/android/credentialmanager/ktx/CredentialKtx.kt b/packages/CredentialManager/shared/src/com/android/credentialmanager/ktx/CredentialKtx.kt
index 892eabf..12cb7ff 100644
--- a/packages/CredentialManager/shared/src/com/android/credentialmanager/ktx/CredentialKtx.kt
+++ b/packages/CredentialManager/shared/src/com/android/credentialmanager/ktx/CredentialKtx.kt
@@ -40,13 +40,13 @@
import androidx.credentials.provider.PublicKeyCredentialEntry
import androidx.credentials.provider.RemoteEntry
import com.android.credentialmanager.IS_AUTO_SELECTED_KEY
-import com.android.credentialmanager.R
import com.android.credentialmanager.model.get.ActionEntryInfo
import com.android.credentialmanager.model.get.AuthenticationEntryInfo
import com.android.credentialmanager.model.get.CredentialEntryInfo
import com.android.credentialmanager.model.CredentialType
import com.android.credentialmanager.model.get.ProviderInfo
import com.android.credentialmanager.model.get.RemoteEntryInfo
+import com.android.credentialmanager.shared.R
import com.android.credentialmanager.TAG
import com.android.credentialmanager.model.EntryInfo
@@ -386,4 +386,4 @@
PackageManager.PackageInfoFlags.of(
(packageManagerFlags).toLong())
)
-}
\ No newline at end of file
+}
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/Entry.kt b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/Entry.kt
index 99a9409..d13d86f 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/Entry.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/Entry.kt
@@ -305,10 +305,14 @@
modifier = Modifier.fillMaxWidth()
) {
if (leftButton != null) {
- leftButton()
+ Box(modifier = Modifier.wrapContentSize().weight(1f, fill = false)) {
+ leftButton()
+ }
}
if (rightButton != null) {
- rightButton()
+ Box(modifier = Modifier.wrapContentSize().weight(1f, fill = false)) {
+ rightButton()
+ }
}
}
}
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/RemoteViewsFactory.kt b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/RemoteViewsFactory.kt
index a46e358..3fb91522 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/RemoteViewsFactory.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/RemoteViewsFactory.kt
@@ -17,7 +17,6 @@
package com.android.credentialmanager.common.ui
import android.content.Context
-import android.content.res.Configuration
import android.widget.RemoteViews
import androidx.core.content.ContextCompat
import com.android.credentialmanager.model.get.CredentialEntryInfo
@@ -27,10 +26,12 @@
class RemoteViewsFactory {
companion object {
- private const val setAdjustViewBoundsMethodName = "setAdjustViewBounds"
- private const val setMaxHeightMethodName = "setMaxHeight"
- private const val setBackgroundResourceMethodName = "setBackgroundResource"
- private const val bulletPoint = "\u2022"
+ private const val SET_ADJUST_VIEW_BOUNDS_METHOD_NAME = "setAdjustViewBounds"
+ private const val SET_MAX_HEIGHT_METHOD_NAME = "setMaxHeight"
+ private const val SET_BACKGROUND_RESOURCE_METHOD_NAME = "setBackgroundResource"
+ private const val BULLET_POINT = "\u2022"
+ // TODO(jbabs): RemoteViews#setViewPadding renders this as 8dp on the display. Debug why.
+ private const val END_ITEMS_PADDING = 28
fun createDropdownPresentation(
context: Context,
@@ -50,18 +51,18 @@
val secondaryText =
if (credentialEntryInfo.displayName != null
&& (credentialEntryInfo.displayName != credentialEntryInfo.userName))
- (credentialEntryInfo.userName + " " + bulletPoint + " "
+ (credentialEntryInfo.userName + " " + BULLET_POINT + " "
+ credentialEntryInfo.credentialTypeDisplayName
- + " " + bulletPoint + " " + credentialEntryInfo.providerDisplayName)
- else (credentialEntryInfo.credentialTypeDisplayName + " " + bulletPoint + " "
+ + " " + BULLET_POINT + " " + credentialEntryInfo.providerDisplayName)
+ else (credentialEntryInfo.credentialTypeDisplayName + " " + BULLET_POINT + " "
+ credentialEntryInfo.providerDisplayName)
remoteViews.setTextViewText(android.R.id.text2, secondaryText)
remoteViews.setImageViewIcon(android.R.id.icon1, icon);
remoteViews.setBoolean(
- android.R.id.icon1, setAdjustViewBoundsMethodName, true);
+ android.R.id.icon1, SET_ADJUST_VIEW_BOUNDS_METHOD_NAME, true);
remoteViews.setInt(
android.R.id.icon1,
- setMaxHeightMethodName,
+ SET_MAX_HEIGHT_METHOD_NAME,
context.resources.getDimensionPixelSize(
com.android.credentialmanager.R.dimen.autofill_icon_size));
remoteViews.setContentDescription(android.R.id.icon1, credentialEntryInfo
@@ -71,11 +72,11 @@
com.android.credentialmanager.R.drawable.fill_dialog_dynamic_list_item_one else
com.android.credentialmanager.R.drawable.fill_dialog_dynamic_list_item_middle
remoteViews.setInt(
- android.R.id.content, setBackgroundResourceMethodName, drawableId);
+ android.R.id.content, SET_BACKGROUND_RESOURCE_METHOD_NAME, drawableId);
if (isFirstEntry) remoteViews.setViewPadding(
com.android.credentialmanager.R.id.credential_card,
/* left=*/0,
- /* top=*/8,
+ /* top=*/END_ITEMS_PADDING,
/* right=*/0,
/* bottom=*/0)
if (isLastEntry) remoteViews.setViewPadding(
@@ -83,7 +84,7 @@
/*left=*/0,
/* top=*/0,
/* right=*/0,
- /* bottom=*/8)
+ /* bottom=*/END_ITEMS_PADDING)
return remoteViews
}
@@ -95,16 +96,16 @@
com.android.credentialmanager
.R.string.dropdown_presentation_more_sign_in_options_text))
remoteViews.setBoolean(
- android.R.id.icon1, setAdjustViewBoundsMethodName, true);
+ android.R.id.icon1, SET_ADJUST_VIEW_BOUNDS_METHOD_NAME, true);
remoteViews.setInt(
android.R.id.icon1,
- setMaxHeightMethodName,
+ SET_MAX_HEIGHT_METHOD_NAME,
context.resources.getDimensionPixelSize(
com.android.credentialmanager.R.dimen.autofill_icon_size));
val drawableId =
com.android.credentialmanager.R.drawable.more_options_list_item
remoteViews.setInt(
- android.R.id.content, setBackgroundResourceMethodName, drawableId);
+ android.R.id.content, SET_BACKGROUND_RESOURCE_METHOD_NAME, drawableId);
return remoteViews
}
}
diff --git a/packages/InputDevices/res/raw/keyboard_layout_azerbaijani.kcm b/packages/InputDevices/res/raw/keyboard_layout_azerbaijani.kcm
index 3f5e894..f2843ed 100644
--- a/packages/InputDevices/res/raw/keyboard_layout_azerbaijani.kcm
+++ b/packages/InputDevices/res/raw/keyboard_layout_azerbaijani.kcm
@@ -18,6 +18,8 @@
type OVERLAY
+map key 86 PLUS
+
### ROW 1
key GRAVE {
@@ -42,13 +44,14 @@
key 3 {
label: '3'
base: '3'
- shift: '\u2166'
+ shift: '\u2116'
}
key 4 {
label: '4'
base: '4'
shift: ';'
+ ralt: '\u20bc'
}
key 5 {
@@ -61,14 +64,12 @@
label: '6'
base: '6'
shift: ':'
- shift+ralt: '^'
}
key 7 {
label: '7'
base: '7'
shift: '?'
- ralt: '&'
}
key 8 {
@@ -176,21 +177,21 @@
key LEFT_BRACKET {
label: '\u00d6'
base: '\u00f6'
- shift: '\u00d6'
+ shift, capslock: '\u00d6'
shift+capslock: '\u00f6'
}
key RIGHT_BRACKET {
label: '\u011e'
base: '\u011f'
- shift: '\u011e'
+ shift, capslock: '\u011e'
shift+capslock: '\u011f'
}
key BACKSLASH {
label: '\\'
base: '\\'
- shift: '|'
+ shift: '/'
}
### ROW 3
@@ -261,19 +262,25 @@
key SEMICOLON {
label: 'I'
base: '\u0131'
- shift: 'I'
+ shift, capslock: 'I'
shift+capslock: '\u0131'
}
key APOSTROPHE {
label: '\u018f'
base: '\u0259'
- shift: '\u018f'
+ shift, capslock: '\u018f'
shift+capslock: '\u0259'
}
### ROW 4
+key PLUS {
+ label: '\\'
+ base: '\\'
+ shift: '/'
+}
+
key Z {
label: 'Z'
base: 'z'
@@ -326,14 +333,14 @@
key COMMA {
label: '\u00c7'
base: '\u00e7'
- shift: '\u00c7'
+ shift, capslock: '\u00c7'
shift+capslock: '\u00e7'
}
key PERIOD {
label: '\u015e'
base: '\u015f'
- shift: '\u015e'
+ shift, capslock: '\u015e'
shift+capslock: '\u015f'
}
diff --git a/packages/InputDevices/res/raw/keyboard_layout_english_uk.kcm b/packages/InputDevices/res/raw/keyboard_layout_english_uk.kcm
index 071f9f4..854c2fd 100644
--- a/packages/InputDevices/res/raw/keyboard_layout_english_uk.kcm
+++ b/packages/InputDevices/res/raw/keyboard_layout_english_uk.kcm
@@ -23,8 +23,8 @@
### ROW 1
key GRAVE {
- label: '`'
- base: '`'
+ label: '\u0300'
+ base: '\u0300'
shift: '\u00AC'
ralt: '\u00A6'
}
@@ -39,6 +39,7 @@
label: '2'
base: '2'
shift: '"'
+ ralt: '\u0308'
}
key 3 {
@@ -64,6 +65,7 @@
label: '6'
base: '6'
shift: '^'
+ ralt: '\u0302'
}
key 7 {
@@ -202,6 +204,7 @@
label: ']'
base: ']'
shift: '}'
+ shift+ralt: '|'
}
### ROW 3
@@ -282,14 +285,16 @@
label: '\''
base: '\''
shift: '@'
+ ralt: '\u0301'
+ shift+ralt: '`'
}
key POUND {
label: '#'
base: '#'
shift: '~'
- ralt: '\\'
- shift+ralt: '|'
+ ralt: '\u0303'
+ shift+ralt: '\\'
}
### ROW 4
diff --git a/packages/InputDevices/res/raw/keyboard_layout_german.kcm b/packages/InputDevices/res/raw/keyboard_layout_german.kcm
index 23ccc9a..fbb9bb6 100644
--- a/packages/InputDevices/res/raw/keyboard_layout_german.kcm
+++ b/packages/InputDevices/res/raw/keyboard_layout_german.kcm
@@ -101,6 +101,7 @@
key SLASH {
label: '\u00df'
base: '\u00df'
+ capslock: '\u1e9e'
shift: '?'
ralt: '\\'
}
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/InstallStart.java b/packages/PackageInstaller/src/com/android/packageinstaller/InstallStart.java
index ef418a5..7c313e8 100644
--- a/packages/PackageInstaller/src/com/android/packageinstaller/InstallStart.java
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/InstallStart.java
@@ -28,6 +28,7 @@
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageInstaller;
+import android.content.pm.PackageInstaller.SessionInfo;
import android.content.pm.PackageManager;
import android.content.pm.ProviderInfo;
import android.net.Uri;
@@ -38,7 +39,6 @@
import android.text.TextUtils;
import android.util.EventLog;
import android.util.Log;
-import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.android.packageinstaller.v2.ui.InstallLaunch;
import java.util.Arrays;
@@ -51,6 +51,7 @@
private static final String TAG = InstallStart.class.getSimpleName();
private PackageManager mPackageManager;
+ private PackageInstaller mPackageInstaller;
private UserManager mUserManager;
private boolean mAbortInstall = false;
private boolean mShouldFinish = true;
@@ -66,7 +67,7 @@
Log.i(TAG, "Using Pia V2");
Intent piaV2 = new Intent(getIntent());
- piaV2.putExtra(InstallLaunch.EXTRA_CALLING_PKG_NAME, getCallingPackage());
+ piaV2.putExtra(InstallLaunch.EXTRA_CALLING_PKG_NAME, getLaunchedFromPackage());
piaV2.putExtra(InstallLaunch.EXTRA_CALLING_PKG_UID, getLaunchedFromUid());
piaV2.setClass(this, InstallLaunch.class);
piaV2.addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT);
@@ -75,6 +76,7 @@
return;
}
mPackageManager = getPackageManager();
+ mPackageInstaller = mPackageManager.getPackageInstaller();
mUserManager = getSystemService(UserManager.class);
Intent intent = getIntent();
@@ -94,12 +96,11 @@
// If the activity was started via a PackageInstaller session, we retrieve the calling
// package from that session
final int sessionId = (isSessionInstall
- ? intent.getIntExtra(PackageInstaller.EXTRA_SESSION_ID, -1)
- : -1);
+ ? intent.getIntExtra(PackageInstaller.EXTRA_SESSION_ID, SessionInfo.INVALID_ID)
+ : SessionInfo.INVALID_ID);
int originatingUidFromSession = callingUid;
- if (callingPackage == null && sessionId != -1) {
- PackageInstaller packageInstaller = getPackageManager().getPackageInstaller();
- PackageInstaller.SessionInfo sessionInfo = packageInstaller.getSessionInfo(sessionId);
+ if (callingPackage == null && sessionId != SessionInfo.INVALID_ID) {
+ PackageInstaller.SessionInfo sessionInfo = mPackageInstaller.getSessionInfo(sessionId);
if (sessionInfo != null) {
callingPackage = sessionInfo.getInstallerPackageName();
callingAttributionTag = sessionInfo.getInstallerAttributionTag();
@@ -188,6 +189,7 @@
nextActivity.putExtra(Intent.EXTRA_ORIGINATING_UID, originatingUid);
nextActivity.putExtra(PackageInstallerActivity.EXTRA_ORIGINATING_UID_FROM_SESSION_INFO,
originatingUidFromSession);
+ nextActivity.putExtra(PackageInstallerActivity.EXTRA_IS_TRUSTED_SOURCE, isTrustedSource);
if (isSessionInstall) {
nextActivity.setClass(this, PackageInstallerActivity.class);
@@ -257,7 +259,7 @@
private ApplicationInfo getSourceInfo(@Nullable String callingPackage) {
if (callingPackage != null) {
try {
- return getPackageManager().getApplicationInfo(callingPackage, 0);
+ return mPackageManager.getApplicationInfo(callingPackage, 0);
} catch (PackageManager.NameNotFoundException ex) {
// ignore
}
@@ -265,8 +267,6 @@
return null;
}
-
- @NonNull
private boolean canPackageQuery(int callingUid, Uri packageUri) {
ProviderInfo info = mPackageManager.resolveContentProvider(packageUri.getAuthority(),
PackageManager.ComponentInfoFlags.of(0));
@@ -295,8 +295,7 @@
if (originatingUid == Process.ROOT_UID) {
return true;
}
- PackageInstaller packageInstaller = getPackageManager().getPackageInstaller();
- PackageInstaller.SessionInfo sessionInfo = packageInstaller.getSessionInfo(sessionId);
+ PackageInstaller.SessionInfo sessionInfo = mPackageInstaller.getSessionInfo(sessionId);
if (sessionInfo == null) {
return false;
}
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/InstallSuccess.java b/packages/PackageInstaller/src/com/android/packageinstaller/InstallSuccess.java
index 215ead3..167d5061 100644
--- a/packages/PackageInstaller/src/com/android/packageinstaller/InstallSuccess.java
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/InstallSuccess.java
@@ -108,18 +108,19 @@
mDialog = builder.create();
mDialog.show();
mDialog.requireViewById(R.id.install_success).setVisibility(View.VISIBLE);
- // Enable or disable "launch" button
- boolean enabled = false;
+ // Show or hide "launch" button
+ boolean visible = false;
if (mLaunchIntent != null) {
List<ResolveInfo> list = getPackageManager().queryIntentActivities(mLaunchIntent,
0);
if (list != null && list.size() > 0) {
- enabled = true;
+ visible = true;
}
}
+ visible = visible && isLauncherActivityEnabled(mLaunchIntent);
Button launchButton = mDialog.getButton(DialogInterface.BUTTON_POSITIVE);
- if (enabled) {
+ if (visible) {
launchButton.setOnClickListener(view -> {
try {
startActivity(mLaunchIntent.addFlags(
@@ -130,7 +131,15 @@
finish();
});
} else {
- launchButton.setEnabled(false);
+ launchButton.setVisibility(View.GONE);
}
}
+
+ private boolean isLauncherActivityEnabled(Intent intent) {
+ if (intent == null || intent.getComponent() == null) {
+ return false;
+ }
+ return getPackageManager().getComponentEnabledSetting(intent.getComponent())
+ != PackageManager.COMPONENT_ENABLED_STATE_DISABLED;
+ }
}
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/PackageInstallerActivity.java b/packages/PackageInstaller/src/com/android/packageinstaller/PackageInstallerActivity.java
index 45bfe54..1b93c10 100644
--- a/packages/PackageInstaller/src/com/android/packageinstaller/PackageInstallerActivity.java
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/PackageInstallerActivity.java
@@ -86,6 +86,7 @@
static final String EXTRA_APP_SNIPPET = "EXTRA_APP_SNIPPET";
static final String EXTRA_ORIGINATING_UID_FROM_SESSION_INFO =
"EXTRA_ORIGINATING_UID_FROM_SESSION_INFO";
+ static final String EXTRA_IS_TRUSTED_SOURCE = "EXTRA_IS_TRUSTED_SOURCE";
private static final String ALLOW_UNKNOWN_SOURCES_KEY =
PackageInstallerActivity.class.getName() + "ALLOW_UNKNOWN_SOURCES_KEY";
@@ -304,21 +305,6 @@
return packagesForUid[0];
}
- private boolean isInstallRequestFromUnknownSource(Intent intent) {
- if (mCallingPackage != null && intent.getBooleanExtra(
- Intent.EXTRA_NOT_UNKNOWN_SOURCE, false)) {
- if (mSourceInfo != null && mSourceInfo.isPrivilegedApp()) {
- // Privileged apps can bypass unknown sources check if they want.
- return false;
- }
- }
- if (mSourceInfo != null && checkPermission(Manifest.permission.INSTALL_PACKAGES,
- -1 /* pid */, mSourceInfo.uid) == PackageManager.PERMISSION_GRANTED) {
- return false;
- }
- return true;
- }
-
private void initiateInstall() {
String pkgName = mPkgInfo.packageName;
// Check if there is already a package on the device with this name
@@ -557,7 +543,7 @@
* Check if it is allowed to install the package and initiate install if allowed.
*/
private void checkIfAllowedAndInitiateInstall() {
- if (mAllowUnknownSources || !isInstallRequestFromUnknownSource(getIntent())) {
+ if (mAllowUnknownSources || getIntent().getBooleanExtra(EXTRA_IS_TRUSTED_SOURCE, false)) {
if (mLocalLOGV) Log.i(TAG, "install allowed");
initiateInstall();
} else {
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallRepository.kt b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallRepository.kt
index aeabbd5..e48c0f4 100644
--- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallRepository.kt
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallRepository.kt
@@ -30,6 +30,7 @@
import android.content.pm.PackageInstaller.SessionInfo
import android.content.pm.PackageInstaller.SessionParams
import android.content.pm.PackageManager
+import android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_DISABLED
import android.net.Uri
import android.os.ParcelFileDescriptor
import android.os.Process
@@ -95,6 +96,7 @@
var stagedSessionId = SessionInfo.INVALID_ID
private set
private var callingUid = Process.INVALID_UID
+ private var originatingUid = Process.INVALID_UID
private var callingPackage: String? = null
private var sessionStager: SessionStager? = null
private lateinit var intent: Intent
@@ -147,7 +149,7 @@
}
val sourceInfo: ApplicationInfo? = getSourceInfo(callingPackage)
// Uid of the source package, with a preference to uid from ApplicationInfo
- val originatingUid = sourceInfo?.uid ?: callingUid
+ originatingUid = sourceInfo?.uid ?: callingUid
appOpRequestInfo = AppOpRequestInfo(
getPackageNameForUid(context, originatingUid, callingPackage),
originatingUid, callingAttributionTag
@@ -281,7 +283,7 @@
context.contentResolver.openAssetFileDescriptor(uri, "r").use { afd ->
val pfd: ParcelFileDescriptor? = afd?.parcelFileDescriptor
val params: SessionParams =
- createSessionParams(intent, pfd, uri.toString())
+ createSessionParams(originatingUid, intent, pfd, uri.toString())
stagedSessionId = packageInstaller.createSession(params)
}
} catch (e: Exception) {
@@ -337,6 +339,7 @@
}
private fun createSessionParams(
+ originatingUid: Int,
intent: Intent,
pfd: ParcelFileDescriptor?,
debugPathName: String,
@@ -353,9 +356,7 @@
params.setOriginatingUri(
intent.getParcelableExtra(Intent.EXTRA_ORIGINATING_URI, Uri::class.java)
)
- params.setOriginatingUid(
- intent.getIntExtra(Intent.EXTRA_ORIGINATING_UID, Process.INVALID_UID)
- )
+ params.setOriginatingUid(originatingUid)
params.setInstallerPackageName(intent.getStringExtra(Intent.EXTRA_INSTALLER_PACKAGE_NAME))
params.setInstallReason(PackageManager.INSTALL_REASON_USER)
// Disable full screen intent usage by for sideloads.
@@ -830,7 +831,8 @@
val resultIntent = if (shouldReturnResult) {
Intent().putExtra(Intent.EXTRA_INSTALL_RESULT, PackageManager.INSTALL_SUCCEEDED)
} else {
- packageManager.getLaunchIntentForPackage(newPackageInfo!!.packageName)
+ val intent = packageManager.getLaunchIntentForPackage(newPackageInfo!!.packageName)
+ if (isLauncherActivityEnabled(intent)) intent else null
}
_installResult.setValue(InstallSuccess(appSnippet, shouldReturnResult, resultIntent))
} else {
@@ -838,6 +840,14 @@
}
}
+ private fun isLauncherActivityEnabled(intent: Intent?): Boolean {
+ if (intent == null || intent.component == null) {
+ return false
+ }
+ return (intent.component?.let { packageManager.getComponentEnabledSetting(it) }
+ != COMPONENT_ENABLED_STATE_DISABLED)
+ }
+
/**
* Cleanup the staged session. Also signal the packageinstaller that an install session is to
* be aborted
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/InstallSuccessFragment.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/InstallSuccessFragment.java
index b2a65faa..e491f9c 100644
--- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/InstallSuccessFragment.java
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/InstallSuccessFragment.java
@@ -23,13 +23,13 @@
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.os.Bundle;
+import android.util.Log;
import android.view.View;
import android.widget.Button;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.DialogFragment;
import com.android.packageinstaller.R;
-import com.android.packageinstaller.v2.model.InstallStage;
import com.android.packageinstaller.v2.model.InstallSuccess;
import com.android.packageinstaller.v2.ui.InstallActionListener;
import java.util.List;
@@ -40,6 +40,7 @@
*/
public class InstallSuccessFragment extends DialogFragment {
+ private static final String LOG_TAG = InstallSuccessFragment.class.getSimpleName();
private final InstallSuccess mDialogData;
private AlertDialog mDialog;
private InstallActionListener mInstallActionListener;
@@ -60,12 +61,15 @@
@Override
public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
View dialogView = getLayoutInflater().inflate(R.layout.install_content_view, null);
- mDialog = new AlertDialog.Builder(requireContext()).setTitle(mDialogData.getAppLabel())
- .setIcon(mDialogData.getAppIcon()).setView(dialogView).setNegativeButton(R.string.done,
+ mDialog = new AlertDialog.Builder(requireContext())
+ .setTitle(mDialogData.getAppLabel())
+ .setIcon(mDialogData.getAppIcon())
+ .setView(dialogView)
+ .setNegativeButton(R.string.done,
(dialog, which) -> mInstallActionListener.onNegativeResponse(
- InstallStage.STAGE_SUCCESS))
- .setPositiveButton(R.string.launch, (dialog, which) -> {
- }).create();
+ mDialogData.getStageCode()))
+ .setPositiveButton(R.string.launch, (dialog, which) -> {})
+ .create();
dialogView.requireViewById(R.id.install_success).setVisibility(View.VISIBLE);
@@ -76,25 +80,28 @@
public void onStart() {
super.onStart();
Button launchButton = mDialog.getButton(DialogInterface.BUTTON_POSITIVE);
- boolean enabled = false;
+ boolean visible = false;
if (mDialogData.getResultIntent() != null) {
List<ResolveInfo> list = mPm.queryIntentActivities(mDialogData.getResultIntent(), 0);
if (list.size() > 0) {
- enabled = true;
+ visible = true;
}
}
- if (enabled) {
+ if (visible) {
launchButton.setOnClickListener(view -> {
+ Log.i(LOG_TAG, "Finished installing and launching " +
+ mDialogData.getAppLabel());
mInstallActionListener.openInstalledApp(mDialogData.getResultIntent());
});
} else {
- launchButton.setEnabled(false);
+ launchButton.setVisibility(View.GONE);
}
}
@Override
public void onCancel(@NonNull DialogInterface dialog) {
super.onCancel(dialog);
+ Log.i(LOG_TAG, "Finished installing " + mDialogData.getAppLabel());
mInstallActionListener.onNegativeResponse(mDialogData.getStageCode());
}
}
diff --git a/packages/SettingsLib/DataStore/tests/src/com/android/settingslib/datastore/KeyedObserverTest.kt b/packages/SettingsLib/DataStore/tests/src/com/android/settingslib/datastore/KeyedObserverTest.kt
new file mode 100644
index 0000000..b52586c
--- /dev/null
+++ b/packages/SettingsLib/DataStore/tests/src/com/android/settingslib/datastore/KeyedObserverTest.kt
@@ -0,0 +1,215 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.datastore
+
+import com.google.common.truth.Truth.assertThat
+import com.google.common.util.concurrent.MoreExecutors.directExecutor
+import java.util.concurrent.Executor
+import java.util.concurrent.atomic.AtomicInteger
+import org.junit.Assert
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.junit.MockitoJUnit
+import org.mockito.junit.MockitoRule
+import org.mockito.kotlin.any
+import org.mockito.kotlin.never
+import org.mockito.kotlin.reset
+import org.mockito.kotlin.verify
+import org.robolectric.RobolectricTestRunner
+
+@RunWith(RobolectricTestRunner::class)
+class KeyedObserverTest {
+ @get:Rule
+ val mockitoRule: MockitoRule = MockitoJUnit.rule()
+
+ @Mock
+ private lateinit var observer1: KeyedObserver<Any?>
+
+ @Mock
+ private lateinit var observer2: KeyedObserver<Any?>
+
+ @Mock
+ private lateinit var keyedObserver1: KeyedObserver<Any>
+
+ @Mock
+ private lateinit var keyedObserver2: KeyedObserver<Any>
+
+ @Mock
+ private lateinit var key1: Any
+
+ @Mock
+ private lateinit var key2: Any
+
+ @Mock
+ private lateinit var executor: Executor
+
+ private val keyedObservable = KeyedDataObservable<Any>()
+
+ @Test
+ fun addObserver_sameExecutor() {
+ keyedObservable.addObserver(observer1, executor)
+ keyedObservable.addObserver(observer1, executor)
+ }
+
+ @Test
+ fun addObserver_keyedObserver_sameExecutor() {
+ keyedObservable.addObserver(key1, keyedObserver1, executor)
+ keyedObservable.addObserver(key1, keyedObserver1, executor)
+ }
+
+ @Test
+ fun addObserver_differentExecutor() {
+ keyedObservable.addObserver(observer1, executor)
+ Assert.assertThrows(IllegalStateException::class.java) {
+ keyedObservable.addObserver(observer1, directExecutor())
+ }
+ }
+
+ @Test
+ fun addObserver_keyedObserver_differentExecutor() {
+ keyedObservable.addObserver(key1, keyedObserver1, executor)
+ Assert.assertThrows(IllegalStateException::class.java) {
+ keyedObservable.addObserver(key1, keyedObserver1, directExecutor())
+ }
+ }
+
+ @Test
+ fun addObserver_weaklyReferenced() {
+ val counter = AtomicInteger()
+ var observer: KeyedObserver<Any?>? = KeyedObserver { _, _ -> counter.incrementAndGet() }
+ keyedObservable.addObserver(observer!!, directExecutor())
+
+ keyedObservable.notifyChange(ChangeReason.UPDATE)
+ assertThat(counter.get()).isEqualTo(1)
+
+ // trigger GC, the observer callback should not be invoked
+ null.also { observer = it }
+ System.gc()
+ System.runFinalization()
+
+ keyedObservable.notifyChange(ChangeReason.UPDATE)
+ assertThat(counter.get()).isEqualTo(1)
+ }
+
+ @Test
+ fun addObserver_keyedObserver_weaklyReferenced() {
+ val counter = AtomicInteger()
+ var keyObserver: KeyedObserver<Any>? = KeyedObserver { _, _ -> counter.incrementAndGet() }
+ keyedObservable.addObserver(key1, keyObserver!!, directExecutor())
+
+ keyedObservable.notifyChange(key1, ChangeReason.UPDATE)
+ assertThat(counter.get()).isEqualTo(1)
+
+ // trigger GC, the observer callback should not be invoked
+ null.also { keyObserver = it }
+ System.gc()
+ System.runFinalization()
+
+ keyedObservable.notifyChange(key1, ChangeReason.UPDATE)
+ assertThat(counter.get()).isEqualTo(1)
+ }
+
+ @Test
+ fun addObserver_notifyObservers_removeObserver() {
+ keyedObservable.addObserver(observer1, directExecutor())
+ keyedObservable.addObserver(observer2, executor)
+
+ keyedObservable.notifyChange(ChangeReason.UPDATE)
+ verify(observer1).onKeyChanged(null, ChangeReason.UPDATE)
+ verify(observer2, never()).onKeyChanged(any(), any())
+ verify(executor).execute(any())
+
+ reset(observer1, executor)
+ keyedObservable.removeObserver(observer2)
+
+ keyedObservable.notifyChange(ChangeReason.DELETE)
+ verify(observer1).onKeyChanged(null, ChangeReason.DELETE)
+ verify(executor, never()).execute(any())
+ }
+
+ @Test
+ fun addObserver_keyedObserver_notifyObservers_removeObserver() {
+ keyedObservable.addObserver(key1, keyedObserver1, directExecutor())
+ keyedObservable.addObserver(key2, keyedObserver2, executor)
+
+ keyedObservable.notifyChange(key1, ChangeReason.UPDATE)
+ verify(keyedObserver1).onKeyChanged(key1, ChangeReason.UPDATE)
+ verify(keyedObserver2, never()).onKeyChanged(any(), any())
+ verify(executor, never()).execute(any())
+
+ reset(keyedObserver1, executor)
+ keyedObservable.removeObserver(key2, keyedObserver2)
+
+ keyedObservable.notifyChange(key1, ChangeReason.DELETE)
+ verify(keyedObserver1).onKeyChanged(key1, ChangeReason.DELETE)
+ verify(executor, never()).execute(any())
+ }
+
+ @Test
+ fun notifyChange_addMoreTypeObservers_checkOnKeyChanged() {
+ keyedObservable.addObserver(observer1, directExecutor())
+ keyedObservable.addObserver(key1, keyedObserver1, directExecutor())
+ keyedObservable.addObserver(key2, keyedObserver2, directExecutor())
+
+ keyedObservable.notifyChange(ChangeReason.UPDATE)
+ verify(observer1).onKeyChanged(null, ChangeReason.UPDATE)
+ verify(keyedObserver1).onKeyChanged(key1, ChangeReason.UPDATE)
+ verify(keyedObserver2).onKeyChanged(key2, ChangeReason.UPDATE)
+
+ reset(observer1, keyedObserver1, keyedObserver2)
+ keyedObservable.notifyChange(key1, ChangeReason.UPDATE)
+
+ verify(observer1).onKeyChanged(key1, ChangeReason.UPDATE)
+ verify(keyedObserver1).onKeyChanged(key1, ChangeReason.UPDATE)
+ verify(keyedObserver2, never()).onKeyChanged(key1, ChangeReason.UPDATE)
+
+ reset(observer1, keyedObserver1, keyedObserver2)
+ keyedObservable.notifyChange(key2, ChangeReason.UPDATE)
+
+ verify(observer1).onKeyChanged(key2, ChangeReason.UPDATE)
+ verify(keyedObserver1, never()).onKeyChanged(key2, ChangeReason.UPDATE)
+ verify(keyedObserver2).onKeyChanged(key2, ChangeReason.UPDATE)
+ }
+
+ @Test
+ fun notifyChange_addObserverWithinCallback() {
+ // ConcurrentModificationException is raised if it is not implemented correctly
+ val observer: KeyedObserver<Any?> = KeyedObserver { _, _ ->
+ keyedObservable.addObserver(observer1, executor)
+ }
+
+ keyedObservable.addObserver(observer, directExecutor())
+
+ keyedObservable.notifyChange(ChangeReason.UPDATE)
+ keyedObservable.removeObserver(observer)
+ }
+
+ @Test
+ fun notifyChange_KeyedObserver_addObserverWithinCallback() {
+ // ConcurrentModificationException is raised if it is not implemented correctly
+ val keyObserver: KeyedObserver<Any?> = KeyedObserver { _, _ ->
+ keyedObservable.addObserver(key1, keyedObserver1, executor)
+ }
+
+ keyedObservable.addObserver(key1, keyObserver, directExecutor())
+
+ keyedObservable.notifyChange(key1, ChangeReason.UPDATE)
+ keyedObservable.removeObserver(key1, keyObserver)
+ }
+}
\ No newline at end of file
diff --git a/packages/SettingsLib/DataStore/tests/src/com/android/settingslib/datastore/ObserverTest.kt b/packages/SettingsLib/DataStore/tests/src/com/android/settingslib/datastore/ObserverTest.kt
index bb791dc..f065829 100644
--- a/packages/SettingsLib/DataStore/tests/src/com/android/settingslib/datastore/ObserverTest.kt
+++ b/packages/SettingsLib/DataStore/tests/src/com/android/settingslib/datastore/ObserverTest.kt
@@ -69,8 +69,7 @@
assertThat(counter.get()).isEqualTo(1)
// trigger GC, the observer callback should not be invoked
- @Suppress("unused")
- observer = null
+ null.also { observer = it }
System.gc()
System.runFinalization()
@@ -100,10 +99,12 @@
@Test
fun notifyChange_addObserverWithinCallback() {
// ConcurrentModificationException is raised if it is not implemented correctly
+ val observer = Observer { observable.addObserver(observer1, executor) }
observable.addObserver(
- { observable.addObserver(observer1, executor) },
+ observer,
MoreExecutors.directExecutor()
)
observable.notifyChange(ChangeReason.UPDATE)
+ observable.removeObserver(observer)
}
}
diff --git a/packages/SettingsLib/ProfileSelector/Android.bp b/packages/SettingsLib/ProfileSelector/Android.bp
index 6dc07b2..4aa67c1 100644
--- a/packages/SettingsLib/ProfileSelector/Android.bp
+++ b/packages/SettingsLib/ProfileSelector/Android.bp
@@ -20,6 +20,7 @@
static_libs: [
"com.google.android.material_material",
"SettingsLibSettingsTheme",
+ "android.os.flags-aconfig-java-export",
],
sdk_version: "system_current",
diff --git a/packages/SettingsLib/ProfileSelector/AndroidManifest.xml b/packages/SettingsLib/ProfileSelector/AndroidManifest.xml
index 80f6b76..303e20c 100644
--- a/packages/SettingsLib/ProfileSelector/AndroidManifest.xml
+++ b/packages/SettingsLib/ProfileSelector/AndroidManifest.xml
@@ -18,5 +18,5 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.android.settingslib.widget.profileselector">
- <uses-sdk android:minSdkVersion="23" />
+ <uses-sdk android:minSdkVersion="29" />
</manifest>
diff --git a/packages/SettingsLib/ProfileSelector/res/values/strings.xml b/packages/SettingsLib/ProfileSelector/res/values/strings.xml
index 68d4047..76ccb65 100644
--- a/packages/SettingsLib/ProfileSelector/res/values/strings.xml
+++ b/packages/SettingsLib/ProfileSelector/res/values/strings.xml
@@ -21,4 +21,6 @@
<string name="settingslib_category_personal">Personal</string>
<!-- Header for items under the work user [CHAR LIMIT=30] -->
<string name="settingslib_category_work">Work</string>
+ <!-- Header for items under the private profile user [CHAR LIMIT=30] -->
+ <string name="settingslib_category_private">Private</string>
</resources>
\ No newline at end of file
diff --git a/packages/SettingsLib/ProfileSelector/src/com/android/settingslib/widget/ProfileSelectFragment.java b/packages/SettingsLib/ProfileSelector/src/com/android/settingslib/widget/ProfileSelectFragment.java
index be5753be..c52386b 100644
--- a/packages/SettingsLib/ProfileSelector/src/com/android/settingslib/widget/ProfileSelectFragment.java
+++ b/packages/SettingsLib/ProfileSelector/src/com/android/settingslib/widget/ProfileSelectFragment.java
@@ -16,31 +16,77 @@
package com.android.settingslib.widget;
+import android.annotation.TargetApi;
import android.app.Activity;
+import android.content.Context;
+import android.content.pm.UserProperties;
+import android.os.Build;
import android.os.Bundle;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.util.ArrayMap;
+import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
+import androidx.core.os.BuildCompat;
import androidx.fragment.app.Fragment;
import androidx.viewpager2.widget.ViewPager2;
+import com.android.settingslib.widget.profileselector.R;
+
import com.google.android.material.tabs.TabLayout;
import com.google.android.material.tabs.TabLayoutMediator;
-import com.android.settingslib.widget.profileselector.R;
+
+import java.util.ArrayList;
+import java.util.List;
/**
* Base fragment class for profile settings.
*/
public abstract class ProfileSelectFragment extends Fragment {
+ private static final String TAG = "ProfileSelectFragment";
+ // UserHandle#USER_NULL is a @TestApi so is not accessible.
+ private static final int USER_NULL = -10000;
+ private static final int DEFAULT_POSITION = 0;
/**
- * Personal or Work profile tab of {@link ProfileSelectFragment}
- * <p>0: Personal tab.
- * <p>1: Work profile tab.
+ * The type of profile tab of {@link ProfileSelectFragment} to show
+ * <ul>
+ * <li>0: Personal tab.
+ * <li>1: Work profile tab.
+ * </ul>
+ *
+ * <p> Please note that this is supported for legacy reasons. Please use
+ * {@link #EXTRA_SHOW_FRAGMENT_USER_ID} instead.
*/
- public static final String EXTRA_SHOW_FRAGMENT_TAB =
- ":settings:show_fragment_tab";
+ public static final String EXTRA_SHOW_FRAGMENT_TAB = ":settings:show_fragment_tab";
+
+ /**
+ * An {@link ArrayList} of users to show. The supported users are: System user, the managed
+ * profile user, and the private profile user. A client should pass all the user ids that need
+ * to be shown in this list. Note that if this list is not provided then, for legacy reasons
+ * see {@link #EXTRA_SHOW_FRAGMENT_TAB}, an attempt will be made to show two tabs: one for the
+ * System user and one for the managed profile user.
+ *
+ * <p>Please note that this MUST be used in conjunction with
+ * {@link #EXTRA_SHOW_FRAGMENT_USER_ID}
+ */
+ public static final String EXTRA_LIST_OF_USER_IDS = ":settings:list_user_ids";
+
+ /**
+ * The user id of the user to be show in {@link ProfileSelectFragment}. Only the below user
+ * types are supported:
+ * <ul>
+ * <li> System user.
+ * <li> Managed profile user.
+ * <li> Private profile user.
+ * </ul>
+ *
+ * <p>Please note that this MUST be used in conjunction with {@link #EXTRA_LIST_OF_USER_IDS}.
+ */
+ public static final String EXTRA_SHOW_FRAGMENT_USER_ID = ":settings:show_fragment_user_id";
/**
* Used in fragment argument with Extra key EXTRA_SHOW_FRAGMENT_TAB
@@ -48,13 +94,23 @@
public static final int PERSONAL_TAB = 0;
/**
- * Used in fragment argument with Extra key EXTRA_SHOW_FRAGMENT_TAB
+ * Used in fragment argument with Extra key EXTRA_SHOW_FRAGMENT_TAB for the managed profile
*/
public static final int WORK_TAB = 1;
+ /**
+ * Please note that private profile is available from API LEVEL
+ * {@link Build.VERSION_CODES.VANILLA_ICE_CREAM} only, therefore PRIVATE_TAB MUST be
+ * passed in {@link #EXTRA_SHOW_FRAGMENT_TAB} and {@link #EXTRA_LIST_OF_PROFILE_TABS} for
+ * {@link Build.VERSION_CODES.VANILLA_ICE_CREAM} or higher API Levels only.
+ */
+ private static final int PRIVATE_TAB = 2;
+
private ViewGroup mContentView;
private ViewPager2 mViewPager;
+ private final ArrayMap<UserHandle, Integer> mProfileTabsByUsers = new ArrayMap<>();
+ private boolean mUsingUserIds = false;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
@@ -67,7 +123,7 @@
if (titleResId > 0) {
activity.setTitle(titleResId);
}
- final int selectedTab = getTabId(activity, getArguments());
+ initProfileTabsToShow();
final View tabContainer = mContentView.findViewById(R.id.tab_container);
mViewPager = tabContainer.findViewById(R.id.view_pager);
@@ -78,16 +134,14 @@
).attach();
tabContainer.setVisibility(View.VISIBLE);
- final TabLayout.Tab tab = tabs.getTabAt(selectedTab);
+ final TabLayout.Tab tab = tabs.getTabAt(getSelectedTabPosition(activity, getArguments()));
tab.select();
return mContentView;
}
/**
- * create Personal or Work profile fragment
- * <p>0: Personal profile.
- * <p>1: Work profile.
+ * Create Personal or Work or Private profile fragment. See {@link #EXTRA_SHOW_FRAGMENT_USER_ID}
*/
public abstract Fragment createFragment(int position);
@@ -99,21 +153,90 @@
return 0;
}
- int getTabId(Activity activity, Bundle bundle) {
+ int getSelectedTabPosition(Activity activity, Bundle bundle) {
if (bundle != null) {
+ final int extraUserId = bundle.getInt(EXTRA_SHOW_FRAGMENT_USER_ID, USER_NULL);
+ if (extraUserId != USER_NULL) {
+ return mProfileTabsByUsers.indexOfKey(UserHandle.of(extraUserId));
+ }
final int extraTab = bundle.getInt(EXTRA_SHOW_FRAGMENT_TAB, -1);
if (extraTab != -1) {
return extraTab;
}
}
- return PERSONAL_TAB;
+ return DEFAULT_POSITION;
+ }
+
+ int getTabCount() {
+ return mUsingUserIds ? mProfileTabsByUsers.size() : 2;
+ }
+
+ void initProfileTabsToShow() {
+ Bundle bundle = getArguments();
+ if (bundle != null) {
+ ArrayList<Integer> userIdsToShow =
+ bundle.getIntegerArrayList(EXTRA_LIST_OF_USER_IDS);
+ if (userIdsToShow != null && !userIdsToShow.isEmpty()) {
+ mUsingUserIds = true;
+ UserManager userManager = getContext().getSystemService(UserManager.class);
+ List<UserHandle> userHandles = userManager.getUserProfiles();
+ for (UserHandle userHandle : userHandles) {
+ if (!userIdsToShow.contains(userHandle.getIdentifier())) {
+ continue;
+ }
+ if (userHandle.isSystem()) {
+ mProfileTabsByUsers.put(userHandle, PERSONAL_TAB);
+ } else if (userManager.isManagedProfile(userHandle.getIdentifier())) {
+ mProfileTabsByUsers.put(userHandle, WORK_TAB);
+ } else if (shouldShowPrivateProfileIfItsOne(userHandle)) {
+ mProfileTabsByUsers.put(userHandle, PRIVATE_TAB);
+ }
+ }
+ }
+ }
+ }
+
+ private int getProfileTabForPosition(int position) {
+ return mUsingUserIds ? mProfileTabsByUsers.valueAt(position) : position;
+ }
+
+ int getUserIdForPosition(int position) {
+ return mUsingUserIds ? mProfileTabsByUsers.keyAt(position).getIdentifier() : position;
}
private CharSequence getPageTitle(int position) {
- if (position == WORK_TAB) {
+ int tab = getProfileTabForPosition(position);
+ if (tab == WORK_TAB) {
return getContext().getString(R.string.settingslib_category_work);
+ } else if (tab == PRIVATE_TAB) {
+ return getContext().getString(R.string.settingslib_category_private);
}
return getString(R.string.settingslib_category_personal);
}
+
+ @TargetApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ private boolean shouldShowUserInQuietMode(UserHandle userHandle, UserManager userManager) {
+ UserProperties userProperties = userManager.getUserProperties(userHandle);
+ return !userManager.isQuietModeEnabled(userHandle)
+ || userProperties.getShowInQuietMode() != UserProperties.SHOW_IN_QUIET_MODE_HIDDEN;
+ }
+
+ // It's sufficient to have this method marked with the appropriate API level because we expect
+ // to be here only for this API level - when then private profile was introduced.
+ @TargetApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ private boolean shouldShowPrivateProfileIfItsOne(UserHandle userHandle) {
+ if (!BuildCompat.isAtLeastV() || !android.os.Flags.allowPrivateProfile()) {
+ return false;
+ }
+ try {
+ Context userContext = getContext().createContextAsUser(userHandle, /* flags= */ 0);
+ UserManager userManager = userContext.getSystemService(UserManager.class);
+ return userManager.isPrivateProfile()
+ && shouldShowUserInQuietMode(userHandle, userManager);
+ } catch (IllegalStateException exception) {
+ Log.i(TAG, "Ignoring this user as the calling package not available in this user.");
+ }
+ return false;
+ }
}
diff --git a/packages/SettingsLib/ProfileSelector/src/com/android/settingslib/widget/ProfileViewPagerAdapter.java b/packages/SettingsLib/ProfileSelector/src/com/android/settingslib/widget/ProfileViewPagerAdapter.java
index f5ab647..37f4f27 100644
--- a/packages/SettingsLib/ProfileSelector/src/com/android/settingslib/widget/ProfileViewPagerAdapter.java
+++ b/packages/SettingsLib/ProfileSelector/src/com/android/settingslib/widget/ProfileViewPagerAdapter.java
@@ -18,7 +18,6 @@
import androidx.fragment.app.Fragment;
import androidx.viewpager2.adapter.FragmentStateAdapter;
-import com.android.settingslib.widget.profileselector.R;
/**
* ViewPager Adapter to handle between TabLayout and ViewPager2
@@ -34,11 +33,11 @@
@Override
public Fragment createFragment(int position) {
- return mParentFragments.createFragment(position);
+ return mParentFragments.createFragment(mParentFragments.getUserIdForPosition(position));
}
@Override
public int getItemCount() {
- return 2;
+ return mParentFragments.getTabCount();
}
}
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/preference/RestrictedSwitchPreferenceModel.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/preference/RestrictedSwitchPreferenceModel.kt
index 5dfecb0..87cd2b8 100644
--- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/preference/RestrictedSwitchPreferenceModel.kt
+++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/preference/RestrictedSwitchPreferenceModel.kt
@@ -70,26 +70,26 @@
is BlockedByAdmin -> {
Box(
Modifier
- .clickable(
- role = Role.Switch,
- onClick = { restrictedMode.sendShowAdminSupportDetailsIntent() },
- )
- .semantics {
- this.toggleableState = ToggleableState(checked())
- },
+ .clickable(
+ role = Role.Switch,
+ onClick = { restrictedMode.sendShowAdminSupportDetailsIntent() },
+ )
+ .semantics {
+ this.toggleableState = ToggleableState(checked())
+ },
) { content() }
}
is BlockedByEcm -> {
Box(
Modifier
- .clickable(
- role = Role.Switch,
- onClick = { restrictedMode.showRestrictedSettingsDetails() },
- )
- .semantics {
- this.toggleableState = ToggleableState(checked())
- },
+ .clickable(
+ role = Role.Switch,
+ onClick = { restrictedMode.showRestrictedSettingsDetails() },
+ )
+ .semantics {
+ this.toggleableState = ToggleableState(checked())
+ },
) { content() }
}
@@ -113,7 +113,7 @@
content: @Composable (SwitchPreferenceModel) -> Unit,
) {
val context = LocalContext.current
- val restrictedSwitchPreferenceModel = remember(restrictedMode) {
+ val restrictedSwitchPreferenceModel = remember(restrictedMode, model.title) {
RestrictedSwitchPreferenceModel(context, model, restrictedMode)
}
restrictedSwitchPreferenceModel.RestrictionWrapper {
diff --git a/packages/SettingsLib/src/com/android/settingslib/Utils.java b/packages/SettingsLib/src/com/android/settingslib/Utils.java
index 87b4c0f..60a0529 100644
--- a/packages/SettingsLib/src/com/android/settingslib/Utils.java
+++ b/packages/SettingsLib/src/com/android/settingslib/Utils.java
@@ -27,6 +27,7 @@
import android.hardware.usb.UsbPort;
import android.hardware.usb.UsbPortStatus;
import android.hardware.usb.flags.Flags;
+import android.icu.text.NumberFormat;
import android.location.LocationManager;
import android.media.AudioManager;
import android.net.NetworkCapabilities;
@@ -67,7 +68,6 @@
import com.android.settingslib.fuelgauge.BatteryStatus;
import com.android.settingslib.utils.BuildCompatUtils;
-import java.text.NumberFormat;
import java.time.Duration;
import java.util.List;
diff --git a/packages/SettingsLib/src/com/android/settingslib/applications/RecentAppOpsAccess.java b/packages/SettingsLib/src/com/android/settingslib/applications/RecentAppOpsAccess.java
index 9a29f22..f73081a 100644
--- a/packages/SettingsLib/src/com/android/settingslib/applications/RecentAppOpsAccess.java
+++ b/packages/SettingsLib/src/com/android/settingslib/applications/RecentAppOpsAccess.java
@@ -23,6 +23,7 @@
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.pm.UserProperties;
import android.graphics.drawable.Drawable;
import android.os.UserHandle;
import android.os.UserManager;
@@ -132,8 +133,9 @@
int uid = ops.getUid();
UserHandle user = UserHandle.getUserHandleForUid(uid);
- // Don't show apps belonging to background users except managed users.
- if (!profiles.contains(user)) {
+ // Don't show apps belonging to background users except for profiles that shouldn't
+ // be shown in quiet mode.
+ if (!profiles.contains(user) || isHideInQuietEnabledForProfile(um, user)) {
continue;
}
@@ -192,6 +194,16 @@
return accesses;
}
+ private boolean isHideInQuietEnabledForProfile(UserManager userManager, UserHandle userHandle) {
+ if (android.multiuser.Flags.enablePrivateSpaceFeatures()
+ && android.multiuser.Flags.handleInterleavedSettingsForPrivateSpace()) {
+ return userManager.isQuietModeEnabled(userHandle)
+ && userManager.getUserProperties(userHandle).getShowInQuietMode()
+ == UserProperties.SHOW_IN_QUIET_MODE_HIDDEN;
+ }
+ return false;
+ }
+
/**
* Creates a Access entry for the given PackageOps.
*
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothCallback.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothCallback.java
index 416b369..baccda7e 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothCallback.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothCallback.java
@@ -163,6 +163,16 @@
default void onAclConnectionStateChanged(
@NonNull CachedBluetoothDevice cachedDevice, int state) {}
+ /**
+ * Called when the Auto-on state is changed for any user. Listens to intent
+ * {@link android.bluetooth.BluetoothAdapter#ACTION_AUTO_ON_STATE_CHANGED }
+ *
+ * @param state the Auto-on state, the possible values are:
+ * {@link android.bluetooth.BluetoothAdapter#AUTO_ON_STATE_ENABLED},
+ * {@link android.bluetooth.BluetoothAdapter#AUTO_ON_STATE_DISABLED}
+ */
+ default void onAutoOnStateChanged(int state) {}
+
@Retention(RetentionPolicy.SOURCE)
@IntDef(prefix = { "STATE_" }, value = {
STATE_DISCONNECTED,
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothEventManager.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothEventManager.java
index 647fcb9..0996d52 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothEventManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothEventManager.java
@@ -133,6 +133,8 @@
addHandler(BluetoothDevice.ACTION_ACL_CONNECTED, new AclStateChangedHandler());
addHandler(BluetoothDevice.ACTION_ACL_DISCONNECTED, new AclStateChangedHandler());
+ addHandler(BluetoothAdapter.ACTION_AUTO_ON_STATE_CHANGED, new AutoOnStateChangedHandler());
+
registerAdapterIntentReceiver();
}
@@ -552,4 +554,21 @@
dispatchAudioModeChanged();
}
}
+
+ private class AutoOnStateChangedHandler implements Handler {
+
+ @Override
+ public void onReceive(Context context, Intent intent, BluetoothDevice device) {
+ String action = intent.getAction();
+ if (action == null) {
+ Log.w(TAG, "AutoOnStateChangedHandler() action is null");
+ return;
+ }
+ int state = intent.getIntExtra(BluetoothAdapter.EXTRA_AUTO_ON_STATE,
+ BluetoothAdapter.ERROR);
+ for (BluetoothCallback callback : mCallbacks) {
+ callback.onAutoOnStateChanged(state);
+ }
+ }
+ }
}
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java
index b8624fd..4777b0d 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java
@@ -1315,8 +1315,7 @@
boolean isActiveAshaHearingAid = mIsActiveDeviceHearingAid;
boolean isActiveLeAudioHearingAid = mIsActiveDeviceLeAudio
&& isConnectedHapClientDevice();
- if ((isActiveAshaHearingAid || isActiveLeAudioHearingAid)
- && stringRes == R.string.bluetooth_active_no_battery_level) {
+ if (isActiveAshaHearingAid || isActiveLeAudioHearingAid) {
final Set<CachedBluetoothDevice> memberDevices = getMemberDevice();
final CachedBluetoothDevice subDevice = getSubDevice();
if (memberDevices.stream().anyMatch(m -> m.isConnected())) {
diff --git a/packages/SettingsLib/src/com/android/settingslib/media/session/MediaSessionManagerExt.kt b/packages/SettingsLib/src/com/android/settingslib/media/session/MediaSessionManagerExt.kt
index cda6b8b..68f471d 100644
--- a/packages/SettingsLib/src/com/android/settingslib/media/session/MediaSessionManagerExt.kt
+++ b/packages/SettingsLib/src/com/android/settingslib/media/session/MediaSessionManagerExt.kt
@@ -17,6 +17,7 @@
package com.android.settingslib.media.session
import android.media.session.MediaController
+import android.media.session.MediaSession
import android.media.session.MediaSessionManager
import android.os.UserHandle
import androidx.concurrent.futures.DirectExecutor
@@ -28,7 +29,7 @@
import kotlinx.coroutines.launch
/** [Flow] for [MediaSessionManager.OnActiveSessionsChangedListener]. */
-val MediaSessionManager.activeMediaChanges: Flow<Collection<MediaController>?>
+val MediaSessionManager.activeMediaChanges: Flow<List<MediaController>?>
get() =
callbackFlow {
val listener =
@@ -42,3 +43,24 @@
awaitClose { removeOnActiveSessionsChangedListener(listener) }
}
.buffer(capacity = Channel.CONFLATED)
+
+/** [Flow] for [MediaSessionManager.RemoteSessionCallback]. */
+val MediaSessionManager.remoteSessionChanges: Flow<MediaSession.Token?>
+ get() =
+ callbackFlow {
+ val callback =
+ object : MediaSessionManager.RemoteSessionCallback {
+ override fun onVolumeChanged(sessionToken: MediaSession.Token, flags: Int) {
+ launch { send(sessionToken) }
+ }
+
+ override fun onDefaultRemoteSessionChanged(
+ sessionToken: MediaSession.Token?
+ ) {
+ launch { send(sessionToken) }
+ }
+ }
+ registerRemoteSessionCallback(DirectExecutor.INSTANCE, callback)
+ awaitClose { unregisterRemoteSessionCallback(callback) }
+ }
+ .buffer(capacity = Channel.CONFLATED)
diff --git a/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/AudioRepository.kt b/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/AudioRepository.kt
index 6730aad..e7fec69 100644
--- a/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/AudioRepository.kt
+++ b/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/AudioRepository.kt
@@ -19,7 +19,6 @@
import android.media.AudioDeviceInfo
import android.media.AudioManager
import android.media.AudioManager.OnCommunicationDeviceChangedListener
-import androidx.concurrent.futures.DirectExecutor
import com.android.internal.util.ConcurrentUtils
import com.android.settingslib.volume.shared.AudioManagerEventsReceiver
import com.android.settingslib.volume.shared.model.AudioManagerEvent
@@ -109,8 +108,8 @@
callbackFlow {
val listener = OnCommunicationDeviceChangedListener { trySend(Unit) }
audioManager.addOnCommunicationDeviceChangedListener(
- DirectExecutor.INSTANCE,
- listener
+ ConcurrentUtils.DIRECT_EXECUTOR,
+ listener,
)
awaitClose { audioManager.removeOnCommunicationDeviceChangedListener(listener) }
@@ -146,7 +145,7 @@
maxVolume = audioManager.getStreamMaxVolume(audioStream.value),
volume = audioManager.getStreamVolume(audioStream.value),
isAffectedByRingerMode = audioManager.isStreamAffectedByRingerMode(audioStream.value),
- isMuted = audioManager.isStreamMute(audioStream.value),
+ isMuted = audioManager.isStreamMute(audioStream.value)
)
}
diff --git a/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/LocalMediaRepository.kt b/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/LocalMediaRepository.kt
index 298dd71e..724dd51 100644
--- a/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/LocalMediaRepository.kt
+++ b/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/LocalMediaRepository.kt
@@ -15,14 +15,10 @@
*/
package com.android.settingslib.volume.data.repository
-import android.media.MediaRouter2Manager
-import android.media.RoutingSessionInfo
import com.android.settingslib.media.LocalMediaManager
import com.android.settingslib.media.MediaDevice
-import com.android.settingslib.volume.data.model.RoutingSession
import com.android.settingslib.volume.shared.AudioManagerEventsReceiver
import com.android.settingslib.volume.shared.model.AudioManagerEvent
-import kotlin.coroutines.CoroutineContext
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
@@ -30,35 +26,23 @@
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.filterIsInstance
-import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.flow.stateIn
-import kotlinx.coroutines.withContext
/** Repository providing data about connected media devices. */
interface LocalMediaRepository {
- /** Available devices list */
- val mediaDevices: StateFlow<Collection<MediaDevice>>
-
/** Currently connected media device */
val currentConnectedDevice: StateFlow<MediaDevice?>
-
- val remoteRoutingSessions: StateFlow<Collection<RoutingSession>>
-
- suspend fun adjustSessionVolume(sessionId: String?, volume: Int)
}
class LocalMediaRepositoryImpl(
audioManagerEventsReceiver: AudioManagerEventsReceiver,
private val localMediaManager: LocalMediaManager,
- private val mediaRouter2Manager: MediaRouter2Manager,
coroutineScope: CoroutineScope,
- private val backgroundContext: CoroutineContext,
) : LocalMediaRepository {
private val devicesChanges =
@@ -94,18 +78,6 @@
}
.shareIn(coroutineScope, SharingStarted.WhileSubscribed(), replay = 0)
- override val mediaDevices: StateFlow<Collection<MediaDevice>> =
- mediaDevicesUpdates
- .mapNotNull {
- if (it is DevicesUpdate.DeviceListUpdate) {
- it.newDevices ?: emptyList()
- } else {
- null
- }
- }
- .flowOn(backgroundContext)
- .stateIn(coroutineScope, SharingStarted.WhileSubscribed(), emptyList())
-
override val currentConnectedDevice: StateFlow<MediaDevice?> =
merge(devicesChanges, mediaDevicesUpdates)
.map { localMediaManager.currentConnectedDevice }
@@ -116,30 +88,6 @@
localMediaManager.currentConnectedDevice
)
- override val remoteRoutingSessions: StateFlow<Collection<RoutingSession>> =
- merge(devicesChanges, mediaDevicesUpdates)
- .onStart { emit(Unit) }
- .map { localMediaManager.remoteRoutingSessions.map(::toRoutingSession) }
- .flowOn(backgroundContext)
- .stateIn(coroutineScope, SharingStarted.WhileSubscribed(), emptyList())
-
- override suspend fun adjustSessionVolume(sessionId: String?, volume: Int) {
- withContext(backgroundContext) {
- if (sessionId == null) {
- localMediaManager.adjustSessionVolume(volume)
- } else {
- localMediaManager.adjustSessionVolume(sessionId, volume)
- }
- }
- }
-
- private fun toRoutingSession(info: RoutingSessionInfo): RoutingSession =
- RoutingSession(
- info,
- isMediaOutputDisabled = mediaRouter2Manager.getTransferableRoutes(info).isEmpty(),
- isVolumeSeekBarEnabled = localMediaManager.shouldEnableVolumeSeekBar(info)
- )
-
private sealed interface DevicesUpdate {
data class DeviceListUpdate(val newDevices: List<MediaDevice>?) : DevicesUpdate
diff --git a/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/MediaControllerRepository.kt b/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/MediaControllerRepository.kt
index 7c231d1..e4ac9fe 100644
--- a/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/MediaControllerRepository.kt
+++ b/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/MediaControllerRepository.kt
@@ -27,18 +27,26 @@
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.filterIsInstance
-import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.stateIn
/** Provides controllers for currently active device media sessions. */
interface MediaControllerRepository {
- /** Current [MediaController]. Null is emitted when there is no active [MediaController]. */
- val activeLocalMediaController: StateFlow<MediaController?>
+ /**
+ * Get a list of controllers for all ongoing sessions. The controllers will be provided in
+ * priority order with the most important controller at index 0.
+ *
+ * This requires the [android.Manifest.permission.MEDIA_CONTENT_CONTROL] permission be held by
+ * the calling app.
+ */
+ val activeSessions: StateFlow<List<MediaController>>
}
class MediaControllerRepositoryImpl(
@@ -49,51 +57,17 @@
backgroundContext: CoroutineContext,
) : MediaControllerRepository {
- private val devicesChanges =
- audioManagerEventsReceiver.events.filterIsInstance(
- AudioManagerEvent.StreamDevicesChanged::class
- )
-
- override val activeLocalMediaController: StateFlow<MediaController?> =
- combine(
- mediaSessionManager.activeMediaChanges.onStart {
- emit(mediaSessionManager.getActiveSessions(null))
- },
- localBluetoothManager?.headsetAudioModeChanges?.onStart { emit(Unit) }
- ?: flowOf(null),
- devicesChanges.onStart { emit(AudioManagerEvent.StreamDevicesChanged) },
- ) { controllers, _, _ ->
- controllers?.let(::findLocalMediaController)
- }
+ override val activeSessions: StateFlow<List<MediaController>> =
+ merge(
+ mediaSessionManager.activeMediaChanges.filterNotNull(),
+ localBluetoothManager?.headsetAudioModeChanges?.map {
+ mediaSessionManager.getActiveSessions(null)
+ } ?: emptyFlow(),
+ audioManagerEventsReceiver.events
+ .filterIsInstance(AudioManagerEvent.StreamDevicesChanged::class)
+ .map { mediaSessionManager.getActiveSessions(null) },
+ )
+ .onStart { emit(mediaSessionManager.getActiveSessions(null)) }
.flowOn(backgroundContext)
- .stateIn(coroutineScope, SharingStarted.WhileSubscribed(), null)
-
- private fun findLocalMediaController(
- controllers: Collection<MediaController>,
- ): MediaController? {
- var localController: MediaController? = null
- val remoteMediaSessionLists: MutableList<String> = ArrayList()
- for (controller in controllers) {
- val playbackInfo: MediaController.PlaybackInfo = controller.playbackInfo ?: continue
- when (playbackInfo.playbackType) {
- MediaController.PlaybackInfo.PLAYBACK_TYPE_REMOTE -> {
- if (localController?.packageName.equals(controller.packageName)) {
- localController = null
- }
- if (!remoteMediaSessionLists.contains(controller.packageName)) {
- remoteMediaSessionLists.add(controller.packageName)
- }
- }
- MediaController.PlaybackInfo.PLAYBACK_TYPE_LOCAL -> {
- if (
- localController == null &&
- !remoteMediaSessionLists.contains(controller.packageName)
- ) {
- localController = controller
- }
- }
- }
- }
- return localController
- }
+ .stateIn(coroutineScope, SharingStarted.WhileSubscribed(), emptyList())
}
diff --git a/packages/SettingsLib/src/com/android/settingslib/volume/domain/interactor/AudioVolumeInteractor.kt b/packages/SettingsLib/src/com/android/settingslib/volume/domain/interactor/AudioVolumeInteractor.kt
index c9ac97d..778653b 100644
--- a/packages/SettingsLib/src/com/android/settingslib/volume/domain/interactor/AudioVolumeInteractor.kt
+++ b/packages/SettingsLib/src/com/android/settingslib/volume/domain/interactor/AudioVolumeInteractor.kt
@@ -66,6 +66,10 @@
}
}
+ fun isMutable(audioStream: AudioStream): Boolean =
+ // Alarm stream doesn't support muting
+ audioStream.value != AudioManager.STREAM_ALARM
+
private suspend fun processVolume(
audioStreamModel: AudioStreamModel,
ringerMode: RingerMode,
diff --git a/packages/SettingsLib/src/com/android/settingslib/volume/domain/interactor/LocalMediaInteractor.kt b/packages/SettingsLib/src/com/android/settingslib/volume/domain/interactor/LocalMediaInteractor.kt
deleted file mode 100644
index f621335..0000000
--- a/packages/SettingsLib/src/com/android/settingslib/volume/domain/interactor/LocalMediaInteractor.kt
+++ /dev/null
@@ -1,57 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.settingslib.volume.domain.interactor
-
-import com.android.settingslib.media.MediaDevice
-import com.android.settingslib.volume.data.repository.LocalMediaRepository
-import com.android.settingslib.volume.domain.model.RoutingSession
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.flow.SharingStarted
-import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.stateIn
-
-class LocalMediaInteractor(
- private val repository: LocalMediaRepository,
- coroutineScope: CoroutineScope,
-) {
-
- /** Available devices list */
- val mediaDevices: StateFlow<Collection<MediaDevice>>
- get() = repository.mediaDevices
-
- /** Currently connected media device */
- val currentConnectedDevice: StateFlow<MediaDevice?>
- get() = repository.currentConnectedDevice
-
- val remoteRoutingSessions: StateFlow<List<RoutingSession>> =
- repository.remoteRoutingSessions
- .map { sessions ->
- sessions.map {
- RoutingSession(
- routingSessionInfo = it.routingSessionInfo,
- isMediaOutputDisabled = it.isMediaOutputDisabled,
- isVolumeSeekBarEnabled =
- it.isVolumeSeekBarEnabled && it.routingSessionInfo.volumeMax > 0
- )
- }
- }
- .stateIn(coroutineScope, SharingStarted.WhileSubscribed(), emptyList())
-
- suspend fun adjustSessionVolume(sessionId: String?, volume: Int) =
- repository.adjustSessionVolume(sessionId, volume)
-}
diff --git a/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/LocalMediaRepositoryImplTest.kt b/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/LocalMediaRepositoryImplTest.kt
index 2d12dae..caf41f2 100644
--- a/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/LocalMediaRepositoryImplTest.kt
+++ b/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/LocalMediaRepositoryImplTest.kt
@@ -15,17 +15,12 @@
*/
package com.android.settingslib.volume.data.repository
-import android.media.MediaRoute2Info
-import android.media.MediaRouter2Manager
-import android.media.RoutingSessionInfo
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.settingslib.media.LocalMediaManager
import com.android.settingslib.media.MediaDevice
-import com.android.settingslib.volume.data.model.RoutingSession
import com.android.settingslib.volume.shared.FakeAudioManagerEventsReceiver
import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.test.TestScope
@@ -37,15 +32,10 @@
import org.mockito.ArgumentCaptor
import org.mockito.Captor
import org.mockito.Mock
-import org.mockito.Mockito.any
-import org.mockito.Mockito.anyInt
-import org.mockito.Mockito.anyString
-import org.mockito.Mockito.mock
import org.mockito.Mockito.verify
import org.mockito.Mockito.`when`
import org.mockito.MockitoAnnotations
-@OptIn(ExperimentalCoroutinesApi::class)
@RunWith(AndroidJUnit4::class)
@SmallTest
class LocalMediaRepositoryImplTest {
@@ -53,7 +43,6 @@
@Mock private lateinit var localMediaManager: LocalMediaManager
@Mock private lateinit var mediaDevice1: MediaDevice
@Mock private lateinit var mediaDevice2: MediaDevice
- @Mock private lateinit var mediaRouter2Manager: MediaRouter2Manager
@Captor
private lateinit var deviceCallbackCaptor: ArgumentCaptor<LocalMediaManager.DeviceCallback>
@@ -71,29 +60,11 @@
LocalMediaRepositoryImpl(
eventsReceiver,
localMediaManager,
- mediaRouter2Manager,
testScope.backgroundScope,
- testScope.testScheduler,
)
}
@Test
- fun mediaDevices_areUpdated() {
- testScope.runTest {
- var mediaDevices: Collection<MediaDevice>? = null
- underTest.mediaDevices.onEach { mediaDevices = it }.launchIn(backgroundScope)
- runCurrent()
- verify(localMediaManager).registerCallback(deviceCallbackCaptor.capture())
- deviceCallbackCaptor.value.onDeviceListUpdate(listOf(mediaDevice1, mediaDevice2))
- runCurrent()
-
- assertThat(mediaDevices).hasSize(2)
- assertThat(mediaDevices).contains(mediaDevice1)
- assertThat(mediaDevices).contains(mediaDevice2)
- }
- }
-
- @Test
fun deviceListUpdated_currentConnectedDeviceUpdated() {
testScope.runTest {
var currentConnectedDevice: MediaDevice? = null
@@ -110,78 +81,4 @@
assertThat(currentConnectedDevice).isEqualTo(mediaDevice1)
}
}
-
- @Test
- fun kek() {
- testScope.runTest {
- `when`(localMediaManager.remoteRoutingSessions)
- .thenReturn(
- listOf(
- testRoutingSessionInfo1,
- testRoutingSessionInfo2,
- testRoutingSessionInfo3,
- )
- )
- `when`(localMediaManager.shouldEnableVolumeSeekBar(any())).then {
- (it.arguments[0] as RoutingSessionInfo) == testRoutingSessionInfo1
- }
- `when`(mediaRouter2Manager.getTransferableRoutes(any<RoutingSessionInfo>())).then {
- if ((it.arguments[0] as RoutingSessionInfo) == testRoutingSessionInfo2) {
- return@then listOf(mock(MediaRoute2Info::class.java))
- }
- emptyList<MediaRoute2Info>()
- }
- var remoteRoutingSessions: Collection<RoutingSession>? = null
- underTest.remoteRoutingSessions
- .onEach { remoteRoutingSessions = it }
- .launchIn(backgroundScope)
-
- runCurrent()
-
- assertThat(remoteRoutingSessions)
- .containsExactlyElementsIn(
- listOf(
- RoutingSession(
- routingSessionInfo = testRoutingSessionInfo1,
- isVolumeSeekBarEnabled = true,
- isMediaOutputDisabled = true,
- ),
- RoutingSession(
- routingSessionInfo = testRoutingSessionInfo2,
- isVolumeSeekBarEnabled = false,
- isMediaOutputDisabled = false,
- ),
- RoutingSession(
- routingSessionInfo = testRoutingSessionInfo3,
- isVolumeSeekBarEnabled = false,
- isMediaOutputDisabled = true,
- )
- )
- )
- }
- }
-
- @Test
- fun adjustSessionVolume_adjusts() {
- testScope.runTest {
- var volume = 0
- `when`(localMediaManager.adjustSessionVolume(anyString(), anyInt())).then {
- volume = it.arguments[1] as Int
- Unit
- }
-
- underTest.adjustSessionVolume("test_session", 10)
-
- assertThat(volume).isEqualTo(10)
- }
- }
-
- private companion object {
- val testRoutingSessionInfo1 =
- RoutingSessionInfo.Builder("id_1", "test.pkg.1").addSelectedRoute("route_1").build()
- val testRoutingSessionInfo2 =
- RoutingSessionInfo.Builder("id_2", "test.pkg.2").addSelectedRoute("route_2").build()
- val testRoutingSessionInfo3 =
- RoutingSessionInfo.Builder("id_3", "test.pkg.3").addSelectedRoute("route_3").build()
- }
}
diff --git a/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/MediaControllerRepositoryImplTest.kt b/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/MediaControllerRepositoryImplTest.kt
index f3d1714..964c3f7 100644
--- a/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/MediaControllerRepositoryImplTest.kt
+++ b/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/MediaControllerRepositoryImplTest.kt
@@ -22,13 +22,10 @@
import android.media.session.PlaybackState
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
-import com.android.settingslib.bluetooth.BluetoothCallback
import com.android.settingslib.bluetooth.BluetoothEventManager
import com.android.settingslib.bluetooth.LocalBluetoothManager
import com.android.settingslib.volume.shared.FakeAudioManagerEventsReceiver
-import com.android.settingslib.volume.shared.model.AudioManagerEvent
import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.test.TestScope
@@ -37,21 +34,15 @@
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
-import org.mockito.ArgumentCaptor
-import org.mockito.Captor
import org.mockito.Mock
import org.mockito.Mockito.any
-import org.mockito.Mockito.verify
import org.mockito.Mockito.`when`
import org.mockito.MockitoAnnotations
-@OptIn(ExperimentalCoroutinesApi::class)
@RunWith(AndroidJUnit4::class)
@SmallTest
class MediaControllerRepositoryImplTest {
- @Captor private lateinit var callbackCaptor: ArgumentCaptor<BluetoothCallback>
-
@Mock private lateinit var mediaSessionManager: MediaSessionManager
@Mock private lateinit var localBluetoothManager: LocalBluetoothManager
@Mock private lateinit var eventManager: BluetoothEventManager
@@ -103,7 +94,7 @@
}
@Test
- fun playingMediaDevicesAvailable_sessionIsActive() {
+ fun mediaDevicesAvailable_returnsAllActiveOnes() {
testScope.runTest {
`when`(mediaSessionManager.getActiveSessions(any()))
.thenReturn(
@@ -112,53 +103,25 @@
statelessMediaController,
errorMediaController,
remoteMediaController,
- localMediaController
+ localMediaController,
)
)
- var mediaController: MediaController? = null
- underTest.activeLocalMediaController
- .onEach { mediaController = it }
- .launchIn(backgroundScope)
+
+ var mediaControllers: Collection<MediaController>? = null
+ underTest.activeSessions.onEach { mediaControllers = it }.launchIn(backgroundScope)
runCurrent()
- eventsReceiver.triggerEvent(AudioManagerEvent.StreamDevicesChanged)
- triggerOnAudioModeChanged()
- runCurrent()
-
- assertThat(mediaController).isSameInstanceAs(localMediaController)
- }
- }
-
- @Test
- fun noPlayingMediaDevicesAvailable_sessionIsInactive() {
- testScope.runTest {
- `when`(mediaSessionManager.getActiveSessions(any()))
- .thenReturn(
- listOf(
- stoppedMediaController,
- statelessMediaController,
- errorMediaController,
- )
+ assertThat(mediaControllers)
+ .containsExactly(
+ stoppedMediaController,
+ statelessMediaController,
+ errorMediaController,
+ remoteMediaController,
+ localMediaController,
)
- var mediaController: MediaController? = null
- underTest.activeLocalMediaController
- .onEach { mediaController = it }
- .launchIn(backgroundScope)
- runCurrent()
-
- eventsReceiver.triggerEvent(AudioManagerEvent.StreamDevicesChanged)
- triggerOnAudioModeChanged()
- runCurrent()
-
- assertThat(mediaController).isNull()
}
}
- private fun triggerOnAudioModeChanged() {
- verify(eventManager).registerCallback(callbackCaptor.capture())
- callbackCaptor.value.onAudioModeChanged()
- }
-
private companion object {
val statePlaying: PlaybackState =
PlaybackState.Builder().setState(PlaybackState.STATE_PLAYING, 0, 0f).build()
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/applications/RecentAppOpsAccessesTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/applications/RecentAppOpsAccessesTest.java
index f9505dd..52622a7 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/applications/RecentAppOpsAccessesTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/applications/RecentAppOpsAccessesTest.java
@@ -32,14 +32,17 @@
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.pm.UserProperties;
import android.os.Process;
import android.os.UserHandle;
import android.os.UserManager;
+import android.platform.test.flag.junit.SetFlagsRule;
import android.util.LongSparseArray;
import com.android.settingslib.testutils.shadow.ShadowPermissionChecker;
import org.junit.Before;
+import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
@@ -58,6 +61,8 @@
@Config(shadows = {ShadowPermissionChecker.class})
public class RecentAppOpsAccessesTest {
+ @Rule
+ public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
private static final int TEST_UID = 1234;
private static final long NOW = 1_000_000_000; // Approximately 9/8/2001
private static final long ONE_MIN_AGO = NOW - TimeUnit.MINUTES.toMillis(1);
@@ -73,6 +78,8 @@
@Mock
private UserManager mUserManager;
@Mock
+ private UserProperties mUserProperties;
+ @Mock
private Clock mClock;
private Context mContext;
private int mTestUserId;
@@ -132,6 +139,58 @@
}
@Test
+ public void testGetAppList_quietModeDisabled_shouldFilterRecentAccesses() {
+ mSetFlagsRule.enableFlags(
+ android.multiuser.Flags.FLAG_SUPPORT_AUTOLOCK_FOR_PRIVATE_SPACE,
+ android.multiuser.Flags.FLAG_HANDLE_INTERLEAVED_SETTINGS_FOR_PRIVATE_SPACE);
+ when(mUserManager.isQuietModeEnabled(any())).thenReturn(false);
+
+ List<RecentAppOpsAccess.Access> requests = mRecentAppOpsAccess.getAppList(false);
+ // Only two of the apps have requested location within 15 min.
+ assertThat(requests).hasSize(2);
+ // Make sure apps are ordered by recency
+ assertThat(requests.get(0).packageName).isEqualTo(TEST_PACKAGE_NAMES[0]);
+ assertThat(requests.get(0).accessFinishTime).isEqualTo(ONE_MIN_AGO);
+ assertThat(requests.get(1).packageName).isEqualTo(TEST_PACKAGE_NAMES[1]);
+ assertThat(requests.get(1).accessFinishTime).isEqualTo(TWENTY_THREE_HOURS_AGO);
+ }
+
+ @Test
+ public void testGetAppList_quietModeEnabledShowInQuietDefault_shouldFilterRecentAccesses() {
+ mSetFlagsRule.enableFlags(
+ android.multiuser.Flags.FLAG_SUPPORT_AUTOLOCK_FOR_PRIVATE_SPACE,
+ android.multiuser.Flags.FLAG_HANDLE_INTERLEAVED_SETTINGS_FOR_PRIVATE_SPACE);
+ when(mUserManager.isQuietModeEnabled(any())).thenReturn(true);
+ when(mUserManager.getUserProperties(any())).thenReturn(mUserProperties);
+ when(mUserProperties.getShowInQuietMode())
+ .thenReturn(UserProperties.SHOW_IN_QUIET_MODE_DEFAULT);
+
+ List<RecentAppOpsAccess.Access> requests = mRecentAppOpsAccess.getAppList(false);
+ // Only two of the apps have requested location within 15 min.
+ assertThat(requests).hasSize(2);
+ // Make sure apps are ordered by recency
+ assertThat(requests.get(0).packageName).isEqualTo(TEST_PACKAGE_NAMES[0]);
+ assertThat(requests.get(0).accessFinishTime).isEqualTo(ONE_MIN_AGO);
+ assertThat(requests.get(1).packageName).isEqualTo(TEST_PACKAGE_NAMES[1]);
+ assertThat(requests.get(1).accessFinishTime).isEqualTo(TWENTY_THREE_HOURS_AGO);
+ }
+
+ @Test
+ public void testGetAppList_quietModeEnabledShowInQuietHidden_shouldNotFilterRecentAccesses() {
+ mSetFlagsRule.enableFlags(
+ android.multiuser.Flags.FLAG_SUPPORT_AUTOLOCK_FOR_PRIVATE_SPACE,
+ android.multiuser.Flags.FLAG_HANDLE_INTERLEAVED_SETTINGS_FOR_PRIVATE_SPACE);
+ when(mUserManager.isQuietModeEnabled(any())).thenReturn(true);
+ when(mUserManager.getUserProperties(any())).thenReturn(mUserProperties);
+ when(mUserProperties.getShowInQuietMode())
+ .thenReturn(UserProperties.SHOW_IN_QUIET_MODE_HIDDEN);
+
+ List<RecentAppOpsAccess.Access> requests = mRecentAppOpsAccess.getAppList(false);
+ // Apps doesn't show up in the list of apps.
+ assertThat(requests).hasSize(0);
+ }
+
+ @Test
public void testGetAppList_shouldNotShowAndroidOS() throws NameNotFoundException {
// Add android OS to the list of apps.
PackageOps androidSystemPackageOps =
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothEventManagerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothEventManagerTest.java
index 13635c3..48bbf4e 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothEventManagerTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothEventManagerTest.java
@@ -18,6 +18,7 @@
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
@@ -489,4 +490,17 @@
verify(mErrorListener).onShowError(any(Context.class), eq(DEVICE_NAME),
eq(R.string.bluetooth_pairing_pin_error_message));
}
+
+ /**
+ * Intent ACTION_AUTO_ON_STATE_CHANGED should dispatch to callback.
+ */
+ @Test
+ public void intentWithExtraState_autoOnStateChangedShouldDispatchToRegisterCallback() {
+ mBluetoothEventManager.registerCallback(mBluetoothCallback);
+ mIntent = new Intent(BluetoothAdapter.ACTION_AUTO_ON_STATE_CHANGED);
+
+ mContext.sendBroadcast(mIntent);
+
+ verify(mBluetoothCallback).onAutoOnStateChanged(anyInt());
+ }
}
diff --git a/packages/SettingsProvider/Android.bp b/packages/SettingsProvider/Android.bp
index 7ec3d24..bf4f60d 100644
--- a/packages/SettingsProvider/Android.bp
+++ b/packages/SettingsProvider/Android.bp
@@ -60,6 +60,7 @@
// because this test is not an instrumentation test. (because the target runs in the system process.)
"SettingsProviderLib",
"androidx.test.rules",
+ "frameworks-base-testutils",
"device_config_service_flags_java",
"flag-junit",
"junit",
diff --git a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java
index eaec617..5629a7b 100644
--- a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java
+++ b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java
@@ -256,8 +256,7 @@
Settings.Secure.HEARING_AID_MEDIA_ROUTING,
Settings.Secure.HEARING_AID_NOTIFICATION_ROUTING,
Settings.Secure.ACCESSIBILITY_FONT_SCALING_HAS_BEEN_CHANGED,
- Settings.Secure.SEARCH_PRESS_HOLD_NAV_HANDLE_ENABLED,
- Settings.Secure.SEARCH_LONG_PRESS_HOME_ENABLED,
+ Settings.Secure.SEARCH_ALL_ENTRYPOINTS_ENABLED,
Settings.Secure.HUB_MODE_TUTORIAL_STATE,
Settings.Secure.STYLUS_BUTTONS_ENABLED,
Settings.Secure.STYLUS_HANDWRITING_ENABLED,
diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java
index 046d6e2..b8d95eb 100644
--- a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java
+++ b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java
@@ -208,8 +208,7 @@
VALIDATORS.put(Secure.ASSIST_TOUCH_GESTURE_ENABLED, BOOLEAN_VALIDATOR);
VALIDATORS.put(Secure.ASSIST_LONG_PRESS_HOME_ENABLED, BOOLEAN_VALIDATOR);
VALIDATORS.put(Secure.VISUAL_QUERY_ACCESSIBILITY_DETECTION_ENABLED, BOOLEAN_VALIDATOR);
- VALIDATORS.put(Secure.SEARCH_PRESS_HOLD_NAV_HANDLE_ENABLED, BOOLEAN_VALIDATOR);
- VALIDATORS.put(Secure.SEARCH_LONG_PRESS_HOME_ENABLED, BOOLEAN_VALIDATOR);
+ VALIDATORS.put(Secure.SEARCH_ALL_ENTRYPOINTS_ENABLED, BOOLEAN_VALIDATOR);
VALIDATORS.put(Secure.VR_DISPLAY_MODE, new DiscreteValueValidator(new String[] {"0", "1"}));
VALIDATORS.put(Secure.NOTIFICATION_BADGING, BOOLEAN_VALIDATOR);
VALIDATORS.put(Secure.NOTIFICATION_DISMISS_RTL, BOOLEAN_VALIDATOR);
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsHelper.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsHelper.java
index 3e0d05c..1eb04ac 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsHelper.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsHelper.java
@@ -98,6 +98,7 @@
sBroadcastOnRestore.add(Settings.Secure.DARK_THEME_CUSTOM_END_TIME);
sBroadcastOnRestore.add(Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_NAVBAR_ENABLED);
sBroadcastOnRestore.add(Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS);
+ sBroadcastOnRestore.add(Settings.Secure.ACCESSIBILITY_QS_TARGETS);
sBroadcastOnRestoreSystemUI = new ArraySet<String>(2);
sBroadcastOnRestoreSystemUI.add(Settings.Secure.QS_TILES);
sBroadcastOnRestoreSystemUI.add(Settings.Secure.QS_AUTO_ADDED_TILES);
@@ -229,6 +230,10 @@
} else if (Settings.System.ACCELEROMETER_ROTATION.equals(name)
&& shouldSkipAutoRotateRestore()) {
return;
+ } else if (Settings.Secure.ACCESSIBILITY_QS_TARGETS.equals(name)) {
+ // Don't write it to setting. Let the broadcast receiver in
+ // AccessibilityManagerService handle restore/merging logic.
+ return;
}
// Default case: write the restored value to settings
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java
index 02d212c..dba3bac 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java
@@ -1950,11 +1950,8 @@
Settings.Secure.ASSIST_LONG_PRESS_HOME_ENABLED,
SecureSettingsProto.Assist.LONG_PRESS_HOME_ENABLED);
dumpSetting(s, p,
- Settings.Secure.SEARCH_PRESS_HOLD_NAV_HANDLE_ENABLED,
- SecureSettingsProto.Assist.SEARCH_PRESS_HOLD_NAV_HANDLE_ENABLED);
- dumpSetting(s, p,
- Settings.Secure.SEARCH_LONG_PRESS_HOME_ENABLED,
- SecureSettingsProto.Assist.SEARCH_LONG_PRESS_HOME_ENABLED);
+ Settings.Secure.SEARCH_ALL_ENTRYPOINTS_ENABLED,
+ SecureSettingsProto.Assist.SEARCH_ALL_ENTRYPOINTS_ENABLED);
dumpSetting(s, p,
Settings.Secure.VISUAL_QUERY_ACCESSIBILITY_DETECTION_ENABLED,
SecureSettingsProto.Assist.VISUAL_QUERY_ACCESSIBILITY_DETECTION_ENABLED);
diff --git a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
index 6eb2dd0..8cafe5f 100644
--- a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
+++ b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
@@ -688,6 +688,7 @@
Settings.Secure.DEVICE_PAIRED,
Settings.Secure.DIALER_DEFAULT_APPLICATION,
Settings.Secure.DISABLED_PRINT_SERVICES,
+ Settings.Secure.DISABLE_SECURE_WINDOWS,
Settings.Secure.DISABLED_SYSTEM_INPUT_METHODS,
Settings.Secure.DOCKED_CLOCK_FACE,
Settings.Secure.DOZE_PULSE_ON_LONG_PRESS,
diff --git a/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsHelperRestoreTest.java b/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsHelperRestoreTest.java
index 197788e..2f8cf4b 100644
--- a/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsHelperRestoreTest.java
+++ b/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsHelperRestoreTest.java
@@ -16,23 +16,31 @@
package com.android.providers.settings;
+import static com.google.common.truth.Truth.assertThat;
+
import static junit.framework.Assert.assertEquals;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
+import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.provider.Settings;
+import android.provider.SettingsStringUtil;
import androidx.test.InstrumentationRegistry;
import androidx.test.runner.AndroidJUnit4;
+import com.android.internal.util.test.BroadcastInterceptingContext;
+
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
+import java.util.concurrent.ExecutionException;
+
/**
* Tests for {@link SettingsHelper#restoreValue(Context, ContentResolver, ContentValues, Uri,
* String, String, int)}. Specifically verifies that we restore critical accessibility settings only
@@ -165,4 +173,33 @@
assertEquals(restoreSettingValue, Settings.Secure.getInt(mContentResolver, settingName));
}
+
+ @Test
+ public void restoreAccessibilityQsTargets_broadcastSent()
+ throws ExecutionException, InterruptedException {
+ BroadcastInterceptingContext interceptingContext = new BroadcastInterceptingContext(
+ mContext);
+ final String settingName = Settings.Secure.ACCESSIBILITY_QS_TARGETS;
+ final String restoreSettingValue = "com.android.server.accessibility/ColorInversion"
+ + SettingsStringUtil.DELIMITER
+ + "com.android.server.accessibility/ColorCorrectionTile";
+ BroadcastInterceptingContext.FutureIntent futureIntent =
+ interceptingContext.nextBroadcastIntent(Intent.ACTION_SETTING_RESTORED);
+
+ mSettingsHelper.restoreValue(
+ interceptingContext,
+ mContentResolver,
+ new ContentValues(2),
+ Settings.Secure.getUriFor(settingName),
+ settingName,
+ restoreSettingValue,
+ Build.VERSION.SDK_INT);
+
+ Intent intentReceived = futureIntent.get();
+ assertThat(intentReceived.getStringExtra(Intent.EXTRA_SETTING_NEW_VALUE))
+ .isEqualTo(restoreSettingValue);
+ assertThat(intentReceived.getIntExtra(
+ Intent.EXTRA_SETTING_RESTORED_FROM_SDK_INT, /* defaultValue= */ 0))
+ .isEqualTo(Build.VERSION.SDK_INT);
+ }
}
diff --git a/packages/Shell/AndroidManifest.xml b/packages/Shell/AndroidManifest.xml
index 02d19dc..5804071 100644
--- a/packages/Shell/AndroidManifest.xml
+++ b/packages/Shell/AndroidManifest.xml
@@ -932,6 +932,9 @@
<uses-permission
android:name="android.permission.OVERRIDE_SYSTEM_KEY_BEHAVIOR_IN_FOCUSED_WINDOW" />
+ <!-- Permission required for Cts test - CtsSettingsTestCases -->
+ <uses-permission android:name="android.permission.PREPARE_FACTORY_RESET" />
+
<application
android:label="@string/app_label"
android:theme="@android:style/Theme.DeviceDefault.DayNight"
diff --git a/packages/SystemUI/accessibility/accessibilitymenu/tests/src/com/android/systemui/accessibility/accessibilitymenu/tests/AccessibilityMenuServiceTest.java b/packages/SystemUI/accessibility/accessibilitymenu/tests/src/com/android/systemui/accessibility/accessibilitymenu/tests/AccessibilityMenuServiceTest.java
index 6546b87..f70ad9e 100644
--- a/packages/SystemUI/accessibility/accessibilitymenu/tests/src/com/android/systemui/accessibility/accessibilitymenu/tests/AccessibilityMenuServiceTest.java
+++ b/packages/SystemUI/accessibility/accessibilitymenu/tests/src/com/android/systemui/accessibility/accessibilitymenu/tests/AccessibilityMenuServiceTest.java
@@ -23,10 +23,10 @@
import static android.accessibilityservice.AccessibilityService.GLOBAL_ACTION_RECENTS;
import static android.accessibilityservice.AccessibilityService.GLOBAL_ACTION_TAKE_SCREENSHOT;
-import static com.android.systemui.accessibility.accessibilitymenu.AccessibilityMenuService.INTENT_OPEN_BLOCKED;
import static com.android.systemui.accessibility.accessibilitymenu.AccessibilityMenuService.INTENT_GLOBAL_ACTION;
import static com.android.systemui.accessibility.accessibilitymenu.AccessibilityMenuService.INTENT_GLOBAL_ACTION_EXTRA;
import static com.android.systemui.accessibility.accessibilitymenu.AccessibilityMenuService.INTENT_HIDE_MENU;
+import static com.android.systemui.accessibility.accessibilitymenu.AccessibilityMenuService.INTENT_OPEN_BLOCKED;
import static com.android.systemui.accessibility.accessibilitymenu.AccessibilityMenuService.INTENT_TOGGLE_MENU;
import static com.android.systemui.accessibility.accessibilitymenu.AccessibilityMenuService.PACKAGE_NAME;
@@ -77,6 +77,8 @@
private static final int TIMEOUT_SERVICE_STATUS_CHANGE_S = 5;
private static final int TIMEOUT_UI_CHANGE_S = 5;
private static final int NO_GLOBAL_ACTION = -1;
+ private static final Intent INTENT_OPEN_MENU = new Intent(INTENT_TOGGLE_MENU)
+ .setPackage(PACKAGE_NAME);
private static Instrumentation sInstrumentation;
private static UiAutomation sUiAutomation;
@@ -152,9 +154,6 @@
@Before
public void setup() throws Throwable {
sOpenBlocked.set(false);
- wakeUpScreen();
- sUiAutomation.executeShellCommand("input keyevent KEYCODE_MENU");
- openMenu();
}
@After
@@ -188,24 +187,17 @@
}
private static void openMenu() throws Throwable {
- openMenu(false);
- }
-
- private static void openMenu(boolean abandonOnBlock) throws Throwable {
- Intent intent = new Intent(INTENT_TOGGLE_MENU);
- intent.setPackage(PACKAGE_NAME);
- sInstrumentation.getContext().sendBroadcast(intent);
+ unlockSignal();
+ sInstrumentation.getContext().sendBroadcast(INTENT_OPEN_MENU);
TestUtils.waitUntil("Timed out before menu could appear.",
TIMEOUT_UI_CHANGE_S,
() -> {
- if (sOpenBlocked.get() && abandonOnBlock) {
- throw new IllegalStateException();
- }
if (isMenuVisible()) {
return true;
} else {
- sInstrumentation.getContext().sendBroadcast(intent);
+ unlockSignal();
+ sInstrumentation.getContext().sendBroadcast(INTENT_OPEN_MENU);
return false;
}
});
@@ -249,6 +241,7 @@
@Test
public void testAdjustBrightness() throws Throwable {
+ openMenu();
Context context = sInstrumentation.getTargetContext();
DisplayManager displayManager = context.getSystemService(
DisplayManager.class);
@@ -264,22 +257,28 @@
context.getDisplayId()).getBrightnessInfo();
try {
- displayManager.setBrightness(context.getDisplayId(), brightnessInfo.brightnessMinimum);
TestUtils.waitUntil("Could not change to minimum brightness",
TIMEOUT_UI_CHANGE_S,
- () -> displayManager.getBrightness(context.getDisplayId())
- == brightnessInfo.brightnessMinimum);
+ () -> {
+ displayManager.setBrightness(
+ context.getDisplayId(), brightnessInfo.brightnessMinimum);
+ return displayManager.getBrightness(context.getDisplayId())
+ == brightnessInfo.brightnessMinimum;
+ });
brightnessUpButton.performAction(CLICK_ID);
TestUtils.waitUntil("Did not detect an increase in brightness.",
TIMEOUT_UI_CHANGE_S,
() -> displayManager.getBrightness(context.getDisplayId())
> brightnessInfo.brightnessMinimum);
- displayManager.setBrightness(context.getDisplayId(), brightnessInfo.brightnessMaximum);
TestUtils.waitUntil("Could not change to maximum brightness",
TIMEOUT_UI_CHANGE_S,
- () -> displayManager.getBrightness(context.getDisplayId())
- == brightnessInfo.brightnessMaximum);
+ () -> {
+ displayManager.setBrightness(
+ context.getDisplayId(), brightnessInfo.brightnessMaximum);
+ return displayManager.getBrightness(context.getDisplayId())
+ == brightnessInfo.brightnessMaximum;
+ });
brightnessDownButton.performAction(CLICK_ID);
TestUtils.waitUntil("Did not detect a decrease in brightness.",
TIMEOUT_UI_CHANGE_S,
@@ -292,6 +291,7 @@
@Test
public void testAdjustVolume() throws Throwable {
+ openMenu();
Context context = sInstrumentation.getTargetContext();
AudioManager audioManager = context.getSystemService(AudioManager.class);
int resetVolume = audioManager.getStreamVolume(AudioManager.STREAM_MUSIC);
@@ -332,6 +332,7 @@
@Test
public void testAssistantButton_opensVoiceAssistant() throws Throwable {
+ openMenu();
AccessibilityNodeInfo assistantButton = findGridButtonInfo(getGridButtonList(),
String.valueOf(ShortcutId.ID_ASSISTANT_VALUE.ordinal()));
Intent expectedIntent = new Intent(Intent.ACTION_VOICE_COMMAND);
@@ -349,6 +350,7 @@
@Test
public void testAccessibilitySettingsButton_opensAccessibilitySettings() throws Throwable {
+ openMenu();
AccessibilityNodeInfo settingsButton = findGridButtonInfo(getGridButtonList(),
String.valueOf(ShortcutId.ID_A11YSETTING_VALUE.ordinal()));
Intent expectedIntent = new Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS);
@@ -364,6 +366,7 @@
@Test
public void testPowerButton_performsGlobalAction() throws Throwable {
+ openMenu();
AccessibilityNodeInfo button = findGridButtonInfo(getGridButtonList(),
String.valueOf(ShortcutId.ID_POWER_VALUE.ordinal()));
@@ -376,6 +379,7 @@
@Test
public void testRecentButton_performsGlobalAction() throws Throwable {
+ openMenu();
AccessibilityNodeInfo button = findGridButtonInfo(getGridButtonList(),
String.valueOf(ShortcutId.ID_RECENT_VALUE.ordinal()));
@@ -388,6 +392,7 @@
@Test
public void testLockButton_performsGlobalAction() throws Throwable {
+ openMenu();
AccessibilityNodeInfo button = findGridButtonInfo(getGridButtonList(),
String.valueOf(ShortcutId.ID_LOCKSCREEN_VALUE.ordinal()));
@@ -400,6 +405,7 @@
@Test
public void testQuickSettingsButton_performsGlobalAction() throws Throwable {
+ openMenu();
AccessibilityNodeInfo button = findGridButtonInfo(getGridButtonList(),
String.valueOf(ShortcutId.ID_QUICKSETTING_VALUE.ordinal()));
@@ -412,6 +418,7 @@
@Test
public void testNotificationsButton_performsGlobalAction() throws Throwable {
+ openMenu();
AccessibilityNodeInfo button = findGridButtonInfo(getGridButtonList(),
String.valueOf(ShortcutId.ID_NOTIFICATION_VALUE.ordinal()));
@@ -424,6 +431,7 @@
@Test
public void testScreenshotButton_performsGlobalAction() throws Throwable {
+ openMenu();
AccessibilityNodeInfo button = findGridButtonInfo(getGridButtonList(),
String.valueOf(ShortcutId.ID_SCREENSHOT_VALUE.ordinal()));
@@ -436,6 +444,7 @@
@Test
public void testOnScreenLock_closesMenu() throws Throwable {
+ openMenu();
closeScreen();
wakeUpScreen();
@@ -447,13 +456,18 @@
closeScreen();
wakeUpScreen();
- boolean blocked = false;
- try {
- openMenu(true);
- } catch (IllegalStateException e) {
- // Expected
- blocked = true;
- }
- assertThat(blocked).isTrue();
+ TestUtils.waitUntil("Did not receive signal that menu cannot open",
+ TIMEOUT_UI_CHANGE_S,
+ () -> {
+ sInstrumentation.getContext().sendBroadcast(INTENT_OPEN_MENU);
+ return sOpenBlocked.get();
+ });
+ }
+
+ private static void unlockSignal() {
+ // MENU unlocks screen,
+ // BACK closes any menu that may appear if the screen wasn't locked.
+ sUiAutomation.executeShellCommand("input keyevent KEYCODE_MENU");
+ sUiAutomation.executeShellCommand("input keyevent KEYCODE_BACK");
}
}
diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig
index 8da5021..f057acc 100644
--- a/packages/SystemUI/aconfig/systemui.aconfig
+++ b/packages/SystemUI/aconfig/systemui.aconfig
@@ -25,6 +25,16 @@
}
flag {
+ name: "notification_minimalism_prototype"
+ namespace: "systemui"
+ description: "Prototype of notification minimalism; the new 'Intermediate' lockscreen customization proposal."
+ bug: "330387368"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
+
+flag {
name: "notification_view_flipper_pausing"
namespace: "systemui"
description: "Pause ViewFlippers inside Notification custom layouts when the shade is closed."
@@ -104,6 +114,13 @@
}
flag {
+ name: "notifications_heads_up_refactor"
+ namespace: "systemui"
+ description: "Use HeadsUpInteractor to feed HUN updates to the NSSL."
+ bug: "325936094"
+}
+
+flag {
name: "pss_app_selector_abrupt_exit_fix"
namespace: "systemui"
description: "Fixes the app selector abruptly disappearing without an animation, when the"
@@ -424,6 +441,13 @@
}
flag {
+ name: "screenshot_shelf_ui"
+ namespace: "systemui"
+ description: "Use new shelf UI flow for screenshots"
+ bug: "329659738"
+}
+
+flag {
name: "run_fingerprint_detect_on_dismissible_keyguard"
namespace: "systemui"
description: "Run fingerprint detect instead of authenticate if the keyguard is dismissible."
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/PlatformSlider.kt b/packages/SystemUI/compose/core/src/com/android/compose/PlatformSlider.kt
index 596a297..4a89e31 100644
--- a/packages/SystemUI/compose/core/src/com/android/compose/PlatformSlider.kt
+++ b/packages/SystemUI/compose/core/src/com/android/compose/PlatformSlider.kt
@@ -18,6 +18,7 @@
package com.android.compose
+import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.Canvas
@@ -266,8 +267,17 @@
label = "PlatformSliderCornersAnimation",
)
- val trackColor = colors.getTrackColor(enabled)
- val indicatorColor = colors.getIndicatorColor(enabled)
+ val trackColor by
+ animateColorAsState(
+ colors.getTrackColor(enabled),
+ label = "PlatformSliderTrackColorAnimation",
+ )
+
+ val indicatorColor by
+ animateColorAsState(
+ colors.getIndicatorColor(enabled),
+ label = "PlatformSliderIndicatorColorAnimation",
+ )
Canvas(modifier.fillMaxSize()) {
val trackCornerRadius = CornerRadius(size.height / 2, size.height / 2)
val trackPath = Path()
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerContent.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerContent.kt
index 621ddf7..0f3d3dc2 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerContent.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerContent.kt
@@ -53,6 +53,7 @@
import androidx.compose.material3.Text
import androidx.compose.material3.windowsizeclass.WindowHeightSizeClass
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@@ -71,6 +72,7 @@
import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
import androidx.compose.ui.unit.times
import com.android.compose.PlatformButton
import com.android.compose.animation.scene.ElementKey
@@ -84,7 +86,9 @@
import com.android.systemui.bouncer.ui.BouncerDialogFactory
import com.android.systemui.bouncer.ui.helper.BouncerSceneLayout
import com.android.systemui.bouncer.ui.viewmodel.AuthMethodBouncerViewModel
+import com.android.systemui.bouncer.ui.viewmodel.BouncerMessageViewModel
import com.android.systemui.bouncer.ui.viewmodel.BouncerViewModel
+import com.android.systemui.bouncer.ui.viewmodel.MessageViewModel
import com.android.systemui.bouncer.ui.viewmodel.PasswordBouncerViewModel
import com.android.systemui.bouncer.ui.viewmodel.PatternBouncerViewModel
import com.android.systemui.bouncer.ui.viewmodel.PinBouncerViewModel
@@ -166,7 +170,7 @@
modifier = Modifier.fillMaxWidth(),
) {
StatusMessage(
- viewModel = viewModel,
+ viewModel = viewModel.message,
modifier = Modifier,
)
@@ -228,7 +232,7 @@
when (authMethod) {
is PinBouncerViewModel -> {
StatusMessage(
- viewModel = viewModel,
+ viewModel = viewModel.message,
modifier = Modifier.align(Alignment.TopCenter),
)
@@ -241,7 +245,7 @@
}
is PatternBouncerViewModel -> {
StatusMessage(
- viewModel = viewModel,
+ viewModel = viewModel.message,
modifier = Modifier.align(Alignment.TopCenter),
)
@@ -280,7 +284,7 @@
modifier = Modifier.fillMaxWidth().align(Alignment.Center),
) {
StatusMessage(
- viewModel = viewModel,
+ viewModel = viewModel.message,
)
OutputArea(viewModel = viewModel, modifier = Modifier.padding(top = 24.dp))
@@ -376,7 +380,7 @@
modifier = Modifier.fillMaxWidth()
) {
StatusMessage(
- viewModel = viewModel,
+ viewModel = viewModel.message,
)
OutputArea(viewModel = viewModel, modifier = Modifier.padding(top = 24.dp))
@@ -441,7 +445,7 @@
modifier = Modifier.fillMaxWidth(),
) {
StatusMessage(
- viewModel = viewModel,
+ viewModel = viewModel.message,
)
OutputArea(viewModel = viewModel, modifier = Modifier.padding(top = 24.dp))
@@ -480,6 +484,7 @@
onChangeScene = {},
transitions = SceneTransitions,
modifier = modifier,
+ enableInterruptions = false,
) {
scene(SceneKeys.ContiguousSceneKey) {
FoldableScene(
@@ -548,26 +553,44 @@
@Composable
private fun StatusMessage(
- viewModel: BouncerViewModel,
+ viewModel: BouncerMessageViewModel,
modifier: Modifier = Modifier,
) {
- val message: BouncerViewModel.MessageViewModel by viewModel.message.collectAsState()
+ val message: MessageViewModel? by viewModel.message.collectAsState()
+
+ DisposableEffect(Unit) {
+ viewModel.onShown()
+ onDispose {}
+ }
Crossfade(
targetState = message,
label = "Bouncer message",
- animationSpec = if (message.isUpdateAnimated) tween() else snap(),
+ animationSpec = if (message?.isUpdateAnimated == true) tween() else snap(),
modifier = modifier.fillMaxWidth(),
- ) {
- Box(
- contentAlignment = Alignment.Center,
+ ) { msg ->
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.fillMaxWidth(),
) {
- Text(
- text = it.text,
- color = MaterialTheme.colorScheme.onSurface,
- style = MaterialTheme.typography.bodyLarge,
- )
+ msg?.let {
+ Text(
+ text = it.text,
+ color = MaterialTheme.colorScheme.onSurface,
+ fontSize = 18.sp,
+ lineHeight = 24.sp,
+ overflow = TextOverflow.Ellipsis,
+ )
+ Spacer(modifier = Modifier.size(10.dp))
+ Text(
+ text = it.secondaryText ?: "",
+ color = MaterialTheme.colorScheme.onSurface,
+ fontSize = 14.sp,
+ lineHeight = 20.sp,
+ overflow = TextOverflow.Ellipsis,
+ maxLines = 2
+ )
+ }
}
}
}
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 2a13d49..c34f2fd 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
@@ -74,10 +74,7 @@
val isImeSwitcherButtonVisible by viewModel.isImeSwitcherButtonVisible.collectAsState()
val selectedUserId by viewModel.selectedUserId.collectAsState()
- DisposableEffect(Unit) {
- viewModel.onShown()
- onDispose { viewModel.onHidden() }
- }
+ DisposableEffect(Unit) { onDispose { viewModel.onHidden() } }
LaunchedEffect(animateFailure) {
if (animateFailure) {
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PatternBouncer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PatternBouncer.kt
index 0a5f5d2..a78c2c0 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PatternBouncer.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PatternBouncer.kt
@@ -72,10 +72,7 @@
centerDotsVertically: Boolean,
modifier: Modifier = Modifier,
) {
- DisposableEffect(Unit) {
- viewModel.onShown()
- onDispose { viewModel.onHidden() }
- }
+ DisposableEffect(Unit) { onDispose { viewModel.onHidden() } }
val colCount = viewModel.columnCount
val rowCount = viewModel.rowCount
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinBouncer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinBouncer.kt
index f505b90..5651a46 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinBouncer.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinBouncer.kt
@@ -72,10 +72,7 @@
verticalSpacing: Dp,
modifier: Modifier = Modifier,
) {
- DisposableEffect(Unit) {
- viewModel.onShown()
- onDispose { viewModel.onHidden() }
- }
+ DisposableEffect(Unit) { onDispose { viewModel.onHidden() } }
val isInputEnabled: Boolean by viewModel.isInputEnabled.collectAsState()
val backspaceButtonAppearance by viewModel.backspaceButtonAppearance.collectAsState()
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 d0c4984..a1d8c29 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
@@ -71,6 +71,7 @@
currentScene,
onChangeScene = { viewModel.onSceneChanged(it) },
transitions = sceneTransitions,
+ enableInterruptions = false,
)
val touchesAllowed by viewModel.touchesAllowed.collectAsState(initial = false)
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenContent.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenContent.kt
index bc4e555..1178cc8 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenContent.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenContent.kt
@@ -74,6 +74,7 @@
transitions =
transitions { sceneKeyByBlueprintId.values.forEach { sceneKey -> to(sceneKey) } },
modifier = modifier,
+ enableInterruptions = false,
) {
sceneKeyByBlueprint.entries.forEach { (blueprint, sceneKey) ->
scene(sceneKey) { with(blueprint) { Content(Modifier.fillMaxSize()) } }
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/ClockTransition.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/ClockTransition.kt
index d9ed497..a12f099 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/ClockTransition.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/ClockTransition.kt
@@ -80,5 +80,14 @@
object ClockElementKeys {
val largeClockElementKey = ElementKey("large-clock")
val smallClockElementKey = ElementKey("small-clock")
+ val weatherSmallClockElementKey = ElementKey("weather-small-clock")
val smartspaceElementKey = ElementKey("smart-space")
}
+
+object WeatherClockElementKeys {
+ val timeElementKey = ElementKey("weather-large-clock-time")
+ val dateElementKey = ElementKey("weather-large-clock-date")
+ val weatherIconElementKey = ElementKey("weather-large-clock-weather-icon")
+ val temperatureElementKey = ElementKey("weather-large-clock-temperature")
+ val dndAlarmElementKey = ElementKey("weather-large-clock-dnd-alarm")
+}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/WeatherClockBlueprint.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/WeatherClockBlueprint.kt
index ee4e2d6..fe774a0 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/WeatherClockBlueprint.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/WeatherClockBlueprint.kt
@@ -23,6 +23,8 @@
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.Layout
@@ -33,11 +35,14 @@
import androidx.compose.ui.unit.dp
import com.android.compose.animation.scene.SceneScope
import com.android.compose.modifiers.padding
+import com.android.keyguard.KeyguardClockSwitch.LARGE
import com.android.systemui.Flags
+import com.android.systemui.customization.R as customizationR
import com.android.systemui.keyguard.domain.interactor.KeyguardBlueprintInteractor.Companion.SPLIT_SHADE_WEATHER_CLOCK_BLUEPRINT_ID
import com.android.systemui.keyguard.domain.interactor.KeyguardBlueprintInteractor.Companion.WEATHER_CLOCK_BLUEPRINT_ID
import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor
import com.android.systemui.keyguard.ui.composable.LockscreenLongPress
+import com.android.systemui.keyguard.ui.composable.modifier.onTopPlacementChanged
import com.android.systemui.keyguard.ui.composable.section.AmbientIndicationSection
import com.android.systemui.keyguard.ui.composable.section.BottomAreaSection
import com.android.systemui.keyguard.ui.composable.section.LockSection
@@ -47,8 +52,8 @@
import com.android.systemui.keyguard.ui.composable.section.SmartSpaceSection
import com.android.systemui.keyguard.ui.composable.section.StatusBarSection
import com.android.systemui.keyguard.ui.composable.section.WeatherClockSection
+import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel
import com.android.systemui.keyguard.ui.viewmodel.LockscreenContentViewModel
-import com.android.systemui.media.controls.ui.composable.MediaCarousel
import com.android.systemui.res.R
import com.android.systemui.shade.LargeScreenHeaderHelper
import dagger.Binds
@@ -71,6 +76,7 @@
private val settingsMenuSection: SettingsMenuSection,
private val clockInteractor: KeyguardClockInteractor,
private val mediaCarouselSection: MediaCarouselSection,
+ private val clockViewModel: KeyguardClockViewModel,
) : ComposableLockscreenSceneBlueprint {
override val id: String = WEATHER_CLOCK_BLUEPRINT_ID
@@ -79,7 +85,7 @@
val isUdfpsVisible = viewModel.isUdfpsVisible
val burnIn = rememberBurnIn(clockInteractor)
val resources = LocalContext.current.resources
-
+ val currentClockState = clockViewModel.currentClock.collectAsState()
LockscreenLongPress(
viewModel = viewModel.longPress,
modifier = modifier,
@@ -91,7 +97,34 @@
modifier = Modifier.fillMaxWidth(),
) {
with(statusBarSection) { StatusBar(modifier = Modifier.fillMaxWidth()) }
- // TODO: Add weather clock for small and large clock
+ val currentClock = currentClockState.value
+ val clockSize by clockViewModel.clockSize.collectAsState()
+ with(weatherClockSection) {
+ if (currentClock == null) {
+ return@with
+ }
+
+ if (clockSize == LARGE) {
+ Time(
+ clock = currentClock,
+ modifier =
+ Modifier.padding(
+ start =
+ dimensionResource(
+ customizationR.dimen.clock_padding_start
+ )
+ )
+ )
+ } else {
+ SmallClock(
+ burnInParams = burnIn.parameters,
+ modifier =
+ Modifier.align(Alignment.Start)
+ .onTopPlacementChanged(burnIn.onSmallClockTopChanged),
+ clock = currentClock
+ )
+ }
+ }
with(smartSpaceSection) {
SmartSpace(
burnInParams = burnIn.parameters,
@@ -119,6 +152,12 @@
)
}
}
+ with(weatherClockSection) {
+ if (currentClock == null || clockSize != LARGE) {
+ return@with
+ }
+ LargeClockSectionBelowSmartspace(clock = currentClock)
+ }
if (!isUdfpsVisible && ambientIndicationSectionOptional.isPresent) {
with(ambientIndicationSectionOptional.get()) {
@@ -234,6 +273,7 @@
private val largeScreenHeaderHelper: LargeScreenHeaderHelper,
private val weatherClockSection: WeatherClockSection,
private val mediaCarouselSection: MediaCarouselSection,
+ private val clockViewModel: KeyguardClockViewModel,
) : ComposableLockscreenSceneBlueprint {
override val id: String = SPLIT_SHADE_WEATHER_CLOCK_BLUEPRINT_ID
@@ -242,7 +282,7 @@
val isUdfpsVisible = viewModel.isUdfpsVisible
val burnIn = rememberBurnIn(clockInteractor)
val resources = LocalContext.current.resources
-
+ val currentClockState = clockViewModel.currentClock.collectAsState()
LockscreenLongPress(
viewModel = viewModel.longPress,
modifier = modifier,
@@ -257,11 +297,42 @@
Row(
modifier = Modifier.fillMaxSize(),
) {
- // TODO: Add weather clock for small and large clock
Column(
modifier = Modifier.fillMaxHeight().weight(weight = 1f),
horizontalAlignment = Alignment.CenterHorizontally,
) {
+ val currentClock = currentClockState.value
+ val clockSize by clockViewModel.clockSize.collectAsState()
+ with(weatherClockSection) {
+ if (currentClock == null) {
+ return@with
+ }
+
+ if (clockSize == LARGE) {
+ Time(
+ clock = currentClock,
+ modifier =
+ Modifier.align(Alignment.Start)
+ .padding(
+ start =
+ dimensionResource(
+ customizationR.dimen
+ .clock_padding_start
+ )
+ )
+ )
+ } else {
+ SmallClock(
+ burnInParams = burnIn.parameters,
+ modifier =
+ Modifier.align(Alignment.Start)
+ .onTopPlacementChanged(
+ burnIn.onSmallClockTopChanged
+ ),
+ clock = currentClock,
+ )
+ }
+ }
with(smartSpaceSection) {
SmartSpace(
burnInParams = burnIn.parameters,
@@ -284,6 +355,14 @@
}
with(mediaCarouselSection) { MediaCarousel() }
+
+ with(weatherClockSection) {
+ if (currentClock == null || clockSize != LARGE) {
+ return@with
+ }
+
+ LargeClockSectionBelowSmartspace(currentClock)
+ }
}
with(notificationSection) {
val splitShadeTopMargin: Dp =
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/DefaultClockSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/DefaultClockSection.kt
index 82e19e7c..2781f39 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/DefaultClockSection.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/DefaultClockSection.kt
@@ -20,6 +20,7 @@
import android.view.ViewGroup
import android.widget.FrameLayout
import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
@@ -32,6 +33,7 @@
import com.android.compose.animation.scene.SceneScope
import com.android.compose.modifiers.padding
import com.android.systemui.customization.R as customizationR
+import com.android.systemui.customization.R
import com.android.systemui.keyguard.ui.composable.blueprint.ClockElementKeys.largeClockElementKey
import com.android.systemui.keyguard.ui.composable.blueprint.ClockElementKeys.smallClockElementKey
import com.android.systemui.keyguard.ui.composable.modifier.burnInAware
@@ -58,20 +60,21 @@
if (currentClock?.smallClock?.view == null) {
return
}
- viewModel.clock = currentClock
-
val context = LocalContext.current
MovableElement(key = smallClockElementKey, modifier = modifier) {
content {
AndroidView(
factory = { context ->
FrameLayout(context).apply {
- addClockView(checkNotNull(currentClock).smallClock.view)
+ ensureClockViewExists(checkNotNull(currentClock).smallClock.view)
}
},
- update = { it.addClockView(checkNotNull(currentClock).smallClock.view) },
+ update = {
+ it.ensureClockViewExists(checkNotNull(currentClock).smallClock.view)
+ },
modifier =
- Modifier.padding(
+ Modifier.height(dimensionResource(R.dimen.small_clock_height))
+ .padding(
horizontal =
dimensionResource(customizationR.dimen.clock_padding_start)
)
@@ -89,27 +92,27 @@
@Composable
fun SceneScope.LargeClock(modifier: Modifier = Modifier) {
val currentClock by viewModel.currentClock.collectAsState()
- viewModel.clock = currentClock
if (currentClock?.largeClock?.view == null) {
return
}
-
MovableElement(key = largeClockElementKey, modifier = modifier) {
content {
AndroidView(
factory = { context ->
FrameLayout(context).apply {
- addClockView(checkNotNull(currentClock).largeClock.view)
+ ensureClockViewExists(checkNotNull(currentClock).largeClock.view)
}
},
- update = { it.addClockView(checkNotNull(currentClock).largeClock.view) },
+ update = {
+ it.ensureClockViewExists(checkNotNull(currentClock).largeClock.view)
+ },
modifier = Modifier.fillMaxSize()
)
}
}
}
- private fun FrameLayout.addClockView(clockView: View) {
+ private fun FrameLayout.ensureClockViewExists(clockView: View) {
if (contains(clockView)) {
return
}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/LockSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/LockSection.kt
index 31d3fa0..9f02201 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/LockSection.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/LockSection.kt
@@ -32,12 +32,12 @@
import com.android.compose.animation.scene.SceneScope
import com.android.keyguard.LockIconView
import com.android.keyguard.LockIconViewController
-import com.android.systemui.Flags.keyguardBottomAreaRefactor
import com.android.systemui.biometrics.AuthController
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.deviceentry.shared.DeviceEntryUdfpsRefactor
import com.android.systemui.flags.FeatureFlagsClassic
import com.android.systemui.flags.Flags
+import com.android.systemui.keyguard.KeyguardBottomAreaRefactor
import com.android.systemui.keyguard.ui.binder.DeviceEntryIconViewBinder
import com.android.systemui.keyguard.ui.composable.blueprint.BlueprintAlignmentLines
import com.android.systemui.keyguard.ui.view.DeviceEntryIconView
@@ -69,7 +69,7 @@
) {
@Composable
fun SceneScope.LockIcon(modifier: Modifier = Modifier) {
- if (!keyguardBottomAreaRefactor() && !DeviceEntryUdfpsRefactor.isEnabled) {
+ if (!KeyguardBottomAreaRefactor.isEnabled && !DeviceEntryUdfpsRefactor.isEnabled) {
return
}
@@ -96,7 +96,7 @@
)
}
} else {
- // keyguardBottomAreaRefactor()
+ // KeyguardBottomAreaRefactor.isEnabled
LockIconView(context, null).apply {
id = R.id.lock_icon_view
lockIconViewController.get().setLockIconView(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 5c9b271..6b86a48 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
@@ -16,50 +16,34 @@
package com.android.systemui.keyguard.ui.composable.section
-import android.content.Context
import android.view.ViewGroup
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.android.compose.animation.scene.SceneScope
-import com.android.systemui.Flags.migrateClocksToBlueprint
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.MigrateClocksToBlueprint
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.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(
- @Application private val context: Context,
private val viewModel: NotificationsPlaceholderViewModel,
- controller: NotificationStackScrollLayoutController,
- sceneContainerFlags: SceneContainerFlags,
sharedNotificationContainer: SharedNotificationContainer,
sharedNotificationContainerViewModel: SharedNotificationContainerViewModel,
stackScrollLayout: NotificationStackScrollLayout,
- notificationStackAppearanceViewModel: NotificationStackAppearanceViewModel,
- ambientState: AmbientState,
- notificationStackSizeCalculator: NotificationStackSizeCalculator,
- @Main private val mainImmediateDispatcher: CoroutineDispatcher,
+ sharedNotificationContainerBinder: SharedNotificationContainerBinder,
) {
init {
- if (!migrateClocksToBlueprint()) {
- throw IllegalStateException("this requires migrateClocksToBlueprint()")
+ if (!MigrateClocksToBlueprint.isEnabled) {
+ throw IllegalStateException("this requires MigrateClocksToBlueprint.isEnabled")
}
// This scene container section moves the NSSL to the SharedNotificationContainer.
// This also requires that SharedNotificationContainer gets moved to the
@@ -73,25 +57,10 @@
sharedNotificationContainer.addNotificationStackScrollLayout(stackScrollLayout)
}
- SharedNotificationContainerBinder.bind(
+ sharedNotificationContainerBinder.bind(
sharedNotificationContainer,
sharedNotificationContainerViewModel,
- sceneContainerFlags,
- controller,
- notificationStackSizeCalculator,
- mainImmediateDispatcher = mainImmediateDispatcher,
)
-
- if (sceneContainerFlags.isEnabled()) {
- NotificationStackAppearanceViewBinder.bind(
- context,
- sharedNotificationContainer,
- notificationStackAppearanceViewModel,
- ambientState,
- controller,
- mainImmediateDispatcher = mainImmediateDispatcher,
- )
- }
}
@Composable
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/TopAreaSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/TopAreaSection.kt
index 7635841..d72d5ca 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/TopAreaSection.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/TopAreaSection.kt
@@ -92,6 +92,7 @@
currentScene = currentScene,
onChangeScene = {},
transitions = ClockTransition.defaultClockTransitions,
+ enableInterruptions = false,
) {
scene(ClockScenes.splitShadeLargeClockScene) {
Row(
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/WeatherClockSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/WeatherClockSection.kt
index 2e7bc2a..d358453 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/WeatherClockSection.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/WeatherClockSection.kt
@@ -16,45 +16,177 @@
package com.android.systemui.keyguard.ui.composable.section
+import android.view.ViewGroup
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.IntrinsicSize
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.dimensionResource
+import androidx.compose.ui.viewinterop.AndroidView
+import com.android.compose.animation.scene.ElementKey
import com.android.compose.animation.scene.SceneScope
+import com.android.compose.modifiers.padding
+import com.android.systemui.customization.R
+import com.android.systemui.keyguard.ui.composable.blueprint.ClockElementKeys.weatherSmallClockElementKey
+import com.android.systemui.keyguard.ui.composable.blueprint.WeatherClockElementKeys
+import com.android.systemui.keyguard.ui.composable.modifier.burnInAware
+import com.android.systemui.keyguard.ui.viewmodel.AodBurnInViewModel
+import com.android.systemui.keyguard.ui.viewmodel.BurnInParameters
+import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel
+import com.android.systemui.plugins.clocks.ClockController
import javax.inject.Inject
/** Provides small clock and large clock composables for the weather clock layout. */
-class WeatherClockSection @Inject constructor() {
+class WeatherClockSection
+@Inject
+constructor(
+ private val viewModel: KeyguardClockViewModel,
+ private val aodBurnInViewModel: AodBurnInViewModel,
+) {
@Composable
fun SceneScope.Time(
+ clock: ClockController,
modifier: Modifier = Modifier,
) {
- // TODO: compose view
+ WeatherElement(
+ weatherClockElementViewId = R.id.weather_clock_time,
+ clock = clock,
+ elementKey = WeatherClockElementKeys.timeElementKey,
+ modifier = modifier.wrapContentSize(),
+ )
}
@Composable
- fun SceneScope.Date(
+ private fun SceneScope.Date(
+ clock: ClockController,
modifier: Modifier = Modifier,
) {
- // TODO: compose view
+ WeatherElement(
+ weatherClockElementViewId = R.id.weather_clock_date,
+ clock = clock,
+ elementKey = WeatherClockElementKeys.dateElementKey,
+ modifier = modifier,
+ )
}
@Composable
- fun SceneScope.Weather(
+ private fun SceneScope.Weather(
+ clock: ClockController,
modifier: Modifier = Modifier,
) {
- // TODO: compose view
+ WeatherElement(
+ weatherClockElementViewId = R.id.weather_clock_weather_icon,
+ clock = clock,
+ elementKey = WeatherClockElementKeys.weatherIconElementKey,
+ modifier = modifier.wrapContentSize(),
+ )
}
@Composable
- fun SceneScope.DndAlarmStatus(
+ private fun SceneScope.DndAlarmStatus(
+ clock: ClockController,
modifier: Modifier = Modifier,
) {
- // TODO: compose view
+ WeatherElement(
+ weatherClockElementViewId = R.id.weather_clock_alarm_dnd,
+ clock = clock,
+ elementKey = WeatherClockElementKeys.dndAlarmElementKey,
+ modifier = modifier.wrapContentSize(),
+ )
}
@Composable
- fun SceneScope.Temperature(
+ private fun SceneScope.Temperature(
+ clock: ClockController,
modifier: Modifier = Modifier,
) {
- // TODO: compose view
+ WeatherElement(
+ weatherClockElementViewId = R.id.weather_clock_temperature,
+ clock = clock,
+ elementKey = WeatherClockElementKeys.temperatureElementKey,
+ modifier = modifier.wrapContentSize(),
+ )
+ }
+
+ @Composable
+ private fun SceneScope.WeatherElement(
+ weatherClockElementViewId: Int,
+ clock: ClockController,
+ elementKey: ElementKey,
+ modifier: Modifier
+ ) {
+ MovableElement(key = elementKey, modifier) {
+ content {
+ AndroidView(
+ factory = {
+ val view =
+ clock.largeClock.layout.views.first {
+ it.id == weatherClockElementViewId
+ }
+ (view.parent as? ViewGroup)?.removeView(view)
+ view
+ },
+ update = {},
+ modifier = modifier
+ )
+ }
+ }
+ }
+
+ @Composable
+ fun SceneScope.LargeClockSectionBelowSmartspace(
+ clock: ClockController,
+ ) {
+ Row(
+ modifier =
+ Modifier.height(IntrinsicSize.Max)
+ .padding(horizontal = dimensionResource(R.dimen.clock_padding_start))
+ ) {
+ Date(clock = clock, modifier = Modifier.wrapContentSize())
+ Box(modifier = Modifier.fillMaxSize()) {
+ Weather(clock = clock, modifier = Modifier.align(Alignment.TopStart))
+ Temperature(clock = clock, modifier = Modifier.align(Alignment.BottomEnd))
+ DndAlarmStatus(clock = clock, modifier = Modifier.align(Alignment.TopEnd))
+ }
+ }
+ }
+
+ @Composable
+ fun SceneScope.SmallClock(
+ burnInParams: BurnInParameters,
+ modifier: Modifier = Modifier,
+ clock: ClockController,
+ ) {
+ val localContext = LocalContext.current
+ MovableElement(key = weatherSmallClockElementKey, modifier) {
+ content {
+ AndroidView(
+ factory = {
+ val view = clock.smallClock.view
+ if (view.parent != null) {
+ (view.parent as? ViewGroup)?.removeView(view)
+ }
+ view
+ },
+ modifier =
+ modifier
+ .height(dimensionResource(R.dimen.small_clock_height))
+ .padding(start = dimensionResource(R.dimen.clock_padding_start))
+ .padding(top = { viewModel.getSmallClockTopMargin(localContext) })
+ .burnInAware(
+ viewModel = aodBurnInViewModel,
+ params = burnInParams,
+ ),
+ update = {},
+ )
+ }
+ }
}
}
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 d780978..9ba5e3b 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
@@ -57,6 +57,7 @@
import androidx.compose.ui.layout.positionInWindow
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntSize
@@ -70,9 +71,10 @@
import com.android.systemui.notifications.ui.composable.Notifications.Form
import com.android.systemui.notifications.ui.composable.Notifications.TransitionThresholds.EXPANSION_FOR_MAX_CORNER_RADIUS
import com.android.systemui.notifications.ui.composable.Notifications.TransitionThresholds.EXPANSION_FOR_MAX_SCRIM_ALPHA
+import com.android.systemui.res.R
import com.android.systemui.scene.shared.model.Scenes
import com.android.systemui.shade.ui.composable.ShadeHeader
-import com.android.systemui.statusbar.notification.stack.ui.viewbinder.NotificationStackAppearanceViewBinder.SCRIM_CORNER_RADIUS
+import com.android.systemui.statusbar.notification.stack.shared.model.StackRounding
import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationsPlaceholderViewModel
import kotlin.math.roundToInt
@@ -139,6 +141,7 @@
) {
val density = LocalDensity.current
val screenCornerRadius = LocalScreenCornerRadius.current
+ val scrimCornerRadius = dimensionResource(R.dimen.notification_scrim_corner_radius)
val scrollState = rememberScrollState()
val syntheticScroll = viewModel.syntheticScroll.collectAsState(0f)
val expansionFraction by viewModel.expandFraction.collectAsState(0f)
@@ -156,6 +159,8 @@
val contentHeight = viewModel.intrinsicContentHeight.collectAsState()
+ val stackRounding = viewModel.stackRounding.collectAsState(StackRounding())
+
// 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.
@@ -222,16 +227,12 @@
.graphicsLayer {
shape =
calculateCornerRadius(
+ scrimCornerRadius,
screenCornerRadius,
{ expansionFraction },
layoutState.isTransitioningBetween(Scenes.Gone, Scenes.Shade)
)
- .let {
- RoundedCornerShape(
- topStart = it,
- topEnd = it,
- )
- }
+ .let { stackRounding.value.toRoundedCornerShape(it) }
clip = true
}
) {
@@ -359,6 +360,7 @@
}
private fun calculateCornerRadius(
+ scrimCornerRadius: Dp,
screenCornerRadius: Dp,
expansionFraction: () -> Float,
transitioning: Boolean,
@@ -366,12 +368,12 @@
return if (transitioning) {
lerp(
start = screenCornerRadius.value,
- stop = SCRIM_CORNER_RADIUS,
- fraction = (expansionFraction() / EXPANSION_FOR_MAX_CORNER_RADIUS).coerceAtMost(1f),
+ stop = scrimCornerRadius.value,
+ fraction = (expansionFraction() / EXPANSION_FOR_MAX_CORNER_RADIUS).coerceIn(0f, 1f),
)
.dp
} else {
- SCRIM_CORNER_RADIUS.dp
+ scrimCornerRadius
}
}
@@ -394,5 +396,16 @@
this
}
+fun StackRounding.toRoundedCornerShape(radius: Dp): RoundedCornerShape {
+ val topRadius = if (roundTop) radius else 0.dp
+ val bottomRadius = if (roundBottom) radius else 0.dp
+ return RoundedCornerShape(
+ topStart = topRadius,
+ topEnd = topRadius,
+ bottomStart = bottomRadius,
+ bottomEnd = bottomRadius,
+ )
+}
+
private const val TAG = "FlexiNotifs"
private val DEBUG_COLOR = Color(1f, 0f, 0f, 0.2f)
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettings.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettings.kt
index bc48dd1..244861c 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettings.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettings.kt
@@ -20,6 +20,7 @@
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
@@ -36,7 +37,8 @@
import com.android.systemui.qs.ui.adapter.QSSceneAdapter
import com.android.systemui.qs.ui.adapter.QSSceneAdapter.State.Companion.Collapsing
import com.android.systemui.qs.ui.adapter.QSSceneAdapter.State.Expanding
-import com.android.systemui.qs.ui.adapter.QSSceneAdapter.State.Unsquishing
+import com.android.systemui.qs.ui.adapter.QSSceneAdapter.State.UnsquishingQQS
+import com.android.systemui.qs.ui.adapter.QSSceneAdapter.State.UnsquishingQS
import com.android.systemui.scene.shared.model.Scenes
object QuickSettings {
@@ -49,6 +51,8 @@
object Elements {
val Content =
ElementKey("QuickSettingsContent", scenePicker = MovableElementScenePicker(SCENES))
+ val QuickQuickSettings = ElementKey("QuickQuickSettings")
+ val SplitShadeQuickSettings = ElementKey("SplitShadeQuickSettings")
val FooterActions = ElementKey("QuickSettingsFooterActions")
}
@@ -78,12 +82,16 @@
is TransitionState.Transition ->
with(transitionState) {
when {
- isSplitShade -> QSSceneAdapter.State.QS
- fromScene == Scenes.Shade && toScene == Scenes.QuickSettings ->
+ isSplitShade -> UnsquishingQS(squishiness)
+ fromScene == Scenes.Shade && toScene == Scenes.QuickSettings -> {
Expanding(progress)
- fromScene == Scenes.QuickSettings && toScene == Scenes.Shade ->
+ }
+ fromScene == Scenes.QuickSettings && toScene == Scenes.Shade -> {
Collapsing(progress)
- fromScene == Scenes.Shade || toScene == Scenes.Shade -> Unsquishing(squishiness)
+ }
+ fromScene == Scenes.Shade || toScene == Scenes.Shade -> {
+ UnsquishingQQS(squishiness)
+ }
fromScene == Scenes.QuickSettings || toScene == Scenes.QuickSettings -> {
QSSceneAdapter.State.QS
}
@@ -119,6 +127,18 @@
squishiness: Float = QuickSettings.SharedValues.SquishinessValues.Default,
) {
val contentState = stateForQuickSettingsContent(isSplitShade, squishiness)
+ val transitionState = layoutState.transitionState
+ val isClosing =
+ transitionState is TransitionState.Transition &&
+ transitionState.progress >= 0.9f && // almost done closing
+ !(layoutState.isTransitioning(to = Scenes.Shade) ||
+ layoutState.isTransitioning(to = Scenes.QuickSettings))
+
+ if (isClosing) {
+ DisposableEffect(Unit) {
+ onDispose { qsSceneAdapter.setState(QSSceneAdapter.State.CLOSED) }
+ }
+ }
MovableElement(
key = QuickSettings.Elements.Content,
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt
index 0fdaabe..fe6701c 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt
@@ -79,6 +79,7 @@
initialScene = currentSceneKey,
canChangeScene = { toScene -> viewModel.canChangeScene(toScene) },
transitions = SceneContainerTransitions,
+ enableInterruptions = false,
)
}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromGoneToShadeTransition.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromGoneToShadeTransition.kt
index 5c6e1c8..9b59708 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromGoneToShadeTransition.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromGoneToShadeTransition.kt
@@ -13,11 +13,18 @@
) {
spec = tween(durationMillis = DefaultDuration.times(durationScale).inWholeMilliseconds.toInt())
- fractionRange(start = .58f) { fade(ShadeHeader.Elements.Clock) }
- fractionRange(start = .58f) { fade(ShadeHeader.Elements.CollapsedContentStart) }
- fractionRange(start = .58f) { fade(ShadeHeader.Elements.CollapsedContentEnd) }
- fractionRange(start = .58f) { fade(ShadeHeader.Elements.PrivacyChip) }
- translate(QuickSettings.Elements.Content, y = -ShadeHeader.Dimensions.CollapsedHeight * .66f)
+ fractionRange(start = .58f) {
+ fade(ShadeHeader.Elements.Clock)
+ fade(ShadeHeader.Elements.CollapsedContentStart)
+ fade(ShadeHeader.Elements.CollapsedContentEnd)
+ fade(ShadeHeader.Elements.PrivacyChip)
+ fade(QuickSettings.Elements.SplitShadeQuickSettings)
+ fade(QuickSettings.Elements.FooterActions)
+ }
+ translate(
+ QuickSettings.Elements.QuickQuickSettings,
+ y = -ShadeHeader.Dimensions.CollapsedHeight * .66f
+ )
translate(Notifications.Elements.NotificationScrim, Edge.Top, false)
}
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 15e7b51..85798ac 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
@@ -55,6 +55,7 @@
import com.android.compose.animation.scene.ElementKey
import com.android.compose.animation.scene.LowestZIndexScenePicker
import com.android.compose.animation.scene.SceneScope
+import com.android.compose.animation.scene.TransitionState
import com.android.compose.animation.scene.UserAction
import com.android.compose.animation.scene.UserActionResult
import com.android.compose.animation.scene.animateSceneFloatAsState
@@ -222,15 +223,17 @@
horizontal = Shade.Dimensions.HorizontalPadding
)
)
- QuickSettings(
- viewModel.qsSceneAdapter,
- {
- (viewModel.qsSceneAdapter.qqsHeight * tileSquishiness)
- .roundToInt()
- },
- isSplitShade = false,
- squishiness = tileSquishiness,
- )
+ Box(Modifier.element(QuickSettings.Elements.QuickQuickSettings)) {
+ QuickSettings(
+ viewModel.qsSceneAdapter,
+ {
+ (viewModel.qsSceneAdapter.qqsHeight * tileSquishiness)
+ .roundToInt()
+ },
+ isSplitShade = false,
+ squishiness = tileSquishiness,
+ )
+ }
MediaIfVisible(
viewModel = viewModel,
@@ -280,6 +283,8 @@
val lifecycleOwner = LocalLifecycleOwner.current
val footerActionsViewModel =
remember(lifecycleOwner, viewModel) { viewModel.getFooterActionsViewModel(lifecycleOwner) }
+ val tileSquishiness by
+ animateSceneFloatAsState(value = 1f, key = QuickSettings.SharedValues.TilesSquishiness)
val navBarBottomHeight = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding()
val density = LocalDensity.current
@@ -290,6 +295,7 @@
}
val quickSettingsScrollState = rememberScrollState()
+ val isScrollable = layoutState.transitionState is TransitionState.Idle
LaunchedEffect(isCustomizing, quickSettingsScrollState) {
if (isCustomizing) {
quickSettingsScrollState.scrollTo(0)
@@ -318,31 +324,41 @@
Column(
verticalArrangement = Arrangement.Top,
modifier =
- Modifier.weight(1f).fillMaxHeight().thenIf(!isCustomizing) {
- Modifier.verticalNestedScrollToScene()
- .verticalScroll(quickSettingsScrollState)
- .clipScrollableContainer(Orientation.Horizontal)
- .padding(bottom = navBarBottomHeight)
- }
+ Modifier.weight(1f).fillMaxSize().thenIf(!isCustomizing) {
+ Modifier.padding(bottom = navBarBottomHeight)
+ },
) {
- QuickSettings(
- qsSceneAdapter = viewModel.qsSceneAdapter,
- heightProvider = { viewModel.qsSceneAdapter.qsHeight },
- isSplitShade = true,
- modifier = Modifier.fillMaxWidth(),
- )
+ Column(
+ modifier =
+ Modifier.fillMaxSize().weight(1f).thenIf(!isCustomizing) {
+ Modifier.verticalNestedScrollToScene()
+ .verticalScroll(
+ quickSettingsScrollState,
+ enabled = isScrollable
+ )
+ .clipScrollableContainer(Orientation.Horizontal)
+ }
+ ) {
+ Box(
+ modifier =
+ Modifier.element(QuickSettings.Elements.SplitShadeQuickSettings)
+ ) {
+ QuickSettings(
+ qsSceneAdapter = viewModel.qsSceneAdapter,
+ heightProvider = { viewModel.qsSceneAdapter.qsHeight },
+ isSplitShade = true,
+ modifier = Modifier.fillMaxWidth(),
+ squishiness = tileSquishiness,
+ )
+ }
- MediaIfVisible(
- viewModel = viewModel,
- mediaCarouselController = mediaCarouselController,
- mediaHost = mediaHost,
- modifier = Modifier.fillMaxWidth(),
- )
-
- Spacer(
- modifier = Modifier.weight(1f),
- )
-
+ MediaIfVisible(
+ viewModel = viewModel,
+ mediaCarouselController = mediaCarouselController,
+ mediaHost = mediaHost,
+ modifier = Modifier.fillMaxWidth(),
+ )
+ }
FooterActionsWithAnimatedVisibility(
viewModel = footerActionsViewModel,
isCustomizing = isCustomizing,
@@ -354,7 +370,8 @@
NotificationScrollingStack(
viewModel = viewModel.notifications,
maxScrimTop = { 0f },
- modifier = Modifier.weight(1f).fillMaxHeight(),
+ modifier =
+ Modifier.weight(1f).fillMaxHeight().padding(bottom = navBarBottomHeight),
)
}
}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlider.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlider.kt
index 2435170..d31064a 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlider.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlider.kt
@@ -17,16 +17,20 @@
package com.android.systemui.volume.panel.component.volume.ui.composable
import androidx.compose.animation.core.animateFloatAsState
-import androidx.compose.foundation.basicMarquee
-import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.size
import androidx.compose.material3.IconButton
import androidx.compose.material3.IconButtonColors
import androidx.compose.material3.LocalContentColor
-import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableFloatStateOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.semantics.ProgressBarRangeInfo
@@ -38,6 +42,7 @@
import androidx.compose.ui.unit.dp
import com.android.compose.PlatformSlider
import com.android.compose.PlatformSliderColors
+import com.android.systemui.common.shared.model.Icon
import com.android.systemui.common.ui.compose.Icon
import com.android.systemui.volume.panel.component.volume.slider.ui.viewmodel.SliderState
@@ -49,16 +54,15 @@
modifier: Modifier = Modifier,
sliderColors: PlatformSliderColors,
) {
- val value by
- animateFloatAsState(targetValue = state.value, label = "VolumeSliderValueAnimation")
+ val value by valueState(state)
PlatformSlider(
modifier =
modifier.clearAndSetSemantics {
if (!state.isEnabled) disabled()
contentDescription = state.label
- // provide a not animated value to the a11y because it fails to announce the settled
- // value when it changes rapidly.
+ // provide a not animated value to the a11y because it fails to announce the
+ // settled value when it changes rapidly.
progressBarRangeInfo = ProgressBarRangeInfo(state.value, state.valueRange)
setProgress { targetValue ->
val targetDirection =
@@ -86,44 +90,64 @@
Text(text = state.valueText, color = LocalContentColor.current)
} else {
state.icon?.let {
- IconButton(
- onClick = onIconTapped,
- colors =
- IconButtonColors(
- contentColor = LocalContentColor.current,
- containerColor = Color.Transparent,
- disabledContentColor = LocalContentColor.current,
- disabledContainerColor = Color.Transparent,
- )
- ) {
- Icon(modifier = Modifier.size(24.dp), icon = it)
- }
+ SliderIcon(
+ icon = it,
+ onIconTapped = onIconTapped,
+ isTappable = state.isMutable,
+ )
}
}
},
colors = sliderColors,
label = {
- Column(modifier = Modifier) {
- Text(
- modifier = Modifier.basicMarquee(),
- text = state.label,
- style = MaterialTheme.typography.titleMedium,
- color = LocalContentColor.current,
- maxLines = 1,
- )
-
- if (!state.isEnabled) {
- state.disabledMessage?.let { message ->
- Text(
- modifier = Modifier.basicMarquee(),
- text = message,
- style = MaterialTheme.typography.bodySmall,
- color = LocalContentColor.current,
- maxLines = 1,
- )
- }
- }
- }
+ VolumeSliderContent(
+ modifier = Modifier,
+ label = state.label,
+ isEnabled = state.isEnabled,
+ disabledMessage = state.disabledMessage,
+ )
}
)
}
+
+@Composable
+private fun valueState(state: SliderState): State<Float> {
+ var prevState by remember { mutableStateOf(state) }
+ // Don't animate slider value when receive the first value and when changing isEnabled state
+ val shouldSkipAnimation =
+ prevState is SliderState.Empty || prevState.isEnabled != state.isEnabled
+ val value =
+ if (shouldSkipAnimation) mutableFloatStateOf(state.value)
+ else animateFloatAsState(targetValue = state.value, label = "VolumeSliderValueAnimation")
+ prevState = state
+ return value
+}
+
+@Composable
+private fun SliderIcon(
+ icon: Icon,
+ onIconTapped: () -> Unit,
+ isTappable: Boolean,
+ modifier: Modifier = Modifier
+) {
+ if (isTappable) {
+ IconButton(
+ modifier = modifier,
+ onClick = onIconTapped,
+ colors =
+ IconButtonColors(
+ contentColor = LocalContentColor.current,
+ containerColor = Color.Transparent,
+ disabledContentColor = LocalContentColor.current,
+ disabledContainerColor = Color.Transparent,
+ ),
+ content = { Icon(modifier = Modifier.size(24.dp), icon = icon) },
+ )
+ } else {
+ Box(
+ modifier = modifier,
+ contentAlignment = Alignment.Center,
+ content = { Icon(modifier = Modifier.size(24.dp), icon = icon) },
+ )
+ }
+}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSliderContent.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSliderContent.kt
new file mode 100644
index 0000000..6b9af23
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSliderContent.kt
@@ -0,0 +1,152 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.volume.panel.component.volume.ui.composable
+
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.core.Animatable
+import androidx.compose.animation.core.AnimationVector1D
+import androidx.compose.animation.core.VectorConverter
+import androidx.compose.animation.expandVertically
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
+import androidx.compose.animation.shrinkVertically
+import androidx.compose.foundation.basicMarquee
+import androidx.compose.material3.LocalContentColor
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.Layout
+import androidx.compose.ui.layout.Measurable
+import androidx.compose.ui.layout.MeasurePolicy
+import androidx.compose.ui.layout.MeasureResult
+import androidx.compose.ui.layout.MeasureScope
+import androidx.compose.ui.layout.layout
+import androidx.compose.ui.layout.layoutId
+import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.util.fastFirst
+import androidx.compose.ui.util.fastFirstOrNull
+import kotlinx.coroutines.launch
+
+private enum class VolumeSliderContentComponent {
+ Label,
+ DisabledMessage,
+}
+
+/** Shows label of the [VolumeSlider]. Also shows [disabledMessage] when not [isEnabled]. */
+@Composable
+fun VolumeSliderContent(
+ label: String,
+ isEnabled: Boolean,
+ disabledMessage: String?,
+ modifier: Modifier = Modifier,
+) {
+ Layout(
+ modifier = modifier.animateContentHeight(),
+ content = {
+ Text(
+ modifier = Modifier.layoutId(VolumeSliderContentComponent.Label).basicMarquee(),
+ text = label,
+ style = MaterialTheme.typography.titleMedium,
+ color = LocalContentColor.current,
+ maxLines = 1,
+ )
+
+ disabledMessage?.let { message ->
+ AnimatedVisibility(
+ modifier = Modifier.layoutId(VolumeSliderContentComponent.DisabledMessage),
+ visible = !isEnabled,
+ enter = expandVertically(expandFrom = Alignment.Top) + fadeIn(),
+ exit = shrinkVertically(shrinkTowards = Alignment.Top) + fadeOut(),
+ ) {
+ Text(
+ modifier = Modifier.basicMarquee(),
+ text = message,
+ style = MaterialTheme.typography.bodySmall,
+ color = LocalContentColor.current,
+ maxLines = 1,
+ )
+ }
+ }
+ },
+ measurePolicy = VolumeSliderContentMeasurePolicy(isEnabled)
+ )
+}
+
+/**
+ * Uses [VolumeSliderContentComponent.Label] width when [isEnabled] and max available width
+ * otherwise. This ensures that the slider always have the correct measurement to position the
+ * content.
+ */
+private class VolumeSliderContentMeasurePolicy(private val isEnabled: Boolean) : MeasurePolicy {
+
+ override fun MeasureScope.measure(
+ measurables: List<Measurable>,
+ constraints: Constraints
+ ): MeasureResult {
+ val labelPlaceable =
+ measurables
+ .fastFirst { it.layoutId == VolumeSliderContentComponent.Label }
+ .measure(constraints)
+ val layoutWidth: Int = constraints.maxWidth
+ val fullLayoutWidth: Int =
+ if (isEnabled) {
+ // PlatformSlider uses half of the available space for the enabled state.
+ // This is using it to allow disabled message to take whole space when animating to
+ // prevent it from jumping left to right
+ layoutWidth * 2
+ } else {
+ layoutWidth
+ }
+
+ val disabledMessagePlaceable =
+ measurables
+ .fastFirstOrNull { it.layoutId == VolumeSliderContentComponent.DisabledMessage }
+ ?.measure(constraints.copy(maxWidth = fullLayoutWidth))
+
+ val layoutHeight = labelPlaceable.height + (disabledMessagePlaceable?.height ?: 0)
+ return layout(layoutWidth, layoutHeight) {
+ labelPlaceable.placeRelative(0, 0, 0f)
+ disabledMessagePlaceable?.placeRelative(0, labelPlaceable.height, 0f)
+ }
+ }
+}
+
+/** Animates composable height changes. */
+@Composable
+private fun Modifier.animateContentHeight(): Modifier {
+ var heightAnimation by remember { mutableStateOf<Animatable<Int, AnimationVector1D>?>(null) }
+ val coroutineScope = rememberCoroutineScope()
+ return layout { measurable, constraints ->
+ val placeable = measurable.measure(constraints)
+ val currentAnimation = heightAnimation
+ val anim =
+ if (currentAnimation == null) {
+ Animatable(placeable.height, Int.VectorConverter).also { heightAnimation = it }
+ } else {
+ coroutineScope.launch { currentAnimation.animateTo(placeable.height) }
+ currentAnimation
+ }
+ layout(placeable.width, anim.value) { placeable.place(0, 0) }
+ }
+}
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 6cff30c..da07f6d 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
@@ -138,8 +138,9 @@
// that will actually animate it.
layoutState.startTransition(transition, transitionKey)
- // The transformation now contains the spec that we should use to instantiate the Animatable.
- val animationSpec = layoutState.transformationSpec.progressSpec
+ // The transition now contains the transformation spec that we should use to instantiate the
+ // Animatable.
+ val animationSpec = transition.transformationSpec.progressSpec
val visibilityThreshold =
(animationSpec as? SpringSpec)?.visibilityThreshold ?: ProgressVisibilityThreshold
val animatable =
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt
index 82083f9..1b06275 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt
@@ -18,10 +18,8 @@
package com.android.compose.animation.scene
-import android.util.Log
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.AnimationVector1D
-import androidx.compose.animation.core.SpringSpec
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
@@ -145,16 +143,6 @@
}
val transitionState = layoutImpl.state.transitionState
- if (transitionState is TransitionState.Transition) {
- // TODO(b/290184746): Better handle interruptions here if state != idle.
- Log.w(
- TAG,
- "start from TransitionState.Transition is not fully supported: from" +
- " ${transitionState.fromScene} to ${transitionState.toScene} " +
- "(progress ${transitionState.progress})"
- )
- }
-
val fromScene = layoutImpl.scene(transitionState.currentScene)
val swipes = computeSwipes(fromScene, startedPosition, pointersDown)
val result =
@@ -269,19 +257,6 @@
fun updateTransition(newTransition: SwipeTransition, force: Boolean = false) {
if (isDrivingTransition || force) {
layoutState.startTransition(newTransition, newTransition.key)
-
- // Initialize SwipeTransition.transformationSpec and .swipeSpec. Note that this must be
- // called right after layoutState.startTransition() is called, because it computes the
- // current layoutState.transformationSpec().
- val transformationSpec = layoutState.transformationSpec
- newTransition.transformationSpec = transformationSpec
- newTransition.swipeSpec =
- transformationSpec.swipeSpec ?: layoutState.transitions.defaultSwipeSpec
- } else {
- // We were not driving the transition and we don't force the update, so the specs won't
- // be used and it doesn't matter which ones we set here.
- newTransition.transformationSpec = TransformationSpec.Empty
- newTransition.swipeSpec = SceneTransitions.DefaultSwipeSpec
}
swipeTransition = newTransition
@@ -616,18 +591,6 @@
override val isUserInputOngoing: Boolean
get() = offsetAnimation == null
- /**
- * The [TransformationSpecImpl] associated to this transition.
- *
- * Note: This is lateinit because this [SwipeTransition] is needed by
- * [BaseSceneTransitionLayoutState] to compute the [TransitionSpec], and it will be set right
- * after [BaseSceneTransitionLayoutState.startTransition] is called with this transition.
- */
- lateinit var transformationSpec: TransformationSpecImpl
-
- /** The spec to use when animating this transition to either [fromScene] or [toScene]. */
- lateinit var swipeSpec: SpringSpec<Float>
-
override val overscrollScope: OverscrollScope =
object : OverscrollScope {
override val absoluteDistance: Float
@@ -701,6 +664,9 @@
coroutineScope
.launch {
try {
+ val swipeSpec =
+ transformationSpec.swipeSpec
+ ?: layoutState.transitions.defaultSwipeSpec
animatable.animateTo(
targetValue = targetOffset,
animationSpec = swipeSpec,
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt
index 15712b5..69f1d45 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt
@@ -203,7 +203,7 @@
measurable: Measurable,
constraints: Constraints,
): MeasureResult {
- val overscrollScene = layoutImpl.state.currentOverscrollSpec?.scene
+ val overscrollScene = layoutImpl.state.currentTransition?.currentOverscrollSpec?.scene
if (overscrollScene != null && overscrollScene != scene.key) {
// There is an overscroll in progress on another scene
// By measuring composable elements, Compose can cache relevant information.
@@ -269,13 +269,12 @@
transition == null ||
transition.fromScene !in element.sceneStates ||
transition.toScene !in element.sceneStates ||
- layoutImpl.state.currentOverscrollSpec?.scene == scene.key
+ transition.currentOverscrollSpec?.scene == scene.key
) {
return true
}
- val sharedTransformation =
- sharedElementTransformation(layoutImpl.state, transition, element.key)
+ val sharedTransformation = sharedElementTransformation(transition, element.key)
if (sharedTransformation?.enabled == false) {
return true
}
@@ -305,23 +304,21 @@
fromSceneZIndex = layoutImpl.scenes.getValue(fromScene).zIndex,
toSceneZIndex = layoutImpl.scenes.getValue(toScene).zIndex,
) == scene
- return chosenByPicker || layoutImpl.state.currentOverscrollSpec?.scene == scene
+ return chosenByPicker || transition.currentOverscrollSpec?.scene == scene
}
private fun isSharedElementEnabled(
- layoutState: BaseSceneTransitionLayoutState,
transition: TransitionState.Transition,
element: ElementKey,
): Boolean {
- return sharedElementTransformation(layoutState, transition, element)?.enabled ?: true
+ return sharedElementTransformation(transition, element)?.enabled ?: true
}
internal fun sharedElementTransformation(
- layoutState: BaseSceneTransitionLayoutState,
transition: TransitionState.Transition,
element: ElementKey,
): SharedElementTransformation? {
- val transformationSpec = layoutState.transformationSpec
+ val transformationSpec = transition.transformationSpec
val sharedInFromScene = transformationSpec.transformations(element, transition.fromScene).shared
val sharedInToScene = transformationSpec.transformations(element, transition.toScene).shared
@@ -360,11 +357,11 @@
}
val isSharedElement = fromState != null && toState != null
- if (isSharedElement && isSharedElementEnabled(layoutImpl.state, transition, element.key)) {
+ if (isSharedElement && isSharedElementEnabled(transition, element.key)) {
return true
}
- return layoutImpl.state.transformationSpec.transformations(element.key, scene.key).alpha == null
+ return transition.transformationSpec.transformations(element.key, scene.key).alpha == null
}
/**
@@ -559,7 +556,7 @@
}
if (transition is TransitionState.HasOverscrollProperties) {
- val overscroll = layoutImpl.state.currentOverscrollSpec
+ val overscroll = transition.currentOverscrollSpec
if (overscroll?.scene == scene.key) {
val elementSpec = overscroll.transformationSpec.transformations(element.key, scene.key)
val propertySpec = transformation(elementSpec) ?: return currentValue()
@@ -597,7 +594,7 @@
// TODO(b/290184746): Support non linear shared paths as well as a way to make sure that shared
// elements follow the finger direction.
val isSharedElement = fromState != null && toState != null
- if (isSharedElement && isSharedElementEnabled(layoutImpl.state, transition, element.key)) {
+ if (isSharedElement && isSharedElementEnabled(transition, element.key)) {
val start = sceneValue(fromState!!)
val end = sceneValue(toState!!)
@@ -607,7 +604,7 @@
}
val transformation =
- transformation(layoutImpl.state.transformationSpec.transformations(element.key, scene.key))
+ transformation(transition.transformationSpec.transformations(element.key, scene.key))
// If there is no transformation explicitly associated to this element value, let's use
// the value given by the system (like the current position and size given by the layout
// pass).
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Scene.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Scene.kt
index af51cee..dc3b612 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Scene.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Scene.kt
@@ -73,7 +73,7 @@
internal class SceneScopeImpl(
private val layoutImpl: SceneTransitionLayoutImpl,
private val scene: Scene,
-) : SceneScope {
+) : SceneScope, ElementStateScope by layoutImpl.elementStateScope {
override val layoutState: SceneTransitionLayoutState = layoutImpl.state
override fun Modifier.element(key: ElementKey): Modifier {
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
index b7e2dd1..c7c874c 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
@@ -96,9 +96,17 @@
modifier: Modifier = Modifier,
swipeSourceDetector: SwipeSourceDetector = DefaultEdgeDetector,
@FloatRange(from = 0.0, to = 0.5) transitionInterceptionThreshold: Float = 0f,
+ enableInterruptions: Boolean = DEFAULT_INTERRUPTIONS_ENABLED,
scenes: SceneTransitionLayoutScope.() -> Unit,
) {
- val state = updateSceneTransitionLayoutState(currentScene, onChangeScene, transitions)
+ val state =
+ updateSceneTransitionLayoutState(
+ currentScene,
+ onChangeScene,
+ transitions,
+ enableInterruptions = enableInterruptions,
+ )
+
SceneTransitionLayout(
state,
modifier,
@@ -131,9 +139,30 @@
*/
@DslMarker annotation class ElementDsl
+/** A scope that can be used to query the target state of an element or scene. */
+interface ElementStateScope {
+ /**
+ * Return the *target* size of [this] element in the given [scene], i.e. the size of the element
+ * when idle, or `null` if the element is not composed and measured in that scene (yet).
+ */
+ fun ElementKey.targetSize(scene: SceneKey): IntSize?
+
+ /**
+ * Return the *target* offset of [this] element in the given [scene], i.e. the size of the
+ * element when idle, or `null` if the element is not composed and placed in that scene (yet).
+ */
+ fun ElementKey.targetOffset(scene: SceneKey): Offset?
+
+ /**
+ * Return the *target* size of [this] scene, i.e. the size of the scene when idle, or `null` if
+ * the scene was never composed.
+ */
+ fun SceneKey.targetSize(): IntSize?
+}
+
@Stable
@ElementDsl
-interface BaseSceneScope {
+interface BaseSceneScope : ElementStateScope {
/** The state of the [SceneTransitionLayout] in which this scene is contained. */
val layoutState: SceneTransitionLayoutState
@@ -415,25 +444,7 @@
): Float
}
-interface UserActionDistanceScope : Density {
- /**
- * Return the *target* size of [this] element in the given [scene], i.e. the size of the element
- * when idle, or `null` if the element is not composed and measured in that scene (yet).
- */
- fun ElementKey.targetSize(scene: SceneKey): IntSize?
-
- /**
- * Return the *target* offset of [this] element in the given [scene], i.e. the size of the
- * element when idle, or `null` if the element is not composed and placed in that scene (yet).
- */
- fun ElementKey.targetOffset(scene: SceneKey): Offset?
-
- /**
- * Return the *target* size of [this] scene, i.e. the size of the scene when idle, or `null` if
- * the scene was never composed.
- */
- fun SceneKey.targetSize(): IntSize?
-}
+interface UserActionDistanceScope : Density, ElementStateScope
/** The user action has a fixed [absoluteDistance]. */
class FixedDistance(private val distance: Dp) : UserActionDistance {
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt
index 25b0895..b1cfdcf 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt
@@ -98,6 +98,7 @@
private val horizontalDraggableHandler: DraggableHandlerImpl
private val verticalDraggableHandler: DraggableHandlerImpl
+ internal val elementStateScope = ElementStateScopeImpl(this)
private var _userActionDistanceScope: UserActionDistanceScope? = null
internal val userActionDistanceScope: UserActionDistanceScope
get() =
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt
index 617a8ea..f13c016 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt
@@ -16,15 +16,16 @@
package com.android.compose.animation.scene
+import android.util.Log
+import androidx.annotation.VisibleForTesting
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.Stable
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
+import androidx.compose.runtime.snapshots.SnapshotStateList
+import androidx.compose.ui.util.fastAll
import androidx.compose.ui.util.fastFilter
import androidx.compose.ui.util.fastForEach
import com.android.compose.animation.scene.transition.link.LinkedTransition
@@ -50,10 +51,21 @@
*/
val transitionState: TransitionState
- /** The current transition, or `null` if we are idle. */
+ /**
+ * The current transition, or `null` if we are idle.
+ *
+ * Note: If you need to handle interruptions and multiple transitions running in parallel, use
+ * [currentTransitions] instead.
+ */
val currentTransition: TransitionState.Transition?
get() = transitionState as? TransitionState.Transition
+ /**
+ * The list of [TransitionState.Transition] currently running. This will be the empty list if we
+ * are idle.
+ */
+ val currentTransitions: List<TransitionState.Transition>
+
/** The [SceneTransitions] used when animating this state. */
val transitions: SceneTransitions
@@ -120,12 +132,14 @@
transitions: SceneTransitions = SceneTransitions.Empty,
canChangeScene: (SceneKey) -> Boolean = { true },
stateLinks: List<StateLink> = emptyList(),
+ enableInterruptions: Boolean = DEFAULT_INTERRUPTIONS_ENABLED,
): MutableSceneTransitionLayoutState {
return MutableSceneTransitionLayoutStateImpl(
initialScene,
transitions,
canChangeScene,
stateLinks,
+ enableInterruptions,
)
}
@@ -154,6 +168,7 @@
transitions: SceneTransitions = SceneTransitions.Empty,
canChangeScene: (SceneKey) -> Boolean = { true },
stateLinks: List<StateLink> = emptyList(),
+ enableInterruptions: Boolean = DEFAULT_INTERRUPTIONS_ENABLED,
): SceneTransitionLayoutState {
return remember {
HoistedSceneTransitionLayoutState(
@@ -162,9 +177,19 @@
onChangeScene,
canChangeScene,
stateLinks,
+ enableInterruptions,
)
}
- .apply { update(currentScene, onChangeScene, canChangeScene, transitions, stateLinks) }
+ .apply {
+ update(
+ currentScene,
+ onChangeScene,
+ canChangeScene,
+ transitions,
+ stateLinks,
+ enableInterruptions,
+ )
+ }
}
@Stable
@@ -204,6 +229,30 @@
/** Whether user input is currently driving the transition. */
abstract val isUserInputOngoing: Boolean
+ /**
+ * The current [TransformationSpecImpl] and [OverscrollSpecImpl] associated to this
+ * transition.
+ *
+ * Important: These will be set exactly once, when this transition is
+ * [started][BaseSceneTransitionLayoutState.startTransition].
+ */
+ internal var transformationSpec: TransformationSpecImpl = TransformationSpec.Empty
+ private var fromOverscrollSpec: OverscrollSpecImpl? = null
+ private var toOverscrollSpec: OverscrollSpecImpl? = null
+
+ /** The current [OverscrollSpecImpl], if this transition is currently overscrolling. */
+ internal val currentOverscrollSpec: OverscrollSpecImpl?
+ get() {
+ if (this !is HasOverscrollProperties) return null
+ val progress = progress
+ val bouncingScene = bouncingScene
+ return when {
+ progress < 0f || bouncingScene == fromScene -> fromOverscrollSpec
+ progress > 1f || bouncingScene == toScene -> toOverscrollSpec
+ else -> null
+ }
+ }
+
init {
check(fromScene != toScene)
}
@@ -232,6 +281,14 @@
return isTransitioning(from = scene, to = other) ||
isTransitioning(from = other, to = scene)
}
+
+ internal fun updateOverscrollSpecs(
+ fromSpec: OverscrollSpecImpl?,
+ toSpec: OverscrollSpecImpl?,
+ ) {
+ fromOverscrollSpec = fromSpec
+ toOverscrollSpec = toSpec
+ }
}
interface HasOverscrollProperties {
@@ -270,38 +327,41 @@
internal abstract class BaseSceneTransitionLayoutState(
initialScene: SceneKey,
protected var stateLinks: List<StateLink>,
+
+ // TODO(b/290930950): Remove this flag.
+ internal var enableInterruptions: Boolean,
) : SceneTransitionLayoutState {
- override var transitionState: TransitionState by
- mutableStateOf(TransitionState.Idle(initialScene))
- protected set
-
/**
- * The current [transformationSpec] associated to [transitionState]. Accessing this value makes
- * sense only if [transitionState] is a [TransitionState.Transition].
+ * The current [TransitionState]. This list will either be:
+ * 1. A list with a single [TransitionState.Idle] element, when we are idle.
+ * 2. A list with one or more [TransitionState.Transition], when we are transitioning.
*/
- internal var transformationSpec: TransformationSpecImpl = TransformationSpec.Empty
+ @VisibleForTesting
+ internal val transitionStates: MutableList<TransitionState> =
+ SnapshotStateList<TransitionState>().apply { add(TransitionState.Idle(initialScene)) }
- private var fromOverscrollSpec: OverscrollSpecImpl? = null
- private var toOverscrollSpec: OverscrollSpecImpl? = null
+ override val transitionState: TransitionState
+ get() = transitionStates.last()
- /**
- * @return the overscroll [OverscrollSpecImpl] if it is defined for the current
- * [transitionState] and we are currently over scrolling.
- */
- internal val currentOverscrollSpec: OverscrollSpecImpl?
+ private val activeTransitionLinks = mutableMapOf<StateLink, LinkedTransition>()
+
+ override val currentTransitions: List<TransitionState.Transition>
get() {
- val transition = currentTransition ?: return null
- if (transition !is TransitionState.HasOverscrollProperties) return null
- val progress = transition.progress
- val bouncingScene = transition.bouncingScene
- return when {
- progress < 0f || bouncingScene == transition.fromScene -> fromOverscrollSpec
- progress > 1f || bouncingScene == transition.toScene -> toOverscrollSpec
- else -> null
+ if (transitionStates.last() is TransitionState.Idle) {
+ check(transitionStates.size == 1)
+ return emptyList()
+ } else {
+ @Suppress("UNCHECKED_CAST")
+ return transitionStates as List<TransitionState.Transition>
}
}
- private val activeTransitionLinks = mutableMapOf<StateLink, LinkedTransition>()
+ /**
+ * The mapping of transitions that are finished, i.e. for which [finishTransition] was called,
+ * to their idle scene.
+ */
+ @VisibleForTesting
+ internal val finishedTransitions = mutableMapOf<TransitionState.Transition, SceneKey>()
/** Whether we can transition to the given [scene]. */
internal abstract fun canChangeScene(scene: SceneKey): Boolean
@@ -324,7 +384,11 @@
return transition.isTransitioningBetween(scene, other)
}
- /** Start a new [transition], instantly interrupting any ongoing transition if there was one. */
+ /**
+ * Start a new [transition], instantly interrupting any ongoing transition if there was one.
+ *
+ * Important: you *must* call [finishTransition] once the transition is finished.
+ */
internal fun startTransition(
transition: TransitionState.Transition,
transitionKey: TransitionKey?,
@@ -333,13 +397,81 @@
val fromScene = transition.fromScene
val toScene = transition.toScene
val orientation = (transition as? TransitionState.HasOverscrollProperties)?.orientation
- transformationSpec =
+
+ // Update the transition specs.
+ transition.transformationSpec =
transitions.transitionSpec(fromScene, toScene, key = transitionKey).transformationSpec()
- fromOverscrollSpec = orientation?.let { transitions.overscrollSpec(fromScene, it) }
- toOverscrollSpec = orientation?.let { transitions.overscrollSpec(toScene, it) }
+ if (orientation != null) {
+ transition.updateOverscrollSpecs(
+ fromSpec = transitions.overscrollSpec(fromScene, orientation),
+ toSpec = transitions.overscrollSpec(toScene, orientation),
+ )
+ } else {
+ transition.updateOverscrollSpecs(fromSpec = null, toSpec = null)
+ }
+
+ // Handle transition links.
cancelActiveTransitionLinks()
setupTransitionLinks(transition)
- transitionState = transition
+
+ if (!enableInterruptions) {
+ // Set the current transition.
+ check(transitionStates.size == 1)
+ transitionStates[0] = transition
+ return
+ }
+
+ when (val currentState = transitionStates.last()) {
+ is TransitionState.Idle -> {
+ // Replace [Idle] by [transition].
+ check(transitionStates.size == 1)
+ transitionStates[0] = transition
+ }
+ is TransitionState.Transition -> {
+ // Force the current transition to finish to currentScene.
+ currentState.finish().invokeOnCompletion {
+ // Make sure [finishTransition] is called at the end of the transition.
+ finishTransition(currentState, currentState.currentScene)
+ }
+
+ // Check that we don't have too many concurrent transitions.
+ if (transitionStates.size >= MAX_CONCURRENT_TRANSITIONS) {
+ Log.wtf(
+ TAG,
+ buildString {
+ appendLine("Potential leak detected in SceneTransitionLayoutState!")
+ appendLine(
+ " Some transition(s) never called STLState.finishTransition()."
+ )
+ appendLine(" Transitions (size=${transitionStates.size}):")
+ transitionStates.fastForEach { state ->
+ val transition = state as TransitionState.Transition
+ val from = transition.fromScene
+ val to = transition.toScene
+ val indicator =
+ if (finishedTransitions.contains(transition)) "x" else " "
+ appendLine(" [$indicator] $from => $to ($transition)")
+ }
+ }
+ )
+
+ // Force finish all transitions.
+ while (currentTransitions.isNotEmpty()) {
+ val transition = transitionStates[0] as TransitionState.Transition
+ finishTransition(transition, transition.currentScene)
+ }
+
+ // We finished all transitions, so we are now idle. We remove this state so that
+ // we end up only with the new transition after appending it.
+ check(transitionStates.size == 1)
+ check(transitionStates[0] is TransitionState.Idle)
+ transitionStates.clear()
+ }
+
+ // Append the new transition.
+ transitionStates.add(transition)
+ }
+ }
}
private fun cancelActiveTransitionLinks() {
@@ -379,13 +511,54 @@
* nothing if [transition] was interrupted since it was started.
*/
internal fun finishTransition(transition: TransitionState.Transition, idleScene: SceneKey) {
- resolveActiveTransitionLinks(idleScene)
- if (transitionState == transition) {
- transitionState = TransitionState.Idle(idleScene)
+ val existingIdleScene = finishedTransitions[transition]
+ if (existingIdleScene != null) {
+ // This transition was already finished.
+ check(idleScene == existingIdleScene) {
+ "Transition $transition was finished multiple times with different " +
+ "idleScene ($existingIdleScene != $idleScene)"
+ }
+ return
+ }
+
+ if (!transitionStates.contains(transition)) {
+ // This transition was already removed from transitionStates.
+ return
+ }
+
+ check(transitionStates.fastAll { it is TransitionState.Transition })
+
+ // Mark this transition as finished and save the scene it is settling at.
+ finishedTransitions[transition] = idleScene
+
+ // Finish all linked transitions.
+ finishActiveTransitionLinks(idleScene)
+
+ // Keep a reference to the idle scene of the last removed transition, in case we remove all
+ // transitions and should settle to Idle.
+ var lastRemovedIdleScene: SceneKey? = null
+
+ // Remove all first n finished transitions.
+ while (transitionStates.isNotEmpty()) {
+ val firstTransition = transitionStates[0]
+ if (!finishedTransitions.contains(firstTransition)) {
+ // Stop here.
+ break
+ }
+
+ // Remove the transition from the list and from the set of finished transitions.
+ transitionStates.removeAt(0)
+ lastRemovedIdleScene = finishedTransitions.remove(firstTransition)
+ }
+
+ // If all transitions are finished, we are idle.
+ if (transitionStates.isEmpty()) {
+ check(finishedTransitions.isEmpty())
+ transitionStates.add(TransitionState.Idle(checkNotNull(lastRemovedIdleScene)))
}
}
- private fun resolveActiveTransitionLinks(idleScene: SceneKey) {
+ private fun finishActiveTransitionLinks(idleScene: SceneKey) {
val previousTransition = this.transitionState as? TransitionState.Transition ?: return
for ((link, linkedTransition) in activeTransitionLinks) {
if (previousTransition.fromScene == idleScene) {
@@ -406,20 +579,39 @@
* Check if a transition is in progress. If the progress value is near 0 or 1, immediately snap
* to the closest scene.
*
+ * Important: Snapping to the closest scene will instantly finish *all* ongoing transitions,
+ * only the progress of the last transition will be checked.
+ *
* @return true if snapped to the closest scene.
*/
internal fun snapToIdleIfClose(threshold: Float): Boolean {
val transition = currentTransition ?: return false
val progress = transition.progress
+
fun isProgressCloseTo(value: Float) = (progress - value).absoluteValue <= threshold
+ fun finishAllTransitions(lastTransitionIdleScene: SceneKey) {
+ // Force finish all transitions.
+ while (currentTransitions.isNotEmpty()) {
+ val transition = transitionStates[0] as TransitionState.Transition
+ val idleScene =
+ if (transitionStates.size == 1) {
+ lastTransitionIdleScene
+ } else {
+ transition.currentScene
+ }
+
+ finishTransition(transition, idleScene)
+ }
+ }
+
return when {
isProgressCloseTo(0f) -> {
- finishTransition(transition, transition.fromScene)
+ finishAllTransitions(transition.fromScene)
true
}
isProgressCloseTo(1f) -> {
- finishTransition(transition, transition.toScene)
+ finishAllTransitions(transition.toScene)
true
}
else -> false
@@ -437,7 +629,8 @@
private var changeScene: (SceneKey) -> Unit,
private var canChangeScene: (SceneKey) -> Boolean,
stateLinks: List<StateLink> = emptyList(),
-) : BaseSceneTransitionLayoutState(initialScene, stateLinks) {
+ enableInterruptions: Boolean = DEFAULT_INTERRUPTIONS_ENABLED,
+) : BaseSceneTransitionLayoutState(initialScene, stateLinks, enableInterruptions) {
private val targetSceneChannel = Channel<SceneKey>(Channel.CONFLATED)
override fun canChangeScene(scene: SceneKey): Boolean = canChangeScene.invoke(scene)
@@ -451,12 +644,14 @@
canChangeScene: (SceneKey) -> Boolean,
transitions: SceneTransitions,
stateLinks: List<StateLink>,
+ enableInterruptions: Boolean,
) {
SideEffect {
this.changeScene = onChangeScene
this.canChangeScene = canChangeScene
this.transitions = transitions
this.stateLinks = stateLinks
+ this.enableInterruptions = enableInterruptions
targetSceneChannel.trySend(currentScene)
}
@@ -482,7 +677,10 @@
override var transitions: SceneTransitions,
private val canChangeScene: (SceneKey) -> Boolean = { true },
stateLinks: List<StateLink> = emptyList(),
-) : MutableSceneTransitionLayoutState, BaseSceneTransitionLayoutState(initialScene, stateLinks) {
+ enableInterruptions: Boolean = DEFAULT_INTERRUPTIONS_ENABLED,
+) :
+ MutableSceneTransitionLayoutState,
+ BaseSceneTransitionLayoutState(initialScene, stateLinks, enableInterruptions) {
override fun setTargetScene(
targetScene: SceneKey,
coroutineScope: CoroutineScope,
@@ -501,3 +699,15 @@
setTargetScene(scene, coroutineScope = this)
}
}
+
+private const val TAG = "SceneTransitionLayoutState"
+
+/** Whether support for interruptions in enabled by default. */
+internal const val DEFAULT_INTERRUPTIONS_ENABLED = true
+
+/**
+ * The max number of concurrent transitions. If the number of transitions goes past this number,
+ * this probably means that there is a leak and we will Log.wtf before clearing the list of
+ * transitions.
+ */
+private const val MAX_CONCURRENT_TRANSITIONS = 100
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/UserActionDistanceScopeImpl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/UserActionDistanceScopeImpl.kt
index 228d19f..b7abb33 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/UserActionDistanceScopeImpl.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/UserActionDistanceScopeImpl.kt
@@ -19,15 +19,9 @@
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.unit.IntSize
-internal class UserActionDistanceScopeImpl(
+internal class ElementStateScopeImpl(
private val layoutImpl: SceneTransitionLayoutImpl,
-) : UserActionDistanceScope {
- override val density: Float
- get() = layoutImpl.density.density
-
- override val fontScale: Float
- get() = layoutImpl.density.fontScale
-
+) : ElementStateScope {
override fun ElementKey.targetSize(scene: SceneKey): IntSize? {
return layoutImpl.elements[this]?.sceneStates?.get(scene)?.targetSize.takeIf {
it != Element.SizeUnspecified
@@ -44,3 +38,13 @@
return layoutImpl.scenes[this]?.targetSize.takeIf { it != IntSize.Zero }
}
}
+
+internal class UserActionDistanceScopeImpl(
+ private val layoutImpl: SceneTransitionLayoutImpl,
+) : UserActionDistanceScope, ElementStateScope by layoutImpl.elementStateScope {
+ override val density: Float
+ get() = layoutImpl.density.density
+
+ override val fontScale: Float
+ get() = layoutImpl.density.fontScale
+}
diff --git a/packages/SystemUI/compose/scene/tests/Android.bp b/packages/SystemUI/compose/scene/tests/Android.bp
index 59cc63a..af13896 100644
--- a/packages/SystemUI/compose/scene/tests/Android.bp
+++ b/packages/SystemUI/compose/scene/tests/Android.bp
@@ -26,7 +26,6 @@
name: "PlatformComposeSceneTransitionLayoutTests",
manifest: "AndroidManifest.xml",
test_suites: ["device-tests"],
- sdk_version: "current",
certificate: "platform",
srcs: [
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt
index 1e9a7e2..2ed51eb 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt
@@ -984,14 +984,14 @@
val scene = layoutState.transitionState.currentScene
// We should have overscroll spec for scene C
assertThat(layoutState.transitions.overscrollSpec(scene, Orientation.Vertical)).isNotNull()
- assertThat(layoutState.currentOverscrollSpec).isNull()
+ assertThat(layoutState.currentTransition?.currentOverscrollSpec).isNull()
val nestedScroll = nestedScrollConnection(nestedScrollBehavior = EdgeAlways)
nestedScroll.scroll(available = downOffset(fractionOfScreen = 0.1f))
// We scrolled down, under scene C there is nothing, so we can use the overscroll spec
- assertThat(layoutState.currentOverscrollSpec).isNotNull()
- assertThat(layoutState.currentOverscrollSpec?.scene).isEqualTo(SceneC)
+ assertThat(layoutState.currentTransition?.currentOverscrollSpec).isNotNull()
+ assertThat(layoutState.currentTransition?.currentOverscrollSpec?.scene).isEqualTo(SceneC)
val transition = layoutState.currentTransition
assertThat(transition).isNotNull()
assertThat(transition!!.progress).isEqualTo(-0.1f)
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt
index 597da9e..2453e25 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt
@@ -595,7 +595,7 @@
}
assertThat(state.currentTransition).isNull()
- assertThat(state.currentOverscrollSpec).isNull()
+ assertThat(state.currentTransition?.currentOverscrollSpec).isNull()
// Swipe by half of verticalSwipeDistance.
rule.onRoot().performTouchInput {
@@ -643,7 +643,7 @@
// Scroll 150% (Scene B overscroll by 50%)
assertThat(transition.progress).isEqualTo(1.5f)
- assertThat(state.currentOverscrollSpec).isNotNull()
+ assertThat(state.currentTransition?.currentOverscrollSpec).isNotNull()
fooElement.assertTopPositionInRootIsEqualTo(overscrollTranslateY * 0.5f)
// animatedFloat cannot overflow (canOverflow = false)
assertThat(animatedFloat).isEqualTo(100f)
@@ -655,7 +655,7 @@
// Scroll 250% (Scene B overscroll by 150%)
assertThat(transition.progress).isEqualTo(2.5f)
- assertThat(state.currentOverscrollSpec).isNotNull()
+ assertThat(state.currentTransition?.currentOverscrollSpec).isNotNull()
fooElement.assertTopPositionInRootIsEqualTo(overscrollTranslateY * 1.5f)
assertThat(animatedFloat).isEqualTo(100f)
}
@@ -707,7 +707,7 @@
}
assertThat(state.currentTransition).isNull()
- assertThat(state.currentOverscrollSpec).isNull()
+ assertThat(state.currentTransition?.currentOverscrollSpec).isNull()
val fooElement = rule.onNodeWithTag(TestElements.Foo.testTag, useUnmergedTree = true)
fooElement.assertTopPositionInRootIsEqualTo(0.dp)
@@ -720,7 +720,7 @@
}
val transition = state.currentTransition
- assertThat(state.currentOverscrollSpec).isNotNull()
+ assertThat(state.currentTransition?.currentOverscrollSpec).isNotNull()
assertThat(transition).isNotNull()
assertThat(transition!!.progress).isEqualTo(-0.5f)
fooElement.assertTopPositionInRootIsEqualTo(overscrollTranslateY * 0.5f)
@@ -732,7 +732,7 @@
// Scroll 150% (Scene B overscroll by 50%)
assertThat(transition.progress).isEqualTo(-1.5f)
- assertThat(state.currentOverscrollSpec).isNotNull()
+ assertThat(state.currentTransition?.currentOverscrollSpec).isNotNull()
fooElement.assertTopPositionInRootIsEqualTo(overscrollTranslateY * 1.5f)
}
@@ -771,7 +771,7 @@
// Scroll 150% (100% scroll + 50% overscroll)
assertThat(transition!!.progress).isEqualTo(1.5f)
- assertThat(state.currentOverscrollSpec).isNotNull()
+ assertThat(state.currentTransition?.currentOverscrollSpec).isNotNull()
fooElement.assertTopPositionInRootIsEqualTo(layoutHeight * 0.5f)
assertThat(animatedFloat).isEqualTo(100f)
@@ -782,7 +782,7 @@
// Scroll 250% (100% scroll + 150% overscroll)
assertThat(transition.progress).isEqualTo(2.5f)
- assertThat(state.currentOverscrollSpec).isNotNull()
+ assertThat(state.currentTransition?.currentOverscrollSpec).isNotNull()
fooElement.assertTopPositionInRootIsEqualTo(layoutHeight * 1.5f)
assertThat(animatedFloat).isEqualTo(100f)
}
@@ -828,7 +828,7 @@
// Scroll 150% (100% scroll + 50% overscroll)
assertThat(transition.progress).isEqualTo(1.5f)
- assertThat(state.currentOverscrollSpec).isNotNull()
+ assertThat(state.currentTransition?.currentOverscrollSpec).isNotNull()
fooElement.assertTopPositionInRootIsEqualTo(layoutHeight * (transition.progress - 1f))
assertThat(animatedFloat).isEqualTo(100f)
@@ -840,7 +840,7 @@
rule.waitUntil(timeoutMillis = 10_000) { transition.progress < 1f }
assertThat(transition.progress).isLessThan(1f)
- assertThat(state.currentOverscrollSpec).isNotNull()
+ assertThat(state.currentTransition?.currentOverscrollSpec).isNotNull()
assertThat(transition.bouncingScene).isEqualTo(transition.toScene)
assertThat(animatedFloat).isEqualTo(100f)
}
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 9baabc3..93e94f8 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
@@ -16,6 +16,7 @@
package com.android.compose.animation.scene
+import android.util.Log
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.test.junit4.createComposeRule
@@ -28,9 +29,12 @@
import com.android.compose.test.runMonotonicClockTest
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.CoroutineStart
+import kotlinx.coroutines.Job
import kotlinx.coroutines.cancelAndJoin
-import kotlinx.coroutines.job
import kotlinx.coroutines.launch
+import kotlinx.coroutines.sync.Mutex
+import kotlinx.coroutines.sync.withLock
+import kotlinx.coroutines.test.runTest
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -271,11 +275,21 @@
}
@Test
- fun linkedTransition_startsLinkButLinkedStateIsTakenOver() {
+ fun linkedTransition_startsLinkButLinkedStateIsTakenOver() = runTest {
val (parentState, childState) = setupLinkedStates()
- val childTransition = transition(SceneA, SceneB)
- val parentTransition = transition(SceneC, SceneA)
+ val childTransition =
+ transition(
+ SceneA,
+ SceneB,
+ onFinish = { launch { /* Do nothing. */} },
+ )
+ val parentTransition =
+ transition(
+ SceneC,
+ SceneA,
+ onFinish = { launch { /* Do nothing. */} },
+ )
childState.startTransition(childTransition, null)
parentState.startTransition(parentTransition, null)
@@ -303,7 +317,7 @@
// Default transition from A to B.
assertThat(state.setTargetScene(SceneB, coroutineScope = this)).isNotNull()
- assertThat(state.transformationSpec.transformations).hasSize(1)
+ assertThat(state.currentTransition?.transformationSpec?.transformations).hasSize(1)
// Go back to A.
state.setTargetScene(SceneA, coroutineScope = this)
@@ -320,14 +334,14 @@
)
)
.isNotNull()
- assertThat(state.transformationSpec.transformations).hasSize(2)
+ assertThat(state.currentTransition?.transformationSpec?.transformations).hasSize(2)
}
@Test
fun snapToIdleIfClose_snapToStart() = runMonotonicClockTest {
val state = MutableSceneTransitionLayoutStateImpl(SceneA, SceneTransitions.Empty)
state.startTransition(
- transition(from = SceneA, to = TestScenes.SceneB, progress = { 0.2f }),
+ transition(from = SceneA, to = SceneB, progress = { 0.2f }),
transitionKey = null
)
assertThat(state.isTransitioning()).isTrue()
@@ -346,7 +360,7 @@
fun snapToIdleIfClose_snapToEnd() = runMonotonicClockTest {
val state = MutableSceneTransitionLayoutStateImpl(SceneA, SceneTransitions.Empty)
state.startTransition(
- transition(from = SceneA, to = TestScenes.SceneB, progress = { 0.8f }),
+ transition(from = SceneA, to = SceneB, progress = { 0.8f }),
transitionKey = null
)
assertThat(state.isTransitioning()).isTrue()
@@ -358,7 +372,35 @@
// Go to the final scene if it is close to 1.
assertThat(state.snapToIdleIfClose(threshold = 0.2f)).isTrue()
assertThat(state.isTransitioning()).isFalse()
- assertThat(state.transitionState).isEqualTo(TransitionState.Idle(TestScenes.SceneB))
+ assertThat(state.transitionState).isEqualTo(TransitionState.Idle(SceneB))
+ }
+
+ @Test
+ fun snapToIdleIfClose_multipleTransitions() = runMonotonicClockTest {
+ val state = MutableSceneTransitionLayoutStateImpl(SceneA, SceneTransitions.Empty)
+
+ val aToB =
+ transition(
+ from = SceneA,
+ to = SceneB,
+ progress = { 0.5f },
+ onFinish = { launch { /* do nothing */} },
+ )
+ state.startTransition(aToB, transitionKey = null)
+ assertThat(state.currentTransitions).containsExactly(aToB).inOrder()
+
+ val bToC = transition(from = SceneB, to = SceneC, progress = { 0.8f })
+ state.startTransition(bToC, transitionKey = null)
+ assertThat(state.currentTransitions).containsExactly(aToB, bToC).inOrder()
+
+ // Ignore the request if the progress is not close to 0 or 1, using the threshold.
+ assertThat(state.snapToIdleIfClose(threshold = 0.1f)).isFalse()
+ assertThat(state.currentTransitions).containsExactly(aToB, bToC).inOrder()
+
+ // Go to the final scene if it is close to 1.
+ assertThat(state.snapToIdleIfClose(threshold = 0.2f)).isTrue()
+ assertThat(state.transitionState).isEqualTo(TransitionState.Idle(SceneC))
+ assertThat(state.currentTransitions).isEmpty()
}
@Test
@@ -435,23 +477,23 @@
overscroll(SceneB, Orientation.Vertical) { fade(TestElements.Foo) }
}
)
- assertThat(state.currentOverscrollSpec).isNull()
+ assertThat(state.currentTransition?.currentOverscrollSpec).isNull()
// overscroll for SceneA is NOT defined
progress.value = -0.1f
- assertThat(state.currentOverscrollSpec).isNull()
+ assertThat(state.currentTransition?.currentOverscrollSpec).isNull()
// scroll from SceneA to SceneB
progress.value = 0.5f
- assertThat(state.currentOverscrollSpec).isNull()
+ assertThat(state.currentTransition?.currentOverscrollSpec).isNull()
progress.value = 1f
- assertThat(state.currentOverscrollSpec).isNull()
+ assertThat(state.currentTransition?.currentOverscrollSpec).isNull()
// overscroll for SceneB is defined
progress.value = 1.1f
- assertThat(state.currentOverscrollSpec).isNotNull()
- assertThat(state.currentOverscrollSpec?.scene).isEqualTo(SceneB)
+ assertThat(state.currentTransition?.currentOverscrollSpec).isNotNull()
+ assertThat(state.currentTransition?.currentOverscrollSpec?.scene).isEqualTo(SceneB)
}
@Test
@@ -465,23 +507,23 @@
overscroll(SceneA, Orientation.Vertical) { fade(TestElements.Foo) }
}
)
- assertThat(state.currentOverscrollSpec).isNull()
+ assertThat(state.currentTransition?.currentOverscrollSpec).isNull()
// overscroll for SceneA is defined
progress.value = -0.1f
- assertThat(state.currentOverscrollSpec).isNotNull()
- assertThat(state.currentOverscrollSpec?.scene).isEqualTo(SceneA)
+ assertThat(state.currentTransition?.currentOverscrollSpec).isNotNull()
+ assertThat(state.currentTransition?.currentOverscrollSpec?.scene).isEqualTo(SceneA)
// scroll from SceneA to SceneB
progress.value = 0.5f
- assertThat(state.currentOverscrollSpec).isNull()
+ assertThat(state.currentTransition?.currentOverscrollSpec).isNull()
progress.value = 1f
- assertThat(state.currentOverscrollSpec).isNull()
+ assertThat(state.currentTransition?.currentOverscrollSpec).isNull()
// overscroll for SceneB is NOT defined
progress.value = 1.1f
- assertThat(state.currentOverscrollSpec).isNull()
+ assertThat(state.currentTransition?.currentOverscrollSpec).isNull()
}
@Test
@@ -492,21 +534,99 @@
progress = { progress.value },
sceneTransitions = transitions {}
)
- assertThat(state.currentOverscrollSpec).isNull()
+ assertThat(state.currentTransition?.currentOverscrollSpec).isNull()
// overscroll for SceneA is NOT defined
progress.value = -0.1f
- assertThat(state.currentOverscrollSpec).isNull()
+ assertThat(state.currentTransition?.currentOverscrollSpec).isNull()
// scroll from SceneA to SceneB
progress.value = 0.5f
- assertThat(state.currentOverscrollSpec).isNull()
+ assertThat(state.currentTransition?.currentOverscrollSpec).isNull()
progress.value = 1f
- assertThat(state.currentOverscrollSpec).isNull()
+ assertThat(state.currentTransition?.currentOverscrollSpec).isNull()
// overscroll for SceneB is NOT defined
progress.value = 1.1f
- assertThat(state.currentOverscrollSpec).isNull()
+ assertThat(state.currentTransition?.currentOverscrollSpec).isNull()
+ }
+
+ @Test
+ fun multipleTransitions() = runTest {
+ val finishingTransitions = mutableSetOf<TransitionState.Transition>()
+ fun onFinish(transition: TransitionState.Transition): Job {
+ // Instead of letting the transition finish, we put the transition in the
+ // finishingTransitions set so that we can verify that finish() is called when expected
+ // and then we call state STLState.finishTransition() ourselves.
+ finishingTransitions.add(transition)
+
+ return backgroundScope.launch {
+ // Try to acquire a locked mutex so that this code never completes.
+ Mutex(locked = true).withLock {}
+ }
+ }
+
+ val state = MutableSceneTransitionLayoutStateImpl(SceneA, EmptyTestTransitions)
+ val aToB = transition(SceneA, SceneB, onFinish = ::onFinish)
+ val bToC = transition(SceneB, SceneC, onFinish = ::onFinish)
+ val cToA = transition(SceneC, SceneA, onFinish = ::onFinish)
+
+ // Starting state.
+ assertThat(finishingTransitions).isEmpty()
+ assertThat(state.currentTransitions).isEmpty()
+
+ // A => B.
+ state.startTransition(aToB, transitionKey = null)
+ assertThat(finishingTransitions).isEmpty()
+ assertThat(state.finishedTransitions).isEmpty()
+ assertThat(state.currentTransitions).containsExactly(aToB).inOrder()
+
+ // B => C. This should automatically call finish() on aToB.
+ state.startTransition(bToC, transitionKey = null)
+ assertThat(finishingTransitions).containsExactly(aToB)
+ assertThat(state.finishedTransitions).isEmpty()
+ assertThat(state.currentTransitions).containsExactly(aToB, bToC).inOrder()
+
+ // C => A. This should automatically call finish() on bToC.
+ state.startTransition(cToA, transitionKey = null)
+ assertThat(finishingTransitions).containsExactly(aToB, bToC)
+ assertThat(state.finishedTransitions).isEmpty()
+ assertThat(state.currentTransitions).containsExactly(aToB, bToC, cToA).inOrder()
+
+ // Mark bToC as finished. The list of current transitions does not change because aToB is
+ // still not marked as finished.
+ state.finishTransition(bToC, idleScene = bToC.currentScene)
+ assertThat(state.finishedTransitions).containsExactly(bToC, bToC.currentScene)
+ assertThat(state.currentTransitions).containsExactly(aToB, bToC, cToA).inOrder()
+
+ // Mark aToB as finished. This will remove both aToB and bToC from the list of transitions.
+ state.finishTransition(aToB, idleScene = aToB.currentScene)
+ assertThat(state.finishedTransitions).isEmpty()
+ assertThat(state.currentTransitions).containsExactly(cToA).inOrder()
+ }
+
+ @Test
+ fun tooManyTransitionsLogsWtfAndClearsTransitions() = runTest {
+ val state = MutableSceneTransitionLayoutStateImpl(SceneA, EmptyTestTransitions)
+
+ fun startTransition() {
+ val transition = transition(SceneA, SceneB, onFinish = { launch { /* do nothing */} })
+ state.startTransition(transition, transitionKey = null)
+ }
+
+ var hasLoggedWtf = false
+ val originalHandler = Log.setWtfHandler { _, _, _ -> hasLoggedWtf = true }
+ try {
+ repeat(100) { startTransition() }
+ assertThat(hasLoggedWtf).isFalse()
+ assertThat(state.currentTransitions).hasSize(100)
+
+ startTransition()
+ assertThat(hasLoggedWtf).isTrue()
+ assertThat(state.currentTransitions).hasSize(1)
+ } finally {
+ Log.setWtfHandler(originalHandler)
+ }
}
}
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt
index efaea71..723a182 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt
@@ -299,6 +299,11 @@
.isWithin(DpOffsetSubject.DefaultTolerance)
.of(DpOffset(expectedOffset, expectedOffset))
+ // Wait for the transition to C to finish.
+ rule.mainClock.advanceTimeBy(TestTransitionDuration)
+ assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Idle::class.java)
+ assertThat(layoutState.transitionState.currentScene).isEqualTo(TestScenes.SceneC)
+
// Go back to scene A. This should happen instantly (once the animation started, i.e. after
// 2 frames) given that we use a snap() animation spec.
currentScene = 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 99372a5..f034c18 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
@@ -547,12 +547,12 @@
}
assertThat(state.isTransitioning(from = TestScenes.SceneA, to = TestScenes.SceneB)).isTrue()
- assertThat(state.transformationSpec.transformations).hasSize(1)
+ assertThat(state.currentTransition?.transformationSpec?.transformations).hasSize(1)
// Move the pointer up to swipe to scene B using the new transition.
rule.onRoot().performTouchInput { moveBy(Offset(0f, -1.dp.toPx()), delayMillis = 1_000) }
assertThat(state.isTransitioning(from = TestScenes.SceneA, to = TestScenes.SceneB)).isTrue()
- assertThat(state.transformationSpec.transformations).hasSize(2)
+ assertThat(state.currentTransition?.transformationSpec?.transformations).hasSize(2)
}
@Test
diff --git a/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/Transition.kt b/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/Transition.kt
index a32fe22..767057b 100644
--- a/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/Transition.kt
+++ b/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/Transition.kt
@@ -29,6 +29,7 @@
isUpOrLeft: Boolean = false,
bouncingScene: SceneKey? = null,
orientation: Orientation = Orientation.Horizontal,
+ onFinish: ((TransitionState.Transition) -> Job)? = null,
): TransitionState.Transition {
return object : TransitionState.Transition(from, to), TransitionState.HasOverscrollProperties {
override val currentScene: SceneKey = from
@@ -46,7 +47,13 @@
}
override fun finish(): Job {
- error("finish() is not supported in test transitions")
+ val onFinish =
+ onFinish
+ ?: error(
+ "onFinish() must be provided if finish() is called on test transitions"
+ )
+
+ return onFinish(this)
}
}
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorTest.kt
index 707777b..b0d03b1 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorTest.kt
@@ -71,34 +71,6 @@
}
@Test
- fun pinAuthMethod() =
- testScope.runTest {
- val message by collectLastValue(underTest.message)
-
- kosmos.fakeAuthenticationRepository.setAuthenticationMethod(
- AuthenticationMethodModel.Pin
- )
- runCurrent()
- underTest.clearMessage()
- assertThat(message).isNull()
-
- underTest.resetMessage()
- assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PIN)
-
- // Wrong input.
- assertThat(underTest.authenticate(listOf(9, 8, 7)))
- .isEqualTo(AuthenticationResult.FAILED)
- assertThat(message).isEqualTo(MESSAGE_WRONG_PIN)
-
- underTest.resetMessage()
- assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PIN)
-
- // Correct input.
- assertThat(underTest.authenticate(FakeAuthenticationRepository.DEFAULT_PIN))
- .isEqualTo(AuthenticationResult.SUCCEEDED)
- }
-
- @Test
fun pinAuthMethod_sim_skipsAuthentication() =
testScope.runTest {
kosmos.fakeAuthenticationRepository.setAuthenticationMethod(
@@ -146,8 +118,6 @@
@Test
fun pinAuthMethod_tryAutoConfirm_withoutAutoConfirmPin() =
testScope.runTest {
- val message by collectLastValue(underTest.message)
-
kosmos.fakeAuthenticationRepository.setAuthenticationMethod(
AuthenticationMethodModel.Pin
)
@@ -156,7 +126,6 @@
// Incomplete input.
assertThat(underTest.authenticate(listOf(1, 2), tryAutoConfirm = true))
.isEqualTo(AuthenticationResult.SKIPPED)
- assertThat(message).isNull()
// Correct input.
assertThat(
@@ -166,28 +135,19 @@
)
)
.isEqualTo(AuthenticationResult.SKIPPED)
- assertThat(message).isNull()
}
@Test
fun passwordAuthMethod() =
testScope.runTest {
- val message by collectLastValue(underTest.message)
kosmos.fakeAuthenticationRepository.setAuthenticationMethod(
AuthenticationMethodModel.Password
)
runCurrent()
- underTest.resetMessage()
- assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PASSWORD)
-
// Wrong input.
assertThat(underTest.authenticate("alohamora".toList()))
.isEqualTo(AuthenticationResult.FAILED)
- assertThat(message).isEqualTo(MESSAGE_WRONG_PASSWORD)
-
- underTest.resetMessage()
- assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PASSWORD)
// Too short input.
assertThat(
@@ -201,7 +161,6 @@
)
)
.isEqualTo(AuthenticationResult.SKIPPED)
- assertThat(message).isEqualTo(MESSAGE_WRONG_PASSWORD)
// Correct input.
assertThat(underTest.authenticate("password".toList()))
@@ -211,13 +170,10 @@
@Test
fun patternAuthMethod() =
testScope.runTest {
- val message by collectLastValue(underTest.message)
kosmos.fakeAuthenticationRepository.setAuthenticationMethod(
AuthenticationMethodModel.Pattern
)
runCurrent()
- underTest.resetMessage()
- assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PATTERN)
// Wrong input.
val wrongPattern =
@@ -231,10 +187,6 @@
assertThat(wrongPattern.size)
.isAtLeast(kosmos.fakeAuthenticationRepository.minPatternLength)
assertThat(underTest.authenticate(wrongPattern)).isEqualTo(AuthenticationResult.FAILED)
- assertThat(message).isEqualTo(MESSAGE_WRONG_PATTERN)
-
- underTest.resetMessage()
- assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PATTERN)
// Too short input.
val tooShortPattern =
@@ -244,10 +196,6 @@
)
assertThat(underTest.authenticate(tooShortPattern))
.isEqualTo(AuthenticationResult.SKIPPED)
- assertThat(message).isEqualTo(MESSAGE_WRONG_PATTERN)
-
- underTest.resetMessage()
- assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PATTERN)
// Correct input.
assertThat(underTest.authenticate(FakeAuthenticationRepository.PATTERN))
@@ -258,7 +206,6 @@
fun lockoutStarted() =
testScope.runTest {
val lockoutStartedEvents by collectValues(underTest.onLockoutStarted)
- val message by collectLastValue(underTest.message)
kosmos.fakeAuthenticationRepository.setAuthenticationMethod(
AuthenticationMethodModel.Pin
@@ -272,17 +219,14 @@
.isEqualTo(AuthenticationResult.FAILED)
if (times < FakeAuthenticationRepository.MAX_FAILED_AUTH_TRIES_BEFORE_LOCKOUT - 1) {
assertThat(lockoutStartedEvents).isEmpty()
- assertThat(message).isNotEmpty()
}
}
assertThat(authenticationInteractor.lockoutEndTimestamp).isNotNull()
assertThat(lockoutStartedEvents.size).isEqualTo(1)
- assertThat(message).isNull()
// Advance the time to finish the lockout:
advanceTimeBy(FakeAuthenticationRepository.LOCKOUT_DURATION_SECONDS.seconds)
assertThat(authenticationInteractor.lockoutEndTimestamp).isNull()
- assertThat(message).isNull()
assertThat(lockoutStartedEvents.size).isEqualTo(1)
// Trigger lockout again:
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/BouncerMessageInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/BouncerMessageInteractorTest.kt
similarity index 83%
rename from packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/BouncerMessageInteractorTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/BouncerMessageInteractorTest.kt
index 701b703..c878e0b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/BouncerMessageInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/BouncerMessageInteractorTest.kt
@@ -17,7 +17,6 @@
package com.android.systemui.bouncer.domain.interactor
import android.content.pm.UserInfo
-import android.os.Handler
import android.testing.TestableLooper
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
@@ -28,27 +27,25 @@
import com.android.systemui.Flags
import com.android.systemui.SysuiTestCase
import com.android.systemui.biometrics.data.repository.FaceSensorInfo
-import com.android.systemui.biometrics.data.repository.FakeFacePropertyRepository
+import com.android.systemui.biometrics.data.repository.fakeFacePropertyRepository
+import com.android.systemui.biometrics.data.repository.fakeFingerprintPropertyRepository
import com.android.systemui.biometrics.shared.model.SensorStrength
import com.android.systemui.bouncer.data.repository.BouncerMessageRepositoryImpl
-import com.android.systemui.bouncer.data.repository.FakeKeyguardBouncerRepository
+import com.android.systemui.bouncer.data.repository.fakeKeyguardBouncerRepository
import com.android.systemui.bouncer.shared.model.BouncerMessageModel
-import com.android.systemui.bouncer.ui.BouncerView
-import com.android.systemui.classifier.FalsingCollector
import com.android.systemui.coroutines.collectLastValue
-import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFaceAuthInteractor
+import com.android.systemui.deviceentry.domain.interactor.deviceEntryFingerprintAuthInteractor
import com.android.systemui.flags.SystemPropertiesHelper
-import com.android.systemui.keyguard.DismissCallbackRegistry
-import com.android.systemui.keyguard.data.repository.FakeBiometricSettingsRepository
-import com.android.systemui.keyguard.data.repository.FakeDeviceEntryFaceAuthRepository
-import com.android.systemui.keyguard.data.repository.FakeDeviceEntryFingerprintAuthRepository
-import com.android.systemui.keyguard.data.repository.FakeTrustRepository
+import com.android.systemui.keyguard.data.repository.fakeBiometricSettingsRepository
+import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFaceAuthRepository
+import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFingerprintAuthRepository
+import com.android.systemui.keyguard.data.repository.fakeTrustRepository
import com.android.systemui.keyguard.shared.model.AuthenticationFlags
+import com.android.systemui.kosmos.testScope
import com.android.systemui.res.R.string.kg_too_many_failed_attempts_countdown
import com.android.systemui.res.R.string.kg_trust_agent_disabled
-import com.android.systemui.statusbar.policy.KeyguardStateController
-import com.android.systemui.user.data.repository.FakeUserRepository
-import com.android.systemui.user.domain.interactor.SelectedUserInteractor
+import com.android.systemui.testKosmos
+import com.android.systemui.user.data.repository.fakeUserRepository
import com.android.systemui.util.mockito.KotlinArgumentCaptor
import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
@@ -61,7 +58,6 @@
import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers.eq
import org.mockito.Mock
-import org.mockito.Mockito.mock
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
@@ -70,34 +66,22 @@
@TestableLooper.RunWithLooper(setAsMainLooper = true)
@RunWith(AndroidJUnit4::class)
class BouncerMessageInteractorTest : SysuiTestCase() {
-
+ private val kosmos = testKosmos()
private val countDownTimerCallback = KotlinArgumentCaptor(CountDownTimerCallback::class.java)
private val repository = BouncerMessageRepositoryImpl()
- private val userRepository = FakeUserRepository()
- private val fakeTrustRepository = FakeTrustRepository()
- private val fakeFacePropertyRepository = FakeFacePropertyRepository()
- private val bouncerRepository = FakeKeyguardBouncerRepository()
- private val fakeDeviceEntryFingerprintAuthRepository =
- FakeDeviceEntryFingerprintAuthRepository()
- private val fakeDeviceEntryFaceAuthRepository = FakeDeviceEntryFaceAuthRepository()
- private val biometricSettingsRepository: FakeBiometricSettingsRepository =
- FakeBiometricSettingsRepository()
+ private val biometricSettingsRepository = kosmos.fakeBiometricSettingsRepository
+ private val testScope = kosmos.testScope
@Mock private lateinit var updateMonitor: KeyguardUpdateMonitor
@Mock private lateinit var securityModel: KeyguardSecurityModel
@Mock private lateinit var countDownTimerUtil: CountDownTimerUtil
@Mock private lateinit var systemPropertiesHelper: SystemPropertiesHelper
- @Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor
- @Mock private lateinit var mSelectedUserInteractor: SelectedUserInteractor
- private lateinit var primaryBouncerInteractor: PrimaryBouncerInteractor
- private lateinit var testScope: TestScope
private lateinit var underTest: BouncerMessageInteractor
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
- userRepository.setUserInfos(listOf(PRIMARY_USER))
- testScope = TestScope()
+ kosmos.fakeUserRepository.setUserInfos(listOf(PRIMARY_USER))
allowTestableLooperAsMainThread()
whenever(securityModel.getSecurityMode(PRIMARY_USER_ID)).thenReturn(PIN)
biometricSettingsRepository.setIsFingerprintAuthCurrentlyAllowed(true)
@@ -105,44 +89,28 @@
}
suspend fun TestScope.init() {
- userRepository.setSelectedUserInfo(PRIMARY_USER)
+ kosmos.fakeUserRepository.setSelectedUserInfo(PRIMARY_USER)
mSetFlagsRule.enableFlags(Flags.FLAG_REVAMPED_BOUNCER_MESSAGES)
- primaryBouncerInteractor =
- PrimaryBouncerInteractor(
- bouncerRepository,
- mock(BouncerView::class.java),
- mock(Handler::class.java),
- mock(KeyguardStateController::class.java),
- mock(KeyguardSecurityModel::class.java),
- mock(PrimaryBouncerCallbackInteractor::class.java),
- mock(FalsingCollector::class.java),
- mock(DismissCallbackRegistry::class.java),
- context,
- keyguardUpdateMonitor,
- fakeTrustRepository,
- testScope.backgroundScope,
- mSelectedUserInteractor,
- mock(DeviceEntryFaceAuthInteractor::class.java),
- )
underTest =
BouncerMessageInteractor(
repository = repository,
- userRepository = userRepository,
+ userRepository = kosmos.fakeUserRepository,
countDownTimerUtil = countDownTimerUtil,
updateMonitor = updateMonitor,
biometricSettingsRepository = biometricSettingsRepository,
- applicationScope = this.backgroundScope,
- trustRepository = fakeTrustRepository,
+ applicationScope = testScope.backgroundScope,
+ trustRepository = kosmos.fakeTrustRepository,
systemPropertiesHelper = systemPropertiesHelper,
- primaryBouncerInteractor = primaryBouncerInteractor,
- facePropertyRepository = fakeFacePropertyRepository,
- deviceEntryFingerprintAuthRepository = fakeDeviceEntryFingerprintAuthRepository,
- faceAuthRepository = fakeDeviceEntryFaceAuthRepository,
+ primaryBouncerInteractor = kosmos.primaryBouncerInteractor,
+ facePropertyRepository = kosmos.fakeFacePropertyRepository,
+ deviceEntryFingerprintAuthInteractor = kosmos.deviceEntryFingerprintAuthInteractor,
+ faceAuthRepository = kosmos.fakeDeviceEntryFaceAuthRepository,
securityModel = securityModel
)
biometricSettingsRepository.setIsFingerprintAuthCurrentlyAllowed(true)
- fakeDeviceEntryFingerprintAuthRepository.setLockedOut(false)
- bouncerRepository.setPrimaryShow(true)
+ kosmos.fakeDeviceEntryFingerprintAuthRepository.setLockedOut(false)
+ kosmos.fakeFingerprintPropertyRepository.supportsSideFps()
+ kosmos.fakeKeyguardBouncerRepository.setPrimaryShow(true)
runCurrent()
}
@@ -268,7 +236,7 @@
init()
val lockoutMessage by collectLastValue(underTest.bouncerMessage)
- fakeDeviceEntryFaceAuthRepository.setLockedOut(true)
+ kosmos.fakeDeviceEntryFaceAuthRepository.setLockedOut(true)
runCurrent()
assertThat(primaryResMessage(lockoutMessage))
@@ -276,7 +244,7 @@
assertThat(secondaryResMessage(lockoutMessage))
.isEqualTo("Can’t unlock with face. Too many attempts.")
- fakeDeviceEntryFaceAuthRepository.setLockedOut(false)
+ kosmos.fakeDeviceEntryFaceAuthRepository.setLockedOut(false)
runCurrent()
assertThat(primaryResMessage(lockoutMessage))
@@ -289,15 +257,17 @@
testScope.runTest {
init()
val lockoutMessage by collectLastValue(underTest.bouncerMessage)
- fakeFacePropertyRepository.setSensorInfo(FaceSensorInfo(1, SensorStrength.STRONG))
- fakeDeviceEntryFaceAuthRepository.setLockedOut(true)
+ kosmos.fakeFacePropertyRepository.setSensorInfo(
+ FaceSensorInfo(1, SensorStrength.STRONG)
+ )
+ kosmos.fakeDeviceEntryFaceAuthRepository.setLockedOut(true)
runCurrent()
assertThat(primaryResMessage(lockoutMessage)).isEqualTo("Enter PIN")
assertThat(secondaryResMessage(lockoutMessage))
.isEqualTo("PIN is required after too many attempts")
- fakeDeviceEntryFaceAuthRepository.setLockedOut(false)
+ kosmos.fakeDeviceEntryFaceAuthRepository.setLockedOut(false)
runCurrent()
assertThat(primaryResMessage(lockoutMessage))
@@ -311,14 +281,14 @@
init()
val lockedOutMessage by collectLastValue(underTest.bouncerMessage)
- fakeDeviceEntryFingerprintAuthRepository.setLockedOut(true)
+ kosmos.fakeDeviceEntryFingerprintAuthRepository.setLockedOut(true)
runCurrent()
assertThat(primaryResMessage(lockedOutMessage)).isEqualTo("Enter PIN")
assertThat(secondaryResMessage(lockedOutMessage))
.isEqualTo("PIN is required after too many attempts")
- fakeDeviceEntryFingerprintAuthRepository.setLockedOut(false)
+ kosmos.fakeDeviceEntryFingerprintAuthRepository.setLockedOut(false)
runCurrent()
assertThat(primaryResMessage(lockedOutMessage))
@@ -327,6 +297,19 @@
}
@Test
+ fun onUdfpsFingerprint_DoesNotShowFingerprintMessage() =
+ testScope.runTest {
+ init()
+ kosmos.fakeDeviceEntryFingerprintAuthRepository.setLockedOut(false)
+ kosmos.fakeFingerprintPropertyRepository.supportsUdfps()
+ val lockedOutMessage by collectLastValue(underTest.bouncerMessage)
+
+ runCurrent()
+
+ assertThat(primaryResMessage(lockedOutMessage)).isEqualTo("Enter PIN")
+ }
+
+ @Test
fun onRestartForMainlineUpdate_shouldProvideRelevantMessage() =
testScope.runTest {
init()
@@ -344,9 +327,10 @@
fun onAuthFlagsChanged_withTrustNotManagedAndNoBiometrics_isANoop() =
testScope.runTest {
init()
- fakeTrustRepository.setTrustUsuallyManaged(false)
+ kosmos.fakeTrustRepository.setTrustUsuallyManaged(false)
biometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(false)
biometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(false)
+ runCurrent()
val defaultMessage = Pair("Enter PIN", null)
@@ -377,12 +361,13 @@
testScope.runTest {
init()
- userRepository.setSelectedUserInfo(PRIMARY_USER)
+ kosmos.fakeUserRepository.setSelectedUserInfo(PRIMARY_USER)
biometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(false)
biometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(false)
+ runCurrent()
- fakeTrustRepository.setCurrentUserTrustManaged(true)
- fakeTrustRepository.setTrustUsuallyManaged(true)
+ kosmos.fakeTrustRepository.setCurrentUserTrustManaged(true)
+ kosmos.fakeTrustRepository.setTrustUsuallyManaged(true)
val defaultMessage = Pair("Enter PIN", null)
@@ -415,8 +400,8 @@
fun authFlagsChanges_withFaceEnrolled_providesDifferentMessages() =
testScope.runTest {
init()
- userRepository.setSelectedUserInfo(PRIMARY_USER)
- fakeTrustRepository.setTrustUsuallyManaged(false)
+ kosmos.fakeUserRepository.setSelectedUserInfo(PRIMARY_USER)
+ kosmos.fakeTrustRepository.setTrustUsuallyManaged(false)
biometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(false)
biometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(true)
@@ -453,12 +438,13 @@
fun authFlagsChanges_withFingerprintEnrolled_providesDifferentMessages() =
testScope.runTest {
init()
- userRepository.setSelectedUserInfo(PRIMARY_USER)
- fakeTrustRepository.setCurrentUserTrustManaged(false)
+ kosmos.fakeUserRepository.setSelectedUserInfo(PRIMARY_USER)
+ kosmos.fakeTrustRepository.setCurrentUserTrustManaged(false)
biometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(false)
biometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(true)
biometricSettingsRepository.setIsFingerprintAuthCurrentlyAllowed(true)
+ runCurrent()
verifyMessagesForAuthFlag(
LockPatternUtils.StrongAuthTracker.STRONG_AUTH_NOT_REQUIRED to
@@ -466,6 +452,7 @@
)
biometricSettingsRepository.setIsFingerprintAuthCurrentlyAllowed(false)
+ runCurrent()
verifyMessagesForAuthFlag(
LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_USER_REQUEST to
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModelTest.kt
index d30e333..c9fa671 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModelTest.kt
@@ -48,6 +48,7 @@
isInputEnabled = MutableStateFlow(true),
simBouncerInteractor = kosmos.simBouncerInteractor,
authenticationMethod = AuthenticationMethodModel.Pin,
+ onIntentionalUserInput = {},
)
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerMessageViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerMessageViewModelTest.kt
new file mode 100644
index 0000000..16ec9aa
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerMessageViewModelTest.kt
@@ -0,0 +1,455 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS 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.bouncer.ui.viewmodel
+
+import android.content.pm.UserInfo
+import android.hardware.biometrics.BiometricFaceConstants
+import android.hardware.fingerprint.FingerprintManager
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.internal.widget.LockPatternUtils
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.authentication.data.repository.FakeAuthenticationRepository
+import com.android.systemui.authentication.data.repository.fakeAuthenticationRepository
+import com.android.systemui.authentication.domain.interactor.authenticationInteractor
+import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
+import com.android.systemui.authentication.shared.model.AuthenticationMethodModel.Pattern
+import com.android.systemui.authentication.shared.model.AuthenticationMethodModel.Pin
+import com.android.systemui.biometrics.data.repository.FaceSensorInfo
+import com.android.systemui.biometrics.data.repository.fakeFacePropertyRepository
+import com.android.systemui.biometrics.data.repository.fakeFingerprintPropertyRepository
+import com.android.systemui.biometrics.shared.model.SensorStrength
+import com.android.systemui.bouncer.domain.interactor.bouncerInteractor
+import com.android.systemui.bouncer.shared.flag.fakeComposeBouncerFlags
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor
+import com.android.systemui.deviceentry.shared.model.ErrorFaceAuthenticationStatus
+import com.android.systemui.deviceentry.shared.model.FailedFaceAuthenticationStatus
+import com.android.systemui.deviceentry.shared.model.HelpFaceAuthenticationStatus
+import com.android.systemui.flags.fakeSystemPropertiesHelper
+import com.android.systemui.keyguard.data.repository.deviceEntryFingerprintAuthRepository
+import com.android.systemui.keyguard.data.repository.fakeBiometricSettingsRepository
+import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFaceAuthRepository
+import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFingerprintAuthRepository
+import com.android.systemui.keyguard.data.repository.fakeTrustRepository
+import com.android.systemui.keyguard.shared.model.AuthenticationFlags
+import com.android.systemui.keyguard.shared.model.ErrorFingerprintAuthenticationStatus
+import com.android.systemui.keyguard.shared.model.FailFingerprintAuthenticationStatus
+import com.android.systemui.keyguard.shared.model.HelpFingerprintAuthenticationStatus
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.res.R
+import com.android.systemui.testKosmos
+import com.android.systemui.user.data.repository.fakeUserRepository
+import com.google.common.truth.Truth.assertThat
+import kotlin.time.Duration.Companion.seconds
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.advanceTimeBy
+import kotlinx.coroutines.test.currentTime
+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 BouncerMessageViewModelTest : SysuiTestCase() {
+ private val kosmos = testKosmos()
+ private val testScope = kosmos.testScope
+ private val authenticationInteractor by lazy { kosmos.authenticationInteractor }
+ private val bouncerInteractor by lazy { kosmos.bouncerInteractor }
+ private lateinit var underTest: BouncerMessageViewModel
+
+ @Before
+ fun setUp() {
+ kosmos.fakeUserRepository.setUserInfos(listOf(PRIMARY_USER))
+ kosmos.fakeComposeBouncerFlags.composeBouncerEnabled = true
+ underTest = kosmos.bouncerMessageViewModel
+ overrideResource(R.string.kg_trust_agent_disabled, "Trust agent is unavailable")
+ kosmos.fakeSystemPropertiesHelper.set(
+ DeviceEntryInteractor.SYS_BOOT_REASON_PROP,
+ "not mainline reboot"
+ )
+ }
+
+ @Test
+ fun message_defaultMessage_basedOnAuthMethod() =
+ testScope.runTest {
+ val message by collectLastValue(underTest.message)
+
+ kosmos.fakeAuthenticationRepository.setAuthenticationMethod(Pin)
+ kosmos.fakeFingerprintPropertyRepository.supportsSideFps()
+ kosmos.fakeBiometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(true)
+ kosmos.fakeBiometricSettingsRepository.setIsFingerprintAuthCurrentlyAllowed(true)
+ runCurrent()
+
+ assertThat(message!!.text).isEqualTo("Unlock with PIN or fingerprint")
+
+ kosmos.fakeAuthenticationRepository.setAuthenticationMethod(Pattern)
+ runCurrent()
+ assertThat(message!!.text).isEqualTo("Unlock with pattern or fingerprint")
+
+ kosmos.fakeAuthenticationRepository.setAuthenticationMethod(
+ AuthenticationMethodModel.Password
+ )
+ runCurrent()
+ assertThat(message!!.text).isEqualTo("Unlock with password or fingerprint")
+ }
+
+ @Test
+ fun message() =
+ testScope.runTest {
+ val message by collectLastValue(underTest.message)
+ kosmos.fakeAuthenticationRepository.setAuthenticationMethod(Pin)
+ assertThat(message?.isUpdateAnimated).isTrue()
+
+ repeat(FakeAuthenticationRepository.MAX_FAILED_AUTH_TRIES_BEFORE_LOCKOUT) {
+ bouncerInteractor.authenticate(WRONG_PIN)
+ }
+ assertThat(message?.isUpdateAnimated).isFalse()
+
+ val lockoutEndMs = authenticationInteractor.lockoutEndTimestamp ?: 0
+ advanceTimeBy(lockoutEndMs - testScope.currentTime)
+ assertThat(message?.isUpdateAnimated).isTrue()
+ }
+
+ @Test
+ fun lockoutMessage() =
+ testScope.runTest {
+ val message by collectLastValue(underTest.message)
+ kosmos.fakeAuthenticationRepository.setAuthenticationMethod(Pin)
+ assertThat(kosmos.fakeAuthenticationRepository.lockoutEndTimestamp).isNull()
+ runCurrent()
+
+ repeat(FakeAuthenticationRepository.MAX_FAILED_AUTH_TRIES_BEFORE_LOCKOUT) { times ->
+ bouncerInteractor.authenticate(WRONG_PIN)
+ runCurrent()
+ if (times < FakeAuthenticationRepository.MAX_FAILED_AUTH_TRIES_BEFORE_LOCKOUT - 1) {
+ assertThat(message?.text).isEqualTo("Wrong PIN. Try again.")
+ assertThat(message?.isUpdateAnimated).isTrue()
+ }
+ }
+ val lockoutSeconds = FakeAuthenticationRepository.LOCKOUT_DURATION_SECONDS
+ assertTryAgainMessage(message?.text, lockoutSeconds)
+ assertThat(message?.isUpdateAnimated).isFalse()
+
+ repeat(FakeAuthenticationRepository.LOCKOUT_DURATION_SECONDS) { time ->
+ advanceTimeBy(1.seconds)
+ val remainingSeconds = lockoutSeconds - time - 1
+ if (remainingSeconds > 0) {
+ assertTryAgainMessage(message?.text, remainingSeconds)
+ }
+ }
+ assertThat(message?.text).isEqualTo("Enter PIN")
+ assertThat(message?.isUpdateAnimated).isTrue()
+ }
+
+ @Test
+ fun defaultMessage_mapsToDeviceEntryRestrictionReason_whenTrustAgentIsEnabled() =
+ testScope.runTest {
+ kosmos.fakeUserRepository.setSelectedUserInfo(PRIMARY_USER)
+ kosmos.fakeBiometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(false)
+ kosmos.fakeBiometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(false)
+ kosmos.fakeTrustRepository.setTrustUsuallyManaged(true)
+ kosmos.fakeTrustRepository.setCurrentUserTrustManaged(false)
+ runCurrent()
+
+ val defaultMessage = Pair("Enter PIN", null)
+
+ verifyMessagesForAuthFlags(
+ LockPatternUtils.StrongAuthTracker.STRONG_AUTH_NOT_REQUIRED to defaultMessage,
+ LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_BOOT to
+ Pair("Enter PIN", "PIN is required after device restarts"),
+ LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_TIMEOUT to
+ Pair("Enter PIN", "Added security required. PIN not used for a while."),
+ LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_DPM_LOCK_NOW to
+ Pair("Enter PIN", "For added security, device was locked by work policy"),
+ LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_USER_REQUEST to
+ Pair("Enter PIN", "Trust agent is unavailable"),
+ LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_TRUSTAGENT_EXPIRED to
+ Pair("Enter PIN", "Trust agent is unavailable"),
+ LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN to
+ Pair("Enter PIN", "PIN is required after lockdown"),
+ LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_FOR_UNATTENDED_UPDATE to
+ Pair("Enter PIN", "PIN required for additional security"),
+ LockPatternUtils.StrongAuthTracker
+ .STRONG_AUTH_REQUIRED_AFTER_NON_STRONG_BIOMETRICS_TIMEOUT to
+ Pair(
+ "Enter PIN",
+ "Added security required. Device wasn’t unlocked for a while."
+ ),
+ )
+ }
+
+ @Test
+ fun defaultMessage_mapsToDeviceEntryRestrictionReason_whenFingerprintIsAvailable() =
+ testScope.runTest {
+ kosmos.fakeUserRepository.setSelectedUserInfo(PRIMARY_USER)
+ kosmos.fakeFingerprintPropertyRepository.supportsSideFps()
+ kosmos.fakeBiometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(true)
+ kosmos.fakeBiometricSettingsRepository.setIsFingerprintAuthCurrentlyAllowed(true)
+ kosmos.fakeTrustRepository.setCurrentUserTrustManaged(false)
+ kosmos.fakeBiometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(false)
+ runCurrent()
+
+ verifyMessagesForAuthFlags(
+ LockPatternUtils.StrongAuthTracker.STRONG_AUTH_NOT_REQUIRED to
+ Pair("Unlock with PIN or fingerprint", null),
+ LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_USER_REQUEST to
+ Pair("Unlock with PIN or fingerprint", null),
+ LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_TRUSTAGENT_EXPIRED to
+ Pair("Unlock with PIN or fingerprint", null),
+ LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_BOOT to
+ Pair("Enter PIN", "PIN is required after device restarts"),
+ LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_TIMEOUT to
+ Pair("Enter PIN", "Added security required. PIN not used for a while."),
+ LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_DPM_LOCK_NOW to
+ Pair("Enter PIN", "For added security, device was locked by work policy"),
+ LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN to
+ Pair("Enter PIN", "PIN is required after lockdown"),
+ LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_FOR_UNATTENDED_UPDATE to
+ Pair("Enter PIN", "PIN required for additional security"),
+ LockPatternUtils.StrongAuthTracker
+ .STRONG_AUTH_REQUIRED_AFTER_NON_STRONG_BIOMETRICS_TIMEOUT to
+ Pair(
+ "Unlock with PIN or fingerprint",
+ "Added security required. Device wasn’t unlocked for a while."
+ ),
+ )
+ }
+
+ @Test
+ fun onFingerprintLockout_messageUpdated() =
+ testScope.runTest {
+ kosmos.fakeUserRepository.setSelectedUserInfo(PRIMARY_USER)
+ kosmos.fakeFingerprintPropertyRepository.supportsSideFps()
+ kosmos.fakeBiometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(true)
+ kosmos.fakeBiometricSettingsRepository.setIsFingerprintAuthCurrentlyAllowed(true)
+
+ val lockedOutMessage by collectLastValue(underTest.message)
+
+ kosmos.fakeDeviceEntryFingerprintAuthRepository.setLockedOut(true)
+ runCurrent()
+
+ assertThat(lockedOutMessage?.text).isEqualTo("Enter PIN")
+ assertThat(lockedOutMessage?.secondaryText)
+ .isEqualTo("PIN is required after too many attempts")
+
+ kosmos.fakeDeviceEntryFingerprintAuthRepository.setLockedOut(false)
+ runCurrent()
+
+ assertThat(lockedOutMessage?.text).isEqualTo("Unlock with PIN or fingerprint")
+ assertThat(lockedOutMessage?.secondaryText.isNullOrBlank()).isTrue()
+ }
+
+ @Test
+ fun onUdfpsFingerprint_DoesNotShowFingerprintMessage() =
+ testScope.runTest {
+ kosmos.fakeUserRepository.setSelectedUserInfo(PRIMARY_USER)
+ kosmos.fakeFingerprintPropertyRepository.supportsUdfps()
+ kosmos.fakeBiometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(true)
+ kosmos.fakeBiometricSettingsRepository.setIsFingerprintAuthCurrentlyAllowed(true)
+ kosmos.fakeDeviceEntryFingerprintAuthRepository.setLockedOut(false)
+ val message by collectLastValue(underTest.message)
+
+ runCurrent()
+
+ assertThat(message?.text).isEqualTo("Enter PIN")
+ }
+
+ @Test
+ fun onRestartForMainlineUpdate_shouldProvideRelevantMessage() =
+ testScope.runTest {
+ kosmos.fakeUserRepository.setSelectedUserInfo(PRIMARY_USER)
+ kosmos.fakeSystemPropertiesHelper.set("sys.boot.reason.last", "reboot,mainline_update")
+ kosmos.fakeBiometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(true)
+ runCurrent()
+
+ verifyMessagesForAuthFlags(
+ LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_BOOT to
+ Pair("Enter PIN", "Device updated. Enter PIN to continue.")
+ )
+ }
+
+ @Test
+ fun onFaceLockout_whenItIsClass3_shouldProvideRelevantMessage() =
+ testScope.runTest {
+ kosmos.fakeUserRepository.setSelectedUserInfo(PRIMARY_USER)
+ kosmos.fakeBiometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(true)
+ val lockoutMessage by collectLastValue(underTest.message)
+ kosmos.fakeFacePropertyRepository.setSensorInfo(
+ FaceSensorInfo(1, SensorStrength.STRONG)
+ )
+ kosmos.fakeDeviceEntryFaceAuthRepository.setLockedOut(true)
+ runCurrent()
+
+ assertThat(lockoutMessage?.text).isEqualTo("Enter PIN")
+ assertThat(lockoutMessage?.secondaryText)
+ .isEqualTo("PIN is required after too many attempts")
+
+ kosmos.fakeDeviceEntryFaceAuthRepository.setLockedOut(false)
+ runCurrent()
+
+ assertThat(lockoutMessage?.text).isEqualTo("Enter PIN")
+ assertThat(lockoutMessage?.secondaryText.isNullOrBlank()).isTrue()
+ }
+
+ @Test
+ fun onFaceLockout_whenItIsNotStrong_shouldProvideRelevantMessage() =
+ testScope.runTest {
+ kosmos.fakeUserRepository.setSelectedUserInfo(PRIMARY_USER)
+ kosmos.fakeBiometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(true)
+ val lockoutMessage by collectLastValue(underTest.message)
+ kosmos.fakeFacePropertyRepository.setSensorInfo(FaceSensorInfo(1, SensorStrength.WEAK))
+ kosmos.fakeDeviceEntryFaceAuthRepository.setLockedOut(true)
+ runCurrent()
+
+ assertThat(lockoutMessage?.text).isEqualTo("Enter PIN")
+ assertThat(lockoutMessage?.secondaryText)
+ .isEqualTo("Can’t unlock with face. Too many attempts.")
+
+ kosmos.fakeDeviceEntryFaceAuthRepository.setLockedOut(false)
+ runCurrent()
+
+ assertThat(lockoutMessage?.text).isEqualTo("Enter PIN")
+ assertThat(lockoutMessage?.secondaryText.isNullOrBlank()).isTrue()
+ }
+
+ @Test
+ fun setFingerprintMessage_propagateValue() =
+ testScope.runTest {
+ val bouncerMessage by collectLastValue(underTest.message)
+
+ kosmos.fakeUserRepository.setSelectedUserInfo(PRIMARY_USER)
+ kosmos.fakeBiometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(true)
+ kosmos.fakeBiometricSettingsRepository.setIsFingerprintAuthCurrentlyAllowed(true)
+ kosmos.fakeFingerprintPropertyRepository.supportsSideFps()
+ runCurrent()
+
+ kosmos.deviceEntryFingerprintAuthRepository.setAuthenticationStatus(
+ HelpFingerprintAuthenticationStatus(1, "some helpful message")
+ )
+ runCurrent()
+ assertThat(bouncerMessage?.text).isEqualTo("Unlock with PIN or fingerprint")
+ assertThat(bouncerMessage?.secondaryText).isEqualTo("some helpful message")
+
+ kosmos.deviceEntryFingerprintAuthRepository.setAuthenticationStatus(
+ FailFingerprintAuthenticationStatus
+ )
+ runCurrent()
+ assertThat(bouncerMessage?.text).isEqualTo("Fingerprint not recognized")
+ assertThat(bouncerMessage?.secondaryText).isEqualTo("Try again or enter PIN")
+
+ kosmos.deviceEntryFingerprintAuthRepository.setAuthenticationStatus(
+ ErrorFingerprintAuthenticationStatus(
+ FingerprintManager.FINGERPRINT_ERROR_LOCKOUT,
+ "locked out"
+ )
+ )
+ runCurrent()
+ assertThat(bouncerMessage?.text).isEqualTo("Enter PIN")
+ assertThat(bouncerMessage?.secondaryText)
+ .isEqualTo("PIN is required after too many attempts")
+ }
+
+ @Test
+ fun setFaceMessage_propagateValue() =
+ testScope.runTest {
+ val bouncerMessage by collectLastValue(underTest.message)
+
+ kosmos.fakeUserRepository.setSelectedUserInfo(PRIMARY_USER)
+ kosmos.fakeBiometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(true)
+ kosmos.fakeBiometricSettingsRepository.setIsFaceAuthCurrentlyAllowed(true)
+ runCurrent()
+
+ kosmos.fakeDeviceEntryFaceAuthRepository.setAuthenticationStatus(
+ HelpFaceAuthenticationStatus(1, "some helpful message")
+ )
+ runCurrent()
+ assertThat(bouncerMessage?.text).isEqualTo("Enter PIN")
+ assertThat(bouncerMessage?.secondaryText).isEqualTo("some helpful message")
+
+ kosmos.fakeDeviceEntryFaceAuthRepository.setAuthenticationStatus(
+ ErrorFaceAuthenticationStatus(
+ BiometricFaceConstants.FACE_ERROR_TIMEOUT,
+ "Try again"
+ )
+ )
+ runCurrent()
+ assertThat(bouncerMessage?.text).isEqualTo("Enter PIN")
+ assertThat(bouncerMessage?.secondaryText).isEqualTo("Try again")
+
+ kosmos.fakeDeviceEntryFaceAuthRepository.setAuthenticationStatus(
+ FailedFaceAuthenticationStatus()
+ )
+ runCurrent()
+ assertThat(bouncerMessage?.text).isEqualTo("Face not recognized")
+ assertThat(bouncerMessage?.secondaryText).isEqualTo("Try again or enter PIN")
+
+ kosmos.fakeDeviceEntryFaceAuthRepository.setAuthenticationStatus(
+ ErrorFaceAuthenticationStatus(
+ BiometricFaceConstants.FACE_ERROR_LOCKOUT,
+ "locked out"
+ )
+ )
+ runCurrent()
+ assertThat(bouncerMessage?.text).isEqualTo("Enter PIN")
+ assertThat(bouncerMessage?.secondaryText)
+ .isEqualTo("Can’t unlock with face. Too many attempts.")
+ }
+
+ private fun TestScope.verifyMessagesForAuthFlags(
+ vararg authFlagToMessagePair: Pair<Int, Pair<String, String?>>
+ ) {
+ val actualMessage by collectLastValue(underTest.message)
+
+ authFlagToMessagePair.forEach { (flag, expectedMessagePair) ->
+ kosmos.fakeBiometricSettingsRepository.setAuthenticationFlags(
+ AuthenticationFlags(userId = PRIMARY_USER_ID, flag = flag)
+ )
+ runCurrent()
+
+ assertThat(actualMessage?.text).isEqualTo(expectedMessagePair.first)
+
+ if (expectedMessagePair.second == null) {
+ assertThat(actualMessage?.secondaryText.isNullOrBlank()).isTrue()
+ } else {
+ assertThat(actualMessage?.secondaryText).isEqualTo(expectedMessagePair.second)
+ }
+ }
+ }
+
+ private fun assertTryAgainMessage(
+ message: String?,
+ time: Int,
+ ) {
+ assertThat(message).contains("Try again in $time second")
+ }
+
+ companion object {
+ private val WRONG_PIN = FakeAuthenticationRepository.DEFAULT_PIN.map { it + 1 }
+ private const val PRIMARY_USER_ID = 0
+ private val PRIMARY_USER =
+ UserInfo(
+ /* id= */ PRIMARY_USER_ID,
+ /* name= */ "primary user",
+ /* flags= */ UserInfo.FLAG_PRIMARY
+ )
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelTest.kt
index 73db175..3afca96 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelTest.kt
@@ -37,7 +37,6 @@
import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
import com.google.common.truth.Truth.assertWithMessage
-import kotlin.time.Duration.Companion.seconds
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.flatMapLatest
@@ -142,54 +141,6 @@
}
@Test
- fun message() =
- testScope.runTest {
- val message by collectLastValue(underTest.message)
- kosmos.fakeAuthenticationRepository.setAuthenticationMethod(Pin)
- assertThat(message?.isUpdateAnimated).isTrue()
-
- repeat(FakeAuthenticationRepository.MAX_FAILED_AUTH_TRIES_BEFORE_LOCKOUT) {
- bouncerInteractor.authenticate(WRONG_PIN)
- }
- assertThat(message?.isUpdateAnimated).isFalse()
-
- val lockoutEndMs = authenticationInteractor.lockoutEndTimestamp ?: 0
- advanceTimeBy(lockoutEndMs - testScope.currentTime)
- assertThat(message?.isUpdateAnimated).isTrue()
- }
-
- @Test
- fun lockoutMessage() =
- testScope.runTest {
- val authMethodViewModel by collectLastValue(underTest.authMethodViewModel)
- val message by collectLastValue(underTest.message)
- kosmos.fakeAuthenticationRepository.setAuthenticationMethod(Pin)
- assertThat(kosmos.fakeAuthenticationRepository.lockoutEndTimestamp).isNull()
- assertThat(authMethodViewModel?.lockoutMessageId).isNotNull()
-
- repeat(FakeAuthenticationRepository.MAX_FAILED_AUTH_TRIES_BEFORE_LOCKOUT) { times ->
- bouncerInteractor.authenticate(WRONG_PIN)
- if (times < FakeAuthenticationRepository.MAX_FAILED_AUTH_TRIES_BEFORE_LOCKOUT - 1) {
- assertThat(message?.text).isEqualTo(bouncerInteractor.message.value)
- assertThat(message?.isUpdateAnimated).isTrue()
- }
- }
- val lockoutSeconds = FakeAuthenticationRepository.LOCKOUT_DURATION_SECONDS
- assertTryAgainMessage(message?.text, lockoutSeconds)
- assertThat(message?.isUpdateAnimated).isFalse()
-
- repeat(FakeAuthenticationRepository.LOCKOUT_DURATION_SECONDS) { time ->
- advanceTimeBy(1.seconds)
- val remainingSeconds = lockoutSeconds - time - 1
- if (remainingSeconds > 0) {
- assertTryAgainMessage(message?.text, remainingSeconds)
- }
- }
- assertThat(message?.text).isEmpty()
- assertThat(message?.isUpdateAnimated).isTrue()
- }
-
- @Test
fun isInputEnabled() =
testScope.runTest {
val isInputEnabled by
@@ -212,25 +163,6 @@
}
@Test
- fun dialogViewModel() =
- testScope.runTest {
- val authMethodViewModel by collectLastValue(underTest.authMethodViewModel)
- val dialogViewModel by collectLastValue(underTest.dialogViewModel)
- kosmos.fakeAuthenticationRepository.setAuthenticationMethod(Pin)
- assertThat(authMethodViewModel?.lockoutMessageId).isNotNull()
-
- repeat(FakeAuthenticationRepository.MAX_FAILED_AUTH_TRIES_BEFORE_LOCKOUT) {
- assertThat(dialogViewModel).isNull()
- bouncerInteractor.authenticate(WRONG_PIN)
- }
- assertThat(dialogViewModel).isNotNull()
- assertThat(dialogViewModel?.text).isNotEmpty()
-
- dialogViewModel?.onDismiss?.invoke()
- assertThat(dialogViewModel).isNull()
- }
-
- @Test
fun isSideBySideSupported() =
testScope.runTest {
val isSideBySideSupported by collectLastValue(underTest.isSideBySideSupported)
@@ -265,13 +197,6 @@
return listOf(None, Pin, Password, Pattern, Sim)
}
- private fun assertTryAgainMessage(
- message: String?,
- time: Int,
- ) {
- assertThat(message).isEqualTo("Try again in $time seconds.")
- }
-
companion object {
private val WRONG_PIN = FakeAuthenticationRepository.DEFAULT_PIN.map { it + 1 }
}
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 df50eb6..71c5785 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
@@ -66,7 +66,6 @@
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 =
@@ -76,6 +75,7 @@
interactor = bouncerInteractor,
inputMethodInteractor = inputMethodInteractor,
selectedUserInteractor = selectedUserInteractor,
+ onIntentionalUserInput = {},
)
@Before
@@ -88,11 +88,9 @@
fun onShown() =
testScope.runTest {
val currentScene by collectLastValue(sceneInteractor.currentScene)
- val message by collectLastValue(bouncerViewModel.message)
val password by collectLastValue(underTest.password)
lockDeviceAndOpenPasswordBouncer()
- assertThat(message?.text).isEqualTo(ENTER_YOUR_PASSWORD)
assertThat(password).isEmpty()
assertThat(currentScene).isEqualTo(Scenes.Bouncer)
assertThat(underTest.authenticationMethod).isEqualTo(AuthenticationMethodModel.Password)
@@ -101,16 +99,13 @@
@Test
fun onHidden_resetsPasswordInputAndMessage() =
testScope.runTest {
- val message by collectLastValue(bouncerViewModel.message)
val password by collectLastValue(underTest.password)
lockDeviceAndOpenPasswordBouncer()
underTest.onPasswordInputChanged("password")
- assertThat(message?.text).isNotEqualTo(ENTER_YOUR_PASSWORD)
assertThat(password).isNotEmpty()
underTest.onHidden()
- assertThat(message?.text).isEqualTo(ENTER_YOUR_PASSWORD)
assertThat(password).isEmpty()
}
@@ -118,13 +113,11 @@
fun onPasswordInputChanged() =
testScope.runTest {
val currentScene by collectLastValue(sceneInteractor.currentScene)
- val message by collectLastValue(bouncerViewModel.message)
val password by collectLastValue(underTest.password)
lockDeviceAndOpenPasswordBouncer()
underTest.onPasswordInputChanged("password")
- assertThat(message?.text).isEmpty()
assertThat(password).isEqualTo("password")
assertThat(currentScene).isEqualTo(Scenes.Bouncer)
}
@@ -144,7 +137,6 @@
@Test
fun onAuthenticateKeyPressed_whenWrong() =
testScope.runTest {
- val message by collectLastValue(bouncerViewModel.message)
val password by collectLastValue(underTest.password)
lockDeviceAndOpenPasswordBouncer()
@@ -152,13 +144,11 @@
underTest.onAuthenticateKeyPressed()
assertThat(password).isEmpty()
- assertThat(message?.text).isEqualTo(WRONG_PASSWORD)
}
@Test
fun onAuthenticateKeyPressed_whenEmpty() =
testScope.runTest {
- val message by collectLastValue(bouncerViewModel.message)
val password by collectLastValue(underTest.password)
kosmos.fakeAuthenticationRepository.setAuthenticationMethod(
AuthenticationMethodModel.Password
@@ -171,14 +161,12 @@
underTest.onAuthenticateKeyPressed()
assertThat(password).isEmpty()
- assertThat(message?.text).isEqualTo(ENTER_YOUR_PASSWORD)
}
@Test
fun onAuthenticateKeyPressed_correctAfterWrong() =
testScope.runTest {
val authResult by collectLastValue(authenticationInteractor.onAuthenticationResult)
- val message by collectLastValue(bouncerViewModel.message)
val password by collectLastValue(underTest.password)
lockDeviceAndOpenPasswordBouncer()
@@ -186,12 +174,10 @@
underTest.onPasswordInputChanged("wrong")
underTest.onAuthenticateKeyPressed()
assertThat(password).isEqualTo("")
- assertThat(message?.text).isEqualTo(WRONG_PASSWORD)
assertThat(authResult).isFalse()
// Enter the correct password:
underTest.onPasswordInputChanged("password")
- assertThat(message?.text).isEmpty()
underTest.onAuthenticateKeyPressed()
@@ -331,10 +317,8 @@
private fun TestScope.switchToScene(toScene: SceneKey) {
val currentScene by collectLastValue(sceneInteractor.currentScene)
- val bouncerShown = currentScene != Scenes.Bouncer && toScene == Scenes.Bouncer
val bouncerHidden = currentScene == Scenes.Bouncer && toScene != Scenes.Bouncer
sceneInteractor.changeScene(toScene, "reason")
- if (bouncerShown) underTest.onShown()
if (bouncerHidden) underTest.onHidden()
runCurrent()
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt
index 91a056d..51b73ee9 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt
@@ -63,6 +63,7 @@
viewModelScope = testScope.backgroundScope,
interactor = bouncerInteractor,
isInputEnabled = MutableStateFlow(true).asStateFlow(),
+ onIntentionalUserInput = {},
)
}
@@ -79,12 +80,10 @@
fun onShown() =
testScope.runTest {
val currentScene by collectLastValue(sceneInteractor.currentScene)
- val message by collectLastValue(bouncerViewModel.message)
val selectedDots by collectLastValue(underTest.selectedDots)
val currentDot by collectLastValue(underTest.currentDot)
lockDeviceAndOpenPatternBouncer()
- assertThat(message?.text).isEqualTo(ENTER_YOUR_PATTERN)
assertThat(selectedDots).isEmpty()
assertThat(currentDot).isNull()
assertThat(currentScene).isEqualTo(Scenes.Bouncer)
@@ -95,14 +94,12 @@
fun onDragStart() =
testScope.runTest {
val currentScene by collectLastValue(sceneInteractor.currentScene)
- val message by collectLastValue(bouncerViewModel.message)
val selectedDots by collectLastValue(underTest.selectedDots)
val currentDot by collectLastValue(underTest.currentDot)
lockDeviceAndOpenPatternBouncer()
underTest.onDragStart()
- assertThat(message?.text).isEmpty()
assertThat(selectedDots).isEmpty()
assertThat(currentDot).isNull()
assertThat(currentScene).isEqualTo(Scenes.Bouncer)
@@ -148,7 +145,6 @@
fun onDragEnd_whenWrong() =
testScope.runTest {
val currentScene by collectLastValue(sceneInteractor.currentScene)
- val message by collectLastValue(bouncerViewModel.message)
val selectedDots by collectLastValue(underTest.selectedDots)
val currentDot by collectLastValue(underTest.currentDot)
lockDeviceAndOpenPatternBouncer()
@@ -159,7 +155,6 @@
assertThat(selectedDots).isEmpty()
assertThat(currentDot).isNull()
- assertThat(message?.text).isEqualTo(WRONG_PATTERN)
assertThat(currentScene).isEqualTo(Scenes.Bouncer)
}
@@ -302,7 +297,6 @@
@Test
fun onDragEnd_whenPatternTooShort() =
testScope.runTest {
- val message by collectLastValue(bouncerViewModel.message)
val dialogViewModel by collectLastValue(bouncerViewModel.dialogViewModel)
lockDeviceAndOpenPatternBouncer()
@@ -325,7 +319,6 @@
underTest.onDragEnd()
- assertWithMessage("Attempt #$attempt").that(message?.text).isEqualTo(WRONG_PATTERN)
assertWithMessage("Attempt #$attempt").that(dialogViewModel).isNull()
}
}
@@ -334,7 +327,6 @@
fun onDragEnd_correctAfterWrong() =
testScope.runTest {
val authResult by collectLastValue(authenticationInteractor.onAuthenticationResult)
- val message by collectLastValue(bouncerViewModel.message)
val selectedDots by collectLastValue(underTest.selectedDots)
val currentDot by collectLastValue(underTest.currentDot)
lockDeviceAndOpenPatternBouncer()
@@ -344,7 +336,6 @@
underTest.onDragEnd()
assertThat(selectedDots).isEmpty()
assertThat(currentDot).isNull()
- assertThat(message?.text).isEqualTo(WRONG_PATTERN)
assertThat(authResult).isFalse()
// Enter the correct pattern:
@@ -370,10 +361,8 @@
private fun TestScope.switchToScene(toScene: SceneKey) {
val currentScene by collectLastValue(sceneInteractor.currentScene)
- val bouncerShown = currentScene != Scenes.Bouncer && toScene == Scenes.Bouncer
val bouncerHidden = currentScene == Scenes.Bouncer && toScene != Scenes.Bouncer
sceneInteractor.changeScene(toScene, "reason")
- if (bouncerShown) underTest.onShown()
if (bouncerHidden) underTest.onHidden()
runCurrent()
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt
index 7b75a37..5647954 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt
@@ -56,7 +56,6 @@
private val sceneInteractor by lazy { kosmos.sceneInteractor }
private val authenticationInteractor by lazy { kosmos.authenticationInteractor }
private val bouncerInteractor by lazy { kosmos.bouncerInteractor }
- private val bouncerViewModel by lazy { kosmos.bouncerViewModel }
private lateinit var underTest: PinBouncerViewModel
@Before
@@ -69,6 +68,7 @@
isInputEnabled = MutableStateFlow(true).asStateFlow(),
simBouncerInteractor = kosmos.simBouncerInteractor,
authenticationMethod = AuthenticationMethodModel.Pin,
+ onIntentionalUserInput = {},
)
overrideResource(R.string.keyguard_enter_your_pin, ENTER_YOUR_PIN)
@@ -78,11 +78,9 @@
@Test
fun onShown() =
testScope.runTest {
- val message by collectLastValue(bouncerViewModel.message)
val pin by collectLastValue(underTest.pinInput.map { it.getPin() })
lockDeviceAndOpenPinBouncer()
- assertThat(message?.text).ignoringCase().isEqualTo(ENTER_YOUR_PIN)
assertThat(pin).isEmpty()
assertThat(underTest.authenticationMethod).isEqualTo(AuthenticationMethodModel.Pin)
}
@@ -98,6 +96,7 @@
isInputEnabled = MutableStateFlow(true).asStateFlow(),
simBouncerInteractor = kosmos.simBouncerInteractor,
authenticationMethod = AuthenticationMethodModel.Sim,
+ onIntentionalUserInput = {},
)
assertThat(underTest.isSimAreaVisible).isTrue()
@@ -126,6 +125,7 @@
isInputEnabled = MutableStateFlow(true).asStateFlow(),
simBouncerInteractor = kosmos.simBouncerInteractor,
authenticationMethod = AuthenticationMethodModel.Sim,
+ onIntentionalUserInput = {},
)
kosmos.fakeAuthenticationRepository.setAutoConfirmFeatureEnabled(true)
val hintedPinLength by collectLastValue(underTest.hintedPinLength)
@@ -136,20 +136,17 @@
@Test
fun onPinButtonClicked() =
testScope.runTest {
- val message by collectLastValue(bouncerViewModel.message)
val pin by collectLastValue(underTest.pinInput.map { it.getPin() })
lockDeviceAndOpenPinBouncer()
underTest.onPinButtonClicked(1)
- assertThat(message?.text).isEmpty()
assertThat(pin).containsExactly(1)
}
@Test
fun onBackspaceButtonClicked() =
testScope.runTest {
- val message by collectLastValue(bouncerViewModel.message)
val pin by collectLastValue(underTest.pinInput.map { it.getPin() })
lockDeviceAndOpenPinBouncer()
@@ -158,7 +155,6 @@
underTest.onBackspaceButtonClicked()
- assertThat(message?.text).isEmpty()
assertThat(pin).isEmpty()
}
@@ -183,7 +179,6 @@
fun onBackspaceButtonLongPressed() =
testScope.runTest {
val currentScene by collectLastValue(sceneInteractor.currentScene)
- val message by collectLastValue(bouncerViewModel.message)
val pin by collectLastValue(underTest.pinInput.map { it.getPin() })
lockDeviceAndOpenPinBouncer()
@@ -195,7 +190,6 @@
underTest.onBackspaceButtonLongPressed()
- assertThat(message?.text).isEmpty()
assertThat(pin).isEmpty()
assertThat(currentScene).isEqualTo(Scenes.Bouncer)
}
@@ -217,7 +211,6 @@
fun onAuthenticateButtonClicked_whenWrong() =
testScope.runTest {
val currentScene by collectLastValue(sceneInteractor.currentScene)
- val message by collectLastValue(bouncerViewModel.message)
val pin by collectLastValue(underTest.pinInput.map { it.getPin() })
lockDeviceAndOpenPinBouncer()
@@ -230,7 +223,6 @@
underTest.onAuthenticateButtonClicked()
assertThat(pin).isEmpty()
- assertThat(message?.text).ignoringCase().isEqualTo(WRONG_PIN)
assertThat(currentScene).isEqualTo(Scenes.Bouncer)
}
@@ -238,7 +230,6 @@
fun onAuthenticateButtonClicked_correctAfterWrong() =
testScope.runTest {
val authResult by collectLastValue(authenticationInteractor.onAuthenticationResult)
- val message by collectLastValue(bouncerViewModel.message)
val pin by collectLastValue(underTest.pinInput.map { it.getPin() })
lockDeviceAndOpenPinBouncer()
@@ -248,13 +239,11 @@
underTest.onPinButtonClicked(4)
underTest.onPinButtonClicked(5) // PIN is now wrong!
underTest.onAuthenticateButtonClicked()
- assertThat(message?.text).ignoringCase().isEqualTo(WRONG_PIN)
assertThat(pin).isEmpty()
assertThat(authResult).isFalse()
// Enter the correct PIN:
FakeAuthenticationRepository.DEFAULT_PIN.forEach(underTest::onPinButtonClicked)
- assertThat(message?.text).isEmpty()
underTest.onAuthenticateButtonClicked()
@@ -277,7 +266,6 @@
fun onAutoConfirm_whenWrong() =
testScope.runTest {
val currentScene by collectLastValue(sceneInteractor.currentScene)
- val message by collectLastValue(bouncerViewModel.message)
val pin by collectLastValue(underTest.pinInput.map { it.getPin() })
kosmos.fakeAuthenticationRepository.setAutoConfirmFeatureEnabled(true)
lockDeviceAndOpenPinBouncer()
@@ -290,7 +278,6 @@
) // PIN is now wrong!
assertThat(pin).isEmpty()
- assertThat(message?.text).ignoringCase().isEqualTo(WRONG_PIN)
assertThat(currentScene).isEqualTo(Scenes.Bouncer)
}
@@ -390,10 +377,8 @@
private fun TestScope.switchToScene(toScene: SceneKey) {
val currentScene by collectLastValue(sceneInteractor.currentScene)
- val bouncerShown = currentScene != Scenes.Bouncer && toScene == Scenes.Bouncer
val bouncerHidden = currentScene == Scenes.Bouncer && toScene != Scenes.Bouncer
sceneInteractor.changeScene(toScene, "reason")
- if (bouncerShown) underTest.onShown()
if (bouncerHidden) underTest.onHidden()
runCurrent()
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt
index 8e2e947..a7e98ea 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt
@@ -18,10 +18,16 @@
import android.app.smartspace.SmartspaceTarget
import android.appwidget.AppWidgetProviderInfo
+import android.content.ActivityNotFoundException
+import android.content.Intent
+import android.content.pm.ActivityInfo
+import android.content.pm.PackageManager
+import android.content.pm.ResolveInfo
import android.content.pm.UserInfo
import android.os.UserHandle
import android.provider.Settings
import android.widget.RemoteViews
+import androidx.activity.result.ActivityResultLauncher
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.internal.logging.UiEventLogger
@@ -39,6 +45,7 @@
import com.android.systemui.communal.shared.model.CommunalWidgetContentModel
import com.android.systemui.communal.ui.viewmodel.CommunalEditModeViewModel
import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.kosmos.testDispatcher
import com.android.systemui.kosmos.testScope
import com.android.systemui.log.logcatLogBuffer
import com.android.systemui.media.controls.ui.view.MediaHost
@@ -46,15 +53,19 @@
import com.android.systemui.smartspace.data.repository.FakeSmartspaceRepository
import com.android.systemui.smartspace.data.repository.fakeSmartspaceRepository
import com.android.systemui.testKosmos
-import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.test.runTest
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyInt
import org.mockito.Mock
import org.mockito.Mockito
+import org.mockito.Mockito.never
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
@@ -64,6 +75,8 @@
@Mock private lateinit var mediaHost: MediaHost
@Mock private lateinit var uiEventLogger: UiEventLogger
@Mock private lateinit var providerInfo: AppWidgetProviderInfo
+ @Mock private lateinit var packageManager: PackageManager
+ @Mock private lateinit var activityResultLauncher: ActivityResultLauncher<Intent>
private val kosmos = testKosmos()
private val testScope = kosmos.testScope
@@ -73,6 +86,8 @@
private lateinit var smartspaceRepository: FakeSmartspaceRepository
private lateinit var mediaRepository: FakeCommunalMediaRepository
+ private val testableResources = context.orCreateTestableResources
+
private lateinit var underTest: CommunalEditModeViewModel
@Before
@@ -96,6 +111,7 @@
mediaHost,
uiEventLogger,
logcatLogBuffer("CommunalEditModeViewModelTest"),
+ kosmos.testDispatcher,
)
}
@@ -217,7 +233,69 @@
verify(uiEventLogger).log(CommunalUiEvent.COMMUNAL_HUB_REORDER_WIDGET_CANCEL)
}
+ @Test
+ fun onOpenWidgetPicker_launchesWidgetPickerActivity() {
+ testScope.runTest {
+ whenever(packageManager.resolveActivity(any(), anyInt())).then {
+ ResolveInfo().apply {
+ activityInfo = ActivityInfo().apply { packageName = WIDGET_PICKER_PACKAGE_NAME }
+ }
+ }
+
+ val success =
+ underTest.onOpenWidgetPicker(
+ testableResources.resources,
+ packageManager,
+ activityResultLauncher
+ )
+
+ verify(activityResultLauncher).launch(any())
+ assertTrue(success)
+ }
+ }
+
+ @Test
+ fun onOpenWidgetPicker_launcherActivityNotResolved_doesNotLaunchWidgetPickerActivity() {
+ testScope.runTest {
+ whenever(packageManager.resolveActivity(any(), anyInt())).thenReturn(null)
+
+ val success =
+ underTest.onOpenWidgetPicker(
+ testableResources.resources,
+ packageManager,
+ activityResultLauncher
+ )
+
+ verify(activityResultLauncher, never()).launch(any())
+ assertFalse(success)
+ }
+ }
+
+ @Test
+ fun onOpenWidgetPicker_activityLaunchThrowsException_failure() {
+ testScope.runTest {
+ whenever(packageManager.resolveActivity(any(), anyInt())).then {
+ ResolveInfo().apply {
+ activityInfo = ActivityInfo().apply { packageName = WIDGET_PICKER_PACKAGE_NAME }
+ }
+ }
+
+ whenever(activityResultLauncher.launch(any()))
+ .thenThrow(ActivityNotFoundException::class.java)
+
+ val success =
+ underTest.onOpenWidgetPicker(
+ testableResources.resources,
+ packageManager,
+ activityResultLauncher,
+ )
+
+ assertFalse(success)
+ }
+ }
+
private companion object {
val MAIN_USER_INFO = UserInfo(0, "primary", UserInfo.FLAG_MAIN)
+ const val WIDGET_PICKER_PACKAGE_NAME = "widget_picker_package_name"
}
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/WidgetInteractionHandlerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/WidgetInteractionHandlerTest.kt
index 69ff5ab..b4f87c4 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/WidgetInteractionHandlerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/WidgetInteractionHandlerTest.kt
@@ -21,6 +21,8 @@
import android.view.View
import android.widget.FrameLayout
import android.widget.RemoteViews.RemoteResponse
+import androidx.core.util.component1
+import androidx.core.util.component2
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
@@ -29,6 +31,7 @@
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.refEq
import org.mockito.Mock
import org.mockito.Mockito.isNull
import org.mockito.Mockito.notNull
@@ -62,6 +65,7 @@
val parent = FrameLayout(context)
val view = CommunalAppWidgetHostView(context)
parent.addView(view)
+ val (fillInIntent, activityOptions) = testResponse.getLaunchOptions(view)
underTest.onInteraction(view, testIntent, testResponse)
@@ -70,6 +74,8 @@
eq(testIntent),
isNull(),
notNull(),
+ refEq(fillInIntent),
+ refEq(activityOptions.toBundle()),
)
}
@@ -78,10 +84,17 @@
val parent = FrameLayout(context)
val view = View(context)
parent.addView(view)
+ val (fillInIntent, activityOptions) = testResponse.getLaunchOptions(view)
underTest.onInteraction(view, testIntent, testResponse)
verify(activityStarter)
- .startPendingIntentMaybeDismissingKeyguard(eq(testIntent), isNull(), isNull())
+ .startPendingIntentMaybeDismissingKeyguard(
+ eq(testIntent),
+ isNull(),
+ isNull(),
+ refEq(fillInIntent),
+ refEq(activityOptions.toBundle()),
+ )
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFingerprintAuthInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFingerprintAuthInteractorTest.kt
similarity index 83%
rename from packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFingerprintAuthInteractorTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFingerprintAuthInteractorTest.kt
index decbdaf..51f9957 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFingerprintAuthInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFingerprintAuthInteractorTest.kt
@@ -26,12 +26,10 @@
import com.android.systemui.kosmos.testScope
import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
-import kotlin.test.Test
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
+import org.junit.Test
import org.junit.runner.RunWith
-@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@RunWith(AndroidJUnit4::class)
class DeviceEntryFingerprintAuthInteractorTest : SysuiTestCase() {
@@ -59,17 +57,20 @@
}
@Test
- fun isSensorUnderDisplay_trueForUdfpsSensorTypes() =
+ fun isFingerprintCurrentlyAllowedInBouncer_trueForNonUdfpsSensorTypes() =
testScope.runTest {
- val isSensorUnderDisplay by collectLastValue(underTest.isSensorUnderDisplay)
+ biometricSettingsRepository.setIsFingerprintAuthCurrentlyAllowed(true)
+
+ val isFingerprintCurrentlyAllowedInBouncer by
+ collectLastValue(underTest.isFingerprintCurrentlyAllowedOnBouncer)
fingerprintPropertyRepository.supportsUdfps()
- assertThat(isSensorUnderDisplay).isTrue()
+ assertThat(isFingerprintCurrentlyAllowedInBouncer).isFalse()
fingerprintPropertyRepository.supportsRearFps()
- assertThat(isSensorUnderDisplay).isFalse()
+ assertThat(isFingerprintCurrentlyAllowedInBouncer).isTrue()
fingerprintPropertyRepository.supportsSideFps()
- assertThat(isSensorUnderDisplay).isFalse()
+ assertThat(isFingerprintCurrentlyAllowedInBouncer).isTrue()
}
}
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 769caaa..36458ed 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
@@ -270,12 +270,61 @@
}
@Test
+ fun transitionValue_canceled_toAnotherState() =
+ testScope.runTest {
+ val transitionValuesGone by collectValues(underTest.transitionValue(state = GONE))
+ val transitionValuesAod by collectValues(underTest.transitionValue(state = AOD))
+ val transitionValuesLs by collectValues(underTest.transitionValue(state = LOCKSCREEN))
+
+ listOf(
+ TransitionStep(GONE, AOD, 0f, STARTED),
+ TransitionStep(GONE, AOD, 0.5f, RUNNING),
+ TransitionStep(GONE, AOD, 0.5f, CANCELED),
+ TransitionStep(AOD, LOCKSCREEN, 0.5f, STARTED),
+ TransitionStep(AOD, LOCKSCREEN, 0.7f, RUNNING),
+ TransitionStep(AOD, LOCKSCREEN, 1f, FINISHED),
+ )
+ .forEach {
+ repository.sendTransitionStep(it)
+ runCurrent()
+ }
+
+ assertThat(transitionValuesGone).isEqualTo(listOf(1f, 0.5f, 0f))
+ assertThat(transitionValuesAod).isEqualTo(listOf(0f, 0.5f, 0.5f, 0.3f, 0f))
+ assertThat(transitionValuesLs).isEqualTo(listOf(0.5f, 0.7f, 1f))
+ }
+
+ @Test
+ fun transitionValue_canceled_backToOriginalState() =
+ testScope.runTest {
+ val transitionValuesGone by collectValues(underTest.transitionValue(state = GONE))
+ val transitionValuesAod by collectValues(underTest.transitionValue(state = AOD))
+
+ listOf(
+ TransitionStep(GONE, AOD, 0f, STARTED),
+ TransitionStep(GONE, AOD, 0.5f, RUNNING),
+ TransitionStep(GONE, AOD, 1f, CANCELED),
+ TransitionStep(AOD, GONE, 0.5f, STARTED),
+ TransitionStep(AOD, GONE, 0.7f, RUNNING),
+ TransitionStep(AOD, GONE, 1f, FINISHED),
+ )
+ .forEach {
+ repository.sendTransitionStep(it)
+ runCurrent()
+ }
+
+ assertThat(transitionValuesGone).isEqualTo(listOf(1f, 0.5f, 0.5f, 0.7f, 1f))
+ assertThat(transitionValuesAod).isEqualTo(listOf(0f, 0.5f, 0.5f, 0.3f, 0f))
+ }
+
+ @Test
fun isInTransitionToAnyState() =
testScope.runTest {
val inTransition by collectValues(underTest.isInTransitionToAnyState)
assertEquals(
listOf(
+ false,
true, // The repo is seeded with a transition from OFF to LOCKSCREEN.
false,
),
@@ -288,6 +337,7 @@
assertEquals(
listOf(
+ false,
true,
false,
true,
@@ -301,6 +351,7 @@
assertEquals(
listOf(
+ false,
true,
false,
true,
@@ -314,6 +365,7 @@
assertEquals(
listOf(
+ false,
true,
false,
true,
@@ -330,6 +382,7 @@
assertEquals(
listOf(
+ false,
true,
false,
),
@@ -345,6 +398,7 @@
assertEquals(
listOf(
+ false,
true,
false,
true,
@@ -359,6 +413,7 @@
assertEquals(
listOf(
+ false,
true,
false,
true,
@@ -379,6 +434,7 @@
assertEquals(
listOf(
+ false,
true,
false,
true,
@@ -398,6 +454,7 @@
assertEquals(
listOf(
+ false,
true,
false,
true,
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToGoneTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToGoneTransitionViewModelTest.kt
index d443851..0cc0c2f 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToGoneTransitionViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToGoneTransitionViewModelTest.kt
@@ -19,6 +19,7 @@
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.coroutines.collectValues
import com.android.systemui.flags.Flags
import com.android.systemui.flags.fakeFeatureFlagsClassic
@@ -32,6 +33,7 @@
import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Test
import org.junit.runner.RunWith
@@ -49,9 +51,7 @@
}
private val testScope = kosmos.testScope
private val keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository
- private val underTest by lazy {
- kosmos.alternateBouncerToGoneTransitionViewModel
- }
+ private val underTest by lazy { kosmos.alternateBouncerToGoneTransitionViewModel }
@Test
fun deviceEntryParentViewDisappear() =
@@ -73,6 +73,61 @@
values.forEach { assertThat(it).isEqualTo(0f) }
}
+ @Test
+ fun lockscreenAlpha() =
+ testScope.runTest {
+ val startAlpha = 0.6f
+ val viewState = ViewStateAccessor(alpha = { startAlpha })
+ val alpha by collectLastValue(underTest.lockscreenAlpha(viewState))
+ runCurrent()
+
+ keyguardTransitionRepository.sendTransitionSteps(
+ listOf(
+ step(0f, TransitionState.STARTED),
+ step(0.25f),
+ step(0.5f),
+ step(0.75f),
+ step(1f),
+ ),
+ testScope,
+ )
+
+ // Alpha starts at the starting value from ViewStateAccessor.
+ keyguardTransitionRepository.sendTransitionStep(
+ step(0f, state = TransitionState.STARTED)
+ )
+ runCurrent()
+ assertThat(alpha).isEqualTo(startAlpha)
+
+ // Alpha finishes in 200ms out of 500ms, check the alpha at the halfway point.
+ val progress = 0.2f
+ keyguardTransitionRepository.sendTransitionStep(step(progress))
+ runCurrent()
+ assertThat(alpha).isEqualTo(0.3f)
+
+ // Alpha ends at 0.
+ keyguardTransitionRepository.sendTransitionStep(step(1f))
+ runCurrent()
+ assertThat(alpha).isEqualTo(0f)
+ }
+
+ @Test
+ fun lockscreenAlpha_zeroInitialAlpha() =
+ testScope.runTest {
+ // ViewState starts at 0 alpha.
+ val viewState = ViewStateAccessor(alpha = { 0f })
+ val alpha by collectValues(underTest.lockscreenAlpha(viewState))
+
+ keyguardTransitionRepository.sendTransitionSteps(
+ from = KeyguardState.ALTERNATE_BOUNCER,
+ to = GONE,
+ testScope
+ )
+
+ // Alpha starts and ends at 0.
+ alpha.forEach { assertThat(it).isEqualTo(0f) }
+ }
+
private fun step(value: Float, state: TransitionState = RUNNING): TransitionStep {
return TransitionStep(
from = KeyguardState.ALTERNATE_BOUNCER,
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToLockscreenTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToLockscreenTransitionViewModelTest.kt
index 0796af0..409c551 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToLockscreenTransitionViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToLockscreenTransitionViewModelTest.kt
@@ -91,27 +91,6 @@
assertThat(bgViewAlpha).isEqualTo(1f)
}
- @Test
- fun deviceEntryBackgroundViewAlpha_rearFpEnrolled_noUpdates() =
- testScope.runTest {
- fingerprintPropertyRepository.supportsRearFps()
- val bgViewAlpha by collectLastValue(underTest.deviceEntryBackgroundViewAlpha)
- keyguardTransitionRepository.sendTransitionStep(step(0f, TransitionState.STARTED))
- assertThat(bgViewAlpha).isNull()
-
- keyguardTransitionRepository.sendTransitionStep(step(0.5f))
- assertThat(bgViewAlpha).isNull()
-
- keyguardTransitionRepository.sendTransitionStep(step(.75f))
- assertThat(bgViewAlpha).isNull()
-
- keyguardTransitionRepository.sendTransitionStep(step(1f))
- assertThat(bgViewAlpha).isNull()
-
- keyguardTransitionRepository.sendTransitionStep(step(1f, TransitionState.FINISHED))
- assertThat(bgViewAlpha).isNull()
- }
-
private fun step(
value: Float,
state: TransitionState = TransitionState.RUNNING
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/MediaTestHelper.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/MediaTestHelper.kt
new file mode 100644
index 0000000..8e44932
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/MediaTestHelper.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS 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.media.controls
+
+import android.R
+import android.app.smartspace.SmartspaceAction
+import android.content.Context
+import android.graphics.drawable.Icon
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
+
+class MediaTestHelper {
+ companion object {
+ /** Returns a list of three mocked recommendations */
+ fun getValidRecommendationList(context: Context): List<SmartspaceAction> {
+ val mediaRecommendationItem =
+ mock<SmartspaceAction> {
+ whenever(icon)
+ .thenReturn(
+ Icon.createWithResource(
+ context,
+ R.drawable.ic_media_play,
+ )
+ )
+ }
+ return listOf(mediaRecommendationItem, mediaRecommendationItem, mediaRecommendationItem)
+ }
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/data/repository/MediaDataRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/data/repository/MediaDataRepositoryTest.kt
new file mode 100644
index 0000000..6c41bc3
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/data/repository/MediaDataRepositoryTest.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.media.controls.data.repository
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.flags.Flags
+import com.android.systemui.flags.fakeFeatureFlagsClassic
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.media.controls.MediaTestHelper
+import com.android.systemui.media.controls.shared.model.MediaData
+import com.android.systemui.media.controls.shared.model.SmartspaceMediaData
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class MediaDataRepositoryTest : SysuiTestCase() {
+
+ private val kosmos = testKosmos()
+ private val testScope = kosmos.testScope
+
+ private val underTest: MediaDataRepository = kosmos.mediaDataRepository
+
+ @Test
+ fun setRecommendation() =
+ testScope.runTest {
+ val smartspaceData by collectLastValue(underTest.smartspaceMediaData)
+ val recommendation = SmartspaceMediaData(isActive = true)
+
+ underTest.setRecommendation(recommendation)
+
+ assertThat(smartspaceData).isEqualTo(recommendation)
+ }
+
+ @Test
+ fun addAndRemoveMediaData() =
+ testScope.runTest {
+ val entries by collectLastValue(underTest.mediaEntries)
+
+ val firstKey = "key1"
+ val firstData = MediaData().copy(isPlaying = true)
+
+ val secondKey = "key2"
+ val secondData = MediaData().copy(resumption = true)
+
+ underTest.addMediaEntry(firstKey, firstData)
+ underTest.addMediaEntry(secondKey, secondData)
+ underTest.addMediaEntry(firstKey, firstData.copy(isPlaying = false))
+
+ assertThat(entries!!.size).isEqualTo(2)
+ assertThat(entries!![firstKey]).isNotEqualTo(firstData)
+
+ underTest.removeMediaEntry(firstKey)
+
+ assertThat(entries!!.size).isEqualTo(1)
+ assertThat(entries!![secondKey]).isEqualTo(secondData)
+ }
+
+ @Test
+ fun setRecommendationInactive() =
+ testScope.runTest {
+ kosmos.fakeFeatureFlagsClassic.set(Flags.MEDIA_RETAIN_RECOMMENDATIONS, true)
+ val smartspaceData by collectLastValue(underTest.smartspaceMediaData)
+ val recommendation =
+ SmartspaceMediaData(
+ targetId = KEY_MEDIA_SMARTSPACE,
+ isActive = true,
+ recommendations = MediaTestHelper.getValidRecommendationList(context),
+ )
+
+ underTest.setRecommendation(recommendation)
+
+ assertThat(smartspaceData).isEqualTo(recommendation)
+
+ underTest.setRecommendationInactive(KEY_MEDIA_SMARTSPACE)
+
+ assertThat(smartspaceData).isNotEqualTo(recommendation)
+ assertThat(smartspaceData!!.isActive).isFalse()
+ }
+
+ @Test
+ fun dismissRecommendation() =
+ testScope.runTest {
+ val smartspaceData by collectLastValue(underTest.smartspaceMediaData)
+ val recommendation =
+ SmartspaceMediaData(
+ targetId = KEY_MEDIA_SMARTSPACE,
+ isActive = true,
+ recommendations = MediaTestHelper.getValidRecommendationList(context),
+ )
+
+ underTest.setRecommendation(recommendation)
+
+ assertThat(smartspaceData).isEqualTo(recommendation)
+
+ underTest.dismissSmartspaceRecommendation(KEY_MEDIA_SMARTSPACE)
+
+ assertThat(smartspaceData!!.isActive).isFalse()
+ }
+
+ companion object {
+ private const val KEY_MEDIA_SMARTSPACE = "MEDIA_SMARTSPACE_ID"
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/data/repository/MediaFilterRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/data/repository/MediaFilterRepositoryTest.kt
new file mode 100644
index 0000000..d39e77d
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/data/repository/MediaFilterRepositoryTest.kt
@@ -0,0 +1,144 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS 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.media.controls.data.repository
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.media.controls.MediaTestHelper
+import com.android.systemui.media.controls.shared.model.MediaData
+import com.android.systemui.media.controls.shared.model.SmartspaceMediaData
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class MediaFilterRepositoryTest : SysuiTestCase() {
+
+ private val kosmos = testKosmos()
+ private val testScope = kosmos.testScope
+
+ private val underTest: MediaFilterRepository = kosmos.mediaFilterRepository
+
+ @Test
+ fun addSelectedUserMediaEntry_activeThenInactivate() =
+ testScope.runTest {
+ val selectedUserEntries by collectLastValue(underTest.selectedUserEntries)
+
+ val userMedia = MediaData().copy(active = true)
+
+ underTest.addSelectedUserMediaEntry(KEY, userMedia)
+
+ assertThat(selectedUserEntries?.get(KEY)).isEqualTo(userMedia)
+
+ underTest.addSelectedUserMediaEntry(KEY, userMedia.copy(active = false))
+
+ assertThat(selectedUserEntries?.get(KEY)).isNotEqualTo(userMedia)
+ assertThat(selectedUserEntries?.get(KEY)?.active).isFalse()
+ }
+
+ @Test
+ fun addSelectedUserMediaEntry_thenRemove_returnsBoolean() =
+ testScope.runTest {
+ val selectedUserEntries by collectLastValue(underTest.selectedUserEntries)
+
+ val userMedia = MediaData()
+
+ underTest.addSelectedUserMediaEntry(KEY, userMedia)
+
+ assertThat(selectedUserEntries?.get(KEY)).isEqualTo(userMedia)
+
+ assertThat(underTest.removeSelectedUserMediaEntry(KEY, userMedia)).isTrue()
+ }
+
+ @Test
+ fun addSelectedUserMediaEntry_thenRemove_returnsValue() =
+ testScope.runTest {
+ val selectedUserEntries by collectLastValue(underTest.selectedUserEntries)
+
+ val userMedia = MediaData()
+
+ underTest.addSelectedUserMediaEntry(KEY, userMedia)
+
+ assertThat(selectedUserEntries?.get(KEY)).isEqualTo(userMedia)
+
+ assertThat(underTest.removeSelectedUserMediaEntry(KEY)).isEqualTo(userMedia)
+ }
+
+ @Test
+ fun addAllUserMediaEntry_activeThenInactivate() =
+ testScope.runTest {
+ val allUserEntries by collectLastValue(underTest.allUserEntries)
+
+ val userMedia = MediaData().copy(active = true)
+
+ underTest.addMediaEntry(KEY, userMedia)
+
+ assertThat(allUserEntries?.get(KEY)).isEqualTo(userMedia)
+
+ underTest.addMediaEntry(KEY, userMedia.copy(active = false))
+
+ assertThat(allUserEntries?.get(KEY)).isNotEqualTo(userMedia)
+ assertThat(allUserEntries?.get(KEY)?.active).isFalse()
+ }
+
+ @Test
+ fun addAllUserMediaEntry_thenRemove_returnsValue() =
+ testScope.runTest {
+ val allUserEntries by collectLastValue(underTest.allUserEntries)
+
+ val userMedia = MediaData()
+
+ underTest.addMediaEntry(KEY, userMedia)
+
+ assertThat(allUserEntries?.get(KEY)).isEqualTo(userMedia)
+
+ assertThat(underTest.removeMediaEntry(KEY)).isEqualTo(userMedia)
+ }
+
+ @Test
+ fun addActiveRecommendation_thenInactive() =
+ testScope.runTest {
+ val smartspaceMediaData by collectLastValue(underTest.smartspaceMediaData)
+
+ val mediaRecommendation =
+ SmartspaceMediaData(
+ targetId = KEY_MEDIA_SMARTSPACE,
+ isActive = true,
+ recommendations = MediaTestHelper.getValidRecommendationList(context),
+ )
+
+ underTest.setRecommendation(mediaRecommendation)
+
+ assertThat(smartspaceMediaData).isEqualTo(mediaRecommendation)
+
+ underTest.setRecommendation(mediaRecommendation.copy(isActive = false))
+
+ assertThat(smartspaceMediaData).isNotEqualTo(mediaRecommendation)
+ assertThat(smartspaceMediaData?.isActive).isFalse()
+ }
+
+ companion object {
+ private const val KEY = "KEY"
+ private const val KEY_MEDIA_SMARTSPACE = "MEDIA_SMARTSPACE_ID"
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/domain/interactor/MediaCarouselInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/domain/interactor/MediaCarouselInteractorTest.kt
new file mode 100644
index 0000000..6e67000
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/domain/interactor/MediaCarouselInteractorTest.kt
@@ -0,0 +1,199 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS 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.media.controls.domain.interactor
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.flags.Flags
+import com.android.systemui.flags.fakeFeatureFlagsClassic
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.media.controls.MediaTestHelper
+import com.android.systemui.media.controls.data.repository.MediaFilterRepository
+import com.android.systemui.media.controls.data.repository.mediaFilterRepository
+import com.android.systemui.media.controls.domain.pipeline.interactor.MediaCarouselInteractor
+import com.android.systemui.media.controls.domain.pipeline.interactor.mediaCarouselInteractor
+import com.android.systemui.media.controls.shared.model.MediaData
+import com.android.systemui.media.controls.shared.model.SmartspaceMediaData
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class MediaCarouselInteractorTest : SysuiTestCase() {
+
+ private val kosmos = testKosmos()
+ private val testScope = kosmos.testScope
+
+ private val mediaFilterRepository: MediaFilterRepository = kosmos.mediaFilterRepository
+ private val underTest: MediaCarouselInteractor = kosmos.mediaCarouselInteractor
+
+ @Test
+ fun addUserMediaEntry_activeThenInactivate() =
+ testScope.runTest {
+ val hasActiveMediaOrRecommendation by
+ collectLastValue(underTest.hasActiveMediaOrRecommendation)
+ val hasActiveMedia by collectLastValue(underTest.hasActiveMedia)
+ val hasAnyMedia by collectLastValue(underTest.hasAnyMedia)
+
+ val userMedia = MediaData().copy(active = true)
+
+ mediaFilterRepository.addSelectedUserMediaEntry(KEY, userMedia)
+
+ assertThat(hasActiveMediaOrRecommendation).isTrue()
+ assertThat(hasActiveMedia).isTrue()
+ assertThat(hasAnyMedia).isTrue()
+
+ mediaFilterRepository.addSelectedUserMediaEntry(KEY, userMedia.copy(active = false))
+
+ assertThat(hasActiveMediaOrRecommendation).isFalse()
+ assertThat(hasActiveMedia).isFalse()
+ assertThat(hasAnyMedia).isTrue()
+ }
+
+ @Test
+ fun addInactiveUserMediaEntry_thenRemove() =
+ testScope.runTest {
+ val hasActiveMediaOrRecommendation by
+ collectLastValue(underTest.hasActiveMediaOrRecommendation)
+ val hasActiveMedia by collectLastValue(underTest.hasActiveMedia)
+ val hasAnyMedia by collectLastValue(underTest.hasAnyMedia)
+
+ val userMedia = MediaData().copy(active = false)
+
+ mediaFilterRepository.addSelectedUserMediaEntry(KEY, userMedia)
+
+ assertThat(hasActiveMediaOrRecommendation).isFalse()
+ assertThat(hasActiveMedia).isFalse()
+ assertThat(hasAnyMedia).isTrue()
+
+ assertThat(mediaFilterRepository.removeSelectedUserMediaEntry(KEY, userMedia)).isTrue()
+
+ assertThat(hasActiveMediaOrRecommendation).isFalse()
+ assertThat(hasActiveMedia).isFalse()
+ assertThat(hasAnyMedia).isFalse()
+ }
+
+ @Test
+ fun addActiveRecommendation_inactiveMedia() =
+ testScope.runTest {
+ val hasActiveMediaOrRecommendation by
+ collectLastValue(underTest.hasActiveMediaOrRecommendation)
+ val hasAnyMediaOrRecommendation by
+ collectLastValue(underTest.hasAnyMediaOrRecommendation)
+ kosmos.fakeFeatureFlagsClassic.set(Flags.MEDIA_RETAIN_RECOMMENDATIONS, false)
+
+ val userMediaRecommendation =
+ SmartspaceMediaData(
+ targetId = KEY_MEDIA_SMARTSPACE,
+ isActive = true,
+ recommendations = MediaTestHelper.getValidRecommendationList(context),
+ )
+ val userMedia = MediaData().copy(active = false)
+
+ mediaFilterRepository.setRecommendation(userMediaRecommendation)
+
+ assertThat(hasActiveMediaOrRecommendation).isTrue()
+ assertThat(hasAnyMediaOrRecommendation).isTrue()
+
+ mediaFilterRepository.addSelectedUserMediaEntry(KEY, userMedia)
+
+ assertThat(hasActiveMediaOrRecommendation).isTrue()
+ assertThat(hasAnyMediaOrRecommendation).isTrue()
+ }
+
+ @Test
+ fun addActiveRecommendation_thenInactive() =
+ testScope.runTest {
+ val hasActiveMediaOrRecommendation by
+ collectLastValue(underTest.hasActiveMediaOrRecommendation)
+ val hasAnyMediaOrRecommendation by
+ collectLastValue(underTest.hasAnyMediaOrRecommendation)
+ kosmos.fakeFeatureFlagsClassic.set(Flags.MEDIA_RETAIN_RECOMMENDATIONS, false)
+
+ val mediaRecommendation =
+ SmartspaceMediaData(
+ targetId = KEY_MEDIA_SMARTSPACE,
+ isActive = true,
+ recommendations = MediaTestHelper.getValidRecommendationList(context),
+ )
+
+ mediaFilterRepository.setRecommendation(mediaRecommendation)
+
+ assertThat(hasActiveMediaOrRecommendation).isTrue()
+ assertThat(hasAnyMediaOrRecommendation).isTrue()
+
+ mediaFilterRepository.setRecommendation(mediaRecommendation.copy(isActive = false))
+
+ assertThat(hasActiveMediaOrRecommendation).isFalse()
+ assertThat(hasAnyMediaOrRecommendation).isFalse()
+ }
+
+ @Test
+ fun addActiveRecommendation_thenInvalid() =
+ testScope.runTest {
+ val hasActiveMediaOrRecommendation by
+ collectLastValue(underTest.hasActiveMediaOrRecommendation)
+ val hasAnyMediaOrRecommendation by
+ collectLastValue(underTest.hasAnyMediaOrRecommendation)
+ kosmos.fakeFeatureFlagsClassic.set(Flags.MEDIA_RETAIN_RECOMMENDATIONS, false)
+
+ val mediaRecommendation =
+ SmartspaceMediaData(
+ targetId = KEY_MEDIA_SMARTSPACE,
+ isActive = true,
+ recommendations = MediaTestHelper.getValidRecommendationList(context),
+ )
+
+ mediaFilterRepository.setRecommendation(mediaRecommendation)
+
+ assertThat(hasActiveMediaOrRecommendation).isTrue()
+ assertThat(hasAnyMediaOrRecommendation).isTrue()
+
+ mediaFilterRepository.setRecommendation(
+ mediaRecommendation.copy(recommendations = listOf())
+ )
+
+ assertThat(hasActiveMediaOrRecommendation).isFalse()
+ assertThat(hasAnyMediaOrRecommendation).isFalse()
+ }
+
+ @Test
+ fun hasAnyMedia_noMediaSet_returnsFalse() =
+ testScope.runTest { assertThat(underTest.hasAnyMedia.value).isFalse() }
+
+ @Test
+ fun hasAnyMediaOrRecommendation_noMediaSet_returnsFalse() =
+ testScope.runTest { assertThat(underTest.hasAnyMediaOrRecommendation.value).isFalse() }
+
+ @Test
+ fun hasActiveMedia_noMediaSet_returnsFalse() =
+ testScope.runTest { assertThat(underTest.hasActiveMedia.value).isFalse() }
+
+ @Test
+ fun hasActiveMediaOrRecommendation_nothingSet_returnsFalse() =
+ testScope.runTest { assertThat(underTest.hasActiveMediaOrRecommendation.value).isFalse() }
+
+ companion object {
+ private const val KEY = "KEY"
+ private const val KEY_MEDIA_SMARTSPACE = "MEDIA_SMARTSPACE_ID"
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/alarm/domain/AlarmTileMapperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/alarm/domain/AlarmTileMapperTest.kt
index c2ce392..f1cd0c8 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/alarm/domain/AlarmTileMapperTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/alarm/domain/AlarmTileMapperTest.kt
@@ -185,7 +185,7 @@
setOf(QSTileState.UserAction.CLICK),
label,
null,
- QSTileState.SideViewIcon.None,
+ QSTileState.SideViewIcon.Chevron,
QSTileState.EnabledState.ENABLED,
Switch::class.qualifiedName
)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/saver/domain/DataSaverDialogDelegateTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/saver/domain/DataSaverDialogDelegateTest.kt
index f24723a..97a10e6 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/saver/domain/DataSaverDialogDelegateTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/saver/domain/DataSaverDialogDelegateTest.kt
@@ -33,7 +33,6 @@
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
-import org.mockito.Mockito.mock
import org.mockito.Mockito.verify
/** Test [DataSaverDialogDelegate]. */
@@ -69,7 +68,7 @@
fun delegateSetsDialogTitleCorrectly() {
val expectedResId = R.string.data_saver_enable_title
- dataSaverDialogDelegate.onCreate(sysuiDialog, null)
+ dataSaverDialogDelegate.beforeCreate(sysuiDialog, null)
verify(sysuiDialog).setTitle(eq(expectedResId))
}
@@ -78,7 +77,7 @@
fun delegateSetsDialogMessageCorrectly() {
val expectedResId = R.string.data_saver_description
- dataSaverDialogDelegate.onCreate(sysuiDialog, null)
+ dataSaverDialogDelegate.beforeCreate(sysuiDialog, null)
verify(sysuiDialog).setMessage(expectedResId)
}
@@ -87,7 +86,7 @@
fun delegateSetsDialogPositiveButtonCorrectly() {
val expectedResId = R.string.data_saver_enable_button
- dataSaverDialogDelegate.onCreate(sysuiDialog, null)
+ dataSaverDialogDelegate.beforeCreate(sysuiDialog, null)
verify(sysuiDialog).setPositiveButton(eq(expectedResId), any())
}
@@ -96,7 +95,7 @@
fun delegateSetsDialogCancelButtonCorrectly() {
val expectedResId = R.string.cancel
- dataSaverDialogDelegate.onCreate(sysuiDialog, null)
+ dataSaverDialogDelegate.beforeCreate(sysuiDialog, null)
verify(sysuiDialog).setNeutralButton(eq(expectedResId), eq(null))
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/work/domain/interactor/WorkModeTileDataInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/work/domain/interactor/WorkModeTileDataInteractorTest.kt
new file mode 100644
index 0000000..8651300
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/work/domain/interactor/WorkModeTileDataInteractorTest.kt
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.tiles.impl.work.domain.interactor
+
+import android.os.UserHandle
+import android.platform.test.annotations.EnabledOnRavenwood
+import android.testing.LeakCheck
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.qs.tiles.base.interactor.DataUpdateTrigger
+import com.android.systemui.qs.tiles.impl.work.domain.model.WorkModeTileModel
+import com.android.systemui.utils.leaks.FakeManagedProfileController
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@EnabledOnRavenwood
+@RunWith(AndroidJUnit4::class)
+class WorkModeTileDataInteractorTest : SysuiTestCase() {
+ private val controller = FakeManagedProfileController(LeakCheck())
+ private val underTest: WorkModeTileDataInteractor = WorkModeTileDataInteractor(controller)
+
+ @Test
+ fun availability_matchesControllerHasActiveProfiles() = runTest {
+ val availability by collectLastValue(underTest.availability(TEST_USER))
+
+ assertThat(availability).isFalse()
+
+ controller.setHasActiveProfile(true)
+ assertThat(availability).isTrue()
+
+ controller.setHasActiveProfile(false)
+ assertThat(availability).isFalse()
+ }
+
+ @Test
+ fun tileData_whenHasActiveProfile_matchesControllerIsEnabled() = runTest {
+ controller.setHasActiveProfile(true)
+ val data by
+ collectLastValue(
+ underTest.tileData(TEST_USER, flowOf(DataUpdateTrigger.InitialRequest))
+ )
+
+ assertThat(data).isInstanceOf(WorkModeTileModel.HasActiveProfile::class.java)
+ assertThat((data as WorkModeTileModel.HasActiveProfile).isEnabled).isFalse()
+
+ controller.isWorkModeEnabled = true
+ assertThat(data).isInstanceOf(WorkModeTileModel.HasActiveProfile::class.java)
+ assertThat((data as WorkModeTileModel.HasActiveProfile).isEnabled).isTrue()
+
+ controller.isWorkModeEnabled = false
+ assertThat(data).isInstanceOf(WorkModeTileModel.HasActiveProfile::class.java)
+ assertThat((data as WorkModeTileModel.HasActiveProfile).isEnabled).isFalse()
+ }
+
+ @Test
+ fun tileData_matchesControllerHasActiveProfile() = runTest {
+ val data by
+ collectLastValue(
+ underTest.tileData(TEST_USER, flowOf(DataUpdateTrigger.InitialRequest))
+ )
+ assertThat(data).isInstanceOf(WorkModeTileModel.NoActiveProfile::class.java)
+
+ controller.setHasActiveProfile(true)
+ assertThat(data).isInstanceOf(WorkModeTileModel.HasActiveProfile::class.java)
+
+ controller.setHasActiveProfile(false)
+ assertThat(data).isInstanceOf(WorkModeTileModel.NoActiveProfile::class.java)
+ }
+
+ private companion object {
+ val TEST_USER = UserHandle.of(1)!!
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/work/domain/interactor/WorkModeTileUserActionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/work/domain/interactor/WorkModeTileUserActionInteractorTest.kt
new file mode 100644
index 0000000..8a63e2c
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/work/domain/interactor/WorkModeTileUserActionInteractorTest.kt
@@ -0,0 +1,115 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.tiles.impl.work.domain.interactor
+
+import android.platform.test.annotations.EnabledOnRavenwood
+import android.provider.Settings
+import android.testing.LeakCheck
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.qs.tiles.base.actions.FakeQSTileIntentUserInputHandler
+import com.android.systemui.qs.tiles.base.actions.QSTileIntentUserInputHandlerSubject
+import com.android.systemui.qs.tiles.base.interactor.QSTileInputTestKtx
+import com.android.systemui.qs.tiles.impl.work.domain.model.WorkModeTileModel
+import com.android.systemui.utils.leaks.FakeManagedProfileController
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@EnabledOnRavenwood
+@RunWith(AndroidJUnit4::class)
+class WorkModeTileUserActionInteractorTest : SysuiTestCase() {
+
+ private val inputHandler = FakeQSTileIntentUserInputHandler()
+ private val profileController = FakeManagedProfileController(LeakCheck())
+
+ private val underTest =
+ WorkModeTileUserActionInteractor(
+ profileController,
+ inputHandler,
+ )
+
+ @Test
+ fun handleClickWhenEnabled() = runTest {
+ val wasEnabled = true
+ profileController.isWorkModeEnabled = wasEnabled
+
+ underTest.handleInput(
+ QSTileInputTestKtx.click(WorkModeTileModel.HasActiveProfile(wasEnabled))
+ )
+
+ assertThat(profileController.isWorkModeEnabled).isEqualTo(!wasEnabled)
+ }
+
+ @Test
+ fun handleClickWhenDisabled() = runTest {
+ val wasEnabled = false
+ profileController.isWorkModeEnabled = wasEnabled
+
+ underTest.handleInput(
+ QSTileInputTestKtx.click(WorkModeTileModel.HasActiveProfile(wasEnabled))
+ )
+
+ assertThat(profileController.isWorkModeEnabled).isEqualTo(!wasEnabled)
+ }
+
+ @Test
+ fun handleClickWhenUnavailable() = runTest {
+ val wasEnabled = false
+ profileController.isWorkModeEnabled = wasEnabled
+
+ underTest.handleInput(QSTileInputTestKtx.click(WorkModeTileModel.NoActiveProfile))
+
+ assertThat(profileController.isWorkModeEnabled).isEqualTo(wasEnabled)
+ }
+
+ @Test
+ fun handleLongClickWhenDisabled() = runTest {
+ val enabled = false
+
+ underTest.handleInput(
+ QSTileInputTestKtx.longClick(WorkModeTileModel.HasActiveProfile(enabled))
+ )
+
+ QSTileIntentUserInputHandlerSubject.assertThat(inputHandler).handledOneIntentInput {
+ assertThat(it.intent.action).isEqualTo(Settings.ACTION_MANAGED_PROFILE_SETTINGS)
+ }
+ }
+
+ @Test
+ fun handleLongClickWhenEnabled() = runTest {
+ val enabled = true
+
+ underTest.handleInput(
+ QSTileInputTestKtx.longClick(WorkModeTileModel.HasActiveProfile(enabled))
+ )
+
+ QSTileIntentUserInputHandlerSubject.assertThat(inputHandler).handledOneIntentInput {
+ assertThat(it.intent.action).isEqualTo(Settings.ACTION_MANAGED_PROFILE_SETTINGS)
+ }
+ }
+
+ @Test
+ fun handleLongClickWhenUnavailable() = runTest {
+ underTest.handleInput(QSTileInputTestKtx.longClick(WorkModeTileModel.NoActiveProfile))
+
+ QSTileIntentUserInputHandlerSubject.assertThat(inputHandler).handledNoInputs()
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/adapter/QSSceneAdapterImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/adapter/QSSceneAdapterImplTest.kt
index 3c0ab24..27c4ec1 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/adapter/QSSceneAdapterImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/adapter/QSSceneAdapterImplTest.kt
@@ -27,9 +27,17 @@
import com.android.systemui.common.ui.data.repository.FakeConfigurationRepository
import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor
import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testCase
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.kosmos.testScope
import com.android.systemui.qs.QSImpl
import com.android.systemui.qs.dagger.QSComponent
import com.android.systemui.qs.dagger.QSSceneComponent
+import com.android.systemui.shade.data.repository.fakeShadeRepository
+import com.android.systemui.shade.domain.interactor.shadeInteractor
+import com.android.systemui.shade.shared.model.ShadeMode
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.argumentCaptor
import com.android.systemui.util.mockito.capture
@@ -41,8 +49,6 @@
import java.util.Locale
import javax.inject.Provider
import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.test.StandardTestDispatcher
-import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Test
@@ -57,8 +63,9 @@
@OptIn(ExperimentalCoroutinesApi::class)
class QSSceneAdapterImplTest : SysuiTestCase() {
- private val testDispatcher = StandardTestDispatcher()
- private val testScope = TestScope(testDispatcher)
+ private val kosmos = Kosmos().apply { testCase = this@QSSceneAdapterImplTest }
+ private val testDispatcher = kosmos.testDispatcher
+ private val testScope = kosmos.testScope
private val qsImplProvider =
object : Provider<QSImpl> {
@@ -107,10 +114,15 @@
}
}
+ private val shadeInteractor = kosmos.shadeInteractor
+ private val dumpManager = mock<DumpManager>()
+
private val underTest =
QSSceneAdapterImpl(
qsSceneComponentFactory,
qsImplProvider,
+ shadeInteractor,
+ dumpManager,
testDispatcher,
testScope.backgroundScope,
configurationInteractor,
@@ -158,12 +170,6 @@
)
verify(this).setListening(false)
verify(this).setExpanded(false)
- verify(this)
- .setTransitionToFullShadeProgress(
- /* isTransitioningToFullShade= */ false,
- /* qsTransitionFraction= */ 1f,
- /* qsSquishinessFraction = */ 1f,
- )
}
}
@@ -187,13 +193,7 @@
/* squishinessFraction= */ 1f,
)
verify(this).setListening(true)
- verify(this).setExpanded(true)
- verify(this)
- .setTransitionToFullShadeProgress(
- /* isTransitioningToFullShade= */ false,
- /* qsTransitionFraction= */ 1f,
- /* qsSquishinessFraction = */ 1f,
- )
+ verify(this).setExpanded(false)
}
}
@@ -218,12 +218,6 @@
)
verify(this).setListening(true)
verify(this).setExpanded(true)
- verify(this)
- .setTransitionToFullShadeProgress(
- /* isTransitioningToFullShade= */ false,
- /* qsTransitionFraction= */ 1f,
- /* qsSquishinessFraction = */ 1f,
- )
}
}
@@ -249,12 +243,6 @@
)
verify(this).setListening(true)
verify(this).setExpanded(true)
- verify(this)
- .setTransitionToFullShadeProgress(
- /* isTransitioningToFullShade= */ false,
- /* qsTransitionFraction= */ 1f,
- /* qsSquishinessFraction = */ 1f,
- )
}
}
@@ -268,7 +256,7 @@
runCurrent()
clearInvocations(qsImpl!!)
- underTest.setState(QSSceneAdapter.State.Unsquishing(squishiness))
+ underTest.setState(QSSceneAdapter.State.UnsquishingQQS(squishiness))
with(qsImpl!!) {
verify(this).setQsVisible(true)
verify(this)
@@ -279,13 +267,7 @@
/* squishinessFraction= */ squishiness,
)
verify(this).setListening(true)
- verify(this).setExpanded(true)
- verify(this)
- .setTransitionToFullShadeProgress(
- /* isTransitioningToFullShade= */ false,
- /* qsTransitionFraction= */ 1f,
- /* qsSquishinessFraction = */ squishiness,
- )
+ verify(this).setExpanded(false)
}
}
@@ -497,4 +479,21 @@
verify(qsImpl!!).applyBottomNavBarToCustomizerPadding(navBarHeight)
}
+
+ @Test
+ fun dispatchSplitShade() =
+ testScope.runTest {
+ val shadeRepository = kosmos.fakeShadeRepository
+ shadeRepository.setShadeMode(ShadeMode.Single)
+ val qsImpl by collectLastValue(underTest.qsImpl)
+
+ underTest.inflate(context)
+ runCurrent()
+
+ verify(qsImpl!!).setInSplitShade(false)
+
+ shadeRepository.setShadeMode(ShadeMode.Split)
+ runCurrent()
+ verify(qsImpl!!).setInSplitShade(true)
+ }
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/adapter/QSSceneAdapterTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/adapter/QSSceneAdapterTest.kt
index e281383..ebd65fd 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/adapter/QSSceneAdapterTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/adapter/QSSceneAdapterTest.kt
@@ -49,9 +49,16 @@
}
@Test
- fun unsquishing_expansionSameAsQQS() {
+ fun unsquishingQQS_expansionSameAsQQS() {
val squishiness = 0.6f
- assertThat(QSSceneAdapter.State.Unsquishing(squishiness).expansion)
+ assertThat(QSSceneAdapter.State.UnsquishingQQS(squishiness).expansion)
.isEqualTo(QSSceneAdapter.State.QQS.expansion)
}
+
+ @Test
+ fun unsquishingQS_expansionSameAsQS() {
+ val squishiness = 0.6f
+ assertThat(QSSceneAdapter.State.UnsquishingQS(squishiness).expansion)
+ .isEqualTo(QSSceneAdapter.State.QS.expansion)
+ }
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt
index cc66f8b..f018cc1 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt
@@ -51,6 +51,8 @@
import com.android.systemui.scene.shared.model.Scenes
import com.android.systemui.scene.shared.model.fakeSceneDataSource
import com.android.systemui.statusbar.NotificationShadeWindowController
+import com.android.systemui.statusbar.notification.data.repository.FakeHeadsUpRowRepository
+import com.android.systemui.statusbar.notification.data.repository.HeadsUpRowRepository
import com.android.systemui.statusbar.notification.stack.data.repository.headsUpNotificationRepository
import com.android.systemui.statusbar.notification.stack.domain.interactor.headsUpNotificationInteractor
import com.android.systemui.statusbar.phone.CentralSurfaces
@@ -175,10 +177,12 @@
transitionStateFlow.value = ObservableTransitionState.Idle(Scenes.Gone)
assertThat(isVisible).isFalse()
- kosmos.headsUpNotificationRepository.hasPinnedHeadsUp.value = true
+ kosmos.headsUpNotificationRepository.activeHeadsUpRows.value =
+ buildNotificationRows(isPinned = true)
assertThat(isVisible).isTrue()
- kosmos.headsUpNotificationRepository.hasPinnedHeadsUp.value = false
+ kosmos.headsUpNotificationRepository.activeHeadsUpRows.value =
+ buildNotificationRows(isPinned = false)
assertThat(isVisible).isFalse()
}
@@ -1070,4 +1074,17 @@
return transitionStateFlow
}
+
+ private fun buildNotificationRows(isPinned: Boolean = false): Set<HeadsUpRowRepository> =
+ setOf(
+ fakeHeadsUpRowRepository(key = "0", isPinned = isPinned),
+ fakeHeadsUpRowRepository(key = "1", isPinned = isPinned),
+ fakeHeadsUpRowRepository(key = "2", isPinned = isPinned),
+ fakeHeadsUpRowRepository(key = "3", isPinned = isPinned),
+ )
+
+ private fun fakeHeadsUpRowRepository(key: String, isPinned: Boolean) =
+ FakeHeadsUpRowRepository(key = key, elementKey = Any()).apply {
+ this.isPinned.value = isPinned
+ }
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt
index 1c54961..d1c4ec3 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt
@@ -95,7 +95,7 @@
scope = testScope.backgroundScope,
)
- private val qsFlexiglassAdapter = FakeQSSceneAdapter({ mock() })
+ private val qsSceneAdapter = FakeQSSceneAdapter({ mock() })
private lateinit var shadeHeaderViewModel: ShadeHeaderViewModel
@@ -122,7 +122,7 @@
applicationScope = testScope.backgroundScope,
deviceEntryInteractor = deviceEntryInteractor,
shadeHeaderViewModel = shadeHeaderViewModel,
- qsSceneAdapter = qsFlexiglassAdapter,
+ qsSceneAdapter = qsSceneAdapter,
notifications = kosmos.notificationsPlaceholderViewModel,
mediaDataManager = mediaDataManager,
shadeInteractor = kosmos.shadeInteractor,
@@ -279,6 +279,20 @@
}
@Test
+ fun upTransitionSceneKey_customizing_noTransition() =
+ testScope.runTest {
+ val destinationScenes by collectLastValue(underTest.destinationScenes)
+
+ qsSceneAdapter.setCustomizing(true)
+ assertThat(
+ destinationScenes!!
+ .keys
+ .filterIsInstance<Swipe>()
+ .filter { it.direction == SwipeDirection.Up }
+ ).isEmpty()
+ }
+
+ @Test
fun shadeMode() =
testScope.runTest {
val shadeMode by collectLastValue(underTest.shadeMode)
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 2689fc1..94539a3 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
@@ -22,7 +22,6 @@
import androidx.test.filters.SmallTest
import com.android.compose.animation.scene.ObservableTransitionState
import com.android.systemui.SysuiTestCase
-import com.android.systemui.common.shared.model.NotificationContainerBounds
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.flags.Flags
import com.android.systemui.flags.fakeFeatureFlagsClassic
@@ -31,6 +30,7 @@
import com.android.systemui.scene.shared.flag.fakeSceneContainerFlags
import com.android.systemui.scene.shared.model.Scenes
import com.android.systemui.scene.shared.model.fakeSceneDataSource
+import com.android.systemui.statusbar.notification.stack.shared.model.StackBounds
import com.android.systemui.statusbar.notification.stack.ui.viewmodel.notificationStackAppearanceViewModel
import com.android.systemui.statusbar.notification.stack.ui.viewmodel.notificationsPlaceholderViewModel
import com.android.systemui.testKosmos
@@ -64,7 +64,7 @@
@Test
fun updateBounds() =
testScope.runTest {
- val bounds by collectLastValue(appearanceViewModel.stackBounds)
+ val clipping by collectLastValue(appearanceViewModel.stackClipping)
val top = 200f
val left = 0f
@@ -76,15 +76,8 @@
right = right,
bottom = bottom
)
- assertThat(bounds)
- .isEqualTo(
- NotificationContainerBounds(
- left = left,
- top = top,
- right = right,
- bottom = bottom
- )
- )
+ assertThat(clipping?.bounds)
+ .isEqualTo(StackBounds(left = left, top = top, right = right, bottom = bottom))
}
@Test
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractorTest.kt
new file mode 100644
index 0000000..bba9991
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractorTest.kt
@@ -0,0 +1,269 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:OptIn(ExperimentalCoroutinesApi::class)
+
+package com.android.systemui.statusbar.notification.domain.interactor
+
+import android.platform.test.annotations.EnableFlags
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.statusbar.notification.data.repository.FakeHeadsUpRowRepository
+import com.android.systemui.statusbar.notification.shared.NotificationsHeadsUpRefactor
+import com.android.systemui.statusbar.notification.stack.data.repository.headsUpNotificationRepository
+import com.android.systemui.statusbar.notification.stack.data.repository.setNotifications
+import com.android.systemui.statusbar.notification.stack.domain.interactor.headsUpNotificationInteractor
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+@EnableFlags(NotificationsHeadsUpRefactor.FLAG_NAME)
+class HeadsUpNotificationInteractorTest : SysuiTestCase() {
+ private val kosmos = testKosmos()
+ private val testScope = kosmos.testScope
+ private val repository = kosmos.headsUpNotificationRepository
+
+ private val underTest = kosmos.headsUpNotificationInteractor
+
+ @Test
+ fun hasPinnedRows_emptyList_false() =
+ testScope.runTest {
+ val hasPinnedRows by collectLastValue(underTest.hasPinnedRows)
+
+ assertThat(hasPinnedRows).isFalse()
+ }
+
+ @Test
+ fun hasPinnedRows_noPinnedRows_false() =
+ testScope.runTest {
+ val hasPinnedRows by collectLastValue(underTest.hasPinnedRows)
+ // WHEN no pinned rows are set
+ repository.setNotifications(
+ fakeHeadsUpRowRepository("key 0"),
+ fakeHeadsUpRowRepository("key 1"),
+ fakeHeadsUpRowRepository("key 2"),
+ )
+ runCurrent()
+
+ // THEN hasPinnedRows is false
+ assertThat(hasPinnedRows).isFalse()
+ }
+
+ @Test
+ fun hasPinnedRows_hasPinnedRows_true() =
+ testScope.runTest {
+ val hasPinnedRows by collectLastValue(underTest.hasPinnedRows)
+ // WHEN a pinned rows is set
+ repository.setNotifications(
+ fakeHeadsUpRowRepository("key 0", isPinned = true),
+ fakeHeadsUpRowRepository("key 1"),
+ fakeHeadsUpRowRepository("key 2"),
+ )
+ runCurrent()
+
+ // THEN hasPinnedRows is true
+ assertThat(hasPinnedRows).isTrue()
+ }
+
+ @Test
+ fun hasPinnedRows_rowGetsPinned_true() =
+ testScope.runTest {
+ val hasPinnedRows by collectLastValue(underTest.hasPinnedRows)
+ // GIVEN no rows are pinned
+ val rows =
+ arrayListOf(
+ fakeHeadsUpRowRepository("key 0"),
+ fakeHeadsUpRowRepository("key 1"),
+ fakeHeadsUpRowRepository("key 2"),
+ )
+ repository.setNotifications(rows)
+ runCurrent()
+
+ // WHEN a row gets pinned
+ rows[0].isPinned.value = true
+ runCurrent()
+
+ // THEN hasPinnedRows updates to true
+ assertThat(hasPinnedRows).isTrue()
+ }
+
+ @Test
+ fun hasPinnedRows_rowGetsUnPinned_false() =
+ testScope.runTest {
+ val hasPinnedRows by collectLastValue(underTest.hasPinnedRows)
+ // GIVEN one row is pinned
+ val rows =
+ arrayListOf(
+ fakeHeadsUpRowRepository("key 0", isPinned = true),
+ fakeHeadsUpRowRepository("key 1"),
+ fakeHeadsUpRowRepository("key 2"),
+ )
+ repository.setNotifications(rows)
+ runCurrent()
+
+ // THEN that row gets unpinned
+ rows[0].isPinned.value = false
+ runCurrent()
+
+ // THEN hasPinnedRows updates to false
+ assertThat(hasPinnedRows).isFalse()
+ }
+
+ @Test
+ fun pinnedRows_noRows_isEmpty() =
+ testScope.runTest {
+ val pinnedHeadsUpRows by collectLastValue(underTest.pinnedHeadsUpRows)
+
+ assertThat(pinnedHeadsUpRows).isEmpty()
+ }
+
+ @Test
+ fun pinnedRows_noPinnedRows_isEmpty() =
+ testScope.runTest {
+ val pinnedHeadsUpRows by collectLastValue(underTest.pinnedHeadsUpRows)
+ // WHEN no rows are pinned
+ repository.setNotifications(
+ fakeHeadsUpRowRepository("key 0"),
+ fakeHeadsUpRowRepository("key 1"),
+ fakeHeadsUpRowRepository("key 2"),
+ )
+ runCurrent()
+
+ // THEN all rows are filtered
+ assertThat(pinnedHeadsUpRows).isEmpty()
+ }
+
+ @Test
+ fun pinnedRows_hasPinnedRows_containsPinnedRows() =
+ testScope.runTest {
+ val pinnedHeadsUpRows by collectLastValue(underTest.pinnedHeadsUpRows)
+ // WHEN some rows are pinned
+ val rows =
+ arrayListOf(
+ fakeHeadsUpRowRepository("key 0", isPinned = true),
+ fakeHeadsUpRowRepository("key 1", isPinned = true),
+ fakeHeadsUpRowRepository("key 2"),
+ )
+ repository.setNotifications(rows)
+ runCurrent()
+
+ // THEN the unpinned rows are filtered
+ assertThat(pinnedHeadsUpRows).containsExactly(rows[0], rows[1])
+ }
+
+ @Test
+ fun pinnedRows_rowGetsPinned_containsPinnedRows() =
+ testScope.runTest {
+ val pinnedHeadsUpRows by collectLastValue(underTest.pinnedHeadsUpRows)
+ // GIVEN some rows are pinned
+ val rows =
+ arrayListOf(
+ fakeHeadsUpRowRepository("key 0", isPinned = true),
+ fakeHeadsUpRowRepository("key 1", isPinned = true),
+ fakeHeadsUpRowRepository("key 2"),
+ )
+ repository.setNotifications(rows)
+ runCurrent()
+
+ // WHEN all rows gets pinned
+ rows[2].isPinned.value = true
+ runCurrent()
+
+ // THEN no rows are filtered
+ assertThat(pinnedHeadsUpRows).containsExactly(rows[0], rows[1], rows[2])
+ }
+
+ @Test
+ fun pinnedRows_allRowsPinned_containsAllRows() =
+ testScope.runTest {
+ val pinnedHeadsUpRows by collectLastValue(underTest.pinnedHeadsUpRows)
+ // WHEN all rows are pinned
+ val rows =
+ arrayListOf(
+ fakeHeadsUpRowRepository("key 0", isPinned = true),
+ fakeHeadsUpRowRepository("key 1", isPinned = true),
+ fakeHeadsUpRowRepository("key 2", isPinned = true),
+ )
+ repository.setNotifications(rows)
+ runCurrent()
+
+ // THEN no rows are filtered
+ assertThat(pinnedHeadsUpRows).containsExactly(rows[0], rows[1], rows[2])
+ }
+
+ @Test
+ fun pinnedRows_rowGetsUnPinned_containsPinnedRows() =
+ testScope.runTest {
+ val pinnedHeadsUpRows by collectLastValue(underTest.pinnedHeadsUpRows)
+ // GIVEN all rows are pinned
+ val rows =
+ arrayListOf(
+ fakeHeadsUpRowRepository("key 0", isPinned = true),
+ fakeHeadsUpRowRepository("key 1", isPinned = true),
+ fakeHeadsUpRowRepository("key 2", isPinned = true),
+ )
+ repository.setNotifications(rows)
+ runCurrent()
+
+ // WHEN a row gets unpinned
+ rows[0].isPinned.value = false
+ runCurrent()
+
+ // THEN the unpinned row is filtered
+ assertThat(pinnedHeadsUpRows).containsExactly(rows[1], rows[2])
+ }
+
+ @Test
+ fun pinnedRows_rowGetsPinnedAndUnPinned_containsTheSameInstance() =
+ testScope.runTest {
+ val pinnedHeadsUpRows by collectLastValue(underTest.pinnedHeadsUpRows)
+
+ val rows =
+ arrayListOf(
+ fakeHeadsUpRowRepository("key 0"),
+ fakeHeadsUpRowRepository("key 1"),
+ fakeHeadsUpRowRepository("key 2"),
+ )
+ repository.setNotifications(rows)
+ runCurrent()
+
+ rows[0].isPinned.value = true
+ runCurrent()
+ assertThat(pinnedHeadsUpRows).containsExactly(rows[0])
+
+ rows[0].isPinned.value = false
+ runCurrent()
+ assertThat(pinnedHeadsUpRows).isEmpty()
+
+ rows[0].isPinned.value = true
+ runCurrent()
+ assertThat(pinnedHeadsUpRows).containsExactly(rows[0])
+ }
+
+ private fun fakeHeadsUpRowRepository(key: String, isPinned: Boolean = false) =
+ FakeHeadsUpRowRepository(key = key, elementKey = Any()).apply {
+ this.isPinned.value = isPinned
+ }
+}
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 ffe6e6d..e3fa89c 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
@@ -19,10 +19,13 @@
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
-import com.android.systemui.common.shared.model.NotificationContainerBounds
import com.android.systemui.coroutines.collectLastValue
-import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.testScope
+import com.android.systemui.shade.data.repository.shadeRepository
+import com.android.systemui.shade.shared.model.ShadeMode
+import com.android.systemui.statusbar.notification.stack.shared.model.StackBounds
+import com.android.systemui.statusbar.notification.stack.shared.model.StackRounding
+import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.test.runTest
import org.junit.Test
@@ -30,10 +33,9 @@
@SmallTest
@RunWith(AndroidJUnit4::class)
-@android.platform.test.annotations.EnabledOnRavenwood
class NotificationStackAppearanceInteractorTest : SysuiTestCase() {
- private val kosmos = Kosmos()
+ private val kosmos = testKosmos()
private val testScope = kosmos.testScope
private val underTest = kosmos.notificationStackAppearanceInteractor
@@ -43,29 +45,39 @@
val stackBounds by collectLastValue(underTest.stackBounds)
val bounds1 =
- NotificationContainerBounds(
+ StackBounds(
top = 100f,
bottom = 200f,
- isAnimated = true,
)
underTest.setStackBounds(bounds1)
assertThat(stackBounds).isEqualTo(bounds1)
val bounds2 =
- NotificationContainerBounds(
+ StackBounds(
top = 200f,
bottom = 300f,
- isAnimated = false,
)
underTest.setStackBounds(bounds2)
assertThat(stackBounds).isEqualTo(bounds2)
}
+ @Test
+ fun stackRounding() =
+ testScope.runTest {
+ val stackRounding by collectLastValue(underTest.stackRounding)
+
+ kosmos.shadeRepository.setShadeMode(ShadeMode.Single)
+ assertThat(stackRounding).isEqualTo(StackRounding(roundTop = true, roundBottom = false))
+
+ kosmos.shadeRepository.setShadeMode(ShadeMode.Split)
+ assertThat(stackRounding).isEqualTo(StackRounding(roundTop = true, roundBottom = true))
+ }
+
@Test(expected = IllegalStateException::class)
fun setStackBounds_withImproperBounds_throwsException() =
testScope.runTest {
underTest.setStackBounds(
- NotificationContainerBounds(
+ StackBounds(
top = 100f,
bottom = 99f,
)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModelTest.kt
index 693de55..2ccc8b4 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModelTest.kt
@@ -22,6 +22,7 @@
import com.android.systemui.common.shared.model.NotificationContainerBounds
import com.android.systemui.keyguard.domain.interactor.keyguardInteractor
import com.android.systemui.statusbar.notification.stack.domain.interactor.notificationStackAppearanceInteractor
+import com.android.systemui.statusbar.notification.stack.shared.model.StackBounds
import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
import org.junit.Test
@@ -36,9 +37,9 @@
fun onBoundsChanged_setsNotificationContainerBounds() {
underTest.onBoundsChanged(left = 5f, top = 5f, right = 5f, bottom = 5f)
assertThat(kosmos.keyguardInteractor.notificationContainerBounds.value)
- .isEqualTo(NotificationContainerBounds(left = 5f, top = 5f, right = 5f, bottom = 5f))
+ .isEqualTo(NotificationContainerBounds(top = 5f, bottom = 5f))
assertThat(kosmos.notificationStackAppearanceInteractor.stackBounds.value)
- .isEqualTo(NotificationContainerBounds(left = 5f, top = 5f, right = 5f, bottom = 5f))
+ .isEqualTo(StackBounds(left = 5f, top = 5f, right = 5f, bottom = 5f))
}
@Test
fun onContentTopChanged_setsContentTop() {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ActivityStarterImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ActivityStarterImplTest.kt
index 8aa0e3fc..c8062fb 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ActivityStarterImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ActivityStarterImplTest.kt
@@ -16,12 +16,15 @@
package com.android.systemui.statusbar.phone
+import android.app.ActivityOptions
import android.app.PendingIntent
import android.content.Intent
+import android.os.Bundle
import android.os.RemoteException
import android.os.UserHandle
import android.view.View
import android.widget.FrameLayout
+import android.window.SplashScreen.SPLASH_SCREEN_STYLE_SOLID_COLOR
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.keyguard.KeyguardUpdateMonitor
@@ -48,6 +51,7 @@
import com.android.systemui.statusbar.window.StatusBarWindowController
import com.android.systemui.util.concurrency.FakeExecutor
import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.argumentCaptor
import com.android.systemui.util.mockito.eq
import com.android.systemui.util.mockito.nullable
import com.android.systemui.util.mockito.whenever
@@ -173,6 +177,53 @@
)
}
+ fun startPendingIntentDismissingKeyguard_fillInIntentAndExtraOptions_sendAndReturnResult() {
+ val pendingIntent = mock(PendingIntent::class.java)
+ val fillInIntent = mock(Intent::class.java)
+ val parent = FrameLayout(context)
+ val view =
+ object : View(context), LaunchableView {
+ override fun setShouldBlockVisibilityChanges(block: Boolean) {}
+ }
+ parent.addView(view)
+ val controller = ActivityTransitionAnimator.Controller.fromView(view)
+ whenever(pendingIntent.isActivity).thenReturn(true)
+ whenever(keyguardStateController.isShowing).thenReturn(true)
+ whenever(deviceProvisionedController.isDeviceProvisioned).thenReturn(true)
+ whenever(activityIntentHelper.wouldPendingShowOverLockscreen(eq(pendingIntent), anyInt()))
+ .thenReturn(false)
+
+ // extra activity options to set on pending intent
+ val activityOptions = mock(ActivityOptions::class.java)
+ activityOptions.splashScreenStyle = SPLASH_SCREEN_STYLE_SOLID_COLOR
+ activityOptions.isPendingIntentBackgroundActivityLaunchAllowedByPermission = false
+ val bundleCaptor = argumentCaptor<Bundle>()
+
+ underTest.startPendingIntentMaybeDismissingKeyguard(
+ intent = pendingIntent,
+ animationController = controller,
+ intentSentUiThreadCallback = null,
+ fillInIntent = fillInIntent,
+ extraOptions = activityOptions.toBundle(),
+ )
+ mainExecutor.runAllReady()
+
+ // Fill-in intent is passed and options contain extra values specified
+ verify(pendingIntent)
+ .sendAndReturnResult(
+ eq(context),
+ eq(0),
+ eq(fillInIntent),
+ nullable(),
+ nullable(),
+ nullable(),
+ bundleCaptor.capture()
+ )
+ val options = ActivityOptions.fromBundle(bundleCaptor.value)
+ assertThat(options.isPendingIntentBackgroundActivityLaunchAllowedByPermission).isFalse()
+ assertThat(options.splashScreenStyle).isEqualTo(SPLASH_SCREEN_STYLE_SOLID_COLOR)
+ }
+
@Test
fun startPendingIntentDismissingKeyguard_associatedView_getAnimatorController() {
val pendingIntent = mock(PendingIntent::class.java)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/AvalancheControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/AvalancheControllerTest.kt
index be63301..30564bb 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/AvalancheControllerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/AvalancheControllerTest.kt
@@ -60,7 +60,7 @@
private val mGlobalSettings = FakeGlobalSettings()
private val mSystemClock = FakeSystemClock()
private val mExecutor = FakeExecutor(mSystemClock)
- private var testableHeadsUpManager: BaseHeadsUpManager? = null
+ private lateinit var testableHeadsUpManager: BaseHeadsUpManager
@Before
fun setUp() {
@@ -88,20 +88,15 @@
}
private fun createHeadsUpEntry(id: Int): BaseHeadsUpManager.HeadsUpEntry {
- val entry = testableHeadsUpManager!!.createHeadsUpEntry()
-
- entry.setEntry(
+ return testableHeadsUpManager.createHeadsUpEntry(
NotificationEntryBuilder()
.setSbn(HeadsUpManagerTestUtil.createSbn(id, Notification.Builder(mContext, "")))
.build()
)
- return entry
}
private fun createFsiHeadsUpEntry(id: Int): BaseHeadsUpManager.HeadsUpEntry {
- val entry = testableHeadsUpManager!!.createHeadsUpEntry()
- entry.setEntry(createFullScreenIntentEntry(id, mContext))
- return entry
+ return testableHeadsUpManager.createHeadsUpEntry(createFullScreenIntentEntry(id, mContext))
}
@Test
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/BaseHeadsUpManagerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/BaseHeadsUpManagerTest.java
index ed0d272..3dc4495 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/BaseHeadsUpManagerTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/BaseHeadsUpManagerTest.java
@@ -38,7 +38,6 @@
import android.app.Notification;
import android.app.PendingIntent;
import android.app.Person;
-import android.content.Intent;
import android.testing.TestableLooper;
import androidx.test.ext.junit.runners.AndroidJUnit4;
@@ -498,16 +497,16 @@
public void testAlertEntryCompareTo_ongoingCallLessThanActiveRemoteInput() {
final BaseHeadsUpManager hum = createHeadsUpManager();
- final BaseHeadsUpManager.HeadsUpEntry ongoingCall = hum.new HeadsUpEntry();
- ongoingCall.setEntry(new NotificationEntryBuilder()
- .setSbn(HeadsUpManagerTestUtil.createSbn(/* id = */ 0,
- new Notification.Builder(mContext, "")
- .setCategory(Notification.CATEGORY_CALL)
- .setOngoing(true)))
- .build());
+ final BaseHeadsUpManager.HeadsUpEntry ongoingCall = hum.new HeadsUpEntry(
+ new NotificationEntryBuilder()
+ .setSbn(HeadsUpManagerTestUtil.createSbn(/* id = */ 0,
+ new Notification.Builder(mContext, "")
+ .setCategory(Notification.CATEGORY_CALL)
+ .setOngoing(true)))
+ .build());
- final BaseHeadsUpManager.HeadsUpEntry activeRemoteInput = hum.new HeadsUpEntry();
- activeRemoteInput.setEntry(HeadsUpManagerTestUtil.createEntry(/* id = */ 1, mContext));
+ final BaseHeadsUpManager.HeadsUpEntry activeRemoteInput = hum.new HeadsUpEntry(
+ HeadsUpManagerTestUtil.createEntry(/* id = */ 1, mContext));
activeRemoteInput.mRemoteInputActive = true;
assertThat(ongoingCall.compareTo(activeRemoteInput)).isLessThan(0);
@@ -518,18 +517,18 @@
public void testAlertEntryCompareTo_incomingCallLessThanActiveRemoteInput() {
final BaseHeadsUpManager hum = createHeadsUpManager();
- final BaseHeadsUpManager.HeadsUpEntry incomingCall = hum.new HeadsUpEntry();
final Person person = new Person.Builder().setName("person").build();
final PendingIntent intent = mock(PendingIntent.class);
- incomingCall.setEntry(new NotificationEntryBuilder()
- .setSbn(HeadsUpManagerTestUtil.createSbn(/* id = */ 0,
- new Notification.Builder(mContext, "")
- .setStyle(Notification.CallStyle
- .forIncomingCall(person, intent, intent))))
- .build());
+ final BaseHeadsUpManager.HeadsUpEntry incomingCall = hum.new HeadsUpEntry(
+ new NotificationEntryBuilder()
+ .setSbn(HeadsUpManagerTestUtil.createSbn(/* id = */ 0,
+ new Notification.Builder(mContext, "")
+ .setStyle(Notification.CallStyle
+ .forIncomingCall(person, intent, intent))))
+ .build());
- final BaseHeadsUpManager.HeadsUpEntry activeRemoteInput = hum.new HeadsUpEntry();
- activeRemoteInput.setEntry(HeadsUpManagerTestUtil.createEntry(/* id = */ 1, mContext));
+ final BaseHeadsUpManager.HeadsUpEntry activeRemoteInput = hum.new HeadsUpEntry(
+ HeadsUpManagerTestUtil.createEntry(/* id = */ 1, mContext));
activeRemoteInput.mRemoteInputActive = true;
assertThat(incomingCall.compareTo(activeRemoteInput)).isLessThan(0);
@@ -541,8 +540,7 @@
final BaseHeadsUpManager hum = createHeadsUpManager();
// Needs full screen intent in order to be pinned
- final BaseHeadsUpManager.HeadsUpEntry entryToPin = hum.new HeadsUpEntry();
- entryToPin.setEntry(
+ final BaseHeadsUpManager.HeadsUpEntry entryToPin = hum.new HeadsUpEntry(
HeadsUpManagerTestUtil.createFullScreenIntentEntry(/* id = */ 0, mContext));
// Note: the standard way to show a notification would be calling showNotification rather
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/TestableHeadsUpManager.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/TestableHeadsUpManager.java
index d8f77f0..3c9dc63 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/TestableHeadsUpManager.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/TestableHeadsUpManager.java
@@ -54,9 +54,10 @@
mStickyForSomeTimeAutoDismissTime = BaseHeadsUpManagerTest.TEST_STICKY_AUTO_DISMISS_TIME;
}
+ @NonNull
@Override
- protected HeadsUpEntry createHeadsUpEntry() {
- mLastCreatedEntry = spy(super.createHeadsUpEntry());
+ protected HeadsUpEntry createHeadsUpEntry(NotificationEntry entry) {
+ mLastCreatedEntry = spy(super.createHeadsUpEntry(entry));
return mLastCreatedEntry;
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/util/kotlin/DisposableHandlesTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/util/kotlin/DisposableHandlesTest.kt
new file mode 100644
index 0000000..a5ad3c3
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/util/kotlin/DisposableHandlesTest.kt
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.util.kotlin
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.DisposableHandle
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class DisposableHandlesTest : SysuiTestCase() {
+ @Test
+ fun disposeWorksOnce() {
+ var handleDisposeCount = 0
+ val underTest = DisposableHandles()
+
+ // Add a handle
+ underTest += DisposableHandle { handleDisposeCount++ }
+
+ // dispose() calls dispose() on children
+ underTest.dispose()
+ assertThat(handleDisposeCount).isEqualTo(1)
+
+ // Once disposed, children are not disposed again
+ underTest.dispose()
+ assertThat(handleDisposeCount).isEqualTo(1)
+ }
+
+ @Test
+ fun replaceCallsDispose() {
+ var handleDisposeCount1 = 0
+ var handleDisposeCount2 = 0
+ val underTest = DisposableHandles()
+ val handle1 = DisposableHandle { handleDisposeCount1++ }
+ val handle2 = DisposableHandle { handleDisposeCount2++ }
+
+ // First add handle1
+ underTest += handle1
+
+ // replace() calls dispose() on existing children
+ underTest.replaceAll(handle2)
+ assertThat(handleDisposeCount1).isEqualTo(1)
+ assertThat(handleDisposeCount2).isEqualTo(0)
+
+ // Once disposed, replaced children are not disposed again
+ underTest.dispose()
+ assertThat(handleDisposeCount1).isEqualTo(1)
+ assertThat(handleDisposeCount2).isEqualTo(1)
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/domain/interactor/AudioVolumeInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/domain/interactor/AudioVolumeInteractorTest.kt
index 3d93654..5358a6d 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/domain/interactor/AudioVolumeInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/domain/interactor/AudioVolumeInteractorTest.kt
@@ -200,6 +200,15 @@
}
}
+ @Test
+ fun alarmStream_isNotMutable() {
+ with(kosmos) {
+ val isMutable = underTest.isMutable(AudioStream(AudioManager.STREAM_ALARM))
+
+ assertThat(isMutable).isFalse()
+ }
+ }
+
private companion object {
val audioStream = AudioStream(AudioManager.STREAM_SYSTEM)
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/bottombar/ui/viewmodel/BottomBarViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/bottombar/ui/viewmodel/BottomBarViewModelTest.kt
index 471c8d8..8e92557 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/bottombar/ui/viewmodel/BottomBarViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/bottombar/ui/viewmodel/BottomBarViewModelTest.kt
@@ -16,6 +16,7 @@
package com.android.systemui.volume.panel.component.bottombar.ui.viewmodel
+import android.app.ActivityManager
import android.content.Intent
import android.provider.Settings
import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -23,6 +24,7 @@
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.kosmos.testScope
+import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.plugins.activityStarter
import com.android.systemui.testKosmos
import com.android.systemui.util.mockito.capture
@@ -49,6 +51,8 @@
@Captor private lateinit var intentCaptor: ArgumentCaptor<Intent>
+ @Captor private lateinit var activityStartedCaptor: ArgumentCaptor<ActivityStarter.Callback>
+
private val kosmos = testKosmos()
private lateinit var underTest: BottomBarViewModel
@@ -80,10 +84,13 @@
runCurrent()
+ verify(activityStarter).startActivity(capture(intentCaptor), eq(true),
+ capture(activityStartedCaptor))
+ assertThat(intentCaptor.value.action).isEqualTo(Settings.ACTION_SOUND_SETTINGS)
+
+ activityStartedCaptor.value.onActivityStarted(ActivityManager.START_SUCCESS)
val volumePanelState by collectLastValue(volumePanelViewModel.volumePanelState)
assertThat(volumePanelState!!.isVisible).isFalse()
- verify(activityStarter).startActivity(capture(intentCaptor), eq(true))
- assertThat(intentCaptor.value.action).isEqualTo(Settings.ACTION_SOUND_SETTINGS)
}
}
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaDeviceSessionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaDeviceSessionInteractorTest.kt
new file mode 100644
index 0000000..b5c5809
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaDeviceSessionInteractorTest.kt
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.volume.panel.component.mediaoutput.domain.interactor
+
+import android.os.Handler
+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.coroutines.collectLastValue
+import com.android.systemui.kosmos.testCase
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.testKosmos
+import com.android.systemui.volume.localMediaController
+import com.android.systemui.volume.mediaControllerRepository
+import com.android.systemui.volume.mediaOutputInteractor
+import com.android.systemui.volume.remoteMediaController
+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
+@RunWith(AndroidJUnit4::class)
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
+class MediaDeviceSessionInteractorTest : SysuiTestCase() {
+
+ private val kosmos = testKosmos()
+
+ private lateinit var underTest: MediaDeviceSessionInteractor
+
+ @Before
+ fun setup() {
+ with(kosmos) {
+ mediaControllerRepository.setActiveSessions(
+ listOf(localMediaController, remoteMediaController)
+ )
+
+ underTest =
+ MediaDeviceSessionInteractor(
+ testScope.testScheduler,
+ Handler(TestableLooper.get(kosmos.testCase).looper),
+ mediaControllerRepository,
+ )
+ }
+ }
+
+ @Test
+ fun playbackInfo_returnsPlaybackInfo() {
+ with(kosmos) {
+ testScope.runTest {
+ val session by collectLastValue(mediaOutputInteractor.defaultActiveMediaSession)
+ runCurrent()
+ val info by collectLastValue(underTest.playbackInfo(session!!))
+ runCurrent()
+
+ assertThat(info).isEqualTo(localMediaController.playbackInfo)
+ }
+ }
+ }
+
+ @Test
+ fun playbackState_returnsPlaybackState() {
+ with(kosmos) {
+ testScope.runTest {
+ val session by collectLastValue(mediaOutputInteractor.defaultActiveMediaSession)
+ runCurrent()
+ val state by collectLastValue(underTest.playbackState(session!!))
+ runCurrent()
+
+ assertThat(state).isEqualTo(localMediaController.playbackState)
+ }
+ }
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/mediaoutput/ui/viewmodel/MediaOutputViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/mediaoutput/ui/viewmodel/MediaOutputViewModelTest.kt
index dcf635e..6f7f20b 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/mediaoutput/ui/viewmodel/MediaOutputViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/mediaoutput/ui/viewmodel/MediaOutputViewModelTest.kt
@@ -29,9 +29,10 @@
import com.android.systemui.testKosmos
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
+import com.android.systemui.volume.localMediaController
import com.android.systemui.volume.localMediaRepository
-import com.android.systemui.volume.mediaController
import com.android.systemui.volume.mediaControllerRepository
+import com.android.systemui.volume.mediaDeviceSessionInteractor
import com.android.systemui.volume.mediaOutputActionsInteractor
import com.android.systemui.volume.mediaOutputInteractor
import com.android.systemui.volume.panel.volumePanelViewModel
@@ -63,6 +64,7 @@
testScope.backgroundScope,
volumePanelViewModel,
mediaOutputActionsInteractor,
+ mediaDeviceSessionInteractor,
mediaOutputInteractor,
)
@@ -74,11 +76,11 @@
)
}
- whenever(mediaController.packageName).thenReturn("test.pkg")
- whenever(mediaController.sessionToken).thenReturn(MediaSession.Token(0, mock {}))
- whenever(mediaController.playbackState).then { playbackStateBuilder.build() }
+ whenever(localMediaController.packageName).thenReturn("test.pkg")
+ whenever(localMediaController.sessionToken).thenReturn(MediaSession.Token(0, mock {}))
+ whenever(localMediaController.playbackState).then { playbackStateBuilder.build() }
- mediaControllerRepository.setActiveLocalMediaController(mediaController)
+ mediaControllerRepository.setActiveSessions(listOf(localMediaController))
}
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/spatial/domain/SpatialAudioAvailabilityCriteriaTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/spatial/domain/SpatialAudioAvailabilityCriteriaTest.kt
index 1ed7f5d..2f69942 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/spatial/domain/SpatialAudioAvailabilityCriteriaTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/spatial/domain/SpatialAudioAvailabilityCriteriaTest.kt
@@ -32,8 +32,8 @@
import com.android.systemui.testKosmos
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
+import com.android.systemui.volume.localMediaController
import com.android.systemui.volume.localMediaRepository
-import com.android.systemui.volume.mediaController
import com.android.systemui.volume.mediaControllerRepository
import com.android.systemui.volume.panel.component.spatial.spatialAudioComponentInteractor
import com.google.common.truth.Truth.assertThat
@@ -66,11 +66,11 @@
}
)
- whenever(mediaController.packageName).thenReturn("test.pkg")
- whenever(mediaController.sessionToken).thenReturn(MediaSession.Token(0, mock {}))
- whenever(mediaController.playbackState).thenReturn(PlaybackState.Builder().build())
+ whenever(localMediaController.packageName).thenReturn("test.pkg")
+ whenever(localMediaController.sessionToken).thenReturn(MediaSession.Token(0, mock {}))
+ whenever(localMediaController.playbackState).thenReturn(PlaybackState.Builder().build())
- mediaControllerRepository.setActiveLocalMediaController(mediaController)
+ mediaControllerRepository.setActiveSessions(listOf(localMediaController))
underTest = SpatialAudioAvailabilityCriteria(spatialAudioComponentInteractor)
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/spatial/domain/interactor/SpatialAudioComponentInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/spatial/domain/interactor/SpatialAudioComponentInteractorTest.kt
index 281b03d..e36ae60 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/spatial/domain/interactor/SpatialAudioComponentInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/spatial/domain/interactor/SpatialAudioComponentInteractorTest.kt
@@ -34,8 +34,8 @@
import com.android.systemui.testKosmos
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
+import com.android.systemui.volume.localMediaController
import com.android.systemui.volume.localMediaRepository
-import com.android.systemui.volume.mediaController
import com.android.systemui.volume.mediaControllerRepository
import com.android.systemui.volume.mediaOutputInteractor
import com.android.systemui.volume.panel.component.spatial.domain.model.SpatialAudioAvailabilityModel
@@ -70,11 +70,11 @@
}
)
- whenever(mediaController.packageName).thenReturn("test.pkg")
- whenever(mediaController.sessionToken).thenReturn(MediaSession.Token(0, mock {}))
- whenever(mediaController.playbackState).thenReturn(PlaybackState.Builder().build())
+ whenever(localMediaController.packageName).thenReturn("test.pkg")
+ whenever(localMediaController.sessionToken).thenReturn(MediaSession.Token(0, mock {}))
+ whenever(localMediaController.playbackState).thenReturn(PlaybackState.Builder().build())
- mediaControllerRepository.setActiveLocalMediaController(mediaController)
+ mediaControllerRepository.setActiveSessions(listOf(localMediaController))
underTest =
SpatialAudioComponentInteractor(
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/ActivityStarter.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/ActivityStarter.java
index 1126ec3..072ec99 100644
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/ActivityStarter.java
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/ActivityStarter.java
@@ -17,6 +17,7 @@
import android.annotation.Nullable;
import android.app.PendingIntent;
import android.content.Intent;
+import android.os.Bundle;
import android.os.UserHandle;
import android.view.View;
@@ -67,6 +68,17 @@
@Nullable ActivityTransitionAnimator.Controller animationController);
/**
+ * Similar to {@link #startPendingIntentMaybeDismissingKeyguard(PendingIntent, Runnable,
+ * ActivityTransitionAnimator.Controller)}, but also specifies a fill-in intent and extra
+ * options that could be used to populate the pending intent and launch the activity.
+ */
+ void startPendingIntentMaybeDismissingKeyguard(PendingIntent intent,
+ @Nullable Runnable intentSentUiThreadCallback,
+ @Nullable ActivityTransitionAnimator.Controller animationController,
+ @Nullable Intent fillInIntent,
+ @Nullable Bundle extraOptions);
+
+ /**
* The intent flag can be specified in startActivity().
*/
void startActivity(Intent intent, boolean onlyProvisioned, boolean dismissShade, int flags);
diff --git a/packages/SystemUI/res/layout/screenshot_shelf.xml b/packages/SystemUI/res/layout/screenshot_shelf.xml
new file mode 100644
index 0000000..ef1a21f
--- /dev/null
+++ b/packages/SystemUI/res/layout/screenshot_shelf.xml
@@ -0,0 +1,160 @@
+<?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.
+ -->
+<com.android.systemui.screenshot.ui.ScreenshotShelfView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+ <ImageView
+ android:id="@+id/actions_container_background"
+ android:visibility="gone"
+ android:layout_height="0dp"
+ android:layout_width="0dp"
+ android:elevation="4dp"
+ android:background="@drawable/action_chip_container_background"
+ android:layout_marginStart="@dimen/overlay_action_container_margin_horizontal"
+ android:layout_marginBottom="@dimen/overlay_action_container_margin_bottom"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="@+id/actions_container"
+ app:layout_constraintEnd_toEndOf="@+id/actions_container"
+ app:layout_constraintBottom_toTopOf="@id/guideline"/>
+ <HorizontalScrollView
+ android:id="@+id/actions_container"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginEnd="@dimen/overlay_action_container_margin_horizontal"
+ android:paddingEnd="@dimen/overlay_action_container_padding_end"
+ android:paddingVertical="@dimen/overlay_action_container_padding_vertical"
+ android:elevation="4dp"
+ android:scrollbars="none"
+ app:layout_constraintHorizontal_bias="0"
+ app:layout_constraintWidth_percent="1.0"
+ app:layout_constraintWidth_max="wrap"
+ app:layout_constraintStart_toEndOf="@+id/screenshot_preview_border"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintBottom_toBottomOf="@id/actions_container_background">
+ <LinearLayout
+ android:id="@+id/screenshot_actions"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content">
+ <include layout="@layout/overlay_action_chip"
+ android:id="@+id/screenshot_share_chip"/>
+ <include layout="@layout/overlay_action_chip"
+ android:id="@+id/screenshot_edit_chip"/>
+ <include layout="@layout/overlay_action_chip"
+ android:id="@+id/screenshot_scroll_chip"
+ android:visibility="gone" />
+ </LinearLayout>
+ </HorizontalScrollView>
+ <View
+ android:id="@+id/screenshot_preview_border"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ android:layout_marginStart="16dp"
+ android:layout_marginTop="@dimen/overlay_border_width_neg"
+ android:layout_marginEnd="@dimen/overlay_border_width_neg"
+ android:layout_marginBottom="14dp"
+ android:elevation="8dp"
+ android:background="@drawable/overlay_border"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="@id/screenshot_preview"
+ app:layout_constraintEnd_toEndOf="@id/screenshot_preview"
+ app:layout_constraintBottom_toBottomOf="parent"/>
+ <ImageView
+ android:id="@+id/screenshot_preview"
+ android:layout_width="@dimen/overlay_x_scale"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="@dimen/overlay_border_width"
+ android:layout_marginBottom="@dimen/overlay_border_width"
+ android:layout_gravity="center"
+ android:elevation="8dp"
+ android:contentDescription="@string/screenshot_edit_description"
+ android:scaleType="fitEnd"
+ android:background="@drawable/overlay_preview_background"
+ android:adjustViewBounds="true"
+ android:clickable="true"
+ app:layout_constraintStart_toStartOf="@id/screenshot_preview_border"
+ app:layout_constraintBottom_toBottomOf="@id/screenshot_preview_border"/>
+ <ImageView
+ android:id="@+id/screenshot_badge"
+ android:layout_width="56dp"
+ android:layout_height="56dp"
+ android:visibility="gone"
+ android:elevation="9dp"
+ app:layout_constraintBottom_toBottomOf="@id/screenshot_preview_border"
+ app:layout_constraintEnd_toEndOf="@id/screenshot_preview_border"/>
+ <FrameLayout
+ android:id="@+id/screenshot_dismiss_button"
+ android:layout_width="@dimen/overlay_dismiss_button_tappable_size"
+ android:layout_height="@dimen/overlay_dismiss_button_tappable_size"
+ android:elevation="11dp"
+ android:visibility="gone"
+ app:layout_constraintStart_toEndOf="@id/screenshot_preview"
+ app:layout_constraintEnd_toEndOf="@id/screenshot_preview"
+ app:layout_constraintTop_toTopOf="@id/screenshot_preview"
+ app:layout_constraintBottom_toTopOf="@id/screenshot_preview"
+ android:contentDescription="@string/screenshot_dismiss_description">
+ <ImageView
+ android:id="@+id/screenshot_dismiss_image"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_margin="@dimen/overlay_dismiss_button_margin"
+ android:background="@drawable/circular_background"
+ android:backgroundTint="?androidprv:attr/materialColorPrimary"
+ android:tint="?androidprv:attr/materialColorOnPrimary"
+ android:padding="4dp"
+ android:src="@drawable/ic_close"/>
+ </FrameLayout>
+ <ImageView
+ android:id="@+id/screenshot_scrollable_preview"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:scaleType="matrix"
+ android:visibility="gone"
+ app:layout_constraintStart_toStartOf="@id/screenshot_preview"
+ app:layout_constraintTop_toTopOf="@id/screenshot_preview"
+ android:elevation="7dp"/>
+
+ <androidx.constraintlayout.widget.Guideline
+ android:id="@+id/guideline"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ app:layout_constraintGuide_end="0dp" />
+
+ <FrameLayout
+ android:id="@+id/screenshot_message_container"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginHorizontal="@dimen/overlay_action_container_margin_horizontal"
+ android:layout_marginTop="4dp"
+ android:layout_marginBottom="@dimen/overlay_action_container_margin_bottom"
+ android:paddingHorizontal="@dimen/overlay_action_container_padding_end"
+ android:paddingVertical="@dimen/overlay_action_container_padding_vertical"
+ android:elevation="4dp"
+ android:background="@drawable/action_chip_container_background"
+ android:visibility="gone"
+ app:layout_constraintTop_toBottomOf="@id/guideline"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintWidth_max="450dp"
+ app:layout_constraintHorizontal_bias="0">
+ <include layout="@layout/screenshot_work_profile_first_run" />
+ <include layout="@layout/screenshot_detection_notice" />
+ </FrameLayout>
+</com.android.systemui.screenshot.ui.ScreenshotShelfView>
diff --git a/packages/SystemUI/res/layout/window_magnification_settings_view.xml b/packages/SystemUI/res/layout/window_magnification_settings_view.xml
index efdb0a3..704cf0b 100644
--- a/packages/SystemUI/res/layout/window_magnification_settings_view.xml
+++ b/packages/SystemUI/res/layout/window_magnification_settings_view.xml
@@ -29,9 +29,13 @@
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
+ android:id="@+id/magnifier_size_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
+ android:singleLine="true"
+ android:scrollHorizontally="true"
+ android:ellipsize="marquee"
android:text="@string/accessibility_magnifier_size"
android:textAppearance="@style/TextAppearance.MagnificationSetting.Title"
android:focusable="true"
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 774bbe5..3029888 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -235,6 +235,8 @@
<string name="screenshot_edit_label">Edit</string>
<!-- Content description indicating that tapping the element will allow editing the screenshot [CHAR LIMIT=NONE] -->
<string name="screenshot_edit_description">Edit screenshot</string>
+ <!-- Label for UI element which allows sharing the screenshot [CHAR LIMIT=30] -->
+ <string name="screenshot_share_label">Share</string>
<!-- Content description indicating that tapping the element will allow sharing the screenshot [CHAR LIMIT=NONE] -->
<string name="screenshot_share_description">Share screenshot</string>
<!-- Label for UI element which allows the user to capture additional off-screen content in a screenshot. [CHAR LIMIT=30] -->
@@ -1437,8 +1439,11 @@
<!-- Indication on the keyguard that appears when a trust agents unlocks the device. [CHAR LIMIT=40] -->
<string name="keyguard_indication_trust_unlocked">Kept unlocked by TrustAgent</string>
- <!-- Message asking the user to authenticate with primary authentication methods (PIN/pattern/password) or biometrics after the device is locked by adaptive auth. [CHAR LIMIT=60] -->
- <string name="kg_prompt_after_adaptive_auth_lock">Theft protection\nDevice locked, too many unlock attempts</string>
+ <!-- Message asking the user to authenticate with primary authentication methods (PIN/pattern/password) or biometrics after the device is locked by adaptive auth. [CHAR LIMIT=70] -->
+ <string name="kg_prompt_after_adaptive_auth_lock">Device was locked, too many authentication attempts</string>
+
+ <!-- Indication on the keyguard that appears after the device is locked by adaptive auth. [CHAR LIMIT=60] -->
+ <string name="keyguard_indication_after_adaptive_auth_lock">Device locked\nFailed authentication</string>
<!-- Accessibility string for current zen mode and selected exit condition. A template that simply concatenates existing mode string and the current condition description. [CHAR LIMIT=20] -->
<string name="zen_mode_and_condition"><xliff:g id="zen_mode" example="Priority interruptions only">%1$s</xliff:g>. <xliff:g id="exit_condition" example="For one hour">%2$s</xliff:g></string>
@@ -1991,8 +1996,6 @@
<string name="group_system_cycle_back">Cycle backward through recent apps</string>
<!-- User visible title for the keyboard shortcut that accesses list of all apps and search. [CHAR LIMIT=70] -->
<string name="group_system_access_all_apps_search">Open apps list</string>
- <!-- User visible title for the keyboard shortcut that hides and (re)showes taskbar. [CHAR LIMIT=70] -->
- <string name="group_system_hide_reshow_taskbar">Show taskbar</string>
<!-- User visible title for the keyboard shortcut that accesses [system] settings. [CHAR LIMIT=70] -->
<string name="group_system_access_system_settings">Open settings</string>
<!-- User visible title for the keyboard shortcut that accesses Assistant app. [CHAR LIMIT=70] -->
@@ -2010,6 +2013,10 @@
<string name="system_multitasking_lhs">Enter split screen with current app to LHS</string>
<!-- User visible title for the keyboard shortcut that switches from split screen to full screen [CHAR LIMIT=70] -->
<string name="system_multitasking_full_screen">Switch from split screen to full screen</string>
+ <!-- User visible title for the keyboard shortcut that switches to app on right or below while using split screen [CHAR LIMIT=70] -->
+ <string name="system_multitasking_splitscreen_focus_rhs">Switch to app on right or below while using split screen</string>
+ <!-- User visible title for the keyboard shortcut that switches to app on left or above while using split screen [CHAR LIMIT=70] -->
+ <string name="system_multitasking_splitscreen_focus_lhs">Switch to app on left or above while using split screen</string>
<!-- User visible title for the keyboard shortcut that replaces an app from one to another during split screen [CHAR LIMIT=70] -->
<string name="system_multitasking_replace">During split screen: replace an app from one to another</string>
diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml
index 59516be..0483a07 100644
--- a/packages/SystemUI/res/values/styles.xml
+++ b/packages/SystemUI/res/values/styles.xml
@@ -962,7 +962,7 @@
<item name="android:windowBackground">@android:color/transparent</item>
<item name="android:backgroundDimEnabled">true</item>
<item name="android:windowCloseOnTouchOutside">true</item>
- <item name="android:windowAnimationStyle">@android:style/Animation.Dialog</item>
+ <item name="android:windowAnimationStyle">@null</item>
</style>
<style name="Widget.SliceView.VolumePanel">
diff --git a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt
index 8a2245d..48271de 100644
--- a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt
+++ b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt
@@ -31,7 +31,6 @@
import androidx.annotation.VisibleForTesting
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.repeatOnLifecycle
-import com.android.systemui.Flags.migrateClocksToBlueprint
import com.android.systemui.broadcast.BroadcastDispatcher
import com.android.systemui.customization.R
import com.android.systemui.dagger.qualifiers.Background
@@ -39,6 +38,7 @@
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.flags.FeatureFlagsClassic
import com.android.systemui.flags.Flags.REGION_SAMPLING
+import com.android.systemui.keyguard.MigrateClocksToBlueprint
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
import com.android.systemui.keyguard.shared.model.KeyguardState.AOD
@@ -328,7 +328,7 @@
object : KeyguardUpdateMonitorCallback() {
override fun onKeyguardVisibilityChanged(visible: Boolean) {
isKeyguardVisible = visible
- if (!migrateClocksToBlueprint()) {
+ if (!MigrateClocksToBlueprint.isEnabled) {
if (!isKeyguardVisible) {
clock?.run {
smallClock.animations.doze(if (isDozing) 1f else 0f)
@@ -368,7 +368,7 @@
}
private fun refreshTime() {
- if (!migrateClocksToBlueprint()) {
+ if (!MigrateClocksToBlueprint.isEnabled) {
return
}
@@ -427,7 +427,7 @@
parent.repeatWhenAttached {
repeatOnLifecycle(Lifecycle.State.CREATED) {
listenForDozing(this)
- if (migrateClocksToBlueprint()) {
+ if (MigrateClocksToBlueprint.isEnabled) {
listenForDozeAmountTransition(this)
listenForAnyStateToAodTransition(this)
} else {
diff --git a/packages/SystemUI/src/com/android/keyguard/ConnectedDisplayKeyguardPresentation.kt b/packages/SystemUI/src/com/android/keyguard/ConnectedDisplayKeyguardPresentation.kt
index 630610d..df77a58 100644
--- a/packages/SystemUI/src/com/android/keyguard/ConnectedDisplayKeyguardPresentation.kt
+++ b/packages/SystemUI/src/com/android/keyguard/ConnectedDisplayKeyguardPresentation.kt
@@ -30,7 +30,7 @@
import android.widget.FrameLayout
import android.widget.FrameLayout.LayoutParams
import com.android.keyguard.dagger.KeyguardStatusViewComponent
-import com.android.systemui.Flags.migrateClocksToBlueprint
+import com.android.systemui.keyguard.MigrateClocksToBlueprint
import com.android.systemui.plugins.clocks.ClockController
import com.android.systemui.plugins.clocks.ClockFaceController
import com.android.systemui.res.R
@@ -95,7 +95,7 @@
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
- if (migrateClocksToBlueprint()) {
+ if (MigrateClocksToBlueprint.isEnabled) {
onCreateV2()
} else {
onCreate()
@@ -132,7 +132,7 @@
}
override fun onAttachedToWindow() {
- if (migrateClocksToBlueprint()) {
+ if (MigrateClocksToBlueprint.isEnabled) {
clockRegistry.registerClockChangeListener(clockChangedListener)
clockEventController.registerListeners(clock!!)
@@ -141,7 +141,7 @@
}
override fun onDetachedFromWindow() {
- if (migrateClocksToBlueprint()) {
+ if (MigrateClocksToBlueprint.isEnabled) {
clockEventController.unregisterListeners()
clockRegistry.unregisterClockChangeListener(clockChangedListener)
}
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java
index 28013c6..4a96e9e 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java
@@ -3,7 +3,6 @@
import static com.android.keyguard.KeyguardStatusAreaView.TRANSLATE_X_CLOCK_DESIGN;
import static com.android.keyguard.KeyguardStatusAreaView.TRANSLATE_Y_CLOCK_DESIGN;
import static com.android.keyguard.KeyguardStatusAreaView.TRANSLATE_Y_CLOCK_SIZE;
-import static com.android.systemui.Flags.migrateClocksToBlueprint;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
@@ -23,6 +22,7 @@
import com.android.app.animation.Interpolators;
import com.android.keyguard.dagger.KeyguardStatusViewScope;
+import com.android.systemui.keyguard.MigrateClocksToBlueprint;
import com.android.systemui.log.LogBuffer;
import com.android.systemui.log.core.LogLevel;
import com.android.systemui.plugins.clocks.ClockController;
@@ -192,7 +192,7 @@
@Override
protected void onFinishInflate() {
super.onFinishInflate();
- if (!migrateClocksToBlueprint()) {
+ if (!MigrateClocksToBlueprint.isEnabled()) {
mSmallClockFrame = findViewById(R.id.lockscreen_clock_view);
mLargeClockFrame = findViewById(R.id.lockscreen_clock_view_large);
mStatusArea = findViewById(R.id.keyguard_status_area);
@@ -266,7 +266,7 @@
}
void updateClockTargetRegions() {
- if (migrateClocksToBlueprint()) {
+ if (MigrateClocksToBlueprint.isEnabled()) {
return;
}
if (mClock != null) {
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java
index e621ffe..5b8eb9d 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java
@@ -21,7 +21,6 @@
import static com.android.keyguard.KeyguardClockSwitch.LARGE;
import static com.android.keyguard.KeyguardClockSwitch.SMALL;
-import static com.android.systemui.Flags.migrateClocksToBlueprint;
import static com.android.systemui.Flags.smartspaceRelocateToBottom;
import static com.android.systemui.flags.Flags.LOCKSCREEN_WALLPAPER_DREAM_ENABLED;
import static com.android.systemui.util.kotlin.JavaAdapterKt.collectFlow;
@@ -45,6 +44,7 @@
import com.android.systemui.dump.DumpManager;
import com.android.systemui.flags.FeatureFlagsClassic;
import com.android.systemui.keyguard.KeyguardUnlockAnimationController;
+import com.android.systemui.keyguard.MigrateClocksToBlueprint;
import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor;
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor;
import com.android.systemui.keyguard.ui.view.InWindowLauncherUnlockAnimationManager;
@@ -202,7 +202,7 @@
mClockChangedListener = new ClockRegistry.ClockChangeListener() {
@Override
public void onCurrentClockChanged() {
- if (!migrateClocksToBlueprint()) {
+ if (!MigrateClocksToBlueprint.isEnabled()) {
setClock(mClockRegistry.createCurrentClock());
}
}
@@ -245,7 +245,7 @@
protected void onInit() {
mKeyguardSliceViewController.init();
- if (!migrateClocksToBlueprint()) {
+ if (!MigrateClocksToBlueprint.isEnabled()) {
mSmallClockFrame = mView.findViewById(R.id.lockscreen_clock_view);
mLargeClockFrame = mView.findViewById(R.id.lockscreen_clock_view_large);
}
@@ -340,7 +340,7 @@
addDateWeatherView();
}
}
- if (!migrateClocksToBlueprint()) {
+ if (!MigrateClocksToBlueprint.isEnabled()) {
setDateWeatherVisibility();
setWeatherVisibility();
}
@@ -348,7 +348,7 @@
}
int getNotificationIconAreaHeight() {
- if (migrateClocksToBlueprint()) {
+ if (MigrateClocksToBlueprint.isEnabled()) {
return 0;
} else if (NotificationIconContainerRefactor.isEnabled()) {
return mAodIconContainer != null ? mAodIconContainer.getHeight() : 0;
@@ -391,7 +391,7 @@
}
private void addDateWeatherView() {
- if (migrateClocksToBlueprint()) {
+ if (MigrateClocksToBlueprint.isEnabled()) {
return;
}
mDateWeatherView = (ViewGroup) mSmartspaceController.buildAndConnectDateView(mView);
@@ -407,7 +407,7 @@
}
private void addWeatherView() {
- if (migrateClocksToBlueprint()) {
+ if (MigrateClocksToBlueprint.isEnabled()) {
return;
}
LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(
@@ -420,7 +420,7 @@
}
private void addSmartspaceView() {
- if (migrateClocksToBlueprint()) {
+ if (MigrateClocksToBlueprint.isEnabled()) {
return;
}
@@ -528,7 +528,7 @@
*/
void updatePosition(int x, float scale, AnimationProperties props, boolean animate) {
x = getCurrentLayoutDirection() == View.LAYOUT_DIRECTION_RTL ? -x : x;
- if (!migrateClocksToBlueprint()) {
+ if (!MigrateClocksToBlueprint.isEnabled()) {
PropertyAnimator.setProperty(mSmallClockFrame, AnimatableProperty.TRANSLATION_X,
x, props, animate);
PropertyAnimator.setProperty(mLargeClockFrame, AnimatableProperty.SCALE_X,
@@ -554,7 +554,7 @@
return 0;
}
- if (migrateClocksToBlueprint()) {
+ if (MigrateClocksToBlueprint.isEnabled()) {
return 0;
}
@@ -589,14 +589,14 @@
}
boolean isClockTopAligned() {
- if (migrateClocksToBlueprint()) {
+ if (MigrateClocksToBlueprint.isEnabled()) {
return mKeyguardClockInteractor.getClockSize().getValue() == LARGE;
}
return mLargeClockFrame.getVisibility() != View.VISIBLE;
}
private void updateAodIcons() {
- if (!migrateClocksToBlueprint()) {
+ if (!MigrateClocksToBlueprint.isEnabled()) {
NotificationIconContainer nic = (NotificationIconContainer)
mView.findViewById(
com.android.systemui.res.R.id.left_aligned_notification_icon_container);
@@ -616,7 +616,7 @@
}
private void setClock(ClockController clock) {
- if (migrateClocksToBlueprint()) {
+ if (MigrateClocksToBlueprint.isEnabled()) {
return;
}
if (clock != null && mLogBuffer != null) {
@@ -630,8 +630,8 @@
@Nullable
public ClockController getClock() {
- if (migrateClocksToBlueprint()) {
- return mKeyguardClockInteractor.getClock();
+ if (MigrateClocksToBlueprint.isEnabled()) {
+ return mKeyguardClockInteractor.getCurrentClock().getValue();
} else {
return mClockEventController.getClock();
}
@@ -642,7 +642,7 @@
}
private void updateDoubleLineClock() {
- if (migrateClocksToBlueprint()) {
+ if (MigrateClocksToBlueprint.isEnabled()) {
return;
}
mCanShowDoubleLineClock = mSecureSettings.getIntForUser(
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java
index 7f9ae5e..603a47e 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java
@@ -20,7 +20,6 @@
import static androidx.constraintlayout.widget.ConstraintSet.PARENT_ID;
import static com.android.internal.jank.InteractionJankMonitor.CUJ_LOCKSCREEN_CLOCK_MOVE_ANIMATION;
-import static com.android.systemui.Flags.migrateClocksToBlueprint;
import static com.android.systemui.util.kotlin.JavaAdapterKt.collectFlow;
import android.animation.Animator;
@@ -52,6 +51,7 @@
import com.android.systemui.Dumpable;
import com.android.systemui.animation.ViewHierarchyAnimator;
import com.android.systemui.dump.DumpManager;
+import com.android.systemui.keyguard.MigrateClocksToBlueprint;
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor;
import com.android.systemui.plugins.clocks.ClockController;
import com.android.systemui.power.domain.interactor.PowerInteractor;
@@ -223,7 +223,7 @@
}
mDumpManager.registerDumpable(getInstanceName(), this);
- if (migrateClocksToBlueprint()) {
+ if (MigrateClocksToBlueprint.isEnabled()) {
startCoroutines(EmptyCoroutineContext.INSTANCE);
mView.setVisibility(View.GONE);
}
@@ -250,7 +250,7 @@
@Override
protected void onViewAttached() {
mStatusArea = mView.findViewById(R.id.keyguard_status_area);
- if (migrateClocksToBlueprint()) {
+ if (MigrateClocksToBlueprint.isEnabled()) {
return;
}
@@ -261,7 +261,7 @@
@Override
protected void onViewDetached() {
- if (migrateClocksToBlueprint()) {
+ if (MigrateClocksToBlueprint.isEnabled()) {
return;
}
@@ -485,7 +485,7 @@
boolean splitShadeEnabled,
boolean shouldBeCentered,
boolean animate) {
- if (migrateClocksToBlueprint()) {
+ if (MigrateClocksToBlueprint.isEnabled()) {
mKeyguardInteractor.setClockShouldBeCentered(shouldBeCentered);
} else {
mKeyguardClockSwitchController.setSplitShadeCentered(
@@ -503,7 +503,7 @@
ConstraintSet constraintSet = new ConstraintSet();
constraintSet.clone(layout);
int guideline;
- if (migrateClocksToBlueprint()) {
+ if (MigrateClocksToBlueprint.isEnabled()) {
guideline = R.id.split_shade_guideline;
} else {
guideline = R.id.qs_edge_guideline;
@@ -548,7 +548,7 @@
&& clock.getLargeClock().getConfig().getHasCustomPositionUpdatedAnimation();
// When migrateClocksToBlueprint is on, customized clock animation is conducted in
// KeyguardClockViewBinder
- if (customClockAnimation && !migrateClocksToBlueprint()) {
+ if (customClockAnimation && !MigrateClocksToBlueprint.isEnabled()) {
// Find the clock, so we can exclude it from this transition.
FrameLayout clockContainerView = mView.findViewById(R.id.lockscreen_clock_view_large);
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardVisibilityHelper.java b/packages/SystemUI/src/com/android/keyguard/KeyguardVisibilityHelper.java
index f5a6cb3..fd8b6d5 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardVisibilityHelper.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardVisibilityHelper.java
@@ -16,7 +16,6 @@
package com.android.keyguard;
-import static com.android.systemui.Flags.migrateClocksToBlueprint;
import static com.android.systemui.statusbar.StatusBarState.KEYGUARD;
import static com.android.systemui.statusbar.StatusBarState.SHADE;
@@ -24,6 +23,7 @@
import android.view.View;
import com.android.app.animation.Interpolators;
+import com.android.systemui.keyguard.MigrateClocksToBlueprint;
import com.android.systemui.log.LogBuffer;
import com.android.systemui.log.core.LogLevel;
import com.android.systemui.statusbar.StatusBarState;
@@ -88,7 +88,7 @@
boolean keyguardFadingAway,
boolean goingToFullShade,
int oldStatusBarState) {
- if (migrateClocksToBlueprint()) {
+ if (MigrateClocksToBlueprint.isEnabled()) {
log("Ignoring KeyguardVisibilityelper, migrateClocksToBlueprint flag on");
return;
}
@@ -113,7 +113,7 @@
animProps.setDelay(0).setDuration(160);
log("goingToFullShade && !keyguardFadingAway");
}
- if (migrateClocksToBlueprint()) {
+ if (MigrateClocksToBlueprint.isEnabled()) {
log("Using LockscreenToGoneTransition 1");
} else {
PropertyAnimator.setProperty(
@@ -171,7 +171,7 @@
animProps,
true /* animate */);
} else if (mScreenOffAnimationController.shouldAnimateInKeyguard()) {
- if (migrateClocksToBlueprint()) {
+ if (MigrateClocksToBlueprint.isEnabled()) {
log("Using GoneToAodTransition");
mKeyguardViewVisibilityAnimating = false;
} else {
@@ -187,7 +187,7 @@
mView.setVisibility(View.VISIBLE);
}
} else {
- if (migrateClocksToBlueprint()) {
+ if (MigrateClocksToBlueprint.isEnabled()) {
log("Using LockscreenToGoneTransition 2");
} else {
log("Direct set Visibility to GONE");
diff --git a/packages/SystemUI/src/com/android/keyguard/LockIconViewController.java b/packages/SystemUI/src/com/android/keyguard/LockIconViewController.java
index 039a2e5..8f1a5f7 100644
--- a/packages/SystemUI/src/com/android/keyguard/LockIconViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/LockIconViewController.java
@@ -22,8 +22,6 @@
import static com.android.keyguard.LockIconView.ICON_FINGERPRINT;
import static com.android.keyguard.LockIconView.ICON_LOCK;
import static com.android.keyguard.LockIconView.ICON_UNLOCK;
-import static com.android.systemui.Flags.keyguardBottomAreaRefactor;
-import static com.android.systemui.Flags.migrateClocksToBlueprint;
import static com.android.systemui.doze.util.BurnInHelperKt.getBurnInOffset;
import static com.android.systemui.flags.Flags.DOZING_MIGRATION_1;
import static com.android.systemui.flags.Flags.LOCKSCREEN_WALLPAPER_DREAM_ENABLED;
@@ -68,6 +66,8 @@
import com.android.systemui.dump.DumpManager;
import com.android.systemui.flags.FeatureFlags;
import com.android.systemui.flags.Flags;
+import com.android.systemui.keyguard.KeyguardBottomAreaRefactor;
+import com.android.systemui.keyguard.MigrateClocksToBlueprint;
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor;
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor;
import com.android.systemui.keyguard.shared.model.TransitionStep;
@@ -453,7 +453,7 @@
private void updateLockIconLocation() {
final float scaleFactor = mAuthController.getScaleFactor();
final int scaledPadding = (int) (mDefaultPaddingPx * scaleFactor);
- if (keyguardBottomAreaRefactor() || migrateClocksToBlueprint()) {
+ if (KeyguardBottomAreaRefactor.isEnabled() || MigrateClocksToBlueprint.isEnabled()) {
mView.getLockIcon().setPadding(scaledPadding, scaledPadding, scaledPadding,
scaledPadding);
} else {
diff --git a/packages/SystemUI/src/com/android/keyguard/dagger/ClockRegistryModule.java b/packages/SystemUI/src/com/android/keyguard/dagger/ClockRegistryModule.java
index a0f15ef..781f6dd 100644
--- a/packages/SystemUI/src/com/android/keyguard/dagger/ClockRegistryModule.java
+++ b/packages/SystemUI/src/com/android/keyguard/dagger/ClockRegistryModule.java
@@ -16,8 +16,6 @@
package com.android.keyguard.dagger;
-import static com.android.systemui.Flags.migrateClocksToBlueprint;
-
import android.content.Context;
import android.content.res.Resources;
import android.view.LayoutInflater;
@@ -28,6 +26,7 @@
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.flags.FeatureFlags;
import com.android.systemui.flags.Flags;
+import com.android.systemui.keyguard.MigrateClocksToBlueprint;
import com.android.systemui.plugins.PluginManager;
import com.android.systemui.plugins.clocks.ClockMessageBuffers;
import com.android.systemui.res.R;
@@ -70,7 +69,7 @@
layoutInflater,
resources,
featureFlags.isEnabled(Flags.STEP_CLOCK_ANIMATION),
- migrateClocksToBlueprint()),
+ MigrateClocksToBlueprint.isEnabled()),
context.getString(R.string.lockscreen_clock_id_fallback),
clockBuffers,
/* keepAllLoaded = */ false,
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationSettings.java b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationSettings.java
index a98990a..ca24ccb 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationSettings.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationSettings.java
@@ -98,6 +98,7 @@
private ImageButton mMediumButton;
private ImageButton mLargeButton;
private Button mDoneButton;
+ private TextView mSizeTitle;
private Button mEditButton;
private ImageButton mFullScreenButton;
private int mLastSelectedButtonIndex = MagnificationSize.NONE;
@@ -521,6 +522,7 @@
mMediumButton = mSettingView.findViewById(R.id.magnifier_medium_button);
mLargeButton = mSettingView.findViewById(R.id.magnifier_large_button);
mDoneButton = mSettingView.findViewById(R.id.magnifier_done_button);
+ mSizeTitle = mSettingView.findViewById(R.id.magnifier_size_title);
mEditButton = mSettingView.findViewById(R.id.magnifier_edit_button);
mFullScreenButton = mSettingView.findViewById(R.id.magnifier_full_button);
mAllowDiagonalScrollingTitle =
@@ -548,6 +550,7 @@
mDoneButton.setOnClickListener(mButtonClickListener);
mFullScreenButton.setOnClickListener(mButtonClickListener);
mEditButton.setOnClickListener(mButtonClickListener);
+ mSizeTitle.setSelected(true);
mAllowDiagonalScrollingTitle.setSelected(true);
mSettingView.setOnApplyWindowInsetsListener((v, insets) -> {
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/SideFpsController.kt b/packages/SystemUI/src/com/android/systemui/biometrics/SideFpsController.kt
index ac99fc6..85f63e9 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/SideFpsController.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/SideFpsController.kt
@@ -126,6 +126,7 @@
field?.let { oldView ->
val lottie = oldView.requireViewById(R.id.sidefps_animation) as LottieAnimationView
lottie.pauseAnimation()
+ lottie.removeAllLottieOnCompositionLoadedListener()
windowManager.removeView(oldView)
orientationListener.disable()
}
@@ -288,7 +289,7 @@
}
private fun onOrientationChanged(@BiometricRequestConstants.RequestReason reason: Int) {
- if (overlayView != null) {
+ if (overlayView?.isAttachedToWindow == true) {
createOverlayForDisplay(reason)
}
}
@@ -322,7 +323,7 @@
)
lottie.addLottieOnCompositionLoadedListener {
// Check that view is not stale, and that overlayView has not been hidden/removed
- if (overlayView != null && overlayView == view) {
+ if (overlayView?.isAttachedToWindow == true && overlayView == view) {
updateOverlayParams(display, it.bounds)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/BiometricStatusInteractor.kt b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/BiometricStatusInteractor.kt
index ed1557c..c4967ec 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/BiometricStatusInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/BiometricStatusInteractor.kt
@@ -23,6 +23,7 @@
import com.android.systemui.keyguard.shared.model.FingerprintAuthenticationStatus
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
/** Encapsulates business logic for interacting with biometric authentication state. */
@@ -52,7 +53,7 @@
} else {
AuthenticationReason.NotRunning
}
- }
+ }.distinctUntilChanged()
override val fingerprintAcquiredStatus: Flow<FingerprintAuthenticationStatus> =
biometricStatusRepository.fingerprintAcquiredStatus
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 d849b3a..94e0854 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
@@ -20,7 +20,6 @@
import com.android.systemui.flags.FeatureFlagsClassic
import com.android.systemui.flags.Flags
import javax.inject.Inject
-import kotlinx.coroutines.flow.MutableStateFlow
/** Provides access to bouncer-related application state. */
@SysUISingleton
@@ -29,9 +28,6 @@
constructor(
private val flags: FeatureFlagsClassic,
) {
- /** The user-facing message to show in the bouncer. */
- 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)
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 d8be1af..aeb564d5 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
@@ -16,13 +16,8 @@
package com.android.systemui.bouncer.domain.interactor
-import android.content.Context
import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor
import com.android.systemui.authentication.domain.interactor.AuthenticationResult
-import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
-import com.android.systemui.authentication.shared.model.AuthenticationMethodModel.Password
-import com.android.systemui.authentication.shared.model.AuthenticationMethodModel.Pattern
-import com.android.systemui.authentication.shared.model.AuthenticationMethodModel.Pin
import com.android.systemui.authentication.shared.model.AuthenticationMethodModel.Sim
import com.android.systemui.bouncer.data.repository.BouncerRepository
import com.android.systemui.classifier.FalsingClassifier
@@ -31,7 +26,6 @@
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFaceAuthInteractor
import com.android.systemui.power.domain.interactor.PowerInteractor
-import com.android.systemui.res.R
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.async
@@ -41,7 +35,6 @@
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.launch
/** Encapsulates business logic and application state accessing use-cases. */
@SysUISingleton
@@ -49,16 +42,14 @@
@Inject
constructor(
@Application private val applicationScope: CoroutineScope,
- @Application private val applicationContext: Context,
private val repository: BouncerRepository,
private val authenticationInteractor: AuthenticationInteractor,
private val deviceEntryFaceAuthInteractor: DeviceEntryFaceAuthInteractor,
private val falsingInteractor: FalsingInteractor,
private val powerInteractor: PowerInteractor,
- private val simBouncerInteractor: SimBouncerInteractor,
) {
- /** The user-facing message to show in the bouncer when lockout is not active. */
- val message: StateFlow<String?> = repository.message
+ private val _onIncorrectBouncerInput = MutableSharedFlow<Unit>()
+ val onIncorrectBouncerInput: SharedFlow<Unit> = _onIncorrectBouncerInput
/** Whether the auto confirm feature is enabled for the currently-selected user. */
val isAutoConfirmEnabled: StateFlow<Boolean> = authenticationInteractor.isAutoConfirmEnabled
@@ -119,25 +110,6 @@
)
}
- fun setMessage(message: String?) {
- repository.message.value = message
- }
-
- /**
- * Resets the user-facing message back to the default according to the current authentication
- * method.
- */
- fun resetMessage() {
- applicationScope.launch {
- setMessage(promptMessage(authenticationInteractor.getAuthenticationMethod()))
- }
- }
-
- /** Removes the user-facing message. */
- fun clearMessage() {
- setMessage(null)
- }
-
/**
* Attempts to authenticate based on the given user input.
*
@@ -176,50 +148,17 @@
.async { authenticationInteractor.authenticate(input, tryAutoConfirm) }
.await()
- if (authenticationInteractor.lockoutEndTimestamp != null) {
- clearMessage()
- } else if (
+ if (
authResult == AuthenticationResult.FAILED ||
(authResult == AuthenticationResult.SKIPPED && !tryAutoConfirm)
) {
- showWrongInputMessage()
+ _onIncorrectBouncerInput.emit(Unit)
}
return authResult
}
- /**
- * Shows the a message notifying the user that their credentials input is wrong.
- *
- * Callers should use this instead of [authenticate] when they know ahead of time that an auth
- * attempt will fail but aren't interested in the other side effects like triggering lockout.
- * For example, if the user entered a pattern that's too short, the system can show the error
- * message without having the attempt trigger lockout.
- */
- private suspend fun showWrongInputMessage() {
- setMessage(wrongInputMessage(authenticationInteractor.getAuthenticationMethod()))
- }
-
/** Notifies that the input method editor (software keyboard) has been hidden by the user. */
suspend fun onImeHiddenByUser() {
_onImeHiddenByUser.emit(Unit)
}
-
- private fun promptMessage(authMethod: AuthenticationMethodModel): String {
- return when (authMethod) {
- is Sim -> simBouncerInteractor.getDefaultMessage()
- is Pin -> applicationContext.getString(R.string.keyguard_enter_your_pin)
- is Password -> applicationContext.getString(R.string.keyguard_enter_your_password)
- is Pattern -> applicationContext.getString(R.string.keyguard_enter_your_pattern)
- else -> ""
- }
- }
-
- private fun wrongInputMessage(authMethod: AuthenticationMethodModel): String {
- return when (authMethod) {
- is Pin -> applicationContext.getString(R.string.kg_wrong_pin)
- is Password -> applicationContext.getString(R.string.kg_wrong_password)
- is Pattern -> applicationContext.getString(R.string.kg_wrong_pattern)
- else -> ""
- }
- }
}
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerMessageInteractor.kt b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerMessageInteractor.kt
index 7f6fc91..d20c607 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerMessageInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerMessageInteractor.kt
@@ -33,15 +33,17 @@
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.deviceentry.data.repository.DeviceEntryFaceAuthRepository
+import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFingerprintAuthInteractor
import com.android.systemui.flags.SystemPropertiesHelper
import com.android.systemui.keyguard.data.repository.BiometricSettingsRepository
-import com.android.systemui.keyguard.data.repository.DeviceEntryFingerprintAuthRepository
import com.android.systemui.keyguard.data.repository.TrustRepository
import com.android.systemui.user.data.repository.UserRepository
-import com.android.systemui.util.kotlin.Quint
+import com.android.systemui.util.kotlin.Sextuple
+import com.android.systemui.util.kotlin.combine
import javax.inject.Inject
import kotlin.math.roundToInt
import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.combine
@@ -56,6 +58,7 @@
private const val TAG = "BouncerMessageInteractor"
/** Handles business logic for the primary bouncer message area. */
+@OptIn(ExperimentalCoroutinesApi::class)
@SysUISingleton
class BouncerMessageInteractor
@Inject
@@ -63,23 +66,24 @@
private val repository: BouncerMessageRepository,
private val userRepository: UserRepository,
private val countDownTimerUtil: CountDownTimerUtil,
- private val updateMonitor: KeyguardUpdateMonitor,
+ updateMonitor: KeyguardUpdateMonitor,
trustRepository: TrustRepository,
biometricSettingsRepository: BiometricSettingsRepository,
private val systemPropertiesHelper: SystemPropertiesHelper,
primaryBouncerInteractor: PrimaryBouncerInteractor,
@Application private val applicationScope: CoroutineScope,
private val facePropertyRepository: FacePropertyRepository,
- deviceEntryFingerprintAuthRepository: DeviceEntryFingerprintAuthRepository,
+ private val deviceEntryFingerprintAuthInteractor: DeviceEntryFingerprintAuthInteractor,
faceAuthRepository: DeviceEntryFaceAuthRepository,
private val securityModel: KeyguardSecurityModel,
) {
- private val isFingerprintAuthCurrentlyAllowed =
- deviceEntryFingerprintAuthRepository.isLockedOut
- .isFalse()
- .and(biometricSettingsRepository.isFingerprintAuthCurrentlyAllowed)
- .stateIn(applicationScope, SharingStarted.Eagerly, false)
+ private val isFingerprintAuthCurrentlyAllowedOnBouncer =
+ deviceEntryFingerprintAuthInteractor.isFingerprintCurrentlyAllowedOnBouncer.stateIn(
+ applicationScope,
+ SharingStarted.Eagerly,
+ false
+ )
private val currentSecurityMode
get() = securityModel.getSecurityMode(currentUserId)
@@ -99,13 +103,13 @@
BiometricSourceType.FACE ->
BouncerMessageStrings.incorrectFaceInput(
currentSecurityMode.toAuthModel(),
- isFingerprintAuthCurrentlyAllowed.value
+ isFingerprintAuthCurrentlyAllowedOnBouncer.value
)
.toMessage()
else ->
BouncerMessageStrings.defaultMessage(
currentSecurityMode.toAuthModel(),
- isFingerprintAuthCurrentlyAllowed.value
+ isFingerprintAuthCurrentlyAllowedOnBouncer.value
)
.toMessage()
}
@@ -144,11 +148,12 @@
biometricSettingsRepository.authenticationFlags,
trustRepository.isCurrentUserTrustManaged,
isAnyBiometricsEnabledAndEnrolled,
- deviceEntryFingerprintAuthRepository.isLockedOut,
+ deviceEntryFingerprintAuthInteractor.isLockedOut,
faceAuthRepository.isLockedOut,
- ::Quint
+ isFingerprintAuthCurrentlyAllowedOnBouncer,
+ ::Sextuple
)
- .map { (flags, _, biometricsEnrolledAndEnabled, fpLockedOut, faceLockedOut) ->
+ .map { (flags, _, biometricsEnrolledAndEnabled, fpLockedOut, faceLockedOut, _) ->
val isTrustUsuallyManaged = trustRepository.isCurrentUserTrustUsuallyManaged.value
val trustOrBiometricsAvailable =
(isTrustUsuallyManaged || biometricsEnrolledAndEnabled)
@@ -193,14 +198,14 @@
} else {
BouncerMessageStrings.faceLockedOut(
currentSecurityMode.toAuthModel(),
- isFingerprintAuthCurrentlyAllowed.value
+ isFingerprintAuthCurrentlyAllowedOnBouncer.value
)
.toMessage()
}
} else if (flags.isSomeAuthRequiredAfterAdaptiveAuthRequest) {
BouncerMessageStrings.authRequiredAfterAdaptiveAuthRequest(
currentSecurityMode.toAuthModel(),
- isFingerprintAuthCurrentlyAllowed.value
+ isFingerprintAuthCurrentlyAllowedOnBouncer.value
)
.toMessage()
} else if (
@@ -209,19 +214,19 @@
) {
BouncerMessageStrings.nonStrongAuthTimeout(
currentSecurityMode.toAuthModel(),
- isFingerprintAuthCurrentlyAllowed.value
+ isFingerprintAuthCurrentlyAllowedOnBouncer.value
)
.toMessage()
} else if (isTrustUsuallyManaged && flags.someAuthRequiredAfterUserRequest) {
BouncerMessageStrings.trustAgentDisabled(
currentSecurityMode.toAuthModel(),
- isFingerprintAuthCurrentlyAllowed.value
+ isFingerprintAuthCurrentlyAllowedOnBouncer.value
)
.toMessage()
} else if (isTrustUsuallyManaged && flags.someAuthRequiredAfterTrustAgentExpired) {
BouncerMessageStrings.trustAgentDisabled(
currentSecurityMode.toAuthModel(),
- isFingerprintAuthCurrentlyAllowed.value
+ isFingerprintAuthCurrentlyAllowedOnBouncer.value
)
.toMessage()
} else if (trustOrBiometricsAvailable && flags.isInUserLockdown) {
@@ -265,7 +270,7 @@
repository.setMessage(
BouncerMessageStrings.incorrectSecurityInput(
currentSecurityMode.toAuthModel(),
- isFingerprintAuthCurrentlyAllowed.value
+ isFingerprintAuthCurrentlyAllowedOnBouncer.value
)
.toMessage()
)
@@ -274,14 +279,22 @@
fun setFingerprintAcquisitionMessage(value: String?) {
if (!Flags.revampedBouncerMessages()) return
repository.setMessage(
- defaultMessage(currentSecurityMode, value, isFingerprintAuthCurrentlyAllowed.value)
+ defaultMessage(
+ currentSecurityMode,
+ value,
+ isFingerprintAuthCurrentlyAllowedOnBouncer.value
+ )
)
}
fun setFaceAcquisitionMessage(value: String?) {
if (!Flags.revampedBouncerMessages()) return
repository.setMessage(
- defaultMessage(currentSecurityMode, value, isFingerprintAuthCurrentlyAllowed.value)
+ defaultMessage(
+ currentSecurityMode,
+ value,
+ isFingerprintAuthCurrentlyAllowedOnBouncer.value
+ )
)
}
@@ -289,7 +302,11 @@
if (!Flags.revampedBouncerMessages()) return
repository.setMessage(
- defaultMessage(currentSecurityMode, value, isFingerprintAuthCurrentlyAllowed.value)
+ defaultMessage(
+ currentSecurityMode,
+ value,
+ isFingerprintAuthCurrentlyAllowedOnBouncer.value
+ )
)
}
@@ -297,7 +314,7 @@
get() =
BouncerMessageStrings.defaultMessage(
currentSecurityMode.toAuthModel(),
- isFingerprintAuthCurrentlyAllowed.value
+ isFingerprintAuthCurrentlyAllowedOnBouncer.value
)
.toMessage()
@@ -355,11 +372,6 @@
private fun Flow<Boolean>.or(anotherFlow: Flow<Boolean>) =
this.combine(anotherFlow) { a, b -> a || b }
-private fun Flow<Boolean>.and(anotherFlow: Flow<Boolean>) =
- this.combine(anotherFlow) { a, b -> a && b }
-
-private fun Flow<Boolean>.isFalse() = this.map { !it }
-
private fun defaultMessage(
securityMode: SecurityMode,
secondaryMessage: String?,
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/BouncerViewModule.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/BouncerViewModule.kt
index f3903de..aebc50f 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/BouncerViewModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/BouncerViewModule.kt
@@ -18,6 +18,7 @@
import android.app.AlertDialog
import android.content.Context
+import com.android.systemui.bouncer.ui.viewmodel.BouncerMessageViewModelModule
import com.android.systemui.bouncer.ui.viewmodel.BouncerViewModelModule
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
@@ -30,6 +31,7 @@
includes =
[
BouncerViewModelModule::class,
+ BouncerMessageViewModelModule::class,
],
)
interface BouncerViewModule {
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModel.kt
index 0d7f6dc..4fbf735 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModel.kt
@@ -57,17 +57,11 @@
*/
@get:StringRes abstract val lockoutMessageId: Int
- /** Notifies that the UI has been shown to the user. */
- fun onShown() {
- interactor.resetMessage()
- }
-
/**
* Notifies that the UI has been hidden from the user (after any transitions have completed).
*/
open fun onHidden() {
clearInput()
- interactor.resetMessage()
}
/** Notifies that the user has placed down a pointer. */
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerMessageViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerMessageViewModel.kt
new file mode 100644
index 0000000..6cb9b16
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerMessageViewModel.kt
@@ -0,0 +1,436 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS 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.bouncer.ui.viewmodel
+
+import android.content.Context
+import android.util.PluralsMessageFormatter
+import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor
+import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
+import com.android.systemui.bouncer.domain.interactor.BouncerInteractor
+import com.android.systemui.bouncer.domain.interactor.SimBouncerInteractor
+import com.android.systemui.bouncer.shared.flag.ComposeBouncerFlags
+import com.android.systemui.bouncer.shared.model.BouncerMessagePair
+import com.android.systemui.bouncer.shared.model.BouncerMessageStrings
+import com.android.systemui.bouncer.shared.model.primaryMessage
+import com.android.systemui.bouncer.shared.model.secondaryMessage
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.deviceentry.domain.interactor.BiometricMessageInteractor
+import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFaceAuthInteractor
+import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFingerprintAuthInteractor
+import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor
+import com.android.systemui.deviceentry.shared.model.DeviceEntryRestrictionReason
+import com.android.systemui.deviceentry.shared.model.FaceFailureMessage
+import com.android.systemui.deviceentry.shared.model.FaceLockoutMessage
+import com.android.systemui.deviceentry.shared.model.FaceTimeoutMessage
+import com.android.systemui.deviceentry.shared.model.FingerprintFailureMessage
+import com.android.systemui.deviceentry.shared.model.FingerprintLockoutMessage
+import com.android.systemui.res.R.string.kg_too_many_failed_attempts_countdown
+import com.android.systemui.user.ui.viewmodel.UserSwitcherViewModel
+import com.android.systemui.user.ui.viewmodel.UserViewModel
+import com.android.systemui.util.kotlin.Utils.Companion.sample
+import com.android.systemui.util.time.SystemClock
+import dagger.Module
+import dagger.Provides
+import kotlin.math.ceil
+import kotlin.math.max
+import kotlin.time.Duration.Companion.seconds
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.emptyFlow
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.launch
+
+/** Holds UI state for the 2-line status message shown on the bouncer. */
+@OptIn(ExperimentalCoroutinesApi::class)
+class BouncerMessageViewModel(
+ @Application private val applicationContext: Context,
+ @Application private val applicationScope: CoroutineScope,
+ private val bouncerInteractor: BouncerInteractor,
+ private val simBouncerInteractor: SimBouncerInteractor,
+ private val authenticationInteractor: AuthenticationInteractor,
+ selectedUser: Flow<UserViewModel>,
+ private val clock: SystemClock,
+ private val biometricMessageInteractor: BiometricMessageInteractor,
+ private val faceAuthInteractor: DeviceEntryFaceAuthInteractor,
+ private val deviceEntryInteractor: DeviceEntryInteractor,
+ private val fingerprintInteractor: DeviceEntryFingerprintAuthInteractor,
+ flags: ComposeBouncerFlags,
+) {
+ /**
+ * A message shown when the user has attempted the wrong credential too many times and now must
+ * wait a while before attempting to authenticate again.
+ *
+ * This is updated every second (countdown) during the lockout. When lockout is not active, this
+ * is `null` and no lockout message should be shown.
+ */
+ private val lockoutMessage: MutableStateFlow<MessageViewModel?> = MutableStateFlow(null)
+
+ /** Whether there is a lockout message that is available to be shown in the status message. */
+ val isLockoutMessagePresent: Flow<Boolean> = lockoutMessage.map { it != null }
+
+ /** The user-facing message to show in the bouncer. */
+ val message: MutableStateFlow<MessageViewModel?> = MutableStateFlow(null)
+
+ /** Initializes the bouncer message to default whenever it is shown. */
+ fun onShown() {
+ showDefaultMessage()
+ }
+
+ /** Reset the message shown on the bouncer to the default message. */
+ fun showDefaultMessage() {
+ resetToDefault.tryEmit(Unit)
+ }
+
+ private val resetToDefault = MutableSharedFlow<Unit>(replay = 1)
+
+ private var lockoutCountdownJob: Job? = null
+
+ private fun defaultBouncerMessageInitializer() {
+ applicationScope.launch {
+ resetToDefault.emit(Unit)
+ authenticationInteractor.authenticationMethod
+ .flatMapLatest { authMethod ->
+ if (authMethod == AuthenticationMethodModel.Sim) {
+ resetToDefault.map {
+ MessageViewModel(simBouncerInteractor.getDefaultMessage())
+ }
+ } else if (authMethod.isSecure) {
+ combine(
+ deviceEntryInteractor.deviceEntryRestrictionReason,
+ lockoutMessage,
+ fingerprintInteractor.isFingerprintCurrentlyAllowedOnBouncer,
+ resetToDefault,
+ ) { deviceEntryRestrictedReason, lockoutMsg, isFpAllowedInBouncer, _ ->
+ lockoutMsg
+ ?: deviceEntryRestrictedReason.toMessage(
+ authMethod,
+ isFpAllowedInBouncer
+ )
+ }
+ } else {
+ emptyFlow()
+ }
+ }
+ .collectLatest { messageViewModel -> message.value = messageViewModel }
+ }
+ }
+
+ private fun listenForSimBouncerEvents() {
+ // Listen for any events from the SIM bouncer and update the message shown on the bouncer.
+ applicationScope.launch {
+ authenticationInteractor.authenticationMethod
+ .flatMapLatest { authMethod ->
+ if (authMethod == AuthenticationMethodModel.Sim) {
+ simBouncerInteractor.bouncerMessageChanged.map { simMsg ->
+ simMsg?.let { MessageViewModel(it) }
+ }
+ } else {
+ emptyFlow()
+ }
+ }
+ .collectLatest {
+ if (it != null) {
+ message.value = it
+ } else {
+ resetToDefault.emit(Unit)
+ }
+ }
+ }
+ }
+
+ private fun listenForFaceMessages() {
+ // Listen for any events from face authentication and update the message shown on the
+ // bouncer.
+ applicationScope.launch {
+ biometricMessageInteractor.faceMessage
+ .sample(
+ authenticationInteractor.authenticationMethod,
+ fingerprintInteractor.isFingerprintCurrentlyAllowedOnBouncer,
+ )
+ .collectLatest { (faceMessage, authMethod, fingerprintAllowedOnBouncer) ->
+ val isFaceAuthStrong = faceAuthInteractor.isFaceAuthStrong()
+ val defaultPrimaryMessage =
+ BouncerMessageStrings.defaultMessage(
+ authMethod,
+ fingerprintAllowedOnBouncer
+ )
+ .primaryMessage
+ .toResString()
+ message.value =
+ when (faceMessage) {
+ is FaceTimeoutMessage ->
+ MessageViewModel(
+ text = defaultPrimaryMessage,
+ secondaryText = faceMessage.message,
+ isUpdateAnimated = true
+ )
+ is FaceLockoutMessage ->
+ if (isFaceAuthStrong)
+ BouncerMessageStrings.class3AuthLockedOut(authMethod)
+ .toMessage()
+ else
+ BouncerMessageStrings.faceLockedOut(
+ authMethod,
+ fingerprintAllowedOnBouncer
+ )
+ .toMessage()
+ is FaceFailureMessage ->
+ BouncerMessageStrings.incorrectFaceInput(
+ authMethod,
+ fingerprintAllowedOnBouncer
+ )
+ .toMessage()
+ else ->
+ MessageViewModel(
+ text = defaultPrimaryMessage,
+ secondaryText = faceMessage.message,
+ isUpdateAnimated = false
+ )
+ }
+ delay(MESSAGE_DURATION)
+ resetToDefault.emit(Unit)
+ }
+ }
+ }
+
+ private fun listenForFingerprintMessages() {
+ applicationScope.launch {
+ // Listen for any events from fingerprint authentication and update the message shown
+ // on the bouncer.
+ biometricMessageInteractor.fingerprintMessage
+ .sample(
+ authenticationInteractor.authenticationMethod,
+ fingerprintInteractor.isFingerprintCurrentlyAllowedOnBouncer
+ )
+ .collectLatest { (fingerprintMessage, authMethod, isFingerprintAllowed) ->
+ val defaultPrimaryMessage =
+ BouncerMessageStrings.defaultMessage(authMethod, isFingerprintAllowed)
+ .primaryMessage
+ .toResString()
+ message.value =
+ when (fingerprintMessage) {
+ is FingerprintLockoutMessage ->
+ BouncerMessageStrings.class3AuthLockedOut(authMethod).toMessage()
+ is FingerprintFailureMessage ->
+ BouncerMessageStrings.incorrectFingerprintInput(authMethod)
+ .toMessage()
+ else ->
+ MessageViewModel(
+ text = defaultPrimaryMessage,
+ secondaryText = fingerprintMessage.message,
+ isUpdateAnimated = false
+ )
+ }
+ delay(MESSAGE_DURATION)
+ resetToDefault.emit(Unit)
+ }
+ }
+ }
+
+ private fun listenForBouncerEvents() {
+ // Keeps the lockout message up-to-date.
+ applicationScope.launch {
+ bouncerInteractor.onLockoutStarted.collect { startLockoutCountdown() }
+ }
+
+ // Listens to relevant bouncer events
+ applicationScope.launch {
+ bouncerInteractor.onIncorrectBouncerInput
+ .sample(
+ authenticationInteractor.authenticationMethod,
+ fingerprintInteractor.isFingerprintCurrentlyAllowedOnBouncer
+ )
+ .collectLatest { (_, authMethod, isFingerprintAllowed) ->
+ message.emit(
+ BouncerMessageStrings.incorrectSecurityInput(
+ authMethod,
+ isFingerprintAllowed
+ )
+ .toMessage()
+ )
+ delay(MESSAGE_DURATION)
+ resetToDefault.emit(Unit)
+ }
+ }
+ }
+
+ private fun DeviceEntryRestrictionReason?.toMessage(
+ authMethod: AuthenticationMethodModel,
+ isFingerprintAllowedOnBouncer: Boolean,
+ ): MessageViewModel {
+ return when (this) {
+ DeviceEntryRestrictionReason.UserLockdown ->
+ BouncerMessageStrings.authRequiredAfterUserLockdown(authMethod)
+ DeviceEntryRestrictionReason.DeviceNotUnlockedSinceReboot ->
+ BouncerMessageStrings.authRequiredAfterReboot(authMethod)
+ DeviceEntryRestrictionReason.PolicyLockdown ->
+ BouncerMessageStrings.authRequiredAfterAdminLockdown(authMethod)
+ DeviceEntryRestrictionReason.UnattendedUpdate ->
+ BouncerMessageStrings.authRequiredForUnattendedUpdate(authMethod)
+ DeviceEntryRestrictionReason.DeviceNotUnlockedSinceMainlineUpdate ->
+ BouncerMessageStrings.authRequiredForMainlineUpdate(authMethod)
+ DeviceEntryRestrictionReason.SecurityTimeout ->
+ BouncerMessageStrings.authRequiredAfterPrimaryAuthTimeout(authMethod)
+ DeviceEntryRestrictionReason.StrongBiometricsLockedOut ->
+ BouncerMessageStrings.class3AuthLockedOut(authMethod)
+ DeviceEntryRestrictionReason.NonStrongFaceLockedOut ->
+ BouncerMessageStrings.faceLockedOut(authMethod, isFingerprintAllowedOnBouncer)
+ DeviceEntryRestrictionReason.NonStrongBiometricsSecurityTimeout ->
+ BouncerMessageStrings.nonStrongAuthTimeout(
+ authMethod,
+ isFingerprintAllowedOnBouncer
+ )
+ DeviceEntryRestrictionReason.TrustAgentDisabled ->
+ BouncerMessageStrings.trustAgentDisabled(authMethod, isFingerprintAllowedOnBouncer)
+ DeviceEntryRestrictionReason.AdaptiveAuthRequest ->
+ BouncerMessageStrings.authRequiredAfterAdaptiveAuthRequest(
+ authMethod,
+ isFingerprintAllowedOnBouncer
+ )
+ else -> BouncerMessageStrings.defaultMessage(authMethod, isFingerprintAllowedOnBouncer)
+ }.toMessage()
+ }
+
+ private fun BouncerMessagePair.toMessage(): MessageViewModel {
+ val primaryMsg = this.primaryMessage.toResString()
+ val secondaryMsg =
+ if (this.secondaryMessage == 0) "" else this.secondaryMessage.toResString()
+ return MessageViewModel(primaryMsg, secondaryText = secondaryMsg, isUpdateAnimated = true)
+ }
+
+ /** Shows the countdown message and refreshes it every second. */
+ private fun startLockoutCountdown() {
+ lockoutCountdownJob?.cancel()
+ lockoutCountdownJob =
+ applicationScope.launch {
+ authenticationInteractor.authenticationMethod.collectLatest { authMethod ->
+ do {
+ val remainingSeconds = remainingLockoutSeconds()
+ val authLockedOutMsg =
+ BouncerMessageStrings.primaryAuthLockedOut(authMethod)
+ lockoutMessage.value =
+ if (remainingSeconds > 0) {
+ MessageViewModel(
+ text =
+ kg_too_many_failed_attempts_countdown.toPluralString(
+ mutableMapOf<String, Any>(
+ Pair("count", remainingSeconds)
+ )
+ ),
+ secondaryText = authLockedOutMsg.secondaryMessage.toResString(),
+ isUpdateAnimated = false
+ )
+ } else {
+ null
+ }
+ delay(1.seconds)
+ } while (remainingSeconds > 0)
+ lockoutCountdownJob = null
+ }
+ }
+ }
+
+ private fun remainingLockoutSeconds(): Int {
+ val endTimestampMs = authenticationInteractor.lockoutEndTimestamp ?: 0
+ val remainingMs = max(0, endTimestampMs - clock.elapsedRealtime())
+ return ceil(remainingMs / 1000f).toInt()
+ }
+
+ private fun Int.toPluralString(formatterArgs: Map<String, Any>): String =
+ PluralsMessageFormatter.format(applicationContext.resources, formatterArgs, this)
+
+ private fun Int.toResString(): String = applicationContext.getString(this)
+
+ init {
+ if (flags.isComposeBouncerOrSceneContainerEnabled()) {
+ applicationScope.launch {
+ // Update the lockout countdown whenever the selected user is switched.
+ selectedUser.collect { startLockoutCountdown() }
+ }
+
+ defaultBouncerMessageInitializer()
+
+ listenForSimBouncerEvents()
+ listenForBouncerEvents()
+ listenForFaceMessages()
+ listenForFingerprintMessages()
+ }
+ }
+
+ companion object {
+ private const val MESSAGE_DURATION = 2000L
+ }
+}
+
+/** Data class that represents the status message show on the bouncer. */
+data class MessageViewModel(
+ val text: String,
+ val secondaryText: String? = null,
+ /**
+ * Whether updates to the message should be cross-animated from one message to another.
+ *
+ * If `false`, no animation should be applied, the message text should just be replaced
+ * instantly.
+ */
+ val isUpdateAnimated: Boolean = true,
+)
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@Module
+object BouncerMessageViewModelModule {
+
+ @Provides
+ @SysUISingleton
+ fun viewModel(
+ @Application applicationContext: Context,
+ @Application applicationScope: CoroutineScope,
+ bouncerInteractor: BouncerInteractor,
+ simBouncerInteractor: SimBouncerInteractor,
+ authenticationInteractor: AuthenticationInteractor,
+ clock: SystemClock,
+ biometricMessageInteractor: BiometricMessageInteractor,
+ faceAuthInteractor: DeviceEntryFaceAuthInteractor,
+ deviceEntryInteractor: DeviceEntryInteractor,
+ fingerprintInteractor: DeviceEntryFingerprintAuthInteractor,
+ flags: ComposeBouncerFlags,
+ userSwitcherViewModel: UserSwitcherViewModel,
+ ): BouncerMessageViewModel {
+ return BouncerMessageViewModel(
+ applicationContext = applicationContext,
+ applicationScope = applicationScope,
+ bouncerInteractor = bouncerInteractor,
+ simBouncerInteractor = simBouncerInteractor,
+ authenticationInteractor = authenticationInteractor,
+ clock = clock,
+ biometricMessageInteractor = biometricMessageInteractor,
+ faceAuthInteractor = faceAuthInteractor,
+ deviceEntryInteractor = deviceEntryInteractor,
+ fingerprintInteractor = fingerprintInteractor,
+ flags = flags,
+ selectedUser = userSwitcherViewModel.selectedUser,
+ )
+ }
+}
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 6287578..5c07cc5 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
@@ -21,7 +21,6 @@
import android.content.Context
import android.graphics.Bitmap
import androidx.core.graphics.drawable.toBitmap
-import com.android.internal.R
import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor
import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
import com.android.systemui.authentication.shared.model.AuthenticationWipeModel
@@ -40,18 +39,12 @@
import com.android.systemui.user.ui.viewmodel.UserActionViewModel
import com.android.systemui.user.ui.viewmodel.UserSwitcherViewModel
import com.android.systemui.user.ui.viewmodel.UserViewModel
-import com.android.systemui.util.time.SystemClock
import dagger.Module
import dagger.Provides
-import kotlin.math.ceil
-import kotlin.math.max
-import kotlin.time.Duration.Companion.seconds
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
-import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
@@ -72,13 +65,13 @@
private val simBouncerInteractor: SimBouncerInteractor,
private val authenticationInteractor: AuthenticationInteractor,
private val selectedUserInteractor: SelectedUserInteractor,
+ private val devicePolicyManager: DevicePolicyManager,
+ bouncerMessageViewModel: BouncerMessageViewModel,
flags: ComposeBouncerFlags,
selectedUser: Flow<UserViewModel>,
users: Flow<List<UserViewModel>>,
userSwitcherMenu: Flow<List<UserActionViewModel>>,
actionButton: Flow<BouncerActionButtonModel?>,
- private val clock: SystemClock,
- private val devicePolicyManager: DevicePolicyManager,
) {
val selectedUserImage: StateFlow<Bitmap?> =
selectedUser
@@ -89,6 +82,8 @@
initialValue = null,
)
+ val message: BouncerMessageViewModel = bouncerMessageViewModel
+
val userSwitcherDropdown: StateFlow<List<UserSwitcherDropdownItemViewModel>> =
combine(
users,
@@ -163,24 +158,6 @@
)
/**
- * A message shown when the user has attempted the wrong credential too many times and now must
- * wait a while before attempting to authenticate again.
- *
- * This is updated every second (countdown) during the lockout duration. When lockout is not
- * active, this is `null` and no lockout message should be shown.
- */
- private val lockoutMessage = MutableStateFlow<String?>(null)
-
- /** The user-facing message to show in the bouncer. */
- val message: StateFlow<MessageViewModel> =
- combine(bouncerInteractor.message, lockoutMessage) { _, _ -> createMessageViewModel() }
- .stateIn(
- scope = applicationScope,
- started = SharingStarted.WhileSubscribed(),
- initialValue = createMessageViewModel(),
- )
-
- /**
* The bouncer action button (Return to Call / Emergency Call). If `null`, the button should not
* be shown.
*/
@@ -222,31 +199,16 @@
)
private val isInputEnabled: StateFlow<Boolean> =
- lockoutMessage
- .map { it == null }
+ bouncerMessageViewModel.isLockoutMessagePresent
+ .map { lockoutMessagePresent -> !lockoutMessagePresent }
.stateIn(
scope = applicationScope,
started = SharingStarted.WhileSubscribed(),
initialValue = authenticationInteractor.lockoutEndTimestamp == null,
)
- private var lockoutCountdownJob: Job? = null
-
init {
if (flags.isComposeBouncerOrSceneContainerEnabled()) {
- // Keeps the lockout dialog up-to-date.
- applicationScope.launch {
- bouncerInteractor.onLockoutStarted.collect {
- showLockoutDialog()
- startLockoutCountdown()
- }
- }
-
- applicationScope.launch {
- // Update the lockout countdown whenever the selected user is switched.
- selectedUser.collect { startLockoutCountdown() }
- }
-
// Keeps the upcoming wipe dialog up-to-date.
applicationScope.launch {
authenticationInteractor.upcomingWipe.collect { wipeModel ->
@@ -256,48 +218,6 @@
}
}
- private fun showLockoutDialog() {
- applicationScope.launch {
- val failedAttempts = authenticationInteractor.failedAuthenticationAttempts.value
- lockoutDialogMessage.value =
- authMethodViewModel.value?.lockoutMessageId?.let { messageId ->
- applicationContext.getString(
- messageId,
- failedAttempts,
- remainingLockoutSeconds()
- )
- }
- }
- }
-
- /** Shows the countdown message and refreshes it every second. */
- private fun startLockoutCountdown() {
- lockoutCountdownJob?.cancel()
- lockoutCountdownJob =
- applicationScope.launch {
- do {
- val remainingSeconds = remainingLockoutSeconds()
- lockoutMessage.value =
- if (remainingSeconds > 0) {
- applicationContext.getString(
- R.string.lockscreen_too_many_failed_attempts_countdown,
- remainingSeconds,
- )
- } else {
- null
- }
- delay(1.seconds)
- } while (remainingSeconds > 0)
- lockoutCountdownJob = null
- }
- }
-
- private fun remainingLockoutSeconds(): Int {
- val endTimestampMs = authenticationInteractor.lockoutEndTimestamp ?: 0
- val remainingMs = max(0, endTimestampMs - clock.elapsedRealtime())
- return ceil(remainingMs / 1000f).toInt()
- }
-
private fun isSideBySideSupported(authMethod: AuthMethodBouncerViewModel?): Boolean {
return isUserSwitcherVisible || authMethod !is PasswordBouncerViewModel
}
@@ -306,15 +226,6 @@
return authMethod !is PasswordBouncerViewModel
}
- private fun createMessageViewModel(): MessageViewModel {
- val isLockedOut = lockoutMessage.value != null
- return MessageViewModel(
- // A lockout message takes precedence over the non-lockout message.
- text = lockoutMessage.value ?: bouncerInteractor.message.value ?: "",
- isUpdateAnimated = !isLockedOut,
- )
- }
-
private fun getChildViewModel(
authenticationMethod: AuthenticationMethodModel,
): AuthMethodBouncerViewModel? {
@@ -336,7 +247,8 @@
interactor = bouncerInteractor,
isInputEnabled = isInputEnabled,
simBouncerInteractor = simBouncerInteractor,
- authenticationMethod = authenticationMethod
+ authenticationMethod = authenticationMethod,
+ onIntentionalUserInput = ::onIntentionalUserInput
)
is AuthenticationMethodModel.Sim ->
PinBouncerViewModel(
@@ -346,6 +258,7 @@
isInputEnabled = isInputEnabled,
simBouncerInteractor = simBouncerInteractor,
authenticationMethod = authenticationMethod,
+ onIntentionalUserInput = ::onIntentionalUserInput
)
is AuthenticationMethodModel.Password ->
PasswordBouncerViewModel(
@@ -354,6 +267,7 @@
interactor = bouncerInteractor,
inputMethodInteractor = inputMethodInteractor,
selectedUserInteractor = selectedUserInteractor,
+ onIntentionalUserInput = ::onIntentionalUserInput
)
is AuthenticationMethodModel.Pattern ->
PatternBouncerViewModel(
@@ -361,11 +275,17 @@
viewModelScope = newViewModelScope,
interactor = bouncerInteractor,
isInputEnabled = isInputEnabled,
+ onIntentionalUserInput = ::onIntentionalUserInput
)
else -> null
}
}
+ private fun onIntentionalUserInput() {
+ message.showDefaultMessage()
+ bouncerInteractor.onIntentionalUserInput()
+ }
+
private fun createChildCoroutineScope(parentScope: CoroutineScope): CoroutineScope {
return CoroutineScope(
SupervisorJob(parent = parentScope.coroutineContext.job) + mainDispatcher
@@ -437,18 +357,6 @@
}
}
- data class MessageViewModel(
- val text: String,
-
- /**
- * Whether updates to the message should be cross-animated from one message to another.
- *
- * If `false`, no animation should be applied, the message text should just be replaced
- * instantly.
- */
- val isUpdateAnimated: Boolean,
- )
-
data class DialogViewModel(
val text: String,
@@ -480,8 +388,8 @@
selectedUserInteractor: SelectedUserInteractor,
flags: ComposeBouncerFlags,
userSwitcherViewModel: UserSwitcherViewModel,
- clock: SystemClock,
devicePolicyManager: DevicePolicyManager,
+ bouncerMessageViewModel: BouncerMessageViewModel,
): BouncerViewModel {
return BouncerViewModel(
applicationContext = applicationContext,
@@ -497,8 +405,8 @@
users = userSwitcherViewModel.users,
userSwitcherMenu = userSwitcherViewModel.menu,
actionButton = actionButtonInteractor.actionButton,
- clock = clock,
devicePolicyManager = devicePolicyManager,
+ bouncerMessageViewModel = bouncerMessageViewModel,
)
}
}
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 b42eda1..052fb6b 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
@@ -40,6 +40,7 @@
viewModelScope: CoroutineScope,
isInputEnabled: StateFlow<Boolean>,
interactor: BouncerInteractor,
+ private val onIntentionalUserInput: () -> Unit,
private val inputMethodInteractor: InputMethodInteractor,
private val selectedUserInteractor: SelectedUserInteractor,
) :
@@ -96,12 +97,8 @@
/** Notifies that the user has changed the password input. */
fun onPasswordInputChanged(newPassword: String) {
- if (this.password.value.isEmpty() && newPassword.isNotEmpty()) {
- interactor.clearMessage()
- }
-
if (newPassword.isNotEmpty()) {
- interactor.onIntentionalUserInput()
+ onIntentionalUserInput()
}
_password.value = newPassword
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModel.kt
index 69f8032..a401600 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModel.kt
@@ -40,6 +40,7 @@
viewModelScope: CoroutineScope,
interactor: BouncerInteractor,
isInputEnabled: StateFlow<Boolean>,
+ private val onIntentionalUserInput: () -> Unit,
) :
AuthMethodBouncerViewModel(
viewModelScope = viewModelScope,
@@ -84,7 +85,7 @@
/** Notifies that the user has started a drag gesture across the dot grid. */
fun onDragStart() {
- interactor.clearMessage()
+ onIntentionalUserInput()
}
/**
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt
index e910a92..62da5c0 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt
@@ -41,6 +41,7 @@
viewModelScope: CoroutineScope,
interactor: BouncerInteractor,
isInputEnabled: StateFlow<Boolean>,
+ private val onIntentionalUserInput: () -> Unit,
private val simBouncerInteractor: SimBouncerInteractor,
authenticationMethod: AuthenticationMethodModel,
) :
@@ -131,11 +132,8 @@
/** Notifies that the user clicked on a PIN button with the given digit value. */
fun onPinButtonClicked(input: Int) {
val pinInput = mutablePinInput.value
- if (pinInput.isEmpty()) {
- interactor.clearMessage()
- }
- interactor.onIntentionalUserInput()
+ onIntentionalUserInput()
mutablePinInput.value = pinInput.append(input)
tryAuthenticate(useAutoConfirm = true)
@@ -149,7 +147,6 @@
/** Notifies that the user long-pressed the backspace button. */
fun onBackspaceButtonLongPressed() {
clearInput()
- interactor.clearMessage()
}
/** Notifies that the user clicked the "enter" button. */
@@ -173,7 +170,6 @@
/** Resets the sim screen and shows a default message. */
private fun onResetSimFlow() {
simBouncerInteractor.resetSimPukUserInput()
- interactor.resetMessage()
clearInput()
}
diff --git a/packages/SystemUI/src/com/android/systemui/common/shared/model/NotificationContainerBounds.kt b/packages/SystemUI/src/com/android/systemui/common/shared/model/NotificationContainerBounds.kt
index 3063ebd..fdd98bec 100644
--- a/packages/SystemUI/src/com/android/systemui/common/shared/model/NotificationContainerBounds.kt
+++ b/packages/SystemUI/src/com/android/systemui/common/shared/model/NotificationContainerBounds.kt
@@ -18,12 +18,8 @@
/** Models the bounds of the notification container. */
data class NotificationContainerBounds(
- /** The position of the left of the container in its window coordinate system, in pixels. */
- val left: Float = 0f,
/** The position of the top of the container in its window coordinate system, in pixels. */
val top: Float = 0f,
- /** The position of the right of the container in its window coordinate system, in pixels. */
- val right: Float = 0f,
/** The position of the bottom of the container in its window coordinate system, in pixels. */
val bottom: Float = 0f,
/** Whether any modifications to top/bottom should be smoothly animated. */
diff --git a/packages/SystemUI/src/com/android/systemui/common/ui/ConfigurationState.kt b/packages/SystemUI/src/com/android/systemui/common/ui/ConfigurationState.kt
index 964eb6f..578389b 100644
--- a/packages/SystemUI/src/com/android/systemui/common/ui/ConfigurationState.kt
+++ b/packages/SystemUI/src/com/android/systemui/common/ui/ConfigurationState.kt
@@ -54,6 +54,18 @@
}
/**
+ * Returns a [Flow] that emits a dimension pixel size that is kept in sync with the device
+ * configuration.
+ *
+ * @see android.content.res.Resources.getDimensionPixelSize
+ */
+ fun getDimensionPixelOffset(@DimenRes id: Int): Flow<Int> {
+ return configurationController.onDensityOrFontScaleChanged.emitOnStart().map {
+ context.resources.getDimensionPixelOffset(id)
+ }
+ }
+
+ /**
* Returns a [Flow] that emits a color that is kept in sync with the device theme.
*
* @see Utils.getColorAttrDefaultColor
diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt
index bfe751a..afa7c37 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt
@@ -16,24 +16,36 @@
package com.android.systemui.communal.ui.viewmodel
+import android.appwidget.AppWidgetManager
+import android.appwidget.AppWidgetProviderInfo
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.content.res.Resources
+import android.util.Log
+import androidx.activity.result.ActivityResultLauncher
import com.android.internal.logging.UiEventLogger
import com.android.systemui.communal.domain.interactor.CommunalInteractor
import com.android.systemui.communal.domain.interactor.CommunalSettingsInteractor
import com.android.systemui.communal.domain.model.CommunalContentModel
import com.android.systemui.communal.shared.log.CommunalUiEvent
import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.log.LogBuffer
import com.android.systemui.log.core.Logger
import com.android.systemui.log.dagger.CommunalLog
import com.android.systemui.media.controls.ui.view.MediaHost
import com.android.systemui.media.dagger.MediaModule
+import com.android.systemui.res.R
import javax.inject.Inject
import javax.inject.Named
+import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.withContext
/** The view model for communal hub in edit mode. */
@SysUISingleton
@@ -45,6 +57,7 @@
@Named(MediaModule.COMMUNAL_HUB) mediaHost: MediaHost,
private val uiEventLogger: UiEventLogger,
@CommunalLog logBuffer: LogBuffer,
+ @Background private val backgroundDispatcher: CoroutineDispatcher,
) : BaseCommunalViewModel(communalInteractor, mediaHost) {
private val logger = Logger(logBuffer, "CommunalEditModeViewModel")
@@ -86,10 +99,77 @@
uiEventLogger.log(CommunalUiEvent.COMMUNAL_HUB_REORDER_WIDGET_CANCEL)
}
- /** Returns the widget categories to show on communal hub. */
- val getCommunalWidgetCategories: Int
- get() = communalSettingsInteractor.communalWidgetCategories.value
+ /** Launch the widget picker activity using the given {@link ActivityResultLauncher}. */
+ suspend fun onOpenWidgetPicker(
+ resources: Resources,
+ packageManager: PackageManager,
+ activityLauncher: ActivityResultLauncher<Intent>
+ ): Boolean =
+ withContext(backgroundDispatcher) {
+ val widgets = communalInteractor.widgetContent.first()
+ val excludeList = widgets.mapTo(ArrayList()) { it.providerInfo }
+ getWidgetPickerActivityIntent(resources, packageManager, excludeList)?.let {
+ try {
+ activityLauncher.launch(it)
+ return@withContext true
+ } catch (e: Exception) {
+ Log.e(TAG, "Failed to launch widget picker activity", e)
+ }
+ }
+ false
+ }
+
+ private fun getWidgetPickerActivityIntent(
+ resources: Resources,
+ packageManager: PackageManager,
+ excludeList: ArrayList<AppWidgetProviderInfo>
+ ): Intent? {
+ val packageName =
+ getLauncherPackageName(packageManager)
+ ?: run {
+ Log.e(TAG, "Couldn't resolve launcher package name")
+ return@getWidgetPickerActivityIntent null
+ }
+
+ return Intent(Intent.ACTION_PICK).apply {
+ setPackage(packageName)
+ putExtra(
+ EXTRA_DESIRED_WIDGET_WIDTH,
+ resources.getDimensionPixelSize(R.dimen.communal_widget_picker_desired_width)
+ )
+ putExtra(
+ EXTRA_DESIRED_WIDGET_HEIGHT,
+ resources.getDimensionPixelSize(R.dimen.communal_widget_picker_desired_height)
+ )
+ putExtra(
+ AppWidgetManager.EXTRA_CATEGORY_FILTER,
+ communalSettingsInteractor.communalWidgetCategories.value
+ )
+ putExtra(EXTRA_UI_SURFACE_KEY, EXTRA_UI_SURFACE_VALUE)
+ putParcelableArrayListExtra(EXTRA_ADDED_APP_WIDGETS_KEY, excludeList)
+ }
+ }
+
+ private fun getLauncherPackageName(packageManager: PackageManager): String? {
+ return packageManager
+ .resolveActivity(
+ Intent(Intent.ACTION_MAIN).also { it.addCategory(Intent.CATEGORY_HOME) },
+ PackageManager.MATCH_DEFAULT_ONLY
+ )
+ ?.activityInfo
+ ?.packageName
+ }
/** Sets whether edit mode is currently open */
fun setEditModeOpen(isOpen: Boolean) = communalInteractor.setEditModeOpen(isOpen)
+
+ companion object {
+ private const val TAG = "CommunalEditModeViewModel"
+
+ private const val EXTRA_DESIRED_WIDGET_WIDTH = "desired_widget_width"
+ private const val EXTRA_DESIRED_WIDGET_HEIGHT = "desired_widget_height"
+ private const val EXTRA_UI_SURFACE_KEY = "ui_surface"
+ private const val EXTRA_UI_SURFACE_VALUE = "widgets_hub"
+ const val EXTRA_ADDED_APP_WIDGETS_KEY = "added_app_widgets"
+ }
}
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 b6ad26b..ba18f01 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt
@@ -16,9 +16,7 @@
package com.android.systemui.communal.widgets
-import android.appwidget.AppWidgetManager
import android.content.Intent
-import android.content.pm.PackageManager
import android.os.Bundle
import android.os.RemoteException
import android.util.Log
@@ -32,6 +30,8 @@
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.ui.Modifier
+import androidx.lifecycle.lifecycleScope
+import com.android.app.tracing.coroutines.launch
import com.android.compose.theme.LocalAndroidColorScheme
import com.android.compose.theme.PlatformTheme
import com.android.internal.logging.UiEventLogger
@@ -43,8 +43,8 @@
import com.android.systemui.log.LogBuffer
import com.android.systemui.log.core.Logger
import com.android.systemui.log.dagger.CommunalLog
-import com.android.systemui.res.R
import javax.inject.Inject
+import kotlinx.coroutines.launch
/** An Activity for editing the widgets that appear in hub mode. */
class EditWidgetsActivity
@@ -57,11 +57,8 @@
@CommunalLog logBuffer: LogBuffer,
) : ComponentActivity() {
companion object {
- private const val EXTRA_IS_PENDING_WIDGET_DRAG = "is_pending_widget_drag"
- private const val EXTRA_DESIRED_WIDGET_WIDTH = "desired_widget_width"
- private const val EXTRA_DESIRED_WIDGET_HEIGHT = "desired_widget_height"
-
private const val TAG = "EditWidgetsActivity"
+ private const val EXTRA_IS_PENDING_WIDGET_DRAG = "is_pending_widget_drag"
const val EXTRA_PRESELECTED_KEY = "preselected_key"
}
@@ -136,39 +133,13 @@
}
private fun onOpenWidgetPicker() {
- val intent = Intent(Intent.ACTION_MAIN).also { it.addCategory(Intent.CATEGORY_HOME) }
- packageManager
- .resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY)
- ?.activityInfo
- ?.packageName
- ?.let { packageName ->
- try {
- addWidgetActivityLauncher.launch(
- Intent(Intent.ACTION_PICK).apply {
- setPackage(packageName)
- putExtra(
- EXTRA_DESIRED_WIDGET_WIDTH,
- resources.getDimensionPixelSize(
- R.dimen.communal_widget_picker_desired_width
- )
- )
- putExtra(
- EXTRA_DESIRED_WIDGET_HEIGHT,
- resources.getDimensionPixelSize(
- R.dimen.communal_widget_picker_desired_height
- )
- )
- putExtra(
- AppWidgetManager.EXTRA_CATEGORY_FILTER,
- communalViewModel.getCommunalWidgetCategories
- )
- }
- )
- } catch (e: Exception) {
- Log.e(TAG, "Failed to launch widget picker activity", e)
- }
- }
- ?: run { Log.e(TAG, "Couldn't resolve launcher package name") }
+ lifecycleScope.launch {
+ communalViewModel.onOpenWidgetPicker(
+ resources,
+ packageManager,
+ addWidgetActivityLauncher
+ )
+ }
}
private fun onEditDone() {
diff --git a/packages/SystemUI/src/com/android/systemui/communal/widgets/WidgetInteractionHandler.kt b/packages/SystemUI/src/com/android/systemui/communal/widgets/WidgetInteractionHandler.kt
index 4c1e77b..778d8cf 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/widgets/WidgetInteractionHandler.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/widgets/WidgetInteractionHandler.kt
@@ -16,9 +16,14 @@
package com.android.systemui.communal.widgets
+import android.app.ActivityOptions
import android.app.PendingIntent
+import android.content.Intent
+import android.util.Pair
import android.view.View
import android.widget.RemoteViews
+import androidx.core.util.component1
+import androidx.core.util.component2
import com.android.systemui.animation.ActivityTransitionAnimator
import com.android.systemui.common.ui.view.getNearestParent
import com.android.systemui.plugins.ActivityStarter
@@ -33,21 +38,33 @@
view: View,
pendingIntent: PendingIntent,
response: RemoteViews.RemoteResponse
- ): Boolean =
- when {
- pendingIntent.isActivity -> startActivity(view, pendingIntent)
- else ->
- RemoteViews.startPendingIntent(view, pendingIntent, response.getLaunchOptions(view))
+ ): Boolean {
+ val launchOptions = response.getLaunchOptions(view)
+ return when {
+ pendingIntent.isActivity ->
+ // Forward the fill-in intent and activity options retrieved from the response
+ // to populate the pending intent, so that list items can launch respective
+ // activities.
+ startActivity(view, pendingIntent, launchOptions)
+ else -> RemoteViews.startPendingIntent(view, pendingIntent, launchOptions)
}
+ }
- private fun startActivity(view: View, pendingIntent: PendingIntent): Boolean {
+ private fun startActivity(
+ view: View,
+ pendingIntent: PendingIntent,
+ launchOptions: Pair<Intent, ActivityOptions>,
+ ): Boolean {
val hostView = view.getNearestParent<CommunalAppWidgetHostView>()
val animationController = hostView?.let(ActivityTransitionAnimator.Controller::fromView)
+ val (fillInIntent, activityOptions) = launchOptions
activityStarter.startPendingIntentMaybeDismissingKeyguard(
pendingIntent,
/* intentSentUiThreadCallback = */ null,
- animationController
+ animationController,
+ fillInIntent,
+ activityOptions.toBundle(),
)
return true
}
diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFingerprintAuthInteractor.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFingerprintAuthInteractor.kt
index 8059993..c4e0ef7 100644
--- a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFingerprintAuthInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFingerprintAuthInteractor.kt
@@ -29,6 +29,8 @@
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.filterIsInstance
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
@OptIn(ExperimentalCoroutinesApi::class)
@@ -72,4 +74,14 @@
*/
val isSensorUnderDisplay =
fingerprintPropertyRepository.sensorType.map(FingerprintSensorType::isUdfps)
+
+ /** Whether fingerprint authentication is currently allowed while on the bouncer. */
+ val isFingerprintCurrentlyAllowedOnBouncer =
+ isSensorUnderDisplay.flatMapLatest { sensorBelowDisplay ->
+ if (sensorBelowDisplay) {
+ flowOf(false)
+ } else {
+ isFingerprintAuthCurrentlyAllowed
+ }
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt b/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt
index 298da13..1bcee74 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt
@@ -23,13 +23,11 @@
import com.android.server.notification.Flags.politeNotifications
import com.android.server.notification.Flags.vibrateWhileUnlocked
import com.android.systemui.Flags.FLAG_COMMUNAL_HUB
-import com.android.systemui.Flags.FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR
-import com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT
import com.android.systemui.Flags.communalHub
-import com.android.systemui.Flags.keyguardBottomAreaRefactor
-import com.android.systemui.Flags.migrateClocksToBlueprint
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.flags.Flags.MIGRATE_KEYGUARD_STATUS_BAR_VIEW
+import com.android.systemui.keyguard.KeyguardBottomAreaRefactor
+import com.android.systemui.keyguard.MigrateClocksToBlueprint
import com.android.systemui.keyguard.shared.ComposeLockscreen
import com.android.systemui.scene.shared.flag.SceneContainerFlag
import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor
@@ -58,11 +56,11 @@
SceneContainerFlag.getMainStaticFlag() dependsOn MIGRATE_KEYGUARD_STATUS_BAR_VIEW
// ComposeLockscreen dependencies
- ComposeLockscreen.token dependsOn keyguardBottomAreaRefactor
- ComposeLockscreen.token dependsOn migrateClocksToBlueprint
+ ComposeLockscreen.token dependsOn KeyguardBottomAreaRefactor.token
+ ComposeLockscreen.token dependsOn MigrateClocksToBlueprint.token
// CommunalHub dependencies
- communalHub dependsOn migrateClocksToBlueprint
+ communalHub dependsOn MigrateClocksToBlueprint.token
}
private inline val politeNotifications
@@ -71,10 +69,6 @@
get() = FlagToken(FLAG_CROSS_APP_POLITE_NOTIFICATIONS, crossAppPoliteNotifications())
private inline val vibrateWhileUnlockedToken: FlagToken
get() = FlagToken(FLAG_VIBRATE_WHILE_UNLOCKED, vibrateWhileUnlocked())
- private inline val keyguardBottomAreaRefactor
- get() = FlagToken(FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR, keyguardBottomAreaRefactor())
- private inline val migrateClocksToBlueprint
- get() = FlagToken(FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT, migrateClocksToBlueprint())
private inline val communalHub
get() = FlagToken(FLAG_COMMUNAL_HUB, communalHub())
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/data/repository/CommandLineKeyboardRepository.kt b/packages/SystemUI/src/com/android/systemui/keyboard/data/repository/CommandLineKeyboardRepository.kt
new file mode 100644
index 0000000..f49cfdd
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/data/repository/CommandLineKeyboardRepository.kt
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package com.android.systemui.keyboard.data.repository
+
+import android.util.Log
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.keyboard.data.model.Keyboard
+import com.android.systemui.keyboard.shared.model.BacklightModel
+import com.android.systemui.statusbar.commandline.Command
+import com.android.systemui.statusbar.commandline.CommandRegistry
+import java.io.PrintWriter
+import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.filterNotNull
+
+/**
+ * Helper class for development to mock various keyboard states with command line. Alternative for
+ * [KeyboardRepositoryImpl] which relies on real data from framework. [KeyboardRepositoryImpl] is
+ * the default implementation so to use this class you need to substitute it in [KeyboardModule].
+ *
+ * For usage information: see [KeyboardCommand.help] or run `adb shell cmd statusbar keyboard`.
+ */
+@SysUISingleton
+class CommandLineKeyboardRepository @Inject constructor(commandRegistry: CommandRegistry) :
+ KeyboardRepository {
+
+ private val _isAnyKeyboardConnected = MutableStateFlow(false)
+ override val isAnyKeyboardConnected: Flow<Boolean> = _isAnyKeyboardConnected
+
+ private val _backlightState: MutableStateFlow<BacklightModel?> = MutableStateFlow(null)
+ // filtering to make sure backlight doesn't have default initial value
+ override val backlight: Flow<BacklightModel> = _backlightState.filterNotNull()
+
+ private val _newlyConnectedKeyboard: MutableStateFlow<Keyboard?> = MutableStateFlow(null)
+ override val newlyConnectedKeyboard: Flow<Keyboard> = _newlyConnectedKeyboard.filterNotNull()
+
+ init {
+ Log.i(TAG, "initializing shell command $COMMAND")
+ commandRegistry.registerCommand(COMMAND) { KeyboardCommand() }
+ }
+
+ inner class KeyboardCommand : Command {
+ override fun execute(pw: PrintWriter, args: List<String>) {
+ Log.i(TAG, "$COMMAND command was called with args: $args")
+ if (args.isEmpty()) {
+ help(pw)
+ return
+ }
+ when (args[0]) {
+ "keyboard-connected" -> _isAnyKeyboardConnected.value = args[1].toBoolean()
+ "backlight" -> {
+ @Suppress("Since15")
+ val level = Math.clamp(args[1].toInt().toLong(), 0, MAX_BACKLIGHT_LEVEL)
+ _backlightState.value = BacklightModel(level, MAX_BACKLIGHT_LEVEL)
+ }
+ "new-keyboard" -> {
+ _newlyConnectedKeyboard.value =
+ Keyboard(vendorId = args[1].toInt(), productId = args[2].toInt())
+ }
+ else -> help(pw)
+ }
+ }
+
+ override fun help(pw: PrintWriter) {
+ pw.println("Usage: adb shell cmd statusbar $COMMAND <command>")
+ pw.println(
+ "Note: this command only mocks setting these values on the framework level" +
+ " but in reality doesn't change anything and is only used for testing UI"
+ )
+ pw.println("Available commands:")
+ pw.println(" keyboard-connected [true|false]")
+ pw.println(" Notify any physical keyboard connected/disconnected.")
+ pw.println(" backlight <level>")
+ pw.println(" Notify new keyboard backlight level: min 0, max $MAX_BACKLIGHT_LEVEL.")
+ pw.println(" new-keyboard <vendor-id> <product-id>")
+ pw.println(" Notify new physical keyboard with specified parameters got connected.")
+ }
+ }
+
+ companion object {
+ private const val TAG = "CommandLineKeyboardRepository"
+ private const val COMMAND = "keyboard"
+ private const val MAX_BACKLIGHT_LEVEL = 5
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/data/repository/KeyboardRepository.kt b/packages/SystemUI/src/com/android/systemui/keyboard/data/repository/KeyboardRepository.kt
index 2fac40a..91d5280 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/data/repository/KeyboardRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/data/repository/KeyboardRepository.kt
@@ -46,6 +46,10 @@
import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.flow.shareIn
+/**
+ * Provides information about physical keyboard states. [CommandLineKeyboardRepository] can be
+ * useful command line-driven implementation during development.
+ */
interface KeyboardRepository {
/** Emits true if any physical keyboard is connected to the device, false otherwise. */
val isAnyKeyboardConnected: Flow<Boolean>
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardBottomAreaRefactor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardBottomAreaRefactor.kt
new file mode 100644
index 0000000..779b27b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardBottomAreaRefactor.kt
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard
+
+import com.android.systemui.Flags
+import com.android.systemui.flags.FlagToken
+import com.android.systemui.flags.RefactorFlagUtils
+
+/** Helper for reading or using the keyguard bottom area refactor flag. */
+@Suppress("NOTHING_TO_INLINE")
+object KeyguardBottomAreaRefactor {
+ /** The aconfig flag name */
+ const val FLAG_NAME = Flags.FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR
+
+ /** A token used for dependency declaration */
+ val token: FlagToken
+ get() = FlagToken(FLAG_NAME, isEnabled)
+
+ /** Is the refactor enabled */
+ @JvmStatic
+ inline val isEnabled
+ get() = Flags.keyguardBottomAreaRefactor()
+
+ /**
+ * Called to ensure code is only run when the flag is enabled. This protects users from the
+ * unintended behaviors caused by accidentally running new logic, while also crashing on an eng
+ * build to ensure that the refactor author catches issues in testing.
+ */
+ @JvmStatic
+ inline fun isUnexpectedlyInLegacyMode() =
+ RefactorFlagUtils.isUnexpectedlyInLegacyMode(isEnabled, FLAG_NAME)
+
+ /**
+ * Called to ensure code is only run when the flag is disabled. This will throw an exception if
+ * the flag is enabled to ensure that the refactor author catches issues in testing.
+ */
+ @JvmStatic
+ inline fun assertInLegacyMode() = RefactorFlagUtils.assertInLegacyMode(isEnabled, FLAG_NAME)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt
index 5565ee2..d9d7470 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt
@@ -36,7 +36,6 @@
import com.android.keyguard.LockIconViewController
import com.android.keyguard.dagger.KeyguardStatusViewComponent
import com.android.systemui.CoreStartable
-import com.android.systemui.Flags.keyguardBottomAreaRefactor
import com.android.systemui.common.ui.ConfigurationState
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.deviceentry.domain.interactor.DeviceEntryHapticsInteractor
@@ -166,7 +165,7 @@
fun bindIndicationArea() {
indicationAreaHandle?.dispose()
- if (!keyguardBottomAreaRefactor()) {
+ if (!KeyguardBottomAreaRefactor.isEnabled) {
keyguardRootView.findViewById<View?>(R.id.keyguard_indication_area)?.let {
keyguardRootView.removeView(it)
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
index 3b34750..f700e03 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
@@ -40,7 +40,6 @@
import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN;
import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_FOR_UNATTENDED_UPDATE;
import static com.android.systemui.DejankUtils.whitelistIpcs;
-import static com.android.systemui.Flags.migrateClocksToBlueprint;
import static com.android.systemui.Flags.notifyPowerManagerUserActivityBackground;
import static com.android.systemui.Flags.refactorGetCurrentUser;
import static com.android.systemui.keyguard.ui.viewmodel.LockscreenToDreamingTransitionViewModel.DREAMING_ANIMATION_DURATION_MS;
@@ -3404,7 +3403,8 @@
}
// Ensure that keyguard becomes visible if the going away animation is canceled
- if (showKeyguard && !KeyguardWmStateRefactor.isEnabled() && migrateClocksToBlueprint()) {
+ if (showKeyguard && !KeyguardWmStateRefactor.isEnabled()
+ && MigrateClocksToBlueprint.isEnabled()) {
mKeyguardInteractor.showKeyguard();
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/MigrateClocksToBlueprint.kt b/packages/SystemUI/src/com/android/systemui/keyguard/MigrateClocksToBlueprint.kt
new file mode 100644
index 0000000..5a2943b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/MigrateClocksToBlueprint.kt
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard
+
+import com.android.systemui.Flags
+import com.android.systemui.flags.FlagToken
+import com.android.systemui.flags.RefactorFlagUtils
+
+/** Helper for reading or using the migrate clocks to blueprint flag. */
+@Suppress("NOTHING_TO_INLINE")
+object MigrateClocksToBlueprint {
+ /** The aconfig flag name */
+ const val FLAG_NAME = Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT
+
+ /** A token used for dependency declaration */
+ val token: FlagToken
+ get() = FlagToken(FLAG_NAME, isEnabled)
+
+ /** Is the refactor enabled */
+ @JvmStatic
+ inline val isEnabled
+ get() = Flags.migrateClocksToBlueprint()
+
+ /**
+ * Called to ensure code is only run when the flag is enabled. This protects users from the
+ * unintended behaviors caused by accidentally running new logic, while also crashing on an eng
+ * build to ensure that the refactor author catches issues in testing.
+ */
+ @JvmStatic
+ inline fun isUnexpectedlyInLegacyMode() =
+ RefactorFlagUtils.isUnexpectedlyInLegacyMode(isEnabled, FLAG_NAME)
+
+ /**
+ * Called to ensure code is only run when the flag is disabled. This will throw an exception if
+ * the flag is enabled to ensure that the refactor author catches issues in testing.
+ */
+ @JvmStatic
+ inline fun assertInLegacyMode() = RefactorFlagUtils.assertInLegacyMode(isEnabled, FLAG_NAME)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardClockRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardClockRepository.kt
index 7ad5aac..3f4d3a8 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardClockRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardClockRepository.kt
@@ -18,7 +18,6 @@
import android.os.UserHandle
import android.provider.Settings
-import androidx.annotation.VisibleForTesting
import com.android.keyguard.ClockEventController
import com.android.keyguard.KeyguardClockSwitch.ClockSize
import com.android.keyguard.KeyguardClockSwitch.LARGE
@@ -52,14 +51,14 @@
val clockSize: StateFlow<Int>
/** clock size selected in picker, DYNAMIC or SMALL */
- val selectedClockSize: Flow<SettingsClockSize>
+ val selectedClockSize: StateFlow<SettingsClockSize>
/** clock id, selected from clock carousel in wallpaper picker */
val currentClockId: Flow<ClockId>
val currentClock: StateFlow<ClockController?>
- val previewClockPair: StateFlow<Pair<ClockController, ClockController>>
+ val previewClock: Flow<ClockController>
val clockEventController: ClockEventController
fun setClockSize(@ClockSize size: Int)
@@ -84,14 +83,19 @@
_clockSize.value = size
}
- override val selectedClockSize: Flow<SettingsClockSize> =
+ override val selectedClockSize: StateFlow<SettingsClockSize> =
secureSettings
.observerFlow(
names = arrayOf(Settings.Secure.LOCKSCREEN_USE_DOUBLE_LINE_CLOCK),
userId = UserHandle.USER_SYSTEM,
)
.onStart { emit(Unit) } // Forces an initial update.
- .map { getClockSize() }
+ .map { withContext(backgroundDispatcher) { getClockSize() } }
+ .stateIn(
+ scope = applicationScope,
+ started = SharingStarted.WhileSubscribed(),
+ initialValue = getClockSize()
+ )
override val currentClockId: Flow<ClockId> =
callbackFlow {
@@ -113,37 +117,35 @@
override val currentClock: StateFlow<ClockController?> =
currentClockId
- .map { clockRegistry.createCurrentClock() }
+ .map {
+ clockEventController.clock = clockRegistry.createCurrentClock()
+ clockEventController.clock
+ }
.stateIn(
scope = applicationScope,
started = SharingStarted.WhileSubscribed(),
initialValue = clockRegistry.createCurrentClock()
)
- override val previewClockPair: StateFlow<Pair<ClockController, ClockController>> =
- currentClockId
- .map { Pair(clockRegistry.createCurrentClock(), clockRegistry.createCurrentClock()) }
- .stateIn(
- scope = applicationScope,
- started = SharingStarted.WhileSubscribed(),
- initialValue =
- Pair(clockRegistry.createCurrentClock(), clockRegistry.createCurrentClock())
- )
+ override val previewClock: Flow<ClockController> =
+ currentClockId.map {
+ // We should create a new instance for each collect call
+ // cause in preview, the same clock will be attached to different view
+ // at the same time
+ clockRegistry.createCurrentClock()
+ }
- @VisibleForTesting
- suspend fun getClockSize(): SettingsClockSize {
- return withContext(backgroundDispatcher) {
- if (
- secureSettings.getIntForUser(
- Settings.Secure.LOCKSCREEN_USE_DOUBLE_LINE_CLOCK,
- 1,
- UserHandle.USER_CURRENT
- ) == 1
- ) {
- SettingsClockSize.DYNAMIC
- } else {
- SettingsClockSize.SMALL
- }
+ private fun getClockSize(): SettingsClockSize {
+ return if (
+ secureSettings.getIntForUser(
+ Settings.Secure.LOCKSCREEN_USE_DOUBLE_LINE_CLOCK,
+ 1,
+ UserHandle.USER_CURRENT
+ ) == 1
+ ) {
+ SettingsClockSize.DYNAMIC
+ } else {
+ SettingsClockSize.SMALL
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt
index 9c68c45..a36bf8b 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt
@@ -119,24 +119,7 @@
init {
// Seed with transitions signaling a boot into lockscreen state. If updating this, please
// also update FakeKeyguardTransitionRepository.
- emitTransition(
- TransitionStep(
- KeyguardState.OFF,
- KeyguardState.LOCKSCREEN,
- 0f,
- TransitionState.STARTED,
- KeyguardTransitionRepositoryImpl::class.simpleName!!,
- )
- )
- emitTransition(
- TransitionStep(
- KeyguardState.OFF,
- KeyguardState.LOCKSCREEN,
- 1f,
- TransitionState.FINISHED,
- KeyguardTransitionRepositoryImpl::class.simpleName!!,
- )
- )
+ initialTransitionSteps.forEach(::emitTransition)
}
override fun startTransition(info: TransitionInfo): UUID? {
@@ -256,5 +239,31 @@
companion object {
private const val TAG = "KeyguardTransitionRepository"
+
+ /**
+ * Transition steps to seed the repository with, so that all of the transition interactor
+ * flows emit reasonable initial values.
+ */
+ val initialTransitionSteps: List<TransitionStep> =
+ listOf(
+ TransitionStep(
+ KeyguardState.OFF,
+ KeyguardState.OFF,
+ 1f,
+ TransitionState.FINISHED,
+ ),
+ TransitionStep(
+ KeyguardState.OFF,
+ KeyguardState.LOCKSCREEN,
+ 0f,
+ TransitionState.STARTED,
+ ),
+ TransitionStep(
+ KeyguardState.OFF,
+ KeyguardState.LOCKSCREEN,
+ 1f,
+ TransitionState.FINISHED,
+ ),
+ )
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt
index 9040e03..d09ee54 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt
@@ -252,5 +252,6 @@
val TO_LOCKSCREEN_DURATION = 500.milliseconds
val TO_GONE_DURATION = DEFAULT_DURATION
val TO_OCCLUDED_DURATION = DEFAULT_DURATION
+ val TO_PRIMARY_BOUNCER_DURATION = DEFAULT_DURATION
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt
index 9a6088d..1f24fc2 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt
@@ -231,5 +231,7 @@
private val DEFAULT_DURATION = 500.milliseconds
val TO_GLANCEABLE_HUB_DURATION = 1.seconds
val TO_LOCKSCREEN_DURATION = 1167.milliseconds
+ val TO_AOD_DURATION = 300.milliseconds
+ val TO_GONE_DURATION = DEFAULT_DURATION
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardBottomAreaInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardBottomAreaInteractor.kt
index b9ec58c..53f2416 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardBottomAreaInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardBottomAreaInteractor.kt
@@ -39,7 +39,7 @@
/** The position of the keyguard clock. */
private val _clockPosition = MutableStateFlow(Position(0, 0))
/** See [ClockSection] */
- @Deprecated("with migrateClocksToBlueprint()")
+ @Deprecated("with MigrateClocksToBlueprint.isEnabled")
val clockPosition: Flow<Position> = _clockPosition.asStateFlow()
fun setClockPosition(x: Int, y: Int) {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardClockInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardClockInteractor.kt
index 2cf9156..d492135 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardClockInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardClockInteractor.kt
@@ -38,14 +38,13 @@
private val keyguardClockRepository: KeyguardClockRepository,
) {
- val selectedClockSize: Flow<SettingsClockSize> = keyguardClockRepository.selectedClockSize
+ val selectedClockSize: StateFlow<SettingsClockSize> = keyguardClockRepository.selectedClockSize
val currentClockId: Flow<ClockId> = keyguardClockRepository.currentClockId
val currentClock: StateFlow<ClockController?> = keyguardClockRepository.currentClock
- val previewClockPair: StateFlow<Pair<ClockController, ClockController>> =
- keyguardClockRepository.previewClockPair
+ val previewClock: Flow<ClockController> = keyguardClockRepository.previewClock
var clock: ClockController? by keyguardClockRepository.clockEventController::clock
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt
index e6655ee..0cd7d18 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt
@@ -91,11 +91,46 @@
}
}
+ val transitions = repository.transitions
+
+ /**
+ * A pair of the most recent STARTED step, and the transition step immediately preceding it. The
+ * transition framework enforces that the previous step is either a CANCELED or FINISHED step,
+ * and that the previous step was *to* the state the STARTED step is *from*.
+ *
+ * This flow can be used to access the previous step to determine whether it was CANCELED or
+ * FINISHED. In the case of a CANCELED step, we can also figure out which state we were coming
+ * from when we were canceled.
+ */
+ val startedStepWithPrecedingStep =
+ transitions
+ .pairwise()
+ .filter { it.newValue.transitionState == TransitionState.STARTED }
+ .shareIn(scope, SharingStarted.Eagerly)
+
init {
+ // Collect non-canceled steps and emit transition values.
scope.launch(mainDispatcher) {
- repository.transitions.collect { step ->
- getTransitionValueFlow(step.from).emit(1f - step.value)
- getTransitionValueFlow(step.to).emit(step.value)
+ repository.transitions
+ .filter { it.transitionState != TransitionState.CANCELED }
+ .collect { step ->
+ getTransitionValueFlow(step.from).emit(1f - step.value)
+ getTransitionValueFlow(step.to).emit(step.value)
+ }
+ }
+
+ // If a transition from state A -> B is canceled in favor of a transition from B -> C, we
+ // need to ensure we emit transitionValue(A) = 0f, since no further steps will be emitted
+ // where the from or to states are A. This would leave transitionValue(A) stuck at an
+ // arbitrary non-zero value.
+ scope.launch(mainDispatcher) {
+ startedStepWithPrecedingStep.collect { (prevStep, startedStep) ->
+ if (
+ prevStep.transitionState == TransitionState.CANCELED &&
+ startedStep.to != prevStep.from
+ ) {
+ getTransitionValueFlow(prevStep.from).emit(0f)
+ }
}
}
}
@@ -202,8 +237,6 @@
val dozingToLockscreenTransition: Flow<TransitionStep> =
repository.transition(DOZING, LOCKSCREEN)
- val transitions = repository.transitions
-
/** Receive all [TransitionStep] matching a filter of [from]->[to] */
fun transition(from: KeyguardState, to: KeyguardState): Flow<TransitionStep> {
return repository.transition(from, to)
@@ -250,21 +283,6 @@
.stateIn(scope, SharingStarted.Eagerly, DOZING)
/**
- * A pair of the most recent STARTED step, and the transition step immediately preceding it. The
- * transition framework enforces that the previous step is either a CANCELED or FINISHED step,
- * and that the previous step was *to* the state the STARTED step is *from*.
- *
- * This flow can be used to access the previous step to determine whether it was CANCELED or
- * FINISHED. In the case of a CANCELED step, we can also figure out which state we were coming
- * from when we were canceled.
- */
- val startedStepWithPrecedingStep =
- transitions
- .pairwise()
- .filter { it.newValue.transitionState == TransitionState.STARTED }
- .stateIn(scope, SharingStarted.Eagerly, null)
-
- /**
* The last [KeyguardState] to which we [TransitionState.FINISHED] a transition.
*
* WARNING: This will NOT emit a value if a transition is CANCELED, and will also not emit a
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 b8ba098..5de1a61 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlow.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlow.kt
@@ -17,10 +17,10 @@
import android.view.animation.Interpolator
import com.android.app.animation.Interpolators.LINEAR
-import com.android.app.tracing.coroutines.launch
import com.android.keyguard.logging.KeyguardTransitionAnimationLogger
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.domain.interactor.KeyguardTransitionInteractor
import com.android.systemui.keyguard.shared.model.Edge
import com.android.systemui.keyguard.shared.model.KeyguardState
@@ -35,6 +35,7 @@
import kotlin.math.min
import kotlin.time.Duration
import kotlin.time.Duration.Companion.milliseconds
+import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.Flow
@@ -42,6 +43,7 @@
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapNotNull
+import kotlinx.coroutines.launch
/**
* Assists in creating sub-flows for a KeyguardTransition. Call [setup] once for a transition, and
@@ -52,13 +54,14 @@
@Inject
constructor(
@Application private val scope: CoroutineScope,
+ @Main private val mainDispatcher: CoroutineDispatcher,
private val transitionInteractor: KeyguardTransitionInteractor,
private val logger: KeyguardTransitionAnimationLogger,
) {
private val transitionMap = mutableMapOf<Edge, MutableSharedFlow<TransitionStep>>()
init {
- scope.launch("KeyguardTransitionAnimationFlow") {
+ scope.launch(mainDispatcher) {
transitionInteractor.transitions.collect {
// FROM->TO
transitionMap[Edge(it.from, it.to)]?.emit(it)
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 4812e03..7e3ddf9 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,9 +26,9 @@
import androidx.constraintlayout.widget.ConstraintSet
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.repeatOnLifecycle
-import com.android.systemui.Flags.keyguardBottomAreaRefactor
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.keyguard.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.IntraBlueprintTransition.Config
@@ -105,7 +105,7 @@
var transition =
if (
- !keyguardBottomAreaRefactor() &&
+ !KeyguardBottomAreaRefactor.isEnabled &&
prevBluePrint != null &&
prevBluePrint != blueprint
) {
@@ -213,9 +213,10 @@
cs: ConstraintSet,
viewModel: KeyguardClockViewModel
) {
- if (!DEBUG || viewModel.clock == null) return
+ val currentClock = viewModel.currentClock.value
+ if (!DEBUG || currentClock == null) return
val smallClockViewId = R.id.lockscreen_clock_view
- val largeClockViewId = viewModel.clock!!.largeClock.layout.views[0].id
+ val largeClockViewId = currentClock.largeClock.layout.views[0].id
Log.i(
TAG,
"applyCsToSmallClock: vis=${cs.getVisibility(smallClockViewId)} " +
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 01596ed..6255f0d 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
@@ -19,6 +19,7 @@
import android.transition.TransitionManager
import android.transition.TransitionSet
import android.view.View.INVISIBLE
+import android.view.ViewGroup
import androidx.annotation.VisibleForTesting
import androidx.constraintlayout.helper.widget.Layer
import androidx.constraintlayout.widget.ConstraintLayout
@@ -27,7 +28,7 @@
import androidx.lifecycle.repeatOnLifecycle
import com.android.keyguard.KeyguardClockSwitch.LARGE
import com.android.keyguard.KeyguardClockSwitch.SMALL
-import com.android.systemui.Flags.migrateClocksToBlueprint
+import com.android.systemui.keyguard.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.IntraBlueprintTransition.Type
@@ -40,7 +41,8 @@
object KeyguardClockViewBinder {
private val TAG = KeyguardClockViewBinder::class.simpleName!!
-
+ // When changing to new clock, we need to remove old clock views from burnInLayer
+ private var lastClock: ClockController? = null
@JvmStatic
fun bind(
clockSection: ClockSection,
@@ -55,28 +57,27 @@
}
}
keyguardRootView.repeatWhenAttached {
- repeatOnLifecycle(Lifecycle.State.STARTED) {
+ repeatOnLifecycle(Lifecycle.State.CREATED) {
launch {
- if (!migrateClocksToBlueprint()) return@launch
+ if (!MigrateClocksToBlueprint.isEnabled) return@launch
viewModel.currentClock.collect { currentClock ->
- cleanupClockViews(viewModel.clock, keyguardRootView, viewModel.burnInLayer)
- viewModel.clock = currentClock
+ cleanupClockViews(currentClock, keyguardRootView, viewModel.burnInLayer)
addClockViews(currentClock, keyguardRootView)
updateBurnInLayer(keyguardRootView, viewModel)
applyConstraints(clockSection, keyguardRootView, true)
}
}
launch {
- if (!migrateClocksToBlueprint()) return@launch
+ if (!MigrateClocksToBlueprint.isEnabled) return@launch
viewModel.clockSize.collect {
updateBurnInLayer(keyguardRootView, viewModel)
blueprintInteractor.refreshBlueprint(Type.ClockSize)
}
}
launch {
- if (!migrateClocksToBlueprint()) return@launch
+ if (!MigrateClocksToBlueprint.isEnabled) return@launch
viewModel.clockShouldBeCentered.collect { clockShouldBeCentered ->
- viewModel.clock?.let {
+ viewModel.currentClock.value?.let {
// Weather clock also has hasCustomPositionUpdatedAnimation as true
// TODO(b/323020908): remove ID check
if (
@@ -91,9 +92,9 @@
}
}
launch {
- if (!migrateClocksToBlueprint()) return@launch
+ if (!MigrateClocksToBlueprint.isEnabled) return@launch
viewModel.isAodIconsVisible.collect { isAodIconsVisible ->
- viewModel.clock?.let {
+ viewModel.currentClock.value?.let {
// Weather clock also has hasCustomPositionUpdatedAnimation as true
if (
viewModel.useLargeClock && it.config.id == "DIGITAL_CLOCK_WEATHER"
@@ -132,11 +133,14 @@
}
private fun cleanupClockViews(
- clockController: ClockController?,
+ currentClock: ClockController?,
rootView: ConstraintLayout,
burnInLayer: Layer?
) {
- clockController?.let { clock ->
+ if (lastClock == currentClock) {
+ return
+ }
+ lastClock?.let { clock ->
clock.smallClock.layout.views.forEach {
burnInLayer?.removeView(it)
rootView.removeView(it)
@@ -150,6 +154,7 @@
}
clock.largeClock.layout.views.forEach { rootView.removeView(it) }
}
+ lastClock = currentClock
}
@VisibleForTesting
@@ -157,11 +162,19 @@
clockController: ClockController?,
rootView: ConstraintLayout,
) {
+ // We'll collect the same clock when exiting wallpaper picker without changing clock
+ // so we need to remove clock views from parent before addView again
clockController?.let { clock ->
clock.smallClock.layout.views.forEach {
+ if (it.parent != null) {
+ (it.parent as ViewGroup).removeView(it)
+ }
rootView.addView(it).apply { it.visibility = INVISIBLE }
}
clock.largeClock.layout.views.forEach {
+ if (it.parent != null) {
+ (it.parent as ViewGroup).removeView(it)
+ }
rootView.addView(it).apply { it.visibility = INVISIBLE }
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardIndicationAreaBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardIndicationAreaBinder.kt
index 841f52d..267d68e 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardIndicationAreaBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardIndicationAreaBinder.kt
@@ -22,8 +22,8 @@
import android.widget.TextView
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.repeatOnLifecycle
-import com.android.systemui.Flags.keyguardBottomAreaRefactor
-import com.android.systemui.Flags.migrateClocksToBlueprint
+import com.android.systemui.keyguard.KeyguardBottomAreaRefactor
+import com.android.systemui.keyguard.MigrateClocksToBlueprint
import com.android.systemui.keyguard.ui.viewmodel.KeyguardIndicationAreaViewModel
import com.android.systemui.lifecycle.repeatWhenAttached
import com.android.systemui.res.R
@@ -69,7 +69,10 @@
launch {
// Do not independently apply alpha, as [KeyguardRootViewModel] should work
// for this and all its children
- if (!(migrateClocksToBlueprint() || keyguardBottomAreaRefactor())) {
+ if (
+ !(MigrateClocksToBlueprint.isEnabled ||
+ KeyguardBottomAreaRefactor.isEnabled)
+ ) {
viewModel.alpha.collect { alpha -> view.alpha = alpha }
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardPreviewClockViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardPreviewClockViewBinder.kt
index 46c354a..d9f12c3 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardPreviewClockViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardPreviewClockViewBinder.kt
@@ -32,7 +32,6 @@
import androidx.core.view.isVisible
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.repeatOnLifecycle
-import com.android.keyguard.ClockEventController
import com.android.systemui.customization.R as customizationR
import com.android.systemui.keyguard.shared.model.SettingsClockSize
import com.android.systemui.keyguard.ui.preview.KeyguardPreviewRenderer
@@ -44,12 +43,10 @@
import com.android.systemui.res.R
import com.android.systemui.util.Utils
import kotlin.reflect.KSuspendFunction1
-import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.launch
/** Binder for the small clock view, large clock view. */
object KeyguardPreviewClockViewBinder {
-
@JvmStatic
fun bind(
largeClockHostView: View,
@@ -72,52 +69,38 @@
@JvmStatic
fun bind(
context: Context,
- displayId: Int,
rootView: ConstraintLayout,
viewModel: KeyguardPreviewClockViewModel,
- clockEventController: ClockEventController,
updateClockAppearance: KSuspendFunction1<ClockController, Unit>,
) {
- // TODO(b/327668072): When this function is called multiple times, the clock view can be
- // gone due to a race condition on removeView and addView.
rootView.repeatWhenAttached {
repeatOnLifecycle(Lifecycle.State.STARTED) {
launch {
- combine(viewModel.selectedClockSize, viewModel.previewClockPair) { _, clock ->
- clock
+ var lastClock: ClockController? = null
+ viewModel.previewClock.collect { currentClock ->
+ lastClock?.let { clock ->
+ (clock.largeClock.layout.views + clock.smallClock.layout.views)
+ .forEach { rootView.removeView(it) }
}
- .collect { previewClockPair ->
- viewModel.lastClockPair?.let { clockPair ->
- (clockPair.first.largeClock.layout.views +
- clockPair.first.smallClock.layout.views)
- .forEach { rootView.removeView(it) }
- (clockPair.second.largeClock.layout.views +
- clockPair.second.smallClock.layout.views)
- .forEach { rootView.removeView(it) }
- }
- viewModel.lastClockPair = previewClockPair
- val clockPreview =
- if (displayId == 0) previewClockPair.first
- else previewClockPair.second
- clockEventController.clock = clockPreview
- updateClockAppearance(clockPreview)
+ lastClock = currentClock
+ updateClockAppearance(currentClock)
- if (viewModel.shouldHighlightSelectedAffordance) {
- (clockPreview.largeClock.layout.views +
- clockPreview.smallClock.layout.views)
- .forEach { it.alpha = KeyguardPreviewRenderer.DIM_ALPHA }
- }
- clockPreview.largeClock.layout.views.forEach {
- (it.parent as? ViewGroup)?.removeView(it)
- rootView.addView(it)
- }
-
- clockPreview.smallClock.layout.views.forEach {
- (it.parent as? ViewGroup)?.removeView(it)
- rootView.addView(it)
- }
- applyPreviewConstraints(context, rootView, viewModel)
+ if (viewModel.shouldHighlightSelectedAffordance) {
+ (currentClock.largeClock.layout.views +
+ currentClock.smallClock.layout.views)
+ .forEach { it.alpha = KeyguardPreviewRenderer.DIM_ALPHA }
}
+ currentClock.largeClock.layout.views.forEach {
+ (it.parent as? ViewGroup)?.removeView(it)
+ rootView.addView(it)
+ }
+
+ currentClock.smallClock.layout.views.forEach {
+ (it.parent as? ViewGroup)?.removeView(it)
+ rootView.addView(it)
+ }
+ applyPreviewConstraints(context, rootView, currentClock, viewModel)
+ }
}
}
}
@@ -170,15 +153,13 @@
private fun applyPreviewConstraints(
context: Context,
rootView: ConstraintLayout,
+ previewClock: ClockController,
viewModel: KeyguardPreviewClockViewModel
) {
val cs = ConstraintSet().apply { clone(rootView) }
- val clockPair = viewModel.previewClockPair.value
applyClockDefaultConstraints(context, cs)
- clockPair.first.largeClock.layout.applyPreviewConstraints(cs)
- clockPair.first.smallClock.layout.applyPreviewConstraints(cs)
- clockPair.second.largeClock.layout.applyPreviewConstraints(cs)
- clockPair.second.smallClock.layout.applyPreviewConstraints(cs)
+ previewClock.largeClock.layout.applyPreviewConstraints(cs)
+ previewClock.smallClock.layout.applyPreviewConstraints(cs)
// When selectedClockSize is the initial value, make both clocks invisible to avoid
// flickering
@@ -194,12 +175,9 @@
SettingsClockSize.SMALL -> VISIBLE
null -> INVISIBLE
}
-
cs.apply {
- setVisibility(clockPair.first.largeClock.layout.views, largeClockVisibility)
- setVisibility(clockPair.first.smallClock.layout.views, smallClockVisibility)
- setVisibility(clockPair.second.largeClock.layout.views, largeClockVisibility)
- setVisibility(clockPair.second.smallClock.layout.views, smallClockVisibility)
+ setVisibility(previewClock.largeClock.layout.views, largeClockVisibility)
+ setVisibility(previewClock.smallClock.layout.views, smallClockVisibility)
}
cs.applyTo(rootView)
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt
index d0246a8..0ed42ef 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt
@@ -36,8 +36,6 @@
import com.android.internal.jank.InteractionJankMonitor
import com.android.internal.jank.InteractionJankMonitor.CUJ_SCREEN_OFF_SHOW_AOD
import com.android.keyguard.KeyguardClockSwitch.MISSING_CLOCK_ID
-import com.android.systemui.Flags.keyguardBottomAreaRefactor
-import com.android.systemui.Flags.migrateClocksToBlueprint
import com.android.systemui.Flags.newAodTransition
import com.android.systemui.common.shared.model.Icon
import com.android.systemui.common.shared.model.Text
@@ -45,6 +43,8 @@
import com.android.systemui.common.ui.ConfigurationState
import com.android.systemui.deviceentry.domain.interactor.DeviceEntryHapticsInteractor
import com.android.systemui.deviceentry.shared.DeviceEntryUdfpsRefactor
+import com.android.systemui.keyguard.KeyguardBottomAreaRefactor
+import com.android.systemui.keyguard.MigrateClocksToBlueprint
import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.keyguard.shared.model.TransitionState
import com.android.systemui.keyguard.ui.viewmodel.BurnInParameters
@@ -109,7 +109,7 @@
val endButton = R.id.end_button
val lockIcon = R.id.lock_icon_view
- if (keyguardBottomAreaRefactor()) {
+ if (KeyguardBottomAreaRefactor.isEnabled) {
view.setOnTouchListener { _, event ->
if (falsingManager?.isFalseTap(FalsingManager.LOW_PENALTY) == false) {
viewModel.setRootViewLastTapPosition(Point(event.x.toInt(), event.y.toInt()))
@@ -143,11 +143,13 @@
}
}
- if (keyguardBottomAreaRefactor() || DeviceEntryUdfpsRefactor.isEnabled) {
+ if (
+ KeyguardBottomAreaRefactor.isEnabled || DeviceEntryUdfpsRefactor.isEnabled
+ ) {
launch {
viewModel.alpha(viewState).collect { alpha ->
view.alpha = alpha
- if (keyguardBottomAreaRefactor()) {
+ if (KeyguardBottomAreaRefactor.isEnabled) {
childViews[statusViewId]?.alpha = alpha
childViews[burnInLayerId]?.alpha = alpha
}
@@ -155,7 +157,7 @@
}
}
- if (migrateClocksToBlueprint()) {
+ if (MigrateClocksToBlueprint.isEnabled) {
launch {
viewModel.burnInLayerVisibility.collect { visibility ->
childViews[burnInLayerId]?.visibility = visibility
@@ -342,13 +344,13 @@
}
}
- if (!migrateClocksToBlueprint()) {
+ if (!MigrateClocksToBlueprint.isEnabled) {
burnInParams.update { current ->
current.copy(clockControllerProvider = clockControllerProvider)
}
}
- if (migrateClocksToBlueprint()) {
+ if (MigrateClocksToBlueprint.isEnabled) {
burnInParams.update { current ->
current.copy(translationY = { childViews[burnInLayerId]?.translationY })
}
@@ -439,7 +441,7 @@
burnInParams.update { current ->
current.copy(
minViewY =
- if (migrateClocksToBlueprint()) {
+ if (MigrateClocksToBlueprint.isEnabled) {
// To ensure burn-in doesn't enroach the top inset, get the min top Y
childViews.entries.fold(Int.MAX_VALUE) { currentMin, (viewId, view) ->
min(
@@ -472,7 +474,7 @@
configuration: ConfigurationState,
screenOffAnimationController: ScreenOffAnimationController,
) {
- if (migrateClocksToBlueprint()) {
+ if (MigrateClocksToBlueprint.isEnabled) {
throw IllegalStateException("should only be called in legacy code paths")
}
if (NotificationIconContainerRefactor.isUnexpectedlyInLegacyMode()) return
@@ -503,7 +505,7 @@
}
when {
!isVisible.isAnimating -> {
- if (!migrateClocksToBlueprint()) {
+ if (!MigrateClocksToBlueprint.isEnabled) {
translationY = 0f
}
visibility =
@@ -553,7 +555,7 @@
animatorListener: Animator.AnimatorListener,
) {
if (animate) {
- if (!migrateClocksToBlueprint()) {
+ if (!MigrateClocksToBlueprint.isEnabled) {
translationY = -iconAppearTranslation.toFloat()
}
alpha = 0f
@@ -561,19 +563,19 @@
.alpha(1f)
.setInterpolator(Interpolators.LINEAR)
.setDuration(AOD_ICONS_APPEAR_DURATION)
- .apply { if (migrateClocksToBlueprint()) animateInIconTranslation() }
+ .apply { if (MigrateClocksToBlueprint.isEnabled) animateInIconTranslation() }
.setListener(animatorListener)
.start()
} else {
alpha = 1.0f
- if (!migrateClocksToBlueprint()) {
+ if (!MigrateClocksToBlueprint.isEnabled) {
translationY = 0f
}
}
}
private fun View.animateInIconTranslation() {
- if (!migrateClocksToBlueprint()) {
+ if (!MigrateClocksToBlueprint.isEnabled) {
animate().animateInIconTranslation().setDuration(AOD_ICONS_APPEAR_DURATION).start()
}
}
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 b77f0c5..9aebf66 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
@@ -21,7 +21,7 @@
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.repeatOnLifecycle
-import com.android.systemui.Flags.migrateClocksToBlueprint
+import com.android.systemui.keyguard.MigrateClocksToBlueprint
import com.android.systemui.keyguard.domain.interactor.KeyguardBlueprintInteractor
import com.android.systemui.keyguard.ui.view.layout.blueprints.transitions.IntraBlueprintTransition.Config
import com.android.systemui.keyguard.ui.view.layout.blueprints.transitions.IntraBlueprintTransition.Type
@@ -41,9 +41,9 @@
blueprintInteractor: KeyguardBlueprintInteractor,
) {
keyguardRootView.repeatWhenAttached {
- repeatOnLifecycle(Lifecycle.State.STARTED) {
+ repeatOnLifecycle(Lifecycle.State.CREATED) {
launch {
- if (!migrateClocksToBlueprint()) return@launch
+ if (!MigrateClocksToBlueprint.isEnabled) return@launch
clockViewModel.hasCustomWeatherDataDisplay.collect { hasCustomWeatherDataDisplay
->
updateDateWeatherToBurnInLayer(
@@ -62,7 +62,7 @@
}
launch {
- if (!migrateClocksToBlueprint()) return@launch
+ if (!MigrateClocksToBlueprint.isEnabled) return@launch
smartspaceViewModel.bcSmartspaceVisibility.collect {
updateBCSmartspaceInBurnInLayer(keyguardRootView, clockViewModel)
blueprintInteractor.refreshBlueprint(
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 7c76e6a..14ab17f 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
@@ -50,8 +50,6 @@
import androidx.core.view.isInvisible
import com.android.keyguard.ClockEventController
import com.android.keyguard.KeyguardClockSwitch
-import com.android.systemui.Flags.keyguardBottomAreaRefactor
-import com.android.systemui.Flags.migrateClocksToBlueprint
import com.android.systemui.animation.view.LaunchableImageView
import com.android.systemui.biometrics.domain.interactor.UdfpsOverlayInteractor
import com.android.systemui.broadcast.BroadcastDispatcher
@@ -61,6 +59,8 @@
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.keyguard.KeyguardBottomAreaRefactor
+import com.android.systemui.keyguard.MigrateClocksToBlueprint
import com.android.systemui.keyguard.ui.binder.KeyguardPreviewClockViewBinder
import com.android.systemui.keyguard.ui.binder.KeyguardPreviewSmartspaceViewBinder
import com.android.systemui.keyguard.ui.binder.KeyguardQuickAffordanceViewBinder
@@ -90,6 +90,7 @@
import com.android.systemui.statusbar.phone.KeyguardBottomAreaView
import com.android.systemui.statusbar.phone.ScreenOffAnimationController
import com.android.systemui.temporarydisplay.chipbar.ChipbarCoordinator
+import com.android.systemui.util.kotlin.DisposableHandles
import com.android.systemui.util.settings.SecureSettings
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
@@ -173,7 +174,7 @@
private lateinit var smallClockHostView: FrameLayout
private var smartSpaceView: View? = null
- private val disposables = mutableSetOf<DisposableHandle>()
+ private val disposables = DisposableHandles()
private var isDestroyed = false
private val shortcutsBindings = mutableSetOf<KeyguardQuickAffordanceViewBinder.Binding>()
@@ -183,9 +184,9 @@
init {
coroutineScope = CoroutineScope(applicationScope.coroutineContext + Job())
- disposables.add(DisposableHandle { coroutineScope.cancel() })
+ disposables += DisposableHandle { coroutineScope.cancel() }
- if (keyguardBottomAreaRefactor()) {
+ if (KeyguardBottomAreaRefactor.isEnabled) {
quickAffordancesCombinedViewModel.enablePreviewMode(
initiallySelectedSlotId =
bundle.getString(
@@ -203,7 +204,7 @@
shouldHighlightSelectedAffordance = shouldHighlightSelectedAffordance,
)
}
- if (migrateClocksToBlueprint()) {
+ if (MigrateClocksToBlueprint.isEnabled) {
clockViewModel.shouldHighlightSelectedAffordance = shouldHighlightSelectedAffordance
}
runBlocking(mainDispatcher) {
@@ -214,7 +215,7 @@
if (hostToken == null) null else InputTransferToken(hostToken),
"KeyguardPreviewRenderer"
)
- disposables.add(DisposableHandle { host.release() })
+ disposables += DisposableHandle { host.release() }
}
}
@@ -230,7 +231,7 @@
setupKeyguardRootView(previewContext, rootView)
- if (!keyguardBottomAreaRefactor()) {
+ if (!KeyguardBottomAreaRefactor.isEnabled) {
setUpBottomArea(rootView)
}
@@ -274,7 +275,7 @@
}
fun onSlotSelected(slotId: String) {
- if (keyguardBottomAreaRefactor()) {
+ if (KeyguardBottomAreaRefactor.isEnabled) {
quickAffordancesCombinedViewModel.onPreviewSlotSelected(slotId = slotId)
} else {
bottomAreaViewModel.onPreviewSlotSelected(slotId = slotId)
@@ -284,8 +285,8 @@
fun destroy() {
isDestroyed = true
lockscreenSmartspaceController.disconnect()
- disposables.forEach { it.dispose() }
- if (keyguardBottomAreaRefactor()) {
+ disposables.dispose()
+ if (KeyguardBottomAreaRefactor.isEnabled) {
shortcutsBindings.forEach { it.destroy() }
}
}
@@ -371,8 +372,8 @@
@OptIn(ExperimentalCoroutinesApi::class)
private fun setupKeyguardRootView(previewContext: Context, rootView: FrameLayout) {
val keyguardRootView = KeyguardRootView(previewContext, null)
- if (!keyguardBottomAreaRefactor()) {
- disposables.add(
+ if (!KeyguardBottomAreaRefactor.isEnabled) {
+ disposables +=
KeyguardRootViewBinder.bind(
keyguardRootView,
keyguardRootViewModel,
@@ -387,7 +388,6 @@
null, // device entry haptics not required for preview mode
null, // falsing manager not required for preview mode
)
- )
}
rootView.addView(
keyguardRootView,
@@ -397,21 +397,22 @@
),
)
- setUpUdfps(previewContext, if (migrateClocksToBlueprint()) keyguardRootView else rootView)
+ setUpUdfps(
+ previewContext,
+ if (MigrateClocksToBlueprint.isEnabled) keyguardRootView else rootView
+ )
- if (keyguardBottomAreaRefactor()) {
+ if (KeyguardBottomAreaRefactor.isEnabled) {
setupShortcuts(keyguardRootView)
}
if (!shouldHideClock) {
setUpClock(previewContext, rootView)
- if (migrateClocksToBlueprint()) {
+ if (MigrateClocksToBlueprint.isEnabled) {
KeyguardPreviewClockViewBinder.bind(
context,
- displayId,
keyguardRootView,
clockViewModel,
- clockController,
::updateClockAppearance
)
} else {
@@ -482,7 +483,7 @@
) as View
// Place the UDFPS view in the proper sensor location
- if (migrateClocksToBlueprint()) {
+ if (MigrateClocksToBlueprint.isEnabled) {
finger.id = R.id.lock_icon_view
parentView.addView(finger)
val cs = ConstraintSet()
@@ -509,7 +510,7 @@
private fun setUpClock(previewContext: Context, parentView: ViewGroup) {
val resources = parentView.resources
- if (!migrateClocksToBlueprint()) {
+ if (!MigrateClocksToBlueprint.isEnabled) {
largeClockHostView = FrameLayout(previewContext)
largeClockHostView.layoutParams =
FrameLayout.LayoutParams(
@@ -547,7 +548,7 @@
}
// TODO (b/283465254): Move the listeners to KeyguardClockRepository
- if (!migrateClocksToBlueprint()) {
+ if (!MigrateClocksToBlueprint.isEnabled) {
val clockChangeListener =
object : ClockRegistry.ClockChangeListener {
override fun onCurrentClockChanged() {
@@ -555,14 +556,12 @@
}
}
clockRegistry.registerClockChangeListener(clockChangeListener)
- disposables.add(
- DisposableHandle {
- clockRegistry.unregisterClockChangeListener(clockChangeListener)
- }
- )
+ disposables += DisposableHandle {
+ clockRegistry.unregisterClockChangeListener(clockChangeListener)
+ }
clockController.registerListeners(parentView)
- disposables.add(DisposableHandle { clockController.unregisterListeners() })
+ disposables += DisposableHandle { clockController.unregisterListeners() }
}
val receiver =
@@ -581,9 +580,9 @@
addAction(Intent.ACTION_TIME_CHANGED)
},
)
- disposables.add(DisposableHandle { broadcastDispatcher.unregisterReceiver(receiver) })
+ disposables += DisposableHandle { broadcastDispatcher.unregisterReceiver(receiver) }
- if (!migrateClocksToBlueprint()) {
+ if (!MigrateClocksToBlueprint.isEnabled) {
val layoutChangeListener =
View.OnLayoutChangeListener { _, _, _, _, _, _, _, _, _ ->
if (clockController.clock !is DefaultClockController) {
@@ -602,9 +601,9 @@
}
}
parentView.addOnLayoutChangeListener(layoutChangeListener)
- disposables.add(
- DisposableHandle { parentView.removeOnLayoutChangeListener(layoutChangeListener) }
- )
+ disposables += DisposableHandle {
+ parentView.removeOnLayoutChangeListener(layoutChangeListener)
+ }
}
onClockChanged()
@@ -631,7 +630,7 @@
}
}
private fun onClockChanged() {
- if (migrateClocksToBlueprint()) {
+ if (MigrateClocksToBlueprint.isEnabled) {
return
}
coroutineScope.launch {
@@ -678,7 +677,7 @@
}
private fun updateLargeClock(clock: ClockController) {
- if (migrateClocksToBlueprint()) {
+ if (MigrateClocksToBlueprint.isEnabled) {
return
}
clock.largeClock.events.onTargetRegionChanged(
@@ -692,7 +691,7 @@
}
private fun updateSmallClock(clock: ClockController) {
- if (migrateClocksToBlueprint()) {
+ if (MigrateClocksToBlueprint.isEnabled) {
return
}
clock.smallClock.events.onTargetRegionChanged(
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/transitions/DeviceEntryIconTransitionModule.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/transitions/DeviceEntryIconTransitionModule.kt
index f20c4ac..3b21141 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/transitions/DeviceEntryIconTransitionModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/transitions/DeviceEntryIconTransitionModule.kt
@@ -22,10 +22,12 @@
import com.android.systemui.keyguard.ui.viewmodel.AodToGoneTransitionViewModel
import com.android.systemui.keyguard.ui.viewmodel.AodToLockscreenTransitionViewModel
import com.android.systemui.keyguard.ui.viewmodel.AodToOccludedTransitionViewModel
+import com.android.systemui.keyguard.ui.viewmodel.AodToPrimaryBouncerTransitionViewModel
import com.android.systemui.keyguard.ui.viewmodel.DozingToGoneTransitionViewModel
import com.android.systemui.keyguard.ui.viewmodel.DozingToLockscreenTransitionViewModel
import com.android.systemui.keyguard.ui.viewmodel.DozingToOccludedTransitionViewModel
import com.android.systemui.keyguard.ui.viewmodel.DozingToPrimaryBouncerTransitionViewModel
+import com.android.systemui.keyguard.ui.viewmodel.DreamingToAodTransitionViewModel
import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel
import com.android.systemui.keyguard.ui.viewmodel.GoneToAodTransitionViewModel
import com.android.systemui.keyguard.ui.viewmodel.GoneToDozingTransitionViewModel
@@ -89,6 +91,12 @@
@Binds
@IntoSet
+ abstract fun aodToPrimaryBouncer(
+ impl: AodToPrimaryBouncerTransitionViewModel
+ ): DeviceEntryIconTransition
+
+ @Binds
+ @IntoSet
abstract fun dozingToGone(impl: DozingToGoneTransitionViewModel): DeviceEntryIconTransition
@Binds
@@ -111,6 +119,10 @@
@Binds
@IntoSet
+ abstract fun dreamingToAod(impl: DreamingToAodTransitionViewModel): DeviceEntryIconTransition
+
+ @Binds
+ @IntoSet
abstract fun dreamingToLockscreen(
impl: DreamingToLockscreenTransitionViewModel
): DeviceEntryIconTransition
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 9c9df80..a215efa 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
@@ -41,7 +41,7 @@
private fun excludeClockAndSmartspaceViews(transition: Transition) {
transition.excludeTarget(SmartspaceView::class.java, true)
- clockViewModel.clock?.let { clock ->
+ clockViewModel.currentClock.value?.let { clock ->
clock.largeClock.layout.views.forEach { view -> transition.excludeTarget(view, true) }
clock.smallClock.layout.views.forEach { view -> transition.excludeTarget(view, true) }
}
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
index 3adeb2a..c69d868 100644
--- 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
@@ -57,7 +57,9 @@
when (config.type) {
Type.NoTransition -> {}
Type.DefaultClockStepping ->
- addTransition(clockViewModel.clock?.let { DefaultClockSteppingTransition(it) })
+ addTransition(
+ clockViewModel.currentClock.value?.let { DefaultClockSteppingTransition(it) }
+ )
else -> addTransition(ClockSizeTransition(config, clockViewModel, smartspaceViewModel))
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AlignShortcutsToUdfpsSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AlignShortcutsToUdfpsSection.kt
index cd46d6c..2e96638 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AlignShortcutsToUdfpsSection.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AlignShortcutsToUdfpsSection.kt
@@ -25,9 +25,9 @@
import androidx.constraintlayout.widget.ConstraintSet.PARENT_ID
import androidx.constraintlayout.widget.ConstraintSet.RIGHT
import androidx.constraintlayout.widget.ConstraintSet.TOP
-import com.android.systemui.Flags.keyguardBottomAreaRefactor
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.deviceentry.shared.DeviceEntryUdfpsRefactor
+import com.android.systemui.keyguard.KeyguardBottomAreaRefactor
import com.android.systemui.keyguard.ui.binder.KeyguardQuickAffordanceViewBinder
import com.android.systemui.keyguard.ui.viewmodel.KeyguardQuickAffordancesCombinedViewModel
import com.android.systemui.keyguard.ui.viewmodel.KeyguardRootViewModel
@@ -49,14 +49,14 @@
private val vibratorHelper: VibratorHelper,
) : BaseShortcutSection() {
override fun addViews(constraintLayout: ConstraintLayout) {
- if (keyguardBottomAreaRefactor()) {
+ if (KeyguardBottomAreaRefactor.isEnabled) {
addLeftShortcut(constraintLayout)
addRightShortcut(constraintLayout)
}
}
override fun bindData(constraintLayout: ConstraintLayout) {
- if (keyguardBottomAreaRefactor()) {
+ if (KeyguardBottomAreaRefactor.isEnabled) {
leftShortcutHandle =
KeyguardQuickAffordanceViewBinder.bind(
constraintLayout.requireViewById(R.id.start_button),
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodBurnInSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodBurnInSection.kt
index 88ce9dc..d639978 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodBurnInSection.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodBurnInSection.kt
@@ -23,7 +23,7 @@
import androidx.constraintlayout.widget.ConstraintSet
import androidx.constraintlayout.widget.ConstraintSet.BOTTOM
import androidx.constraintlayout.widget.ConstraintSet.PARENT_ID
-import com.android.systemui.Flags.migrateClocksToBlueprint
+import com.android.systemui.keyguard.MigrateClocksToBlueprint
import com.android.systemui.keyguard.shared.model.KeyguardSection
import com.android.systemui.keyguard.ui.view.KeyguardRootView
import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel
@@ -47,7 +47,7 @@
}
}
override fun addViews(constraintLayout: ConstraintLayout) {
- if (!migrateClocksToBlueprint()) {
+ if (!MigrateClocksToBlueprint.isEnabled) {
return
}
@@ -62,14 +62,14 @@
}
override fun bindData(constraintLayout: ConstraintLayout) {
- if (!migrateClocksToBlueprint()) {
+ if (!MigrateClocksToBlueprint.isEnabled) {
return
}
clockViewModel.burnInLayer = burnInLayer
}
override fun applyConstraints(constraintSet: ConstraintSet) {
- if (!migrateClocksToBlueprint()) {
+ if (!MigrateClocksToBlueprint.isEnabled) {
return
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodNotificationIconsSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodNotificationIconsSection.kt
index 3d9c04e..2832e9d 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodNotificationIconsSection.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodNotificationIconsSection.kt
@@ -26,8 +26,8 @@
import androidx.constraintlayout.widget.ConstraintSet.PARENT_ID
import androidx.constraintlayout.widget.ConstraintSet.START
import androidx.constraintlayout.widget.ConstraintSet.TOP
-import com.android.systemui.Flags.migrateClocksToBlueprint
import com.android.systemui.common.ui.ConfigurationState
+import com.android.systemui.keyguard.MigrateClocksToBlueprint
import com.android.systemui.keyguard.shared.model.KeyguardSection
import com.android.systemui.res.R
import com.android.systemui.statusbar.notification.icon.ui.viewbinder.AlwaysOnDisplayNotificationIconViewStore
@@ -58,7 +58,7 @@
private lateinit var nic: NotificationIconContainer
override fun addViews(constraintLayout: ConstraintLayout) {
- if (!migrateClocksToBlueprint()) {
+ if (!MigrateClocksToBlueprint.isEnabled) {
return
}
nic =
@@ -77,7 +77,7 @@
}
override fun bindData(constraintLayout: ConstraintLayout) {
- if (!migrateClocksToBlueprint()) {
+ if (!MigrateClocksToBlueprint.isEnabled) {
return
}
@@ -98,7 +98,7 @@
}
override fun applyConstraints(constraintSet: ConstraintSet) {
- if (!migrateClocksToBlueprint()) {
+ if (!MigrateClocksToBlueprint.isEnabled) {
return
}
val bottomMargin =
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 a183b72..881467f 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
@@ -30,8 +30,8 @@
import androidx.constraintlayout.widget.ConstraintSet.TOP
import androidx.constraintlayout.widget.ConstraintSet.VISIBLE
import androidx.constraintlayout.widget.ConstraintSet.WRAP_CONTENT
-import com.android.systemui.Flags
import com.android.systemui.customization.R as customizationR
+import com.android.systemui.keyguard.MigrateClocksToBlueprint
import com.android.systemui.keyguard.domain.interactor.KeyguardBlueprintInteractor
import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor
import com.android.systemui.keyguard.shared.model.KeyguardSection
@@ -70,7 +70,7 @@
override fun addViews(constraintLayout: ConstraintLayout) {}
override fun bindData(constraintLayout: ConstraintLayout) {
- if (!Flags.migrateClocksToBlueprint()) {
+ if (!MigrateClocksToBlueprint.isEnabled) {
return
}
KeyguardClockViewBinder.bind(
@@ -83,10 +83,10 @@
}
override fun applyConstraints(constraintSet: ConstraintSet) {
- if (!Flags.migrateClocksToBlueprint()) {
+ if (!MigrateClocksToBlueprint.isEnabled) {
return
}
- clockInteractor.clock?.let { clock ->
+ keyguardClockViewModel.currentClock.value?.let { clock ->
constraintSet.applyDeltaFrom(buildConstraints(clock, constraintSet))
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntrySection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntrySection.kt
index 8fd8bec..4c846e4 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntrySection.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntrySection.kt
@@ -28,13 +28,12 @@
import androidx.constraintlayout.widget.ConstraintSet
import com.android.keyguard.LockIconView
import com.android.keyguard.LockIconViewController
-import com.android.systemui.Flags.keyguardBottomAreaRefactor
-import com.android.systemui.Flags.migrateClocksToBlueprint
import com.android.systemui.biometrics.AuthController
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.deviceentry.shared.DeviceEntryUdfpsRefactor
import com.android.systemui.flags.FeatureFlags
import com.android.systemui.flags.Flags
+import com.android.systemui.keyguard.KeyguardBottomAreaRefactor
import com.android.systemui.keyguard.shared.model.KeyguardSection
import com.android.systemui.keyguard.ui.binder.DeviceEntryIconViewBinder
import com.android.systemui.keyguard.ui.view.DeviceEntryIconView
@@ -72,8 +71,8 @@
override fun addViews(constraintLayout: ConstraintLayout) {
if (
- !keyguardBottomAreaRefactor() &&
- !migrateClocksToBlueprint() &&
+ !KeyguardBottomAreaRefactor.isEnabled &&
+ !DeviceEntryUdfpsRefactor.isEnabled &&
!DeviceEntryUdfpsRefactor.isEnabled
) {
return
@@ -87,7 +86,7 @@
if (DeviceEntryUdfpsRefactor.isEnabled) {
DeviceEntryIconView(context, null).apply { id = deviceEntryIconViewId }
} else {
- // keyguardBottomAreaRefactor() or migrateClocksToBlueprint()
+ // KeyguardBottomAreaRefactor.isEnabled or MigrateClocksToBlueprint.isEnabled
LockIconView(context, null).apply { id = R.id.lock_icon_view }
}
constraintLayout.addView(view)
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultIndicationAreaSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultIndicationAreaSection.kt
index 3361343..af0528a 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultIndicationAreaSection.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultIndicationAreaSection.kt
@@ -21,7 +21,7 @@
import android.view.ViewGroup
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.constraintlayout.widget.ConstraintSet
-import com.android.systemui.Flags.keyguardBottomAreaRefactor
+import com.android.systemui.keyguard.KeyguardBottomAreaRefactor
import com.android.systemui.keyguard.shared.model.KeyguardSection
import com.android.systemui.keyguard.ui.binder.KeyguardIndicationAreaBinder
import com.android.systemui.keyguard.ui.view.KeyguardIndicationArea
@@ -42,14 +42,14 @@
private var indicationAreaHandle: DisposableHandle? = null
override fun addViews(constraintLayout: ConstraintLayout) {
- if (keyguardBottomAreaRefactor()) {
+ if (KeyguardBottomAreaRefactor.isEnabled) {
val view = KeyguardIndicationArea(context, null)
constraintLayout.addView(view)
}
}
override fun bindData(constraintLayout: ConstraintLayout) {
- if (keyguardBottomAreaRefactor()) {
+ if (KeyguardBottomAreaRefactor.isEnabled) {
indicationAreaHandle =
KeyguardIndicationAreaBinder.bind(
constraintLayout.requireViewById(R.id.keyguard_indication_area),
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultNotificationStackScrollLayoutSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultNotificationStackScrollLayoutSection.kt
index 6a3b920..380e361 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultNotificationStackScrollLayoutSection.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultNotificationStackScrollLayoutSection.kt
@@ -25,58 +25,42 @@
import androidx.constraintlayout.widget.ConstraintSet.START
import androidx.constraintlayout.widget.ConstraintSet.TOP
import com.android.systemui.Flags.centralizedStatusBarHeightFix
-import com.android.systemui.Flags.migrateClocksToBlueprint
-import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.keyguard.MigrateClocksToBlueprint
import com.android.systemui.res.R
-import com.android.systemui.scene.shared.flag.SceneContainerFlags
import com.android.systemui.shade.LargeScreenHeaderHelper
import com.android.systemui.shade.NotificationPanelView
-import com.android.systemui.statusbar.notification.stack.AmbientState
-import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController
-import com.android.systemui.statusbar.notification.stack.NotificationStackSizeCalculator
import com.android.systemui.statusbar.notification.stack.ui.view.SharedNotificationContainer
-import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationStackAppearanceViewModel
+import com.android.systemui.statusbar.notification.stack.ui.viewbinder.SharedNotificationContainerBinder
import com.android.systemui.statusbar.notification.stack.ui.viewmodel.SharedNotificationContainerViewModel
import dagger.Lazy
import javax.inject.Inject
-import kotlinx.coroutines.CoroutineDispatcher
/** Single column format for notifications (default for phones) */
class DefaultNotificationStackScrollLayoutSection
@Inject
constructor(
context: Context,
- sceneContainerFlags: SceneContainerFlags,
notificationPanelView: NotificationPanelView,
sharedNotificationContainer: SharedNotificationContainer,
sharedNotificationContainerViewModel: SharedNotificationContainerViewModel,
- notificationStackAppearanceViewModel: NotificationStackAppearanceViewModel,
- ambientState: AmbientState,
- controller: NotificationStackScrollLayoutController,
- notificationStackSizeCalculator: NotificationStackSizeCalculator,
+ sharedNotificationContainerBinder: SharedNotificationContainerBinder,
private val largeScreenHeaderHelperLazy: Lazy<LargeScreenHeaderHelper>,
- @Main mainDispatcher: CoroutineDispatcher,
) :
NotificationStackScrollLayoutSection(
context,
- sceneContainerFlags,
notificationPanelView,
sharedNotificationContainer,
sharedNotificationContainerViewModel,
- notificationStackAppearanceViewModel,
- ambientState,
- controller,
- notificationStackSizeCalculator,
- mainDispatcher,
+ sharedNotificationContainerBinder,
) {
override fun applyConstraints(constraintSet: ConstraintSet) {
- if (!migrateClocksToBlueprint()) {
+ if (!MigrateClocksToBlueprint.isEnabled) {
return
}
constraintSet.apply {
val bottomMargin =
context.resources.getDimensionPixelSize(R.dimen.keyguard_status_view_bottom_margin)
- if (migrateClocksToBlueprint()) {
+ if (MigrateClocksToBlueprint.isEnabled) {
val useLargeScreenHeader =
context.resources.getBoolean(R.bool.config_use_large_screen_shade_header)
val marginTopLargeScreen =
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultSettingsPopupMenuSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultSettingsPopupMenuSection.kt
index a203c53..32e76d0 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultSettingsPopupMenuSection.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultSettingsPopupMenuSection.kt
@@ -29,9 +29,9 @@
import androidx.constraintlayout.widget.ConstraintSet.VISIBILITY_MODE_IGNORE
import androidx.constraintlayout.widget.ConstraintSet.WRAP_CONTENT
import androidx.core.view.isVisible
-import com.android.systemui.Flags.keyguardBottomAreaRefactor
import com.android.systemui.animation.view.LaunchableLinearLayout
import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.keyguard.KeyguardBottomAreaRefactor
import com.android.systemui.keyguard.shared.model.KeyguardSection
import com.android.systemui.keyguard.ui.binder.KeyguardSettingsViewBinder
import com.android.systemui.keyguard.ui.viewmodel.KeyguardLongPressViewModel
@@ -56,7 +56,7 @@
private var settingsPopupMenuHandle: DisposableHandle? = null
override fun addViews(constraintLayout: ConstraintLayout) {
- if (!keyguardBottomAreaRefactor()) {
+ if (!KeyguardBottomAreaRefactor.isEnabled) {
return
}
val view =
@@ -71,7 +71,7 @@
}
override fun bindData(constraintLayout: ConstraintLayout) {
- if (keyguardBottomAreaRefactor()) {
+ if (KeyguardBottomAreaRefactor.isEnabled) {
settingsPopupMenuHandle =
KeyguardSettingsViewBinder.bind(
constraintLayout.requireViewById<View>(R.id.keyguard_settings_button),
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultShortcutsSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultShortcutsSection.kt
index 0c0eb8a..45b8257 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultShortcutsSection.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultShortcutsSection.kt
@@ -25,8 +25,8 @@
import androidx.constraintlayout.widget.ConstraintSet.PARENT_ID
import androidx.constraintlayout.widget.ConstraintSet.RIGHT
import androidx.constraintlayout.widget.ConstraintSet.VISIBILITY_MODE_IGNORE
-import com.android.systemui.Flags.keyguardBottomAreaRefactor
import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.keyguard.KeyguardBottomAreaRefactor
import com.android.systemui.keyguard.ui.binder.KeyguardQuickAffordanceViewBinder
import com.android.systemui.keyguard.ui.viewmodel.KeyguardQuickAffordancesCombinedViewModel
import com.android.systemui.keyguard.ui.viewmodel.KeyguardRootViewModel
@@ -48,14 +48,14 @@
private val vibratorHelper: VibratorHelper,
) : BaseShortcutSection() {
override fun addViews(constraintLayout: ConstraintLayout) {
- if (keyguardBottomAreaRefactor()) {
+ if (KeyguardBottomAreaRefactor.isEnabled) {
addLeftShortcut(constraintLayout)
addRightShortcut(constraintLayout)
}
}
override fun bindData(constraintLayout: ConstraintLayout) {
- if (keyguardBottomAreaRefactor()) {
+ if (KeyguardBottomAreaRefactor.isEnabled) {
leftShortcutHandle =
KeyguardQuickAffordanceViewBinder.bind(
constraintLayout.requireViewById(R.id.start_button),
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultStatusViewSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultStatusViewSection.kt
index 6e8605b..45641db 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultStatusViewSection.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultStatusViewSection.kt
@@ -31,8 +31,8 @@
import androidx.constraintlayout.widget.ConstraintSet.TOP
import com.android.keyguard.KeyguardStatusView
import com.android.keyguard.dagger.KeyguardStatusViewComponent
-import com.android.systemui.Flags.migrateClocksToBlueprint
import com.android.systemui.keyguard.KeyguardViewConfigurator
+import com.android.systemui.keyguard.MigrateClocksToBlueprint
import com.android.systemui.keyguard.shared.model.KeyguardSection
import com.android.systemui.media.controls.ui.controller.KeyguardMediaController
import com.android.systemui.res.R
@@ -58,7 +58,7 @@
private val statusViewId = R.id.keyguard_status_view
override fun addViews(constraintLayout: ConstraintLayout) {
- if (!migrateClocksToBlueprint()) {
+ if (!MigrateClocksToBlueprint.isEnabled) {
return
}
// At startup, 2 views with the ID `R.id.keyguard_status_view` will be available.
@@ -83,7 +83,7 @@
}
override fun bindData(constraintLayout: ConstraintLayout) {
- if (migrateClocksToBlueprint()) {
+ if (MigrateClocksToBlueprint.isEnabled) {
constraintLayout.findViewById<KeyguardStatusView?>(R.id.keyguard_status_view)?.let {
val statusViewComponent =
keyguardStatusViewComponentFactory.build(it, context.display)
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultUdfpsAccessibilityOverlaySection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultUdfpsAccessibilityOverlaySection.kt
index 3265d79..2abb7ba 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultUdfpsAccessibilityOverlaySection.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultUdfpsAccessibilityOverlaySection.kt
@@ -20,10 +20,10 @@
import android.content.Context
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.constraintlayout.widget.ConstraintSet
-import com.android.systemui.Flags
import com.android.systemui.deviceentry.ui.binder.UdfpsAccessibilityOverlayBinder
import com.android.systemui.deviceentry.ui.view.UdfpsAccessibilityOverlay
import com.android.systemui.deviceentry.ui.viewmodel.DeviceEntryUdfpsAccessibilityOverlayViewModel
+import com.android.systemui.keyguard.KeyguardBottomAreaRefactor
import com.android.systemui.keyguard.shared.model.KeyguardSection
import com.android.systemui.res.R
import javax.inject.Inject
@@ -66,7 +66,7 @@
ConstraintSet.BOTTOM,
)
- if (Flags.keyguardBottomAreaRefactor()) {
+ if (KeyguardBottomAreaRefactor.isEnabled) {
connect(
viewId,
ConstraintSet.BOTTOM,
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/KeyguardSliceViewSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/KeyguardSliceViewSection.kt
index d572c51..a17c5e5 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/KeyguardSliceViewSection.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/KeyguardSliceViewSection.kt
@@ -22,7 +22,7 @@
import androidx.constraintlayout.widget.Barrier
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.constraintlayout.widget.ConstraintSet
-import com.android.systemui.Flags.migrateClocksToBlueprint
+import com.android.systemui.keyguard.MigrateClocksToBlueprint
import com.android.systemui.keyguard.shared.model.KeyguardSection
import com.android.systemui.res.R
import com.android.systemui.statusbar.lockscreen.LockscreenSmartspaceController
@@ -34,7 +34,7 @@
val smartspaceController: LockscreenSmartspaceController,
) : KeyguardSection() {
override fun addViews(constraintLayout: ConstraintLayout) {
- if (!migrateClocksToBlueprint()) return
+ if (!MigrateClocksToBlueprint.isEnabled) return
if (smartspaceController.isEnabled()) return
constraintLayout.findViewById<View?>(R.id.keyguard_slice_view)?.let {
@@ -46,7 +46,7 @@
override fun bindData(constraintLayout: ConstraintLayout) {}
override fun applyConstraints(constraintSet: ConstraintSet) {
- if (!migrateClocksToBlueprint()) return
+ if (!MigrateClocksToBlueprint.isEnabled) return
if (smartspaceController.isEnabled()) return
constraintSet.apply {
@@ -81,7 +81,7 @@
}
override fun removeViews(constraintLayout: ConstraintLayout) {
- if (!migrateClocksToBlueprint()) return
+ if (!MigrateClocksToBlueprint.isEnabled) return
if (smartspaceController.isEnabled()) return
constraintLayout.removeView(R.id.keyguard_slice_view)
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/NotificationStackScrollLayoutSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/NotificationStackScrollLayoutSection.kt
index 5dea7cb..2b601cd 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
@@ -25,38 +25,26 @@
import androidx.constraintlayout.widget.ConstraintSet
import androidx.constraintlayout.widget.ConstraintSet.BOTTOM
import androidx.constraintlayout.widget.ConstraintSet.TOP
-import com.android.systemui.Flags.migrateClocksToBlueprint
import com.android.systemui.deviceentry.shared.DeviceEntryUdfpsRefactor
+import com.android.systemui.keyguard.MigrateClocksToBlueprint
import com.android.systemui.keyguard.shared.model.KeyguardSection
import com.android.systemui.res.R
-import com.android.systemui.scene.shared.flag.SceneContainerFlags
import com.android.systemui.shade.NotificationPanelView
-import com.android.systemui.statusbar.notification.stack.AmbientState
-import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController
-import com.android.systemui.statusbar.notification.stack.NotificationStackSizeCalculator
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.SharedNotificationContainerViewModel
-import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.DisposableHandle
abstract class NotificationStackScrollLayoutSection
constructor(
protected val context: Context,
- private val sceneContainerFlags: SceneContainerFlags,
private val notificationPanelView: NotificationPanelView,
private val sharedNotificationContainer: SharedNotificationContainer,
private val sharedNotificationContainerViewModel: SharedNotificationContainerViewModel,
- private val notificationStackAppearanceViewModel: NotificationStackAppearanceViewModel,
- private val ambientState: AmbientState,
- private val controller: NotificationStackScrollLayoutController,
- private val notificationStackSizeCalculator: NotificationStackSizeCalculator,
- private val mainDispatcher: CoroutineDispatcher,
+ private val sharedNotificationContainerBinder: SharedNotificationContainerBinder,
) : KeyguardSection() {
private val placeHolderId = R.id.nssl_placeholder
- private val disposableHandles: MutableList<DisposableHandle> = mutableListOf()
+ private var disposableHandle: DisposableHandle? = null
/**
* Align the notification placeholder bottom to the top of either the lock icon or the ambient
@@ -82,7 +70,7 @@
}
override fun addViews(constraintLayout: ConstraintLayout) {
- if (!migrateClocksToBlueprint()) {
+ if (!MigrateClocksToBlueprint.isEnabled) {
return
}
// This moves the existing NSSL view to a different parent, as the controller is a
@@ -98,43 +86,21 @@
}
override fun bindData(constraintLayout: ConstraintLayout) {
- if (!migrateClocksToBlueprint()) {
+ if (!MigrateClocksToBlueprint.isEnabled) {
return
}
- disposeHandles()
- disposableHandles.add(
- SharedNotificationContainerBinder.bind(
+ disposableHandle?.dispose()
+ disposableHandle =
+ sharedNotificationContainerBinder.bind(
sharedNotificationContainer,
sharedNotificationContainerViewModel,
- sceneContainerFlags,
- controller,
- notificationStackSizeCalculator,
- mainImmediateDispatcher = mainDispatcher,
)
- )
-
- if (sceneContainerFlags.isEnabled()) {
- disposableHandles.add(
- NotificationStackAppearanceViewBinder.bind(
- context,
- sharedNotificationContainer,
- notificationStackAppearanceViewModel,
- ambientState,
- controller,
- mainImmediateDispatcher = mainDispatcher,
- )
- )
- }
}
override fun removeViews(constraintLayout: ConstraintLayout) {
- disposeHandles()
+ disposableHandle?.dispose()
+ disposableHandle = null
constraintLayout.removeView(placeHolderId)
}
-
- private fun disposeHandles() {
- disposableHandles.forEach { it.dispose() }
- disposableHandles.clear()
- }
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SmartspaceSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SmartspaceSection.kt
index b0f7a25..1847d27 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SmartspaceSection.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SmartspaceSection.kt
@@ -23,8 +23,8 @@
import androidx.constraintlayout.widget.Barrier
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.constraintlayout.widget.ConstraintSet
-import com.android.systemui.Flags.migrateClocksToBlueprint
import com.android.systemui.keyguard.KeyguardUnlockAnimationController
+import com.android.systemui.keyguard.MigrateClocksToBlueprint
import com.android.systemui.keyguard.domain.interactor.KeyguardBlueprintInteractor
import com.android.systemui.keyguard.domain.interactor.KeyguardSmartspaceInteractor
import com.android.systemui.keyguard.shared.model.KeyguardSection
@@ -56,7 +56,7 @@
private var pastVisibility: Int = -1
override fun addViews(constraintLayout: ConstraintLayout) {
- if (!migrateClocksToBlueprint()) return
+ if (!MigrateClocksToBlueprint.isEnabled) return
if (!keyguardSmartspaceViewModel.isSmartspaceEnabled) return
smartspaceView = smartspaceController.buildAndConnectView(constraintLayout)
weatherView = smartspaceController.buildAndConnectWeatherView(constraintLayout)
@@ -83,7 +83,7 @@
}
override fun bindData(constraintLayout: ConstraintLayout) {
- if (!migrateClocksToBlueprint()) return
+ if (!MigrateClocksToBlueprint.isEnabled) return
if (!keyguardSmartspaceViewModel.isSmartspaceEnabled) return
KeyguardSmartspaceViewBinder.bind(
constraintLayout,
@@ -94,7 +94,7 @@
}
override fun applyConstraints(constraintSet: ConstraintSet) {
- if (!migrateClocksToBlueprint()) return
+ if (!MigrateClocksToBlueprint.isEnabled) return
if (!keyguardSmartspaceViewModel.isSmartspaceEnabled) return
val horizontalPaddingStart =
context.resources.getDimensionPixelSize(R.dimen.below_clock_padding_start) +
@@ -191,7 +191,7 @@
}
override fun removeViews(constraintLayout: ConstraintLayout) {
- if (!migrateClocksToBlueprint()) return
+ if (!MigrateClocksToBlueprint.isEnabled) return
if (!keyguardSmartspaceViewModel.isSmartspaceEnabled) return
listOf(smartspaceView, dateView, weatherView).forEach {
it?.let {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeMediaSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeMediaSection.kt
index 21e9455..5dbba75 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeMediaSection.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeMediaSection.kt
@@ -28,7 +28,7 @@
import androidx.constraintlayout.widget.ConstraintSet.PARENT_ID
import androidx.constraintlayout.widget.ConstraintSet.START
import androidx.constraintlayout.widget.ConstraintSet.TOP
-import com.android.systemui.Flags.migrateClocksToBlueprint
+import com.android.systemui.keyguard.MigrateClocksToBlueprint
import com.android.systemui.keyguard.shared.model.KeyguardSection
import com.android.systemui.media.controls.ui.controller.KeyguardMediaController
import com.android.systemui.res.R
@@ -46,7 +46,7 @@
private val mediaContainerId = R.id.status_view_media_container
override fun addViews(constraintLayout: ConstraintLayout) {
- if (!migrateClocksToBlueprint()) {
+ if (!MigrateClocksToBlueprint.isEnabled) {
return
}
@@ -73,7 +73,7 @@
override fun bindData(constraintLayout: ConstraintLayout) {}
override fun applyConstraints(constraintSet: ConstraintSet) {
- if (!migrateClocksToBlueprint()) {
+ if (!MigrateClocksToBlueprint.isEnabled) {
return
}
@@ -87,7 +87,7 @@
}
override fun removeViews(constraintLayout: ConstraintLayout) {
- if (!migrateClocksToBlueprint()) {
+ if (!MigrateClocksToBlueprint.isEnabled) {
return
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeNotificationStackScrollLayoutSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeNotificationStackScrollLayoutSection.kt
index 2545302..1a73866 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeNotificationStackScrollLayoutSection.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeNotificationStackScrollLayoutSection.kt
@@ -23,51 +23,33 @@
import androidx.constraintlayout.widget.ConstraintSet.PARENT_ID
import androidx.constraintlayout.widget.ConstraintSet.START
import androidx.constraintlayout.widget.ConstraintSet.TOP
-import com.android.systemui.Flags.migrateClocksToBlueprint
-import com.android.systemui.dagger.qualifiers.Main
-import com.android.systemui.keyguard.ui.viewmodel.KeyguardSmartspaceViewModel
+import com.android.systemui.keyguard.MigrateClocksToBlueprint
import com.android.systemui.res.R
-import com.android.systemui.scene.shared.flag.SceneContainerFlags
import com.android.systemui.shade.NotificationPanelView
-import com.android.systemui.statusbar.notification.stack.AmbientState
-import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController
-import com.android.systemui.statusbar.notification.stack.NotificationStackSizeCalculator
import com.android.systemui.statusbar.notification.stack.ui.view.SharedNotificationContainer
-import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationStackAppearanceViewModel
+import com.android.systemui.statusbar.notification.stack.ui.viewbinder.SharedNotificationContainerBinder
import com.android.systemui.statusbar.notification.stack.ui.viewmodel.SharedNotificationContainerViewModel
import javax.inject.Inject
-import kotlinx.coroutines.CoroutineDispatcher
/** Large-screen format for notifications, shown as two columns on the device */
class SplitShadeNotificationStackScrollLayoutSection
@Inject
constructor(
context: Context,
- sceneContainerFlags: SceneContainerFlags,
notificationPanelView: NotificationPanelView,
sharedNotificationContainer: SharedNotificationContainer,
sharedNotificationContainerViewModel: SharedNotificationContainerViewModel,
- notificationStackAppearanceViewModel: NotificationStackAppearanceViewModel,
- ambientState: AmbientState,
- controller: NotificationStackScrollLayoutController,
- notificationStackSizeCalculator: NotificationStackSizeCalculator,
- private val smartspaceViewModel: KeyguardSmartspaceViewModel,
- @Main mainDispatcher: CoroutineDispatcher,
+ sharedNotificationContainerBinder: SharedNotificationContainerBinder,
) :
NotificationStackScrollLayoutSection(
context,
- sceneContainerFlags,
notificationPanelView,
sharedNotificationContainer,
sharedNotificationContainerViewModel,
- notificationStackAppearanceViewModel,
- ambientState,
- controller,
- notificationStackSizeCalculator,
- mainDispatcher,
+ sharedNotificationContainerBinder,
) {
override fun applyConstraints(constraintSet: ConstraintSet) {
- if (!migrateClocksToBlueprint()) {
+ if (!MigrateClocksToBlueprint.isEnabled) {
return
}
constraintSet.apply {
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
index 6184c82..4d3a78d 100644
--- 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
@@ -216,7 +216,9 @@
captureSmartspace = !viewModel.useLargeClock && smartspaceViewModel.isSmartspaceEnabled
if (viewModel.useLargeClock) {
- viewModel.clock?.let { it.largeClock.layout.views.forEach { addTarget(it) } }
+ viewModel.currentClock.value?.let {
+ it.largeClock.layout.views.forEach { addTarget(it) }
+ }
} else {
addTarget(R.id.lockscreen_clock_view)
}
@@ -276,7 +278,9 @@
if (viewModel.useLargeClock) {
addTarget(R.id.lockscreen_clock_view)
} else {
- viewModel.clock?.let { it.largeClock.layout.views.forEach { addTarget(it) } }
+ viewModel.currentClock.value?.let {
+ it.largeClock.layout.views.forEach { addTarget(it) }
+ }
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToGoneTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToGoneTransitionViewModel.kt
index d26356e..ac2713d 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToGoneTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToGoneTransitionViewModel.kt
@@ -16,6 +16,7 @@
package com.android.systemui.keyguard.ui.viewmodel
+import android.util.MathUtils
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.keyguard.domain.interactor.FromAlternateBouncerTransitionInteractor.Companion.TO_GONE_DURATION
import com.android.systemui.keyguard.shared.model.KeyguardState
@@ -47,13 +48,16 @@
to = KeyguardState.GONE,
)
- val lockscreenAlpha: Flow<Float> =
- transitionAnimation.sharedFlow(
+ fun lockscreenAlpha(viewState: ViewStateAccessor): Flow<Float> {
+ var startAlpha = 1f
+ return transitionAnimation.sharedFlow(
duration = 200.milliseconds,
- onStep = { 1 - it },
+ onStart = { startAlpha = viewState.alpha() },
+ onStep = { MathUtils.lerp(startAlpha, 0f, it) },
onFinish = { 0f },
- onCancel = { 1f },
+ onCancel = { startAlpha },
)
+ }
/** Scrim alpha values */
val scrimAlpha: Flow<ScrimAlpha> =
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodAlphaViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodAlphaViewModel.kt
index 5741b94..1e5f5a7 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodAlphaViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodAlphaViewModel.kt
@@ -18,8 +18,8 @@
package com.android.systemui.keyguard.ui.viewmodel
-import com.android.systemui.Flags.migrateClocksToBlueprint
import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.keyguard.MigrateClocksToBlueprint
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
import com.android.systemui.keyguard.shared.model.KeyguardState.AOD
@@ -60,7 +60,7 @@
emit(goneToAodAlpha)
} else if (step.from == GONE && step.to == DOZING) {
emit(goneToDozingAlpha)
- } else if (!migrateClocksToBlueprint()) {
+ } else if (!MigrateClocksToBlueprint.isEnabled) {
emit(keyguardAlpha)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModel.kt
index f961e08..2054932 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModel.kt
@@ -22,9 +22,9 @@
import android.util.MathUtils
import com.android.app.animation.Interpolators
import com.android.keyguard.KeyguardClockSwitch
-import com.android.systemui.Flags
import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor
import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.keyguard.MigrateClocksToBlueprint
import com.android.systemui.keyguard.domain.interactor.BurnInInteractor
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
@@ -145,7 +145,7 @@
// Ensure the desired translation doesn't encroach on the top inset
val burnInY = MathUtils.lerp(0, burnIn.translationY, interpolated).toInt()
val translationY =
- if (Flags.migrateClocksToBlueprint()) {
+ if (MigrateClocksToBlueprint.isEnabled) {
max(params.topInset - params.minViewY, burnInY)
} else {
max(params.topInset, params.minViewY + burnInY) - params.minViewY
@@ -168,8 +168,8 @@
private fun clockController(
provider: Provider<ClockController>?,
): Provider<ClockController>? {
- return if (Flags.migrateClocksToBlueprint()) {
- Provider { keyguardClockViewModel.clock }
+ return if (MigrateClocksToBlueprint.isEnabled) {
+ Provider { keyguardClockViewModel.currentClock.value }
} else {
provider
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodToPrimaryBouncerTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodToPrimaryBouncerTransitionViewModel.kt
new file mode 100644
index 0000000..9a23007
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodToPrimaryBouncerTransitionViewModel.kt
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.ui.viewmodel
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.keyguard.domain.interactor.FromAodTransitionInteractor
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow
+import com.android.systemui.keyguard.ui.transitions.DeviceEntryIconTransition
+import javax.inject.Inject
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.Flow
+
+/**
+ * Breaks down AOD->PRIMARY BOUNCER transition into discrete steps for corresponding views to
+ * consume.
+ */
+@ExperimentalCoroutinesApi
+@SysUISingleton
+class AodToPrimaryBouncerTransitionViewModel
+@Inject
+constructor(
+ animationFlow: KeyguardTransitionAnimationFlow,
+) : DeviceEntryIconTransition {
+ private val transitionAnimation =
+ animationFlow.setup(
+ duration = FromAodTransitionInteractor.TO_PRIMARY_BOUNCER_DURATION,
+ from = KeyguardState.AOD,
+ to = KeyguardState.PRIMARY_BOUNCER,
+ )
+
+ override val deviceEntryParentViewAlpha: Flow<Float> =
+ transitionAnimation.immediatelyTransitionTo(0f)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryBackgroundViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryBackgroundViewModel.kt
index 4c0a949..1b91c49 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryBackgroundViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryBackgroundViewModel.kt
@@ -55,6 +55,8 @@
lockscreenToDozingTransitionViewModel: LockscreenToDozingTransitionViewModel,
dozingToLockscreenTransitionViewModel: DozingToLockscreenTransitionViewModel,
alternateBouncerToDozingTransitionViewModel: AlternateBouncerToDozingTransitionViewModel,
+ dreamingToAodTransitionViewModel: DreamingToAodTransitionViewModel,
+ primaryBouncerToLockscreenTransitionViewModel: PrimaryBouncerToLockscreenTransitionViewModel,
) {
val color: Flow<Int> =
deviceEntryIconViewModel.useBackgroundProtection.flatMapLatest { useBackground ->
@@ -96,6 +98,9 @@
lockscreenToDozingTransitionViewModel.deviceEntryBackgroundViewAlpha,
dozingToLockscreenTransitionViewModel.deviceEntryBackgroundViewAlpha,
alternateBouncerToDozingTransitionViewModel.deviceEntryBackgroundViewAlpha,
+ dreamingToAodTransitionViewModel.deviceEntryBackgroundViewAlpha,
+ primaryBouncerToLockscreenTransitionViewModel
+ .deviceEntryBackgroundViewAlpha,
)
.merge()
.onStart {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModel.kt
index 1a01897..bc4fd1c 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModel.kt
@@ -19,6 +19,7 @@
import android.animation.FloatEvaluator
import android.animation.IntEvaluator
import com.android.keyguard.KeyguardViewController
+import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor
import com.android.systemui.deviceentry.domain.interactor.DeviceEntrySourceInteractor
import com.android.systemui.deviceentry.domain.interactor.DeviceEntryUdfpsInteractor
@@ -33,9 +34,11 @@
import com.android.systemui.util.kotlin.sample
import dagger.Lazy
import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
@@ -45,6 +48,7 @@
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.onStart
+import kotlinx.coroutines.flow.shareIn
/** Models the UI state for the containing device entry icon & long-press handling view. */
@ExperimentalCoroutinesApi
@@ -62,6 +66,7 @@
private val keyguardViewController: Lazy<KeyguardViewController>,
private val deviceEntryInteractor: DeviceEntryInteractor,
private val deviceEntrySourceInteractor: DeviceEntrySourceInteractor,
+ @Application private val scope: CoroutineScope,
) {
val isUdfpsSupported: StateFlow<Boolean> = deviceEntryUdfpsInteractor.isUdfpsSupported
private val intEvaluator = IntEvaluator()
@@ -73,7 +78,10 @@
private val qsProgress: Flow<Float> = shadeInteractor.qsExpansion.onStart { emit(0f) }
private val shadeExpansion: Flow<Float> = shadeInteractor.shadeExpansion.onStart { emit(0f) }
private val transitionAlpha: Flow<Float> =
- transitions.map { it.deviceEntryParentViewAlpha }.merge()
+ transitions
+ .map { it.deviceEntryParentViewAlpha }
+ .merge()
+ .shareIn(scope, SharingStarted.WhileSubscribed())
private val alphaMultiplierFromShadeExpansion: Flow<Float> =
combine(
showingAlternateBouncer,
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToAodTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToAodTransitionViewModel.kt
new file mode 100644
index 0000000..0fa7475
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToAodTransitionViewModel.kt
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.ui.viewmodel
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.deviceentry.domain.interactor.DeviceEntryUdfpsInteractor
+import com.android.systemui.keyguard.domain.interactor.FromDreamingTransitionInteractor
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow
+import com.android.systemui.keyguard.ui.transitions.DeviceEntryIconTransition
+import javax.inject.Inject
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.emptyFlow
+import kotlinx.coroutines.flow.flatMapLatest
+
+/** Breaks down DREAMING->AOD transition into discrete steps for corresponding views to consume. */
+@ExperimentalCoroutinesApi
+@SysUISingleton
+class DreamingToAodTransitionViewModel
+@Inject
+constructor(
+ deviceEntryUdfpsInteractor: DeviceEntryUdfpsInteractor,
+ animationFlow: KeyguardTransitionAnimationFlow,
+) : DeviceEntryIconTransition {
+ private val transitionAnimation =
+ animationFlow.setup(
+ duration = FromDreamingTransitionInteractor.TO_AOD_DURATION,
+ from = KeyguardState.DREAMING,
+ to = KeyguardState.AOD,
+ )
+
+ val deviceEntryBackgroundViewAlpha: Flow<Float> =
+ transitionAnimation.immediatelyTransitionTo(0f)
+ override val deviceEntryParentViewAlpha: Flow<Float> =
+ deviceEntryUdfpsInteractor.isUdfpsEnrolledAndEnabled.flatMapLatest { udfpsEnrolledAndEnabled
+ ->
+ if (udfpsEnrolledAndEnabled) {
+ transitionAnimation.sharedFlow(
+ duration = FromDreamingTransitionInteractor.TO_AOD_DURATION,
+ onStep = { it },
+ onFinish = { 1f },
+ )
+ } else {
+ emptyFlow()
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToGoneTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToGoneTransitionViewModel.kt
new file mode 100644
index 0000000..ec7b931
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToGoneTransitionViewModel.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS 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.viewmodel
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.keyguard.domain.interactor.FromDreamingTransitionInteractor
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow
+import kotlinx.coroutines.flow.Flow
+import javax.inject.Inject
+
+@SysUISingleton
+class DreamingToGoneTransitionViewModel
+@Inject
+constructor(
+ animationFlow: KeyguardTransitionAnimationFlow,
+) {
+
+ private val transitionAnimation =
+ animationFlow.setup(
+ duration = FromDreamingTransitionInteractor.TO_GONE_DURATION,
+ from = KeyguardState.DREAMING,
+ to = KeyguardState.GONE,
+ )
+
+ /** Lockscreen views alpha */
+ val lockscreenAlpha: Flow<Float> = transitionAnimation.immediatelyTransitionTo(0f)
+
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GoneToLockscreenTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GoneToLockscreenTransitionViewModel.kt
index e0b1c50..a2ce408 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GoneToLockscreenTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GoneToLockscreenTransitionViewModel.kt
@@ -43,9 +43,11 @@
transitionAnimation.sharedFlow(
duration = 250.milliseconds,
onStep = { it },
- onCancel = { 0f },
+ onCancel = { 1f },
)
+ val lockscreenAlpha: Flow<Float> = shortcutsAlpha
+
val deviceEntryBackgroundViewAlpha: Flow<Float> =
transitionAnimation.immediatelyTransitionTo(1f)
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModel.kt
index b6622e5..1c1c33a 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModel.kt
@@ -26,7 +26,6 @@
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
import com.android.systemui.keyguard.shared.ComposeLockscreen
import com.android.systemui.keyguard.shared.model.SettingsClockSize
-import com.android.systemui.plugins.clocks.ClockController
import com.android.systemui.res.R
import com.android.systemui.shade.domain.interactor.ShadeInteractor
import com.android.systemui.shade.shared.model.ShadeMode
@@ -54,8 +53,6 @@
val useLargeClock: Boolean
get() = clockSize.value == LARGE
- var clock: ClockController? by keyguardClockInteractor::clock
-
val clockSize =
combine(keyguardClockInteractor.selectedClockSize, keyguardClockInteractor.clockSize) {
selectedSize,
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardIndicationAreaViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardIndicationAreaViewModel.kt
index e35e065..8409f15 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardIndicationAreaViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardIndicationAreaViewModel.kt
@@ -16,10 +16,10 @@
package com.android.systemui.keyguard.ui.viewmodel
-import com.android.systemui.Flags.keyguardBottomAreaRefactor
-import com.android.systemui.Flags.migrateClocksToBlueprint
import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor
import com.android.systemui.doze.util.BurnInHelperWrapper
+import com.android.systemui.keyguard.KeyguardBottomAreaRefactor
+import com.android.systemui.keyguard.MigrateClocksToBlueprint
import com.android.systemui.keyguard.domain.interactor.BurnInInteractor
import com.android.systemui.keyguard.domain.interactor.KeyguardBottomAreaInteractor
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
@@ -52,7 +52,7 @@
/** An observable for whether the indication area should be padded. */
val isIndicationAreaPadded: Flow<Boolean> =
- if (keyguardBottomAreaRefactor()) {
+ if (KeyguardBottomAreaRefactor.isEnabled) {
combine(shortcutsCombinedViewModel.startButton, shortcutsCombinedViewModel.endButton) {
startButtonModel,
endButtonModel ->
@@ -79,7 +79,7 @@
/** An observable for the x-offset by which the indication area should be translated. */
val indicationAreaTranslationX: Flow<Float> =
- if (migrateClocksToBlueprint() || keyguardBottomAreaRefactor()) {
+ if (MigrateClocksToBlueprint.isEnabled || KeyguardBottomAreaRefactor.isEnabled) {
burnIn.map { it.translationX.toFloat() }
} else {
bottomAreaInteractor.clockPosition.map { it.x.toFloat() }.distinctUntilChanged()
@@ -87,7 +87,7 @@
/** Returns an observable for the y-offset by which the indication area should be translated. */
fun indicationAreaTranslationY(defaultBurnInOffset: Int): Flow<Float> {
- return if (migrateClocksToBlueprint()) {
+ return if (MigrateClocksToBlueprint.isEnabled) {
burnIn.map { it.translationY.toFloat() }
} else {
keyguardInteractor.dozeAmount
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardPreviewClockViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardPreviewClockViewModel.kt
index b9ff259..4f2c6f5 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardPreviewClockViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardPreviewClockViewModel.kt
@@ -24,10 +24,8 @@
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.stateIn
/** View model for the small clock view, large clock view. */
class KeyguardPreviewClockViewModel
@@ -45,15 +43,7 @@
val isSmallClockVisible: Flow<Boolean> =
interactor.selectedClockSize.map { it == SettingsClockSize.SMALL }
- var lastClockPair: Pair<ClockController, ClockController>? = null
+ val previewClock: Flow<ClockController> = interactor.previewClock
- val previewClockPair: StateFlow<Pair<ClockController, ClockController>> =
- interactor.previewClockPair
-
- val selectedClockSize: StateFlow<SettingsClockSize?> =
- interactor.selectedClockSize.stateIn(
- scope = applicationScope,
- started = SharingStarted.WhileSubscribed(),
- initialValue = null
- )
+ val selectedClockSize: StateFlow<SettingsClockSize?> = interactor.selectedClockSize
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt
index 55a4025..5337ca3 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt
@@ -85,10 +85,13 @@
private val dozingToLockscreenTransitionViewModel: DozingToLockscreenTransitionViewModel,
private val dozingToOccludedTransitionViewModel: DozingToOccludedTransitionViewModel,
private val dreamingToLockscreenTransitionViewModel: DreamingToLockscreenTransitionViewModel,
+ private val dreamingToGoneTransitionViewModel: DreamingToGoneTransitionViewModel,
private val glanceableHubToLockscreenTransitionViewModel:
GlanceableHubToLockscreenTransitionViewModel,
private val goneToAodTransitionViewModel: GoneToAodTransitionViewModel,
private val goneToDozingTransitionViewModel: GoneToDozingTransitionViewModel,
+ private val goneToDreamingTransitionViewModel: GoneToDreamingTransitionViewModel,
+ private val goneToLockscreenTransitionViewModel: GoneToLockscreenTransitionViewModel,
private val lockscreenToAodTransitionViewModel: LockscreenToAodTransitionViewModel,
private val lockscreenToDozingTransitionViewModel: LockscreenToDozingTransitionViewModel,
private val lockscreenToDreamingTransitionViewModel: LockscreenToDreamingTransitionViewModel,
@@ -136,14 +139,20 @@
}
.distinctUntilChanged()
+ private val lockscreenToGoneTransitionRunning: Flow<Boolean> =
+ keyguardTransitionInteractor
+ .isInTransitionWhere { from, to -> from == LOCKSCREEN && to == GONE }
+ .onStart { emit(false) }
+
private val alphaOnShadeExpansion: Flow<Float> =
combineTransform(
+ lockscreenToGoneTransitionRunning,
isOnLockscreen,
shadeInteractor.qsExpansion,
shadeInteractor.shadeExpansion,
- ) { isOnLockscreen, qsExpansion, shadeExpansion ->
+ ) { lockscreenToGoneTransitionRunning, isOnLockscreen, qsExpansion, shadeExpansion ->
// Fade out quickly as the shade expands
- if (isOnLockscreen) {
+ if (isOnLockscreen && !lockscreenToGoneTransitionRunning) {
val alpha =
1f -
MathUtils.constrainedMap(
@@ -197,17 +206,20 @@
merge(
alphaOnShadeExpansion,
keyguardInteractor.dismissAlpha.filterNotNull(),
- alternateBouncerToGoneTransitionViewModel.lockscreenAlpha,
+ alternateBouncerToGoneTransitionViewModel.lockscreenAlpha(viewState),
aodToGoneTransitionViewModel.lockscreenAlpha(viewState),
aodToLockscreenTransitionViewModel.lockscreenAlpha(viewState),
aodToOccludedTransitionViewModel.lockscreenAlpha(viewState),
dozingToGoneTransitionViewModel.lockscreenAlpha(viewState),
dozingToLockscreenTransitionViewModel.lockscreenAlpha,
dozingToOccludedTransitionViewModel.lockscreenAlpha(viewState),
+ dreamingToGoneTransitionViewModel.lockscreenAlpha,
dreamingToLockscreenTransitionViewModel.lockscreenAlpha,
glanceableHubToLockscreenTransitionViewModel.keyguardAlpha,
goneToAodTransitionViewModel.enterFromTopAnimationAlpha,
goneToDozingTransitionViewModel.lockscreenAlpha,
+ goneToDreamingTransitionViewModel.lockscreenAlpha,
+ goneToLockscreenTransitionViewModel.lockscreenAlpha,
lockscreenToAodTransitionViewModel.lockscreenAlpha(viewState),
lockscreenToDozingTransitionViewModel.lockscreenAlpha,
lockscreenToDreamingTransitionViewModel.lockscreenAlpha,
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToLockscreenTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToLockscreenTransitionViewModel.kt
index 34c9ac9..2575041 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToLockscreenTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToLockscreenTransitionViewModel.kt
@@ -18,7 +18,6 @@
import com.android.app.animation.Interpolators.EMPHASIZED_ACCELERATE
import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.deviceentry.domain.interactor.DeviceEntryUdfpsInteractor
import com.android.systemui.keyguard.domain.interactor.FromPrimaryBouncerTransitionInteractor
import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow
@@ -27,8 +26,6 @@
import kotlin.time.Duration.Companion.milliseconds
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.emptyFlow
-import kotlinx.coroutines.flow.flatMapLatest
/**
* Breaks down PRIMARY BOUNCER->LOCKSCREEN transition into discrete steps for corresponding views to
@@ -39,7 +36,6 @@
class PrimaryBouncerToLockscreenTransitionViewModel
@Inject
constructor(
- deviceEntryUdfpsInteractor: DeviceEntryUdfpsInteractor,
animationFlow: KeyguardTransitionAnimationFlow,
) : DeviceEntryIconTransition {
private val transitionAnimation =
@@ -49,15 +45,6 @@
to = KeyguardState.LOCKSCREEN,
)
- val deviceEntryBackgroundViewAlpha: Flow<Float> =
- deviceEntryUdfpsInteractor.isUdfpsSupported.flatMapLatest { isUdfps ->
- if (isUdfps) {
- transitionAnimation.immediatelyTransitionTo(1f)
- } else {
- emptyFlow()
- }
- }
-
val shortcutsAlpha: Flow<Float> =
transitionAnimation.sharedFlow(
duration = 250.milliseconds,
@@ -67,6 +54,8 @@
val lockscreenAlpha: Flow<Float> = shortcutsAlpha
+ val deviceEntryBackgroundViewAlpha: Flow<Float> =
+ transitionAnimation.immediatelyTransitionTo(1f)
override val deviceEntryParentViewAlpha: Flow<Float> =
transitionAnimation.immediatelyTransitionTo(1f)
}
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/data/repository/MediaDataRepository.kt b/packages/SystemUI/src/com/android/systemui/media/controls/data/repository/MediaDataRepository.kt
new file mode 100644
index 0000000..b6fd287
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/data/repository/MediaDataRepository.kt
@@ -0,0 +1,123 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS 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.media.controls.data.repository
+
+import android.util.Log
+import com.android.systemui.Dumpable
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.media.controls.shared.model.MediaData
+import com.android.systemui.media.controls.shared.model.SmartspaceMediaData
+import com.android.systemui.media.controls.util.MediaFlags
+import java.io.PrintWriter
+import javax.inject.Inject
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+
+private const val TAG = "MediaDataRepository"
+private const val DEBUG = true
+
+/** A repository that holds the state of all media controls in carousel. */
+@SysUISingleton
+class MediaDataRepository
+@Inject
+constructor(
+ private val mediaFlags: MediaFlags,
+ dumpManager: DumpManager,
+) : Dumpable {
+
+ private val _mediaEntries: MutableStateFlow<Map<String, MediaData>> =
+ MutableStateFlow(LinkedHashMap())
+ val mediaEntries: StateFlow<Map<String, MediaData>> = _mediaEntries.asStateFlow()
+
+ private val _smartspaceMediaData: MutableStateFlow<SmartspaceMediaData> =
+ MutableStateFlow(SmartspaceMediaData())
+ val smartspaceMediaData: StateFlow<SmartspaceMediaData> = _smartspaceMediaData.asStateFlow()
+
+ init {
+ dumpManager.registerNormalDumpable(TAG, this)
+ }
+
+ /** Updates the recommendation data with a new smartspace media data. */
+ fun setRecommendation(recommendation: SmartspaceMediaData) {
+ _smartspaceMediaData.value = recommendation
+ }
+
+ /**
+ * Marks the recommendation data as inactive.
+ *
+ * @return true if the recommendation was actually marked as inactive, false otherwise.
+ */
+ fun setRecommendationInactive(key: String): Boolean {
+ if (!mediaFlags.isPersistentSsCardEnabled()) {
+ Log.e(TAG, "Only persistent recommendation can be inactive!")
+ return false
+ }
+ if (DEBUG) Log.d(TAG, "Setting smartspace recommendation inactive")
+
+ if (smartspaceMediaData.value.targetId != key || !smartspaceMediaData.value.isValid()) {
+ // If this doesn't match, or we've already invalidated the data, no action needed
+ return false
+ }
+
+ setRecommendation(smartspaceMediaData.value.copy(isActive = false))
+ return true
+ }
+
+ /**
+ * Marks the recommendation data as dismissed.
+ *
+ * @return true if the recommendation was dismissed or already inactive, false otherwise.
+ */
+ fun dismissSmartspaceRecommendation(key: String): Boolean {
+ val data = smartspaceMediaData.value
+ if (data.targetId != key || !data.isValid()) {
+ // If this doesn't match, or we've already invalidated the data, no action needed
+ return false
+ }
+
+ if (DEBUG) Log.d(TAG, "Dismissing Smartspace media target")
+ if (data.isActive) {
+ setRecommendation(
+ SmartspaceMediaData(
+ targetId = smartspaceMediaData.value.targetId,
+ instanceId = smartspaceMediaData.value.instanceId
+ )
+ )
+ }
+ return true
+ }
+
+ fun removeMediaEntry(key: String): MediaData? {
+ val entries = LinkedHashMap<String, MediaData>(_mediaEntries.value)
+ val mediaData = entries.remove(key)
+ _mediaEntries.value = entries
+ return mediaData
+ }
+
+ fun addMediaEntry(key: String, data: MediaData): MediaData? {
+ val entries = LinkedHashMap<String, MediaData>(_mediaEntries.value)
+ val mediaData = entries.put(key, data)
+ _mediaEntries.value = entries
+ return mediaData
+ }
+
+ override fun dump(pw: PrintWriter, args: Array<out String>) {
+ pw.apply { println("mediaEntries: ${mediaEntries.value}") }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/data/repository/MediaFilterRepository.kt b/packages/SystemUI/src/com/android/systemui/media/controls/data/repository/MediaFilterRepository.kt
new file mode 100644
index 0000000..b94a4af
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/data/repository/MediaFilterRepository.kt
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS 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.media.controls.data.repository
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.media.controls.shared.model.MediaData
+import com.android.systemui.media.controls.shared.model.SmartspaceMediaData
+import javax.inject.Inject
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+
+/** A repository that holds the state of filtered media data on the device. */
+@SysUISingleton
+class MediaFilterRepository @Inject constructor() {
+
+ /** Key of media control that recommendations card reactivated. */
+ private val _reactivatedKey: MutableStateFlow<String?> = MutableStateFlow(null)
+ val reactivatedKey: StateFlow<String?> = _reactivatedKey.asStateFlow()
+
+ private val _smartspaceMediaData: MutableStateFlow<SmartspaceMediaData> =
+ MutableStateFlow(SmartspaceMediaData())
+ val smartspaceMediaData: StateFlow<SmartspaceMediaData> = _smartspaceMediaData.asStateFlow()
+
+ private val _selectedUserEntries: MutableStateFlow<Map<String, MediaData>> =
+ MutableStateFlow(LinkedHashMap())
+ val selectedUserEntries: StateFlow<Map<String, MediaData>> = _selectedUserEntries.asStateFlow()
+
+ private val _allUserEntries: MutableStateFlow<Map<String, MediaData>> =
+ MutableStateFlow(LinkedHashMap())
+ val allUserEntries: StateFlow<Map<String, MediaData>> = _allUserEntries.asStateFlow()
+
+ fun addMediaEntry(key: String, data: MediaData) {
+ val entries = LinkedHashMap<String, MediaData>(_allUserEntries.value)
+ entries[key] = data
+ _allUserEntries.value = entries
+ }
+
+ /**
+ * Removes the media entry corresponding to the given [key].
+ *
+ * @return media data if an entry is actually removed, `null` otherwise.
+ */
+ fun removeMediaEntry(key: String): MediaData? {
+ val entries = LinkedHashMap<String, MediaData>(_allUserEntries.value)
+ val mediaData = entries.remove(key)
+ _allUserEntries.value = entries
+ return mediaData
+ }
+
+ fun addSelectedUserMediaEntry(key: String, data: MediaData) {
+ val entries = LinkedHashMap<String, MediaData>(_selectedUserEntries.value)
+ entries[key] = data
+ _selectedUserEntries.value = entries
+ }
+
+ /**
+ * Removes selected user media entry given the corresponding key.
+ *
+ * @return media data if an entry is actually removed, `null` otherwise.
+ */
+ fun removeSelectedUserMediaEntry(key: String): MediaData? {
+ val entries = LinkedHashMap<String, MediaData>(_selectedUserEntries.value)
+ val mediaData = entries.remove(key)
+ _selectedUserEntries.value = entries
+ return mediaData
+ }
+
+ /**
+ * Removes selected user media entry given a key and media data.
+ *
+ * @return true if media data is removed, false otherwise.
+ */
+ fun removeSelectedUserMediaEntry(key: String, data: MediaData): Boolean {
+ val entries = LinkedHashMap<String, MediaData>(_selectedUserEntries.value)
+ val succeed = entries.remove(key, data)
+ if (!succeed) {
+ return false
+ }
+ _selectedUserEntries.value = entries
+ return true
+ }
+
+ fun clearSelectedUserMedia() {
+ _selectedUserEntries.value = LinkedHashMap()
+ }
+
+ /** Updates recommendation data with a new smartspace media data. */
+ fun setRecommendation(smartspaceMediaData: SmartspaceMediaData) {
+ _smartspaceMediaData.value = smartspaceMediaData
+ }
+
+ /** Updates media control key that recommendations card reactivated. */
+ fun setReactivatedKey(key: String?) {
+ _reactivatedKey.value = key
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/MediaDomainModule.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/MediaDomainModule.kt
new file mode 100644
index 0000000..e0c5419
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/MediaDomainModule.kt
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.media.controls.domain
+
+import com.android.systemui.CoreStartable
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.media.controls.domain.pipeline.LegacyMediaDataManagerImpl
+import com.android.systemui.media.controls.domain.pipeline.MediaDataManager
+import com.android.systemui.media.controls.domain.pipeline.MediaDataProcessor
+import com.android.systemui.media.controls.domain.pipeline.interactor.MediaCarouselInteractor
+import com.android.systemui.media.controls.util.MediaFlags
+import dagger.Binds
+import dagger.Module
+import dagger.Provides
+import dagger.multibindings.ClassKey
+import dagger.multibindings.IntoMap
+import javax.inject.Provider
+
+/** Dagger module for injecting media controls domain interfaces. */
+@Module
+interface MediaDomainModule {
+
+ @Binds
+ @IntoMap
+ @ClassKey(MediaCarouselInteractor::class)
+ fun bindMediaCarouselInteractor(interactor: MediaCarouselInteractor): CoreStartable
+
+ @Binds
+ @IntoMap
+ @ClassKey(MediaDataProcessor::class)
+ fun bindMediaDataProcessor(interactor: MediaDataProcessor): CoreStartable
+ companion object {
+
+ @Provides
+ @SysUISingleton
+ fun providesMediaDataManager(
+ legacyProvider: Provider<LegacyMediaDataManagerImpl>,
+ newProvider: Provider<MediaCarouselInteractor>,
+ mediaFlags: MediaFlags,
+ ): MediaDataManager {
+ return if (mediaFlags.isMediaControlsRefactorEnabled()) {
+ newProvider.get()
+ } else {
+ legacyProvider.get()
+ }
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilter.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataFilterImpl.kt
similarity index 98%
rename from packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilter.kt
rename to packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataFilterImpl.kt
index bc539ef..c02478b 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilter.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataFilterImpl.kt
@@ -61,7 +61,7 @@
* This is added at the end of the pipeline since we may still need to handle callbacks from
* background users (e.g. timeouts).
*/
-class MediaDataFilter
+class LegacyMediaDataFilterImpl
@Inject
constructor(
private val context: Context,
@@ -74,9 +74,9 @@
private val mediaFlags: MediaFlags,
) : MediaDataManager.Listener {
private val _listeners: MutableSet<MediaDataManager.Listener> = mutableSetOf()
- internal val listeners: Set<MediaDataManager.Listener>
+ val listeners: Set<MediaDataManager.Listener>
get() = _listeners.toSet()
- internal lateinit var mediaDataManager: MediaDataManager
+ lateinit var mediaDataManager: MediaDataManager
private val allEntries: LinkedHashMap<String, MediaData> = LinkedHashMap()
// The filtered userEntries, which will be a subset of all userEntries in MediaDataManager
@@ -279,7 +279,7 @@
val mediaKeys = userEntries.keys.toSet()
mediaKeys.forEach {
// Force updates to listeners, needed for re-activated card
- mediaDataManager.setTimedOut(it, timedOut = true, forceUpdate = true)
+ mediaDataManager.setInactive(it, timedOut = true, forceUpdate = true)
}
if (smartspaceMediaData.isActive) {
val dismissIntent = smartspaceMediaData.dismissIntent
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImpl.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImpl.kt
new file mode 100644
index 0000000..3a831156
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImpl.kt
@@ -0,0 +1,1693 @@
+/*
+ * 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.systemui.media.controls.domain.pipeline
+
+import android.annotation.SuppressLint
+import android.app.ActivityOptions
+import android.app.BroadcastOptions
+import android.app.Notification
+import android.app.Notification.EXTRA_SUBSTITUTE_APP_NAME
+import android.app.PendingIntent
+import android.app.StatusBarManager
+import android.app.UriGrantsManager
+import android.app.smartspace.SmartspaceAction
+import android.app.smartspace.SmartspaceConfig
+import android.app.smartspace.SmartspaceManager
+import android.app.smartspace.SmartspaceSession
+import android.app.smartspace.SmartspaceTarget
+import android.content.BroadcastReceiver
+import android.content.ContentProvider
+import android.content.ContentResolver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.content.pm.ApplicationInfo
+import android.content.pm.PackageManager
+import android.graphics.Bitmap
+import android.graphics.ImageDecoder
+import android.graphics.drawable.Animatable
+import android.graphics.drawable.Icon
+import android.media.MediaDescription
+import android.media.MediaMetadata
+import android.media.session.MediaController
+import android.media.session.MediaSession
+import android.media.session.PlaybackState
+import android.net.Uri
+import android.os.Parcelable
+import android.os.Process
+import android.os.UserHandle
+import android.provider.Settings
+import android.service.notification.StatusBarNotification
+import android.support.v4.media.MediaMetadataCompat
+import android.text.TextUtils
+import android.util.Log
+import android.util.Pair as APair
+import androidx.media.utils.MediaConstants
+import com.android.app.tracing.traceSection
+import com.android.internal.annotations.Keep
+import com.android.internal.logging.InstanceId
+import com.android.keyguard.KeyguardUpdateMonitor
+import com.android.systemui.Dumpable
+import com.android.systemui.broadcast.BroadcastDispatcher
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.media.controls.domain.pipeline.MediaDataManager.Companion.isMediaNotification
+import com.android.systemui.media.controls.domain.resume.MediaResumeListener
+import com.android.systemui.media.controls.domain.resume.ResumeMediaBrowser
+import com.android.systemui.media.controls.shared.model.EXTRA_KEY_TRIGGER_SOURCE
+import com.android.systemui.media.controls.shared.model.EXTRA_VALUE_TRIGGER_PERIODIC
+import com.android.systemui.media.controls.shared.model.MediaAction
+import com.android.systemui.media.controls.shared.model.MediaButton
+import com.android.systemui.media.controls.shared.model.MediaData
+import com.android.systemui.media.controls.shared.model.MediaDeviceData
+import com.android.systemui.media.controls.shared.model.SmartspaceMediaData
+import com.android.systemui.media.controls.shared.model.SmartspaceMediaDataProvider
+import com.android.systemui.media.controls.ui.view.MediaViewHolder
+import com.android.systemui.media.controls.util.MediaControllerFactory
+import com.android.systemui.media.controls.util.MediaDataUtils
+import com.android.systemui.media.controls.util.MediaFlags
+import com.android.systemui.media.controls.util.MediaUiEventLogger
+import com.android.systemui.plugins.ActivityStarter
+import com.android.systemui.plugins.BcSmartspaceDataPlugin
+import com.android.systemui.res.R
+import com.android.systemui.statusbar.NotificationMediaManager.isConnectingState
+import com.android.systemui.statusbar.NotificationMediaManager.isPlayingState
+import com.android.systemui.statusbar.notification.row.HybridGroupManager
+import com.android.systemui.tuner.TunerService
+import com.android.systemui.util.Assert
+import com.android.systemui.util.Utils
+import com.android.systemui.util.concurrency.DelayableExecutor
+import com.android.systemui.util.concurrency.ThreadFactory
+import com.android.systemui.util.time.SystemClock
+import java.io.IOException
+import java.io.PrintWriter
+import java.util.concurrent.Executor
+import javax.inject.Inject
+
+// URI fields to try loading album art from
+private val ART_URIS =
+ arrayOf(
+ MediaMetadata.METADATA_KEY_ALBUM_ART_URI,
+ MediaMetadata.METADATA_KEY_ART_URI,
+ MediaMetadata.METADATA_KEY_DISPLAY_ICON_URI
+ )
+
+private const val TAG = "MediaDataManager"
+private const val DEBUG = true
+private const val EXTRAS_SMARTSPACE_DISMISS_INTENT_KEY = "dismiss_intent"
+
+private val LOADING =
+ MediaData(
+ userId = -1,
+ initialized = false,
+ app = null,
+ appIcon = null,
+ artist = null,
+ song = null,
+ artwork = null,
+ actions = emptyList(),
+ actionsToShowInCompact = emptyList(),
+ packageName = "INVALID",
+ token = null,
+ clickIntent = null,
+ device = null,
+ active = true,
+ resumeAction = null,
+ instanceId = InstanceId.fakeInstanceId(-1),
+ appUid = Process.INVALID_UID
+ )
+
+internal val EMPTY_SMARTSPACE_MEDIA_DATA =
+ SmartspaceMediaData(
+ targetId = "INVALID",
+ isActive = false,
+ packageName = "INVALID",
+ cardAction = null,
+ recommendations = emptyList(),
+ dismissIntent = null,
+ headphoneConnectionTimeMillis = 0,
+ instanceId = InstanceId.fakeInstanceId(-1),
+ expiryTimeMs = 0,
+ )
+
+const val MEDIA_TITLE_ERROR_MESSAGE = "Invalid media data: title is null or blank."
+
+/**
+ * Allow recommendations from smartspace to show in media controls. Requires
+ * [Utils.useQsMediaPlayer] to be enabled. On by default, but can be disabled by setting to 0
+ */
+private fun allowMediaRecommendations(context: Context): Boolean {
+ val flag =
+ Settings.Secure.getInt(
+ context.contentResolver,
+ Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION,
+ 1
+ )
+ return Utils.useQsMediaPlayer(context) && flag > 0
+}
+
+/** A class that facilitates management and loading of Media Data, ready for binding. */
+@SysUISingleton
+class LegacyMediaDataManagerImpl(
+ private val context: Context,
+ @Background private val backgroundExecutor: Executor,
+ @Main private val uiExecutor: Executor,
+ @Main private val foregroundExecutor: DelayableExecutor,
+ private val mediaControllerFactory: MediaControllerFactory,
+ private val broadcastDispatcher: BroadcastDispatcher,
+ dumpManager: DumpManager,
+ mediaTimeoutListener: MediaTimeoutListener,
+ mediaResumeListener: MediaResumeListener,
+ mediaSessionBasedFilter: MediaSessionBasedFilter,
+ private val mediaDeviceManager: MediaDeviceManager,
+ mediaDataCombineLatest: MediaDataCombineLatest,
+ private val mediaDataFilter: LegacyMediaDataFilterImpl,
+ private val activityStarter: ActivityStarter,
+ private val smartspaceMediaDataProvider: SmartspaceMediaDataProvider,
+ private var useMediaResumption: Boolean,
+ private val useQsMediaPlayer: Boolean,
+ private val systemClock: SystemClock,
+ private val tunerService: TunerService,
+ private val mediaFlags: MediaFlags,
+ private val logger: MediaUiEventLogger,
+ private val smartspaceManager: SmartspaceManager?,
+ private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
+) : Dumpable, BcSmartspaceDataPlugin.SmartspaceTargetListener, MediaDataManager {
+
+ companion object {
+ // UI surface label for subscribing Smartspace updates.
+ @JvmField val SMARTSPACE_UI_SURFACE_LABEL = "media_data_manager"
+
+ // Smartspace package name's extra key.
+ @JvmField val EXTRAS_MEDIA_SOURCE_PACKAGE_NAME = "package_name"
+
+ // Maximum number of actions allowed in compact view
+ @JvmField val MAX_COMPACT_ACTIONS = 3
+
+ // Maximum number of actions allowed in expanded view
+ @JvmField val MAX_NOTIFICATION_ACTIONS = MediaViewHolder.genericButtonIds.size
+ }
+
+ private val themeText =
+ com.android.settingslib.Utils.getColorAttr(
+ context,
+ com.android.internal.R.attr.textColorPrimary
+ )
+ .defaultColor
+
+ // Internal listeners are part of the internal pipeline. External listeners (those registered
+ // with [MediaDeviceManager.addListener]) receive events after they have propagated through
+ // the internal pipeline.
+ // Another way to think of the distinction between internal and external listeners is the
+ // following. Internal listeners are listeners that MediaDataManager depends on, and external
+ // listeners are listeners that depend on MediaDataManager.
+ // TODO(b/159539991#comment5): Move internal listeners to separate package.
+ private val internalListeners: MutableSet<MediaDataManager.Listener> = mutableSetOf()
+ private val mediaEntries: LinkedHashMap<String, MediaData> = LinkedHashMap()
+ // There should ONLY be at most one Smartspace media recommendation.
+ var smartspaceMediaData: SmartspaceMediaData = EMPTY_SMARTSPACE_MEDIA_DATA
+ @Keep private var smartspaceSession: SmartspaceSession? = null
+ private var allowMediaRecommendations = allowMediaRecommendations(context)
+
+ private val artworkWidth =
+ context.resources.getDimensionPixelSize(
+ com.android.internal.R.dimen.config_mediaMetadataBitmapMaxSize
+ )
+ private val artworkHeight =
+ context.resources.getDimensionPixelSize(R.dimen.qs_media_session_height_expanded)
+
+ @SuppressLint("WrongConstant") // sysui allowed to call STATUS_BAR_SERVICE
+ private val statusBarManager =
+ context.getSystemService(Context.STATUS_BAR_SERVICE) as StatusBarManager
+
+ /** Check whether this notification is an RCN */
+ private fun isRemoteCastNotification(sbn: StatusBarNotification): Boolean {
+ return sbn.notification.extras.containsKey(Notification.EXTRA_MEDIA_REMOTE_DEVICE)
+ }
+
+ @Inject
+ constructor(
+ context: Context,
+ threadFactory: ThreadFactory,
+ @Main uiExecutor: Executor,
+ @Main foregroundExecutor: DelayableExecutor,
+ mediaControllerFactory: MediaControllerFactory,
+ dumpManager: DumpManager,
+ broadcastDispatcher: BroadcastDispatcher,
+ mediaTimeoutListener: MediaTimeoutListener,
+ mediaResumeListener: MediaResumeListener,
+ mediaSessionBasedFilter: MediaSessionBasedFilter,
+ mediaDeviceManager: MediaDeviceManager,
+ mediaDataCombineLatest: MediaDataCombineLatest,
+ mediaDataFilter: LegacyMediaDataFilterImpl,
+ activityStarter: ActivityStarter,
+ smartspaceMediaDataProvider: SmartspaceMediaDataProvider,
+ clock: SystemClock,
+ tunerService: TunerService,
+ mediaFlags: MediaFlags,
+ logger: MediaUiEventLogger,
+ smartspaceManager: SmartspaceManager?,
+ keyguardUpdateMonitor: KeyguardUpdateMonitor,
+ ) : this(
+ context,
+ // Loading bitmap for UMO background can take longer time, so it cannot run on the default
+ // background thread. Use a custom thread for media.
+ threadFactory.buildExecutorOnNewThread(TAG),
+ uiExecutor,
+ foregroundExecutor,
+ mediaControllerFactory,
+ broadcastDispatcher,
+ dumpManager,
+ mediaTimeoutListener,
+ mediaResumeListener,
+ mediaSessionBasedFilter,
+ mediaDeviceManager,
+ mediaDataCombineLatest,
+ mediaDataFilter,
+ activityStarter,
+ smartspaceMediaDataProvider,
+ Utils.useMediaResumption(context),
+ Utils.useQsMediaPlayer(context),
+ clock,
+ tunerService,
+ mediaFlags,
+ logger,
+ smartspaceManager,
+ keyguardUpdateMonitor,
+ )
+
+ private val appChangeReceiver =
+ object : BroadcastReceiver() {
+ override fun onReceive(context: Context, intent: Intent) {
+ when (intent.action) {
+ Intent.ACTION_PACKAGES_SUSPENDED -> {
+ val packages = intent.getStringArrayExtra(Intent.EXTRA_CHANGED_PACKAGE_LIST)
+ packages?.forEach { removeAllForPackage(it) }
+ }
+ Intent.ACTION_PACKAGE_REMOVED,
+ Intent.ACTION_PACKAGE_RESTARTED -> {
+ intent.data?.encodedSchemeSpecificPart?.let { removeAllForPackage(it) }
+ }
+ }
+ }
+ }
+
+ init {
+ dumpManager.registerDumpable(TAG, this)
+
+ // Initialize the internal processing pipeline. The listeners at the front of the pipeline
+ // are set as internal listeners so that they receive events. From there, events are
+ // propagated through the pipeline. The end of the pipeline is currently mediaDataFilter,
+ // so it is responsible for dispatching events to external listeners. To achieve this,
+ // external listeners that are registered with [MediaDataManager.addListener] are actually
+ // registered as listeners to mediaDataFilter.
+ addInternalListener(mediaTimeoutListener)
+ addInternalListener(mediaResumeListener)
+ addInternalListener(mediaSessionBasedFilter)
+ mediaSessionBasedFilter.addListener(mediaDeviceManager)
+ mediaSessionBasedFilter.addListener(mediaDataCombineLatest)
+ mediaDeviceManager.addListener(mediaDataCombineLatest)
+ mediaDataCombineLatest.addListener(mediaDataFilter)
+
+ // Set up links back into the pipeline for listeners that need to send events upstream.
+ mediaTimeoutListener.timeoutCallback = { key: String, timedOut: Boolean ->
+ setInactive(key, timedOut)
+ }
+ mediaTimeoutListener.stateCallback = { key: String, state: PlaybackState ->
+ updateState(key, state)
+ }
+ mediaTimeoutListener.sessionCallback = { key: String -> onSessionDestroyed(key) }
+ mediaResumeListener.setManager(this)
+ mediaDataFilter.mediaDataManager = this
+
+ val suspendFilter = IntentFilter(Intent.ACTION_PACKAGES_SUSPENDED)
+ broadcastDispatcher.registerReceiver(appChangeReceiver, suspendFilter, null, UserHandle.ALL)
+
+ val uninstallFilter =
+ IntentFilter().apply {
+ addAction(Intent.ACTION_PACKAGE_REMOVED)
+ addAction(Intent.ACTION_PACKAGE_RESTARTED)
+ addDataScheme("package")
+ }
+ // BroadcastDispatcher does not allow filters with data schemes
+ context.registerReceiver(appChangeReceiver, uninstallFilter)
+
+ // Register for Smartspace data updates.
+ smartspaceMediaDataProvider.registerListener(this)
+ smartspaceSession =
+ smartspaceManager?.createSmartspaceSession(
+ SmartspaceConfig.Builder(context, SMARTSPACE_UI_SURFACE_LABEL).build()
+ )
+ smartspaceSession?.let {
+ it.addOnTargetsAvailableListener(
+ // Use a main uiExecutor thread listening to Smartspace updates instead of using
+ // the existing background executor.
+ // SmartspaceSession has scheduled routine updates which can be unpredictable on
+ // test simulators, using the backgroundExecutor makes it's hard to test the threads
+ // numbers.
+ uiExecutor,
+ SmartspaceSession.OnTargetsAvailableListener { targets ->
+ smartspaceMediaDataProvider.onTargetsAvailable(targets)
+ }
+ )
+ }
+ smartspaceSession?.let { it.requestSmartspaceUpdate() }
+ tunerService.addTunable(
+ object : TunerService.Tunable {
+ override fun onTuningChanged(key: String?, newValue: String?) {
+ allowMediaRecommendations = allowMediaRecommendations(context)
+ if (!allowMediaRecommendations) {
+ dismissSmartspaceRecommendation(
+ key = smartspaceMediaData.targetId,
+ delay = 0L
+ )
+ }
+ }
+ },
+ Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION
+ )
+ }
+
+ override fun destroy() {
+ smartspaceMediaDataProvider.unregisterListener(this)
+ smartspaceSession?.close()
+ smartspaceSession = null
+ context.unregisterReceiver(appChangeReceiver)
+ }
+
+ override fun onNotificationAdded(key: String, sbn: StatusBarNotification) {
+ if (useQsMediaPlayer && isMediaNotification(sbn)) {
+ var isNewlyActiveEntry = false
+ Assert.isMainThread()
+ val oldKey = findExistingEntry(key, sbn.packageName)
+ if (oldKey == null) {
+ val instanceId = logger.getNewInstanceId()
+ val temp =
+ LOADING.copy(
+ packageName = sbn.packageName,
+ instanceId = instanceId,
+ createdTimestampMillis = systemClock.currentTimeMillis(),
+ )
+ mediaEntries.put(key, temp)
+ isNewlyActiveEntry = true
+ } else if (oldKey != key) {
+ // Resume -> active conversion; move to new key
+ val oldData = mediaEntries.remove(oldKey)!!
+ isNewlyActiveEntry = true
+ mediaEntries.put(key, oldData)
+ }
+ loadMediaData(key, sbn, oldKey, isNewlyActiveEntry)
+ } else {
+ onNotificationRemoved(key)
+ }
+ }
+
+ private fun removeAllForPackage(packageName: String) {
+ Assert.isMainThread()
+ val toRemove = mediaEntries.filter { it.value.packageName == packageName }
+ toRemove.forEach { removeEntry(it.key) }
+ }
+
+ override fun setResumeAction(key: String, action: Runnable?) {
+ mediaEntries.get(key)?.let {
+ it.resumeAction = action
+ it.hasCheckedForResume = true
+ }
+ }
+
+ override fun addResumptionControls(
+ userId: Int,
+ desc: MediaDescription,
+ action: Runnable,
+ token: MediaSession.Token,
+ appName: String,
+ appIntent: PendingIntent,
+ packageName: String
+ ) {
+ // Resume controls don't have a notification key, so store by package name instead
+ if (!mediaEntries.containsKey(packageName)) {
+ val instanceId = logger.getNewInstanceId()
+ val appUid =
+ try {
+ context.packageManager.getApplicationInfo(packageName, 0)?.uid!!
+ } catch (e: PackageManager.NameNotFoundException) {
+ Log.w(TAG, "Could not get app UID for $packageName", e)
+ Process.INVALID_UID
+ }
+
+ val resumeData =
+ LOADING.copy(
+ packageName = packageName,
+ resumeAction = action,
+ hasCheckedForResume = true,
+ instanceId = instanceId,
+ appUid = appUid,
+ createdTimestampMillis = systemClock.currentTimeMillis(),
+ )
+ mediaEntries.put(packageName, resumeData)
+ logSingleVsMultipleMediaAdded(appUid, packageName, instanceId)
+ logger.logResumeMediaAdded(appUid, packageName, instanceId)
+ }
+ backgroundExecutor.execute {
+ loadMediaDataInBgForResumption(
+ userId,
+ desc,
+ action,
+ token,
+ appName,
+ appIntent,
+ packageName
+ )
+ }
+ }
+
+ /**
+ * Check if there is an existing entry that matches the key or package name. Returns the key
+ * that matches, or null if not found.
+ */
+ private fun findExistingEntry(key: String, packageName: String): String? {
+ if (mediaEntries.containsKey(key)) {
+ return key
+ }
+ // Check if we already had a resume player
+ if (mediaEntries.containsKey(packageName)) {
+ return packageName
+ }
+ return null
+ }
+
+ private fun loadMediaData(
+ key: String,
+ sbn: StatusBarNotification,
+ oldKey: String?,
+ isNewlyActiveEntry: Boolean = false,
+ ) {
+ backgroundExecutor.execute { loadMediaDataInBg(key, sbn, oldKey, isNewlyActiveEntry) }
+ }
+
+ /** Add a listener for changes in this class */
+ override fun addListener(listener: MediaDataManager.Listener) {
+ // mediaDataFilter is the current end of the internal pipeline. Register external
+ // listeners as listeners to it.
+ mediaDataFilter.addListener(listener)
+ }
+
+ /** Remove a listener for changes in this class */
+ override fun removeListener(listener: MediaDataManager.Listener) {
+ // Since mediaDataFilter is the current end of the internal pipelie, external listeners
+ // have been registered to it. So, they need to be removed from it too.
+ mediaDataFilter.removeListener(listener)
+ }
+
+ /** Add a listener for internal events. */
+ private fun addInternalListener(listener: MediaDataManager.Listener) =
+ internalListeners.add(listener)
+
+ /**
+ * Notify internal listeners of media loaded event.
+ *
+ * External listeners registered with [addListener] will be notified after the event propagates
+ * through the internal listener pipeline.
+ */
+ private fun notifyMediaDataLoaded(key: String, oldKey: String?, info: MediaData) {
+ internalListeners.forEach { it.onMediaDataLoaded(key, oldKey, info) }
+ }
+
+ /**
+ * Notify internal listeners of Smartspace media loaded event.
+ *
+ * External listeners registered with [addListener] will be notified after the event propagates
+ * through the internal listener pipeline.
+ */
+ private fun notifySmartspaceMediaDataLoaded(key: String, info: SmartspaceMediaData) {
+ internalListeners.forEach { it.onSmartspaceMediaDataLoaded(key, info) }
+ }
+
+ /**
+ * Notify internal listeners of media removed event.
+ *
+ * External listeners registered with [addListener] will be notified after the event propagates
+ * through the internal listener pipeline.
+ */
+ private fun notifyMediaDataRemoved(key: String) {
+ internalListeners.forEach { it.onMediaDataRemoved(key) }
+ }
+
+ /**
+ * Notify internal listeners of Smartspace media removed event.
+ *
+ * External listeners registered with [addListener] will be notified after the event propagates
+ * through the internal listener pipeline.
+ *
+ * @param immediately indicates should apply the UI changes immediately, otherwise wait until
+ * the next refresh-round before UI becomes visible. Should only be true if the update is
+ * initiated by user's interaction.
+ */
+ private fun notifySmartspaceMediaDataRemoved(key: String, immediately: Boolean) {
+ internalListeners.forEach { it.onSmartspaceMediaDataRemoved(key, immediately) }
+ }
+
+ /**
+ * Called whenever the player has been paused or stopped for a while, or swiped from QQS. This
+ * will make the player not active anymore, hiding it from QQS and Keyguard.
+ *
+ * @see MediaData.active
+ */
+ override fun setInactive(key: String, timedOut: Boolean, forceUpdate: Boolean) {
+ mediaEntries[key]?.let {
+ if (timedOut && !forceUpdate) {
+ // Only log this event when media expires on its own
+ logger.logMediaTimeout(it.appUid, it.packageName, it.instanceId)
+ }
+ if (it.active == !timedOut && !forceUpdate) {
+ if (it.resumption) {
+ if (DEBUG) Log.d(TAG, "timing out resume player $key")
+ dismissMediaData(key, 0L /* delay */)
+ }
+ return
+ }
+ // Update last active if media was still active.
+ if (it.active) {
+ it.lastActive = systemClock.elapsedRealtime()
+ }
+ it.active = !timedOut
+ if (DEBUG) Log.d(TAG, "Updating $key timedOut: $timedOut")
+ onMediaDataLoaded(key, key, it)
+ }
+
+ if (key == smartspaceMediaData.targetId) {
+ if (DEBUG) Log.d(TAG, "smartspace card expired")
+ dismissSmartspaceRecommendation(key, delay = 0L)
+ }
+ }
+
+ /** Called when the player's [PlaybackState] has been updated with new actions and/or state */
+ private fun updateState(key: String, state: PlaybackState) {
+ mediaEntries.get(key)?.let {
+ val token = it.token
+ if (token == null) {
+ if (DEBUG) Log.d(TAG, "State updated, but token was null")
+ return
+ }
+ val actions =
+ createActionsFromState(
+ it.packageName,
+ mediaControllerFactory.create(it.token),
+ UserHandle(it.userId)
+ )
+
+ // Control buttons
+ // If flag is enabled and controller has a PlaybackState,
+ // create actions from session info
+ // otherwise, no need to update semantic actions.
+ val data =
+ if (actions != null) {
+ it.copy(semanticActions = actions, isPlaying = isPlayingState(state.state))
+ } else {
+ it.copy(isPlaying = isPlayingState(state.state))
+ }
+ if (DEBUG) Log.d(TAG, "State updated outside of notification")
+ onMediaDataLoaded(key, key, data)
+ }
+ }
+
+ private fun removeEntry(key: String, logEvent: Boolean = true) {
+ mediaEntries.remove(key)?.let {
+ if (logEvent) {
+ logger.logMediaRemoved(it.appUid, it.packageName, it.instanceId)
+ }
+ }
+ notifyMediaDataRemoved(key)
+ }
+
+ /** Dismiss a media entry. Returns false if the key was not found. */
+ override fun dismissMediaData(key: String, delay: Long): Boolean {
+ val existed = mediaEntries[key] != null
+ backgroundExecutor.execute {
+ mediaEntries[key]?.let { mediaData ->
+ if (mediaData.isLocalSession()) {
+ mediaData.token?.let {
+ val mediaController = mediaControllerFactory.create(it)
+ mediaController.transportControls.stop()
+ }
+ }
+ }
+ }
+ foregroundExecutor.executeDelayed({ removeEntry(key) }, delay)
+ return existed
+ }
+
+ /**
+ * Called whenever the recommendation has been expired or removed by the user. This will remove
+ * the recommendation card entirely from the carousel.
+ */
+ override fun dismissSmartspaceRecommendation(key: String, delay: Long) {
+ if (smartspaceMediaData.targetId != key || !smartspaceMediaData.isValid()) {
+ // If this doesn't match, or we've already invalidated the data, no action needed
+ return
+ }
+
+ if (DEBUG) Log.d(TAG, "Dismissing Smartspace media target")
+ if (smartspaceMediaData.isActive) {
+ smartspaceMediaData =
+ EMPTY_SMARTSPACE_MEDIA_DATA.copy(
+ targetId = smartspaceMediaData.targetId,
+ instanceId = smartspaceMediaData.instanceId
+ )
+ }
+ foregroundExecutor.executeDelayed(
+ { notifySmartspaceMediaDataRemoved(smartspaceMediaData.targetId, immediately = true) },
+ delay
+ )
+ }
+
+ /** Called when the recommendation card should no longer be visible in QQS or lockscreen */
+ override fun setRecommendationInactive(key: String) {
+ if (!mediaFlags.isPersistentSsCardEnabled()) {
+ Log.e(TAG, "Only persistent recommendation can be inactive!")
+ return
+ }
+ if (DEBUG) Log.d(TAG, "Setting smartspace recommendation inactive")
+
+ if (smartspaceMediaData.targetId != key || !smartspaceMediaData.isValid()) {
+ // If this doesn't match, or we've already invalidated the data, no action needed
+ return
+ }
+
+ smartspaceMediaData = smartspaceMediaData.copy(isActive = false)
+ notifySmartspaceMediaDataLoaded(smartspaceMediaData.targetId, smartspaceMediaData)
+ }
+
+ private fun loadMediaDataInBgForResumption(
+ userId: Int,
+ desc: MediaDescription,
+ resumeAction: Runnable,
+ token: MediaSession.Token,
+ appName: String,
+ appIntent: PendingIntent,
+ packageName: String
+ ) {
+ if (desc.title.isNullOrBlank()) {
+ Log.e(TAG, "Description incomplete")
+ // Delete the placeholder entry
+ mediaEntries.remove(packageName)
+ return
+ }
+
+ if (DEBUG) {
+ Log.d(TAG, "adding track for $userId from browser: $desc")
+ }
+
+ val currentEntry = mediaEntries.get(packageName)
+ val appUid = currentEntry?.appUid ?: Process.INVALID_UID
+
+ // Album art
+ var artworkBitmap = desc.iconBitmap
+ if (artworkBitmap == null && desc.iconUri != null) {
+ artworkBitmap = loadBitmapFromUriForUser(desc.iconUri!!, userId, appUid, packageName)
+ }
+ val artworkIcon =
+ if (artworkBitmap != null) {
+ Icon.createWithBitmap(artworkBitmap)
+ } else {
+ null
+ }
+
+ val instanceId = currentEntry?.instanceId ?: logger.getNewInstanceId()
+ val isExplicit =
+ desc.extras?.getLong(MediaConstants.METADATA_KEY_IS_EXPLICIT) ==
+ MediaConstants.METADATA_VALUE_ATTRIBUTE_PRESENT
+
+ val progress =
+ if (mediaFlags.isResumeProgressEnabled()) {
+ MediaDataUtils.getDescriptionProgress(desc.extras)
+ } else null
+
+ val mediaAction = getResumeMediaAction(resumeAction)
+ val lastActive = systemClock.elapsedRealtime()
+ val createdTimestampMillis = currentEntry?.createdTimestampMillis ?: 0L
+ foregroundExecutor.execute {
+ onMediaDataLoaded(
+ packageName,
+ null,
+ MediaData(
+ userId,
+ true,
+ appName,
+ null,
+ desc.subtitle,
+ desc.title,
+ artworkIcon,
+ listOf(mediaAction),
+ listOf(0),
+ MediaButton(playOrPause = mediaAction),
+ packageName,
+ token,
+ appIntent,
+ device = null,
+ active = false,
+ resumeAction = resumeAction,
+ resumption = true,
+ notificationKey = packageName,
+ hasCheckedForResume = true,
+ lastActive = lastActive,
+ createdTimestampMillis = createdTimestampMillis,
+ instanceId = instanceId,
+ appUid = appUid,
+ isExplicit = isExplicit,
+ resumeProgress = progress,
+ )
+ )
+ }
+ }
+
+ fun loadMediaDataInBg(
+ key: String,
+ sbn: StatusBarNotification,
+ oldKey: String?,
+ isNewlyActiveEntry: Boolean = false,
+ ) {
+ val token =
+ sbn.notification.extras.getParcelable(
+ Notification.EXTRA_MEDIA_SESSION,
+ MediaSession.Token::class.java
+ )
+ if (token == null) {
+ return
+ }
+ val mediaController = mediaControllerFactory.create(token)
+ val metadata = mediaController.metadata
+ val notif: Notification = sbn.notification
+
+ val appInfo =
+ notif.extras.getParcelable(
+ Notification.EXTRA_BUILDER_APPLICATION_INFO,
+ ApplicationInfo::class.java
+ )
+ ?: getAppInfoFromPackage(sbn.packageName)
+
+ // App name
+ val appName = getAppName(sbn, appInfo)
+
+ // Song name
+ var song: CharSequence? = metadata?.getString(MediaMetadata.METADATA_KEY_DISPLAY_TITLE)
+ if (song.isNullOrBlank()) {
+ song = metadata?.getString(MediaMetadata.METADATA_KEY_TITLE)
+ }
+ if (song.isNullOrBlank()) {
+ song = HybridGroupManager.resolveTitle(notif)
+ }
+ if (song.isNullOrBlank()) {
+ // For apps that don't include a title, log and add a placeholder
+ song = context.getString(R.string.controls_media_empty_title, appName)
+ try {
+ statusBarManager.logBlankMediaTitle(sbn.packageName, sbn.user.identifier)
+ } catch (e: RuntimeException) {
+ Log.e(TAG, "Error reporting blank media title for package ${sbn.packageName}")
+ }
+ }
+
+ // Album art
+ var artworkBitmap = metadata?.let { loadBitmapFromUri(it) }
+ if (artworkBitmap == null) {
+ artworkBitmap = metadata?.getBitmap(MediaMetadata.METADATA_KEY_ART)
+ }
+ if (artworkBitmap == null) {
+ artworkBitmap = metadata?.getBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART)
+ }
+ val artWorkIcon =
+ if (artworkBitmap == null) {
+ notif.getLargeIcon()
+ } else {
+ Icon.createWithBitmap(artworkBitmap)
+ }
+
+ // App Icon
+ val smallIcon = sbn.notification.smallIcon
+
+ // Explicit Indicator
+ var isExplicit = false
+ val mediaMetadataCompat = MediaMetadataCompat.fromMediaMetadata(metadata)
+ isExplicit =
+ mediaMetadataCompat?.getLong(MediaConstants.METADATA_KEY_IS_EXPLICIT) ==
+ MediaConstants.METADATA_VALUE_ATTRIBUTE_PRESENT
+
+ // Artist name
+ var artist: CharSequence? = metadata?.getString(MediaMetadata.METADATA_KEY_ARTIST)
+ if (artist.isNullOrBlank()) {
+ artist = HybridGroupManager.resolveText(notif)
+ }
+
+ // Device name (used for remote cast notifications)
+ var device: MediaDeviceData? = null
+ if (isRemoteCastNotification(sbn)) {
+ val extras = sbn.notification.extras
+ val deviceName = extras.getCharSequence(Notification.EXTRA_MEDIA_REMOTE_DEVICE, null)
+ val deviceIcon = extras.getInt(Notification.EXTRA_MEDIA_REMOTE_ICON, -1)
+ val deviceIntent =
+ extras.getParcelable(
+ Notification.EXTRA_MEDIA_REMOTE_INTENT,
+ PendingIntent::class.java
+ )
+ Log.d(TAG, "$key is RCN for $deviceName")
+
+ if (deviceName != null && deviceIcon > -1) {
+ // Name and icon must be present, but intent may be null
+ val enabled = deviceIntent != null && deviceIntent.isActivity
+ val deviceDrawable =
+ Icon.createWithResource(sbn.packageName, deviceIcon)
+ .loadDrawable(sbn.getPackageContext(context))
+ device =
+ MediaDeviceData(
+ enabled,
+ deviceDrawable,
+ deviceName,
+ deviceIntent,
+ showBroadcastButton = false
+ )
+ }
+ }
+
+ // Control buttons
+ // If flag is enabled and controller has a PlaybackState, create actions from session info
+ // Otherwise, use the notification actions
+ var actionIcons: List<MediaAction> = emptyList()
+ var actionsToShowCollapsed: List<Int> = emptyList()
+ val semanticActions = createActionsFromState(sbn.packageName, mediaController, sbn.user)
+ if (semanticActions == null) {
+ val actions = createActionsFromNotification(sbn)
+ actionIcons = actions.first
+ actionsToShowCollapsed = actions.second
+ }
+
+ val playbackLocation =
+ if (isRemoteCastNotification(sbn)) MediaData.PLAYBACK_CAST_REMOTE
+ else if (
+ mediaController.playbackInfo?.playbackType ==
+ MediaController.PlaybackInfo.PLAYBACK_TYPE_LOCAL
+ )
+ MediaData.PLAYBACK_LOCAL
+ else MediaData.PLAYBACK_CAST_LOCAL
+ val isPlaying = mediaController.playbackState?.let { isPlayingState(it.state) } ?: null
+
+ val currentEntry = mediaEntries.get(key)
+ val instanceId = currentEntry?.instanceId ?: logger.getNewInstanceId()
+ val appUid = appInfo?.uid ?: Process.INVALID_UID
+
+ if (isNewlyActiveEntry) {
+ logSingleVsMultipleMediaAdded(appUid, sbn.packageName, instanceId)
+ logger.logActiveMediaAdded(appUid, sbn.packageName, instanceId, playbackLocation)
+ } else if (playbackLocation != currentEntry?.playbackLocation) {
+ logger.logPlaybackLocationChange(appUid, sbn.packageName, instanceId, playbackLocation)
+ }
+
+ val lastActive = systemClock.elapsedRealtime()
+ val createdTimestampMillis = currentEntry?.createdTimestampMillis ?: 0L
+ foregroundExecutor.execute {
+ val resumeAction: Runnable? = mediaEntries[key]?.resumeAction
+ val hasCheckedForResume = mediaEntries[key]?.hasCheckedForResume == true
+ val active = mediaEntries[key]?.active ?: true
+ onMediaDataLoaded(
+ key,
+ oldKey,
+ MediaData(
+ sbn.normalizedUserId,
+ true,
+ appName,
+ smallIcon,
+ artist,
+ song,
+ artWorkIcon,
+ actionIcons,
+ actionsToShowCollapsed,
+ semanticActions,
+ sbn.packageName,
+ token,
+ notif.contentIntent,
+ device,
+ active,
+ resumeAction = resumeAction,
+ playbackLocation = playbackLocation,
+ notificationKey = key,
+ hasCheckedForResume = hasCheckedForResume,
+ isPlaying = isPlaying,
+ isClearable = !sbn.isOngoing,
+ lastActive = lastActive,
+ createdTimestampMillis = createdTimestampMillis,
+ instanceId = instanceId,
+ appUid = appUid,
+ isExplicit = isExplicit,
+ )
+ )
+ }
+ }
+
+ private fun logSingleVsMultipleMediaAdded(
+ appUid: Int,
+ packageName: String,
+ instanceId: InstanceId
+ ) {
+ if (mediaEntries.size == 1) {
+ logger.logSingleMediaPlayerInCarousel(appUid, packageName, instanceId)
+ } else if (mediaEntries.size == 2) {
+ // Since this method is only called when there is a new media session added.
+ // logging needed once there is more than one media session in carousel.
+ logger.logMultipleMediaPlayersInCarousel(appUid, packageName, instanceId)
+ }
+ }
+
+ private fun getAppInfoFromPackage(packageName: String): ApplicationInfo? {
+ try {
+ return context.packageManager.getApplicationInfo(packageName, 0)
+ } catch (e: PackageManager.NameNotFoundException) {
+ Log.w(TAG, "Could not get app info for $packageName", e)
+ }
+ return null
+ }
+
+ private fun getAppName(sbn: StatusBarNotification, appInfo: ApplicationInfo?): String {
+ val name = sbn.notification.extras.getString(EXTRA_SUBSTITUTE_APP_NAME)
+ if (name != null) {
+ return name
+ }
+
+ return if (appInfo != null) {
+ context.packageManager.getApplicationLabel(appInfo).toString()
+ } else {
+ sbn.packageName
+ }
+ }
+
+ /** Generate action buttons based on notification actions */
+ private fun createActionsFromNotification(
+ sbn: StatusBarNotification
+ ): Pair<List<MediaAction>, List<Int>> {
+ val notif = sbn.notification
+ val actionIcons: MutableList<MediaAction> = ArrayList()
+ val actions = notif.actions
+ var actionsToShowCollapsed =
+ notif.extras.getIntArray(Notification.EXTRA_COMPACT_ACTIONS)?.toMutableList()
+ ?: mutableListOf()
+ if (actionsToShowCollapsed.size > MAX_COMPACT_ACTIONS) {
+ Log.e(
+ TAG,
+ "Too many compact actions for ${sbn.key}," +
+ "limiting to first $MAX_COMPACT_ACTIONS"
+ )
+ actionsToShowCollapsed = actionsToShowCollapsed.subList(0, MAX_COMPACT_ACTIONS)
+ }
+
+ if (actions != null) {
+ for ((index, action) in actions.withIndex()) {
+ if (index == MAX_NOTIFICATION_ACTIONS) {
+ Log.w(
+ TAG,
+ "Too many notification actions for ${sbn.key}," +
+ " limiting to first $MAX_NOTIFICATION_ACTIONS"
+ )
+ break
+ }
+ if (action.getIcon() == null) {
+ if (DEBUG) Log.i(TAG, "No icon for action $index ${action.title}")
+ actionsToShowCollapsed.remove(index)
+ continue
+ }
+ val runnable =
+ if (action.actionIntent != null) {
+ Runnable {
+ if (action.actionIntent.isActivity) {
+ activityStarter.startPendingIntentDismissingKeyguard(
+ action.actionIntent
+ )
+ } else if (action.isAuthenticationRequired()) {
+ activityStarter.dismissKeyguardThenExecute(
+ {
+ var result = sendPendingIntent(action.actionIntent)
+ result
+ },
+ {},
+ true
+ )
+ } else {
+ sendPendingIntent(action.actionIntent)
+ }
+ }
+ } else {
+ null
+ }
+ val mediaActionIcon =
+ if (action.getIcon()?.getType() == Icon.TYPE_RESOURCE) {
+ Icon.createWithResource(sbn.packageName, action.getIcon()!!.getResId())
+ } else {
+ action.getIcon()
+ }
+ .setTint(themeText)
+ .loadDrawable(context)
+ val mediaAction = MediaAction(mediaActionIcon, runnable, action.title, null)
+ actionIcons.add(mediaAction)
+ }
+ }
+ return Pair(actionIcons, actionsToShowCollapsed)
+ }
+
+ /**
+ * Generates action button info for this media session based on the PlaybackState
+ *
+ * @param packageName Package name for the media app
+ * @param controller MediaController for the current session
+ * @return a Pair consisting of a list of media actions, and a list of ints representing which
+ *
+ * ```
+ * of those actions should be shown in the compact player
+ * ```
+ */
+ private fun createActionsFromState(
+ packageName: String,
+ controller: MediaController,
+ user: UserHandle
+ ): MediaButton? {
+ val state = controller.playbackState
+ if (state == null || !mediaFlags.areMediaSessionActionsEnabled(packageName, user)) {
+ return null
+ }
+
+ // First, check for standard actions
+ val playOrPause =
+ if (isConnectingState(state.state)) {
+ // Spinner needs to be animating to render anything. Start it here.
+ val drawable =
+ context.getDrawable(com.android.internal.R.drawable.progress_small_material)
+ (drawable as Animatable).start()
+ MediaAction(
+ drawable,
+ null, // no action to perform when clicked
+ context.getString(R.string.controls_media_button_connecting),
+ context.getDrawable(R.drawable.ic_media_connecting_container),
+ // Specify a rebind id to prevent the spinner from restarting on later binds.
+ com.android.internal.R.drawable.progress_small_material
+ )
+ } else if (isPlayingState(state.state)) {
+ getStandardAction(controller, state.actions, PlaybackState.ACTION_PAUSE)
+ } else {
+ getStandardAction(controller, state.actions, PlaybackState.ACTION_PLAY)
+ }
+ val prevButton =
+ getStandardAction(controller, state.actions, PlaybackState.ACTION_SKIP_TO_PREVIOUS)
+ val nextButton =
+ getStandardAction(controller, state.actions, PlaybackState.ACTION_SKIP_TO_NEXT)
+
+ // Then, create a way to build any custom actions that will be needed
+ val customActions =
+ state.customActions
+ .asSequence()
+ .filterNotNull()
+ .map { getCustomAction(state, packageName, controller, it) }
+ .iterator()
+ fun nextCustomAction() = if (customActions.hasNext()) customActions.next() else null
+
+ // Finally, assign the remaining button slots: play/pause A B C D
+ // A = previous, else custom action (if not reserved)
+ // B = next, else custom action (if not reserved)
+ // C and D are always custom actions
+ val reservePrev =
+ controller.extras?.getBoolean(
+ MediaConstants.SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_PREV
+ ) == true
+ val reserveNext =
+ controller.extras?.getBoolean(
+ MediaConstants.SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_NEXT
+ ) == true
+
+ val prevOrCustom =
+ if (prevButton != null) {
+ prevButton
+ } else if (!reservePrev) {
+ nextCustomAction()
+ } else {
+ null
+ }
+
+ val nextOrCustom =
+ if (nextButton != null) {
+ nextButton
+ } else if (!reserveNext) {
+ nextCustomAction()
+ } else {
+ null
+ }
+
+ return MediaButton(
+ playOrPause,
+ nextOrCustom,
+ prevOrCustom,
+ nextCustomAction(),
+ nextCustomAction(),
+ reserveNext,
+ reservePrev
+ )
+ }
+
+ /**
+ * Create a [MediaAction] for a given action and media session
+ *
+ * @param controller MediaController for the session
+ * @param stateActions The actions included with the session's [PlaybackState]
+ * @param action A [PlaybackState.Actions] value representing what action to generate. One of:
+ * ```
+ * [PlaybackState.ACTION_PLAY]
+ * [PlaybackState.ACTION_PAUSE]
+ * [PlaybackState.ACTION_SKIP_TO_PREVIOUS]
+ * [PlaybackState.ACTION_SKIP_TO_NEXT]
+ * @return
+ * ```
+ *
+ * A [MediaAction] with correct values set, or null if the state doesn't support it
+ */
+ private fun getStandardAction(
+ controller: MediaController,
+ stateActions: Long,
+ @PlaybackState.Actions action: Long
+ ): MediaAction? {
+ if (!includesAction(stateActions, action)) {
+ return null
+ }
+
+ return when (action) {
+ PlaybackState.ACTION_PLAY -> {
+ MediaAction(
+ context.getDrawable(R.drawable.ic_media_play),
+ { controller.transportControls.play() },
+ context.getString(R.string.controls_media_button_play),
+ context.getDrawable(R.drawable.ic_media_play_container)
+ )
+ }
+ PlaybackState.ACTION_PAUSE -> {
+ MediaAction(
+ context.getDrawable(R.drawable.ic_media_pause),
+ { controller.transportControls.pause() },
+ context.getString(R.string.controls_media_button_pause),
+ context.getDrawable(R.drawable.ic_media_pause_container)
+ )
+ }
+ PlaybackState.ACTION_SKIP_TO_PREVIOUS -> {
+ MediaAction(
+ context.getDrawable(R.drawable.ic_media_prev),
+ { controller.transportControls.skipToPrevious() },
+ context.getString(R.string.controls_media_button_prev),
+ null
+ )
+ }
+ PlaybackState.ACTION_SKIP_TO_NEXT -> {
+ MediaAction(
+ context.getDrawable(R.drawable.ic_media_next),
+ { controller.transportControls.skipToNext() },
+ context.getString(R.string.controls_media_button_next),
+ null
+ )
+ }
+ else -> null
+ }
+ }
+
+ /** Check whether the actions from a [PlaybackState] include a specific action */
+ private fun includesAction(stateActions: Long, @PlaybackState.Actions action: Long): Boolean {
+ if (
+ (action == PlaybackState.ACTION_PLAY || action == PlaybackState.ACTION_PAUSE) &&
+ (stateActions and PlaybackState.ACTION_PLAY_PAUSE > 0L)
+ ) {
+ return true
+ }
+ return (stateActions and action != 0L)
+ }
+
+ /** Get a [MediaAction] representing a [PlaybackState.CustomAction] */
+ private fun getCustomAction(
+ state: PlaybackState,
+ packageName: String,
+ controller: MediaController,
+ customAction: PlaybackState.CustomAction
+ ): MediaAction {
+ return MediaAction(
+ Icon.createWithResource(packageName, customAction.icon).loadDrawable(context),
+ { controller.transportControls.sendCustomAction(customAction, customAction.extras) },
+ customAction.name,
+ null
+ )
+ }
+
+ /** Load a bitmap from the various Art metadata URIs */
+ private fun loadBitmapFromUri(metadata: MediaMetadata): Bitmap? {
+ for (uri in ART_URIS) {
+ val uriString = metadata.getString(uri)
+ if (!TextUtils.isEmpty(uriString)) {
+ val albumArt = loadBitmapFromUri(Uri.parse(uriString))
+ if (albumArt != null) {
+ if (DEBUG) Log.d(TAG, "loaded art from $uri")
+ return albumArt
+ }
+ }
+ }
+ return null
+ }
+
+ private fun sendPendingIntent(intent: PendingIntent): Boolean {
+ return try {
+ val options = BroadcastOptions.makeBasic()
+ options.setInteractive(true)
+ options.setPendingIntentBackgroundActivityStartMode(
+ ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED
+ )
+ intent.send(options.toBundle())
+ true
+ } catch (e: PendingIntent.CanceledException) {
+ Log.d(TAG, "Intent canceled", e)
+ false
+ }
+ }
+
+ /** Returns a bitmap if the user can access the given URI, else null */
+ private fun loadBitmapFromUriForUser(
+ uri: Uri,
+ userId: Int,
+ appUid: Int,
+ packageName: String,
+ ): Bitmap? {
+ try {
+ val ugm = UriGrantsManager.getService()
+ ugm.checkGrantUriPermission_ignoreNonSystem(
+ appUid,
+ packageName,
+ ContentProvider.getUriWithoutUserId(uri),
+ Intent.FLAG_GRANT_READ_URI_PERMISSION,
+ ContentProvider.getUserIdFromUri(uri, userId)
+ )
+ return loadBitmapFromUri(uri)
+ } catch (e: SecurityException) {
+ Log.e(TAG, "Failed to get URI permission: $e")
+ }
+ return null
+ }
+
+ /**
+ * Load a bitmap from a URI
+ *
+ * @param uri the uri to load
+ * @return bitmap, or null if couldn't be loaded
+ */
+ private fun loadBitmapFromUri(uri: Uri): Bitmap? {
+ // ImageDecoder requires a scheme of the following types
+ if (uri.scheme == null) {
+ return null
+ }
+
+ if (
+ !uri.scheme.equals(ContentResolver.SCHEME_CONTENT) &&
+ !uri.scheme.equals(ContentResolver.SCHEME_ANDROID_RESOURCE) &&
+ !uri.scheme.equals(ContentResolver.SCHEME_FILE)
+ ) {
+ return null
+ }
+
+ val source = ImageDecoder.createSource(context.contentResolver, uri)
+ return try {
+ ImageDecoder.decodeBitmap(source) { decoder, info, _ ->
+ val width = info.size.width
+ val height = info.size.height
+ val scale =
+ MediaDataUtils.getScaleFactor(
+ APair(width, height),
+ APair(artworkWidth, artworkHeight)
+ )
+
+ // Downscale if needed
+ if (scale != 0f && scale < 1) {
+ decoder.setTargetSize((scale * width).toInt(), (scale * height).toInt())
+ }
+ decoder.allocator = ImageDecoder.ALLOCATOR_SOFTWARE
+ }
+ } catch (e: IOException) {
+ Log.e(TAG, "Unable to load bitmap", e)
+ null
+ } catch (e: RuntimeException) {
+ Log.e(TAG, "Unable to load bitmap", e)
+ null
+ }
+ }
+
+ private fun getResumeMediaAction(action: Runnable): MediaAction {
+ return MediaAction(
+ Icon.createWithResource(context, R.drawable.ic_media_play)
+ .setTint(themeText)
+ .loadDrawable(context),
+ action,
+ context.getString(R.string.controls_media_resume),
+ context.getDrawable(R.drawable.ic_media_play_container)
+ )
+ }
+
+ fun onMediaDataLoaded(key: String, oldKey: String?, data: MediaData) =
+ traceSection("MediaDataManager#onMediaDataLoaded") {
+ Assert.isMainThread()
+ if (mediaEntries.containsKey(key)) {
+ // Otherwise this was removed already
+ mediaEntries.put(key, data)
+ notifyMediaDataLoaded(key, oldKey, data)
+ }
+ }
+
+ override fun onSmartspaceTargetsUpdated(targets: List<Parcelable>) {
+ if (!allowMediaRecommendations) {
+ if (DEBUG) Log.d(TAG, "Smartspace recommendation is disabled in Settings.")
+ return
+ }
+
+ val mediaTargets = targets.filterIsInstance<SmartspaceTarget>()
+ when (mediaTargets.size) {
+ 0 -> {
+ if (!smartspaceMediaData.isActive) {
+ return
+ }
+ if (DEBUG) {
+ Log.d(TAG, "Set Smartspace media to be inactive for the data update")
+ }
+ if (mediaFlags.isPersistentSsCardEnabled()) {
+ // Smartspace uses this signal to hide the card (e.g. when it expires or user
+ // disconnects headphones), so treat as setting inactive when flag is on
+ smartspaceMediaData = smartspaceMediaData.copy(isActive = false)
+ notifySmartspaceMediaDataLoaded(
+ smartspaceMediaData.targetId,
+ smartspaceMediaData,
+ )
+ } else {
+ smartspaceMediaData =
+ EMPTY_SMARTSPACE_MEDIA_DATA.copy(
+ targetId = smartspaceMediaData.targetId,
+ instanceId = smartspaceMediaData.instanceId,
+ )
+ notifySmartspaceMediaDataRemoved(
+ smartspaceMediaData.targetId,
+ immediately = false,
+ )
+ }
+ }
+ 1 -> {
+ val newMediaTarget = mediaTargets.get(0)
+ if (smartspaceMediaData.targetId == newMediaTarget.smartspaceTargetId) {
+ // The same Smartspace updates can be received. Skip the duplicate updates.
+ return
+ }
+ if (DEBUG) Log.d(TAG, "Forwarding Smartspace media update.")
+ smartspaceMediaData = toSmartspaceMediaData(newMediaTarget)
+ notifySmartspaceMediaDataLoaded(smartspaceMediaData.targetId, smartspaceMediaData)
+ }
+ else -> {
+ // There should NOT be more than 1 Smartspace media update. When it happens, it
+ // indicates a bad state or an error. Reset the status accordingly.
+ Log.wtf(TAG, "More than 1 Smartspace Media Update. Resetting the status...")
+ notifySmartspaceMediaDataRemoved(
+ smartspaceMediaData.targetId,
+ immediately = false,
+ )
+ smartspaceMediaData = EMPTY_SMARTSPACE_MEDIA_DATA
+ }
+ }
+ }
+
+ override fun onNotificationRemoved(key: String) {
+ Assert.isMainThread()
+ val removed = mediaEntries.remove(key) ?: return
+ if (keyguardUpdateMonitor.isUserInLockdown(removed.userId)) {
+ logger.logMediaRemoved(removed.appUid, removed.packageName, removed.instanceId)
+ } else if (isAbleToResume(removed)) {
+ convertToResumePlayer(key, removed)
+ } else if (mediaFlags.isRetainingPlayersEnabled()) {
+ handlePossibleRemoval(key, removed, notificationRemoved = true)
+ } else {
+ notifyMediaDataRemoved(key)
+ logger.logMediaRemoved(removed.appUid, removed.packageName, removed.instanceId)
+ }
+ }
+
+ private fun onSessionDestroyed(key: String) {
+ if (DEBUG) Log.d(TAG, "session destroyed for $key")
+ val entry = mediaEntries.remove(key) ?: return
+ // Clear token since the session is no longer valid
+ val updated = entry.copy(token = null)
+ handlePossibleRemoval(key, updated)
+ }
+
+ private fun isAbleToResume(data: MediaData): Boolean {
+ val isEligibleForResume =
+ data.isLocalSession() ||
+ (mediaFlags.isRemoteResumeAllowed() &&
+ data.playbackLocation != MediaData.PLAYBACK_CAST_REMOTE)
+ return useMediaResumption && data.resumeAction != null && isEligibleForResume
+ }
+
+ /**
+ * Convert to resume state if the player is no longer valid and active, then notify listeners
+ * that the data was updated. Does not convert to resume state if the player is still valid, or
+ * if it was removed before becoming inactive. (Assumes that [removed] was removed from
+ * [mediaEntries] before this function was called)
+ */
+ private fun handlePossibleRemoval(
+ key: String,
+ removed: MediaData,
+ notificationRemoved: Boolean = false
+ ) {
+ val hasSession = removed.token != null
+ if (hasSession && removed.semanticActions != null) {
+ // The app was using session actions, and the session is still valid: keep player
+ if (DEBUG) Log.d(TAG, "Notification removed but using session actions $key")
+ mediaEntries.put(key, removed)
+ notifyMediaDataLoaded(key, key, removed)
+ } else if (!notificationRemoved && removed.semanticActions == null) {
+ // The app was using notification actions, and notif wasn't removed yet: keep player
+ if (DEBUG) Log.d(TAG, "Session destroyed but using notification actions $key")
+ mediaEntries.put(key, removed)
+ notifyMediaDataLoaded(key, key, removed)
+ } else if (removed.active && !isAbleToResume(removed)) {
+ // This player was still active - it didn't last long enough to time out,
+ // and its app doesn't normally support resume: remove
+ if (DEBUG) Log.d(TAG, "Removing still-active player $key")
+ notifyMediaDataRemoved(key)
+ logger.logMediaRemoved(removed.appUid, removed.packageName, removed.instanceId)
+ } else if (mediaFlags.isRetainingPlayersEnabled() || isAbleToResume(removed)) {
+ // Convert to resume
+ if (DEBUG) {
+ Log.d(
+ TAG,
+ "Notification ($notificationRemoved) and/or session " +
+ "($hasSession) gone for inactive player $key"
+ )
+ }
+ convertToResumePlayer(key, removed)
+ } else {
+ // Retaining players flag is off and app doesn't support resume: remove player.
+ if (DEBUG) Log.d(TAG, "Removing player $key")
+ notifyMediaDataRemoved(key)
+ logger.logMediaRemoved(removed.appUid, removed.packageName, removed.instanceId)
+ }
+ }
+
+ /** Set the given [MediaData] as a resume state player and notify listeners */
+ private fun convertToResumePlayer(key: String, data: MediaData) {
+ if (DEBUG) Log.d(TAG, "Converting $key to resume")
+ // Resumption controls must have a title.
+ if (data.song.isNullOrBlank()) {
+ Log.e(TAG, "Description incomplete")
+ notifyMediaDataRemoved(key)
+ logger.logMediaRemoved(data.appUid, data.packageName, data.instanceId)
+ return
+ }
+ // Move to resume key (aka package name) if that key doesn't already exist.
+ val resumeAction = data.resumeAction?.let { getResumeMediaAction(it) }
+ val actions = resumeAction?.let { listOf(resumeAction) } ?: emptyList()
+ val launcherIntent =
+ context.packageManager.getLaunchIntentForPackage(data.packageName)?.let {
+ PendingIntent.getActivity(context, 0, it, PendingIntent.FLAG_IMMUTABLE)
+ }
+ val lastActive =
+ if (data.active) {
+ systemClock.elapsedRealtime()
+ } else {
+ data.lastActive
+ }
+ val updated =
+ data.copy(
+ token = null,
+ actions = actions,
+ semanticActions = MediaButton(playOrPause = resumeAction),
+ actionsToShowInCompact = listOf(0),
+ active = false,
+ resumption = true,
+ isPlaying = false,
+ isClearable = true,
+ clickIntent = launcherIntent,
+ lastActive = lastActive,
+ )
+ val pkg = data.packageName
+ val migrate = mediaEntries.put(pkg, updated) == null
+ // Notify listeners of "new" controls when migrating or removed and update when not
+ Log.d(TAG, "migrating? $migrate from $key -> $pkg")
+ if (migrate) {
+ notifyMediaDataLoaded(key = pkg, oldKey = key, info = updated)
+ } else {
+ // Since packageName is used for the key of the resumption controls, it is
+ // possible that another notification has already been reused for the resumption
+ // controls of this package. In this case, rather than renaming this player as
+ // packageName, just remove it and then send a update to the existing resumption
+ // controls.
+ notifyMediaDataRemoved(key)
+ notifyMediaDataLoaded(key = pkg, oldKey = pkg, info = updated)
+ }
+ logger.logActiveConvertedToResume(updated.appUid, pkg, updated.instanceId)
+
+ // Limit total number of resume controls
+ val resumeEntries = mediaEntries.filter { (key, data) -> data.resumption }
+ val numResume = resumeEntries.size
+ if (numResume > ResumeMediaBrowser.MAX_RESUMPTION_CONTROLS) {
+ resumeEntries
+ .toList()
+ .sortedBy { (key, data) -> data.lastActive }
+ .subList(0, numResume - ResumeMediaBrowser.MAX_RESUMPTION_CONTROLS)
+ .forEach { (key, data) ->
+ Log.d(TAG, "Removing excess control $key")
+ mediaEntries.remove(key)
+ notifyMediaDataRemoved(key)
+ logger.logMediaRemoved(data.appUid, data.packageName, data.instanceId)
+ }
+ }
+ }
+
+ override fun setMediaResumptionEnabled(isEnabled: Boolean) {
+ if (useMediaResumption == isEnabled) {
+ return
+ }
+
+ useMediaResumption = isEnabled
+
+ if (!useMediaResumption) {
+ // Remove any existing resume controls
+ val filtered = mediaEntries.filter { !it.value.active }
+ filtered.forEach {
+ mediaEntries.remove(it.key)
+ notifyMediaDataRemoved(it.key)
+ logger.logMediaRemoved(it.value.appUid, it.value.packageName, it.value.instanceId)
+ }
+ }
+ }
+
+ /** Invoked when the user has dismissed the media carousel */
+ override fun onSwipeToDismiss() = mediaDataFilter.onSwipeToDismiss()
+
+ /** Are there any media notifications active, including the recommendations? */
+ override fun hasActiveMediaOrRecommendation() = mediaDataFilter.hasActiveMediaOrRecommendation()
+
+ /**
+ * Are there any media entries we should display, including the recommendations?
+ * - If resumption is enabled, this will include inactive players
+ * - If resumption is disabled, we only want to show active players
+ */
+ override fun hasAnyMediaOrRecommendation() = mediaDataFilter.hasAnyMediaOrRecommendation()
+
+ /** Are there any resume media notifications active, excluding the recommendations? */
+ override fun hasActiveMedia() = mediaDataFilter.hasActiveMedia()
+
+ /**
+ * Are there any resume media notifications active, excluding the recommendations?
+ * - If resumption is enabled, this will include inactive players
+ * - If resumption is disabled, we only want to show active players
+ */
+ override fun hasAnyMedia() = mediaDataFilter.hasAnyMedia()
+ override fun isRecommendationActive() = smartspaceMediaData.isActive
+
+ /**
+ * Converts the pass-in SmartspaceTarget to SmartspaceMediaData
+ *
+ * @return An empty SmartspaceMediaData with the valid target Id is returned if the
+ * SmartspaceTarget's data is invalid.
+ */
+ private fun toSmartspaceMediaData(target: SmartspaceTarget): SmartspaceMediaData {
+ val baseAction: SmartspaceAction? = target.baseAction
+ val dismissIntent =
+ baseAction?.extras?.getParcelable(EXTRAS_SMARTSPACE_DISMISS_INTENT_KEY) as Intent?
+
+ val isActive =
+ when {
+ !mediaFlags.isPersistentSsCardEnabled() -> true
+ baseAction == null -> true
+ else -> {
+ val triggerSource = baseAction.extras?.getString(EXTRA_KEY_TRIGGER_SOURCE)
+ triggerSource != EXTRA_VALUE_TRIGGER_PERIODIC
+ }
+ }
+
+ packageName(target)?.let {
+ return SmartspaceMediaData(
+ targetId = target.smartspaceTargetId,
+ isActive = isActive,
+ packageName = it,
+ cardAction = target.baseAction,
+ recommendations = target.iconGrid,
+ dismissIntent = dismissIntent,
+ headphoneConnectionTimeMillis = target.creationTimeMillis,
+ instanceId = logger.getNewInstanceId(),
+ expiryTimeMs = target.expiryTimeMillis,
+ )
+ }
+ return EMPTY_SMARTSPACE_MEDIA_DATA.copy(
+ targetId = target.smartspaceTargetId,
+ isActive = isActive,
+ dismissIntent = dismissIntent,
+ headphoneConnectionTimeMillis = target.creationTimeMillis,
+ instanceId = logger.getNewInstanceId(),
+ expiryTimeMs = target.expiryTimeMillis,
+ )
+ }
+
+ private fun packageName(target: SmartspaceTarget): String? {
+ val recommendationList = target.iconGrid
+ if (recommendationList == null || recommendationList.isEmpty()) {
+ Log.w(TAG, "Empty or null media recommendation list.")
+ return null
+ }
+ for (recommendation in recommendationList) {
+ val extras = recommendation.extras
+ extras?.let {
+ it.getString(EXTRAS_MEDIA_SOURCE_PACKAGE_NAME)?.let { packageName ->
+ return packageName
+ }
+ }
+ }
+ Log.w(TAG, "No valid package name is provided.")
+ return null
+ }
+
+ override fun dump(pw: PrintWriter, args: Array<out String>) {
+ pw.apply {
+ println("internalListeners: $internalListeners")
+ println("externalListeners: ${mediaDataFilter.listeners}")
+ println("mediaEntries: $mediaEntries")
+ println("useMediaResumption: $useMediaResumption")
+ println("allowMediaRecommendations: $allowMediaRecommendations")
+ }
+ mediaDeviceManager.dump(pw)
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilter.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilterImpl.kt
similarity index 76%
copy from packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilter.kt
copy to packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilterImpl.kt
index bc539ef..a65db35 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilter.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilterImpl.kt
@@ -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.
@@ -24,6 +24,7 @@
import com.android.internal.annotations.VisibleForTesting
import com.android.systemui.broadcast.BroadcastSender
import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.media.controls.data.repository.MediaFilterRepository
import com.android.systemui.media.controls.shared.model.EXTRA_KEY_TRIGGER_RESUME
import com.android.systemui.media.controls.shared.model.MediaData
import com.android.systemui.media.controls.shared.model.SmartspaceMediaData
@@ -36,7 +37,6 @@
import java.util.concurrent.Executor
import java.util.concurrent.TimeUnit
import javax.inject.Inject
-import kotlin.collections.LinkedHashMap
private const val TAG = "MediaDataFilter"
private const val DEBUG = true
@@ -46,14 +46,6 @@
private const val RESUMABLE_MEDIA_MAX_AGE_SECONDS_KEY = "resumable_media_max_age_seconds"
/**
- * Maximum age of a media control to re-activate on smartspace signal. If there is no media control
- * available within this time window, smartspace recommendations will be shown instead.
- */
-@VisibleForTesting
-internal val SMARTSPACE_MAX_AGE =
- SystemProperties.getLong("debug.sysui.smartspace_max_age", TimeUnit.MINUTES.toMillis(30))
-
-/**
* Filters data updates from [MediaDataCombineLatest] based on the current user ID, and handles user
* switches (removing entries for the previous user, adding back entries for the current user). Also
* filters out smartspace updates in favor of local recent media, when avaialble.
@@ -61,28 +53,23 @@
* This is added at the end of the pipeline since we may still need to handle callbacks from
* background users (e.g. timeouts).
*/
-class MediaDataFilter
+class MediaDataFilterImpl
@Inject
constructor(
private val context: Context,
- private val userTracker: UserTracker,
+ userTracker: UserTracker,
private val broadcastSender: BroadcastSender,
private val lockscreenUserManager: NotificationLockscreenUserManager,
@Main private val executor: Executor,
private val systemClock: SystemClock,
private val logger: MediaUiEventLogger,
private val mediaFlags: MediaFlags,
+ private val mediaFilterRepository: MediaFilterRepository,
) : MediaDataManager.Listener {
private val _listeners: MutableSet<MediaDataManager.Listener> = mutableSetOf()
- internal val listeners: Set<MediaDataManager.Listener>
+ val listeners: Set<MediaDataManager.Listener>
get() = _listeners.toSet()
- internal lateinit var mediaDataManager: MediaDataManager
-
- private val allEntries: LinkedHashMap<String, MediaData> = LinkedHashMap()
- // The filtered userEntries, which will be a subset of all userEntries in MediaDataManager
- private val userEntries: LinkedHashMap<String, MediaData> = LinkedHashMap()
- private var smartspaceMediaData: SmartspaceMediaData = EMPTY_SMARTSPACE_MEDIA_DATA
- private var reactivatedKey: String? = null
+ lateinit var mediaDataManager: MediaDataManager
// Ensure the field (and associated reference) isn't removed during optimization.
@KeepForWeakReference
@@ -110,9 +97,9 @@
isSsReactivated: Boolean
) {
if (oldKey != null && oldKey != key) {
- allEntries.remove(oldKey)
+ mediaFilterRepository.removeMediaEntry(oldKey)
}
- allEntries.put(key, data)
+ mediaFilterRepository.addMediaEntry(key, data)
if (
!lockscreenUserManager.isCurrentProfile(data.userId) ||
@@ -122,9 +109,9 @@
}
if (oldKey != null && oldKey != key) {
- userEntries.remove(oldKey)
+ mediaFilterRepository.removeSelectedUserMediaEntry(oldKey)
}
- userEntries.put(key, data)
+ mediaFilterRepository.addSelectedUserMediaEntry(key, data)
// Notify listeners
listeners.forEach { it.onMediaDataLoaded(key, oldKey, data) }
@@ -144,10 +131,12 @@
// Override the pass-in value here, as the order of Smartspace card is only determined here.
var shouldPrioritizeMutable = false
- smartspaceMediaData = data
+ mediaFilterRepository.setRecommendation(data)
// Before forwarding the smartspace target, first check if we have recently inactive media
- val sorted = userEntries.toSortedMap(compareBy { userEntries.get(it)?.lastActive ?: -1 })
+ val selectedUserEntries = mediaFilterRepository.selectedUserEntries.value
+ val sorted =
+ selectedUserEntries.toSortedMap(compareBy { selectedUserEntries[it]?.lastActive ?: -1 })
val timeSinceActive = timeSinceActiveForMostRecentMedia(sorted)
var smartspaceMaxAgeMillis = SMARTSPACE_MAX_AGE
data.cardAction?.extras?.let {
@@ -162,7 +151,10 @@
val shouldTriggerResume =
data.cardAction?.extras?.getBoolean(EXTRA_KEY_TRIGGER_RESUME, true) ?: true
val shouldReactivate =
- shouldTriggerResume && !hasActiveMedia() && hasAnyMedia() && data.isActive
+ shouldTriggerResume &&
+ !selectedUserEntries.any { it.value.active } &&
+ selectedUserEntries.isNotEmpty() &&
+ data.isActive
if (timeSinceActive < smartspaceMaxAgeMillis) {
// It could happen there are existing active media resume cards, then we don't need to
@@ -171,8 +163,8 @@
val lastActiveKey = sorted.lastKey() // most recently active
// Notify listeners to consider this media active
Log.d(TAG, "reactivating $lastActiveKey instead of smartspace")
- reactivatedKey = lastActiveKey
- val mediaData = sorted.get(lastActiveKey)!!.copy(active = true)
+ mediaFilterRepository.setReactivatedKey(lastActiveKey)
+ val mediaData = sorted[lastActiveKey]!!.copy(active = true)
logger.logRecommendationActivated(
mediaData.appUid,
mediaData.packageName,
@@ -199,6 +191,7 @@
Log.d(TAG, "Invalid recommendation data. Skip showing the rec card")
return
}
+ val smartspaceMediaData = mediaFilterRepository.smartspaceMediaData.value
logger.logRecommendationAdded(
smartspaceMediaData.packageName,
smartspaceMediaData.instanceId
@@ -207,8 +200,8 @@
}
override fun onMediaDataRemoved(key: String) {
- allEntries.remove(key)
- userEntries.remove(key)?.let {
+ mediaFilterRepository.removeMediaEntry(key)
+ mediaFilterRepository.removeSelectedUserMediaEntry(key)?.let {
// Only notify listeners if something actually changed
listeners.forEach { it.onMediaDataRemoved(key) }
}
@@ -216,24 +209,26 @@
override fun onSmartspaceMediaDataRemoved(key: String, immediately: Boolean) {
// First check if we had reactivated media instead of forwarding smartspace
- reactivatedKey?.let {
+ mediaFilterRepository.reactivatedKey.value?.let {
val lastActiveKey = it
- reactivatedKey = null
+ mediaFilterRepository.setReactivatedKey(null)
Log.d(TAG, "expiring reactivated key $lastActiveKey")
// Notify listeners to update with actual active value
- userEntries.get(lastActiveKey)?.let { mediaData ->
- listeners.forEach {
- it.onMediaDataLoaded(lastActiveKey, lastActiveKey, mediaData, immediately)
+ mediaFilterRepository.selectedUserEntries.value[lastActiveKey]?.let { mediaData ->
+ listeners.forEach { listener ->
+ listener.onMediaDataLoaded(lastActiveKey, lastActiveKey, mediaData, immediately)
}
}
}
+ val smartspaceMediaData = mediaFilterRepository.smartspaceMediaData.value
if (smartspaceMediaData.isActive) {
- smartspaceMediaData =
+ mediaFilterRepository.setRecommendation(
EMPTY_SMARTSPACE_MEDIA_DATA.copy(
targetId = smartspaceMediaData.targetId,
instanceId = smartspaceMediaData.instanceId
)
+ )
}
listeners.forEach { it.onSmartspaceMediaDataRemoved(key, immediately) }
}
@@ -241,11 +236,11 @@
@VisibleForTesting
internal fun handleProfileChanged() {
// TODO(b/317221348) re-add media removed when profile is available.
- allEntries.forEach { (key, data) ->
+ mediaFilterRepository.allUserEntries.value.forEach { (key, data) ->
if (!lockscreenUserManager.isProfileAvailable(data.userId)) {
// Only remove media when the profile is unavailable.
if (DEBUG) Log.d(TAG, "Removing $key after profile change")
- userEntries.remove(key, data)
+ mediaFilterRepository.removeSelectedUserMediaEntry(key, data)
listeners.forEach { listener -> listener.onMediaDataRemoved(key) }
}
}
@@ -255,19 +250,19 @@
internal fun handleUserSwitched() {
// If the user changes, remove all current MediaData objects and inform listeners
val listenersCopy = listeners
- val keyCopy = userEntries.keys.toMutableList()
+ val keyCopy = mediaFilterRepository.selectedUserEntries.value.keys.toMutableList()
// Clear the list first, to make sure callbacks from listeners if we have any entries
// are up to date
- userEntries.clear()
+ mediaFilterRepository.clearSelectedUserMedia()
keyCopy.forEach {
if (DEBUG) Log.d(TAG, "Removing $it after user change")
listenersCopy.forEach { listener -> listener.onMediaDataRemoved(it) }
}
- allEntries.forEach { (key, data) ->
+ mediaFilterRepository.allUserEntries.value.forEach { (key, data) ->
if (lockscreenUserManager.isCurrentProfile(data.userId)) {
if (DEBUG) Log.d(TAG, "Re-adding $key after user change")
- userEntries.put(key, data)
+ mediaFilterRepository.addSelectedUserMediaEntry(key, data)
listenersCopy.forEach { listener -> listener.onMediaDataLoaded(key, null, data) }
}
}
@@ -276,11 +271,12 @@
/** Invoked when the user has dismissed the media carousel */
fun onSwipeToDismiss() {
if (DEBUG) Log.d(TAG, "Media carousel swiped away")
- val mediaKeys = userEntries.keys.toSet()
+ val mediaKeys = mediaFilterRepository.selectedUserEntries.value.keys.toSet()
mediaKeys.forEach {
// Force updates to listeners, needed for re-activated card
- mediaDataManager.setTimedOut(it, timedOut = true, forceUpdate = true)
+ mediaDataManager.setInactive(it, timedOut = true, forceUpdate = true)
}
+ val smartspaceMediaData = mediaFilterRepository.smartspaceMediaData.value
if (smartspaceMediaData.isActive) {
val dismissIntent = smartspaceMediaData.dismissIntent
if (dismissIntent == null) {
@@ -298,14 +294,15 @@
}
if (mediaFlags.isPersistentSsCardEnabled()) {
- smartspaceMediaData = smartspaceMediaData.copy(isActive = false)
+ mediaFilterRepository.setRecommendation(smartspaceMediaData.copy(isActive = false))
mediaDataManager.setRecommendationInactive(smartspaceMediaData.targetId)
} else {
- smartspaceMediaData =
+ mediaFilterRepository.setRecommendation(
EMPTY_SMARTSPACE_MEDIA_DATA.copy(
targetId = smartspaceMediaData.targetId,
instanceId = smartspaceMediaData.instanceId,
)
+ )
mediaDataManager.dismissSmartspaceRecommendation(
smartspaceMediaData.targetId,
delay = 0L,
@@ -314,29 +311,6 @@
}
}
- /** Are there any active media entries, including the recommendation? */
- fun hasActiveMediaOrRecommendation() =
- userEntries.any { it.value.active } ||
- (smartspaceMediaData.isActive &&
- (smartspaceMediaData.isValid() || reactivatedKey != null))
-
- /** Are there any media entries we should display? */
- fun hasAnyMediaOrRecommendation(): Boolean {
- val hasSmartspace =
- if (mediaFlags.isPersistentSsCardEnabled()) {
- smartspaceMediaData.isValid()
- } else {
- smartspaceMediaData.isActive && smartspaceMediaData.isValid()
- }
- return userEntries.isNotEmpty() || hasSmartspace
- }
-
- /** Are there any media notifications active (excluding the recommendation)? */
- fun hasActiveMedia() = userEntries.any { it.value.active }
-
- /** Are there any media entries we should display (excluding the recommendation)? */
- fun hasAnyMedia() = userEntries.isNotEmpty()
-
/** Add a listener for filtered [MediaData] changes */
fun addListener(listener: MediaDataManager.Listener) = _listeners.add(listener)
@@ -346,7 +320,7 @@
/**
* Return the time since last active for the most-recent media.
*
- * @param sortedEntries userEntries sorted from the earliest to the most-recent.
+ * @param sortedEntries selectedUserEntries sorted from the earliest to the most-recent.
* @return The duration in milliseconds from the most-recent media's last active timestamp to
* the present. MAX_VALUE will be returned if there is no media.
*/
@@ -359,6 +333,21 @@
val now = systemClock.elapsedRealtime()
val lastActiveKey = sortedEntries.lastKey() // most recently active
- return sortedEntries.get(lastActiveKey)?.let { now - it.lastActive } ?: Long.MAX_VALUE
+ return sortedEntries[lastActiveKey]?.let { now - it.lastActive } ?: Long.MAX_VALUE
+ }
+
+ companion object {
+ /**
+ * Maximum age of a media control to re-activate on smartspace signal. If there is no media
+ * control available within this time window, smartspace recommendations will be shown
+ * instead.
+ */
+ @VisibleForTesting
+ internal val SMARTSPACE_MAX_AGE: Long
+ get() =
+ SystemProperties.getLong(
+ "debug.sysui.smartspace_max_age",
+ TimeUnit.MINUTES.toMillis(30)
+ )
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataManager.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataManager.kt
index 865c49e..2b1070c 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataManager.kt
@@ -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.
@@ -16,424 +16,39 @@
package com.android.systemui.media.controls.domain.pipeline
-import android.annotation.SuppressLint
-import android.app.ActivityOptions
-import android.app.BroadcastOptions
-import android.app.Notification
-import android.app.Notification.EXTRA_SUBSTITUTE_APP_NAME
import android.app.PendingIntent
-import android.app.StatusBarManager
-import android.app.UriGrantsManager
-import android.app.smartspace.SmartspaceAction
-import android.app.smartspace.SmartspaceConfig
-import android.app.smartspace.SmartspaceManager
-import android.app.smartspace.SmartspaceSession
-import android.app.smartspace.SmartspaceTarget
-import android.content.BroadcastReceiver
-import android.content.ContentProvider
-import android.content.ContentResolver
-import android.content.Context
-import android.content.Intent
-import android.content.IntentFilter
-import android.content.pm.ApplicationInfo
-import android.content.pm.PackageManager
-import android.graphics.Bitmap
-import android.graphics.ImageDecoder
-import android.graphics.drawable.Animatable
-import android.graphics.drawable.Icon
import android.media.MediaDescription
-import android.media.MediaMetadata
-import android.media.session.MediaController
import android.media.session.MediaSession
-import android.media.session.PlaybackState
-import android.net.Uri
-import android.os.Parcelable
-import android.os.Process
-import android.os.UserHandle
-import android.provider.Settings
import android.service.notification.StatusBarNotification
-import android.support.v4.media.MediaMetadataCompat
-import android.text.TextUtils
-import android.util.Log
-import android.util.Pair as APair
-import androidx.media.utils.MediaConstants
-import com.android.app.tracing.traceSection
-import com.android.internal.annotations.Keep
-import com.android.internal.logging.InstanceId
-import com.android.keyguard.KeyguardUpdateMonitor
-import com.android.systemui.Dumpable
-import com.android.systemui.broadcast.BroadcastDispatcher
-import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.dagger.qualifiers.Background
-import com.android.systemui.dagger.qualifiers.Main
-import com.android.systemui.dump.DumpManager
-import com.android.systemui.media.controls.domain.resume.MediaResumeListener
-import com.android.systemui.media.controls.domain.resume.ResumeMediaBrowser
-import com.android.systemui.media.controls.shared.model.EXTRA_KEY_TRIGGER_SOURCE
-import com.android.systemui.media.controls.shared.model.EXTRA_VALUE_TRIGGER_PERIODIC
-import com.android.systemui.media.controls.shared.model.MediaAction
-import com.android.systemui.media.controls.shared.model.MediaButton
import com.android.systemui.media.controls.shared.model.MediaData
-import com.android.systemui.media.controls.shared.model.MediaDeviceData
import com.android.systemui.media.controls.shared.model.SmartspaceMediaData
-import com.android.systemui.media.controls.shared.model.SmartspaceMediaDataProvider
-import com.android.systemui.media.controls.ui.view.MediaViewHolder
-import com.android.systemui.media.controls.util.MediaControllerFactory
-import com.android.systemui.media.controls.util.MediaDataUtils
-import com.android.systemui.media.controls.util.MediaFlags
-import com.android.systemui.media.controls.util.MediaUiEventLogger
-import com.android.systemui.plugins.ActivityStarter
-import com.android.systemui.plugins.BcSmartspaceDataPlugin
-import com.android.systemui.res.R
-import com.android.systemui.statusbar.NotificationMediaManager.isConnectingState
-import com.android.systemui.statusbar.NotificationMediaManager.isPlayingState
-import com.android.systemui.statusbar.notification.row.HybridGroupManager
-import com.android.systemui.tuner.TunerService
-import com.android.systemui.util.Assert
-import com.android.systemui.util.Utils
-import com.android.systemui.util.concurrency.DelayableExecutor
-import com.android.systemui.util.concurrency.ThreadFactory
-import com.android.systemui.util.time.SystemClock
-import java.io.IOException
-import java.io.PrintWriter
-import java.util.concurrent.Executor
-import javax.inject.Inject
-// URI fields to try loading album art from
-private val ART_URIS =
- arrayOf(
- MediaMetadata.METADATA_KEY_ALBUM_ART_URI,
- MediaMetadata.METADATA_KEY_ART_URI,
- MediaMetadata.METADATA_KEY_DISPLAY_ICON_URI
- )
+/** Facilitates management and loading of Media Data, ready for binding. */
+interface MediaDataManager {
-private const val TAG = "MediaDataManager"
-private const val DEBUG = true
-private const val EXTRAS_SMARTSPACE_DISMISS_INTENT_KEY = "dismiss_intent"
+ /** Add a listener for changes in this class */
+ fun addListener(listener: Listener)
-private val LOADING =
- MediaData(
- userId = -1,
- initialized = false,
- app = null,
- appIcon = null,
- artist = null,
- song = null,
- artwork = null,
- actions = emptyList(),
- actionsToShowInCompact = emptyList(),
- packageName = "INVALID",
- token = null,
- clickIntent = null,
- device = null,
- active = true,
- resumeAction = null,
- instanceId = InstanceId.fakeInstanceId(-1),
- appUid = Process.INVALID_UID
- )
+ /** Remove a listener for changes in this class */
+ fun removeListener(listener: Listener)
-internal val EMPTY_SMARTSPACE_MEDIA_DATA =
- SmartspaceMediaData(
- targetId = "INVALID",
- isActive = false,
- packageName = "INVALID",
- cardAction = null,
- recommendations = emptyList(),
- dismissIntent = null,
- headphoneConnectionTimeMillis = 0,
- instanceId = InstanceId.fakeInstanceId(-1),
- expiryTimeMs = 0,
- )
+ /**
+ * Called whenever the player has been paused or stopped for a while, or swiped from QQS. This
+ * will make the player not active anymore, hiding it from QQS and Keyguard.
+ *
+ * @see MediaData.active
+ */
+ fun setInactive(key: String, timedOut: Boolean, forceUpdate: Boolean = false)
-const val MEDIA_TITLE_ERROR_MESSAGE = "Invalid media data: title is null or blank."
+ /** Invoked when media notification is added. */
+ fun onNotificationAdded(key: String, sbn: StatusBarNotification)
-fun isMediaNotification(sbn: StatusBarNotification): Boolean {
- return sbn.notification.isMediaNotification()
-}
+ fun destroy()
-/**
- * Allow recommendations from smartspace to show in media controls. Requires
- * [Utils.useQsMediaPlayer] to be enabled. On by default, but can be disabled by setting to 0
- */
-private fun allowMediaRecommendations(context: Context): Boolean {
- val flag =
- Settings.Secure.getInt(
- context.contentResolver,
- Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION,
- 1
- )
- return Utils.useQsMediaPlayer(context) && flag > 0
-}
+ /** Sets resume action. */
+ fun setResumeAction(key: String, action: Runnable?)
-/** A class that facilitates management and loading of Media Data, ready for binding. */
-@SysUISingleton
-class MediaDataManager(
- private val context: Context,
- @Background private val backgroundExecutor: Executor,
- @Main private val uiExecutor: Executor,
- @Main private val foregroundExecutor: DelayableExecutor,
- private val mediaControllerFactory: MediaControllerFactory,
- private val broadcastDispatcher: BroadcastDispatcher,
- dumpManager: DumpManager,
- mediaTimeoutListener: MediaTimeoutListener,
- mediaResumeListener: MediaResumeListener,
- mediaSessionBasedFilter: MediaSessionBasedFilter,
- mediaDeviceManager: MediaDeviceManager,
- mediaDataCombineLatest: MediaDataCombineLatest,
- private val mediaDataFilter: MediaDataFilter,
- private val activityStarter: ActivityStarter,
- private val smartspaceMediaDataProvider: SmartspaceMediaDataProvider,
- private var useMediaResumption: Boolean,
- private val useQsMediaPlayer: Boolean,
- private val systemClock: SystemClock,
- private val tunerService: TunerService,
- private val mediaFlags: MediaFlags,
- private val logger: MediaUiEventLogger,
- private val smartspaceManager: SmartspaceManager?,
- private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
-) : Dumpable, BcSmartspaceDataPlugin.SmartspaceTargetListener {
-
- companion object {
- // UI surface label for subscribing Smartspace updates.
- @JvmField val SMARTSPACE_UI_SURFACE_LABEL = "media_data_manager"
-
- // Smartspace package name's extra key.
- @JvmField val EXTRAS_MEDIA_SOURCE_PACKAGE_NAME = "package_name"
-
- // Maximum number of actions allowed in compact view
- @JvmField val MAX_COMPACT_ACTIONS = 3
-
- // Maximum number of actions allowed in expanded view
- @JvmField val MAX_NOTIFICATION_ACTIONS = MediaViewHolder.genericButtonIds.size
- }
-
- private val themeText =
- com.android.settingslib.Utils.getColorAttr(
- context,
- com.android.internal.R.attr.textColorPrimary
- )
- .defaultColor
-
- // Internal listeners are part of the internal pipeline. External listeners (those registered
- // with [MediaDeviceManager.addListener]) receive events after they have propagated through
- // the internal pipeline.
- // Another way to think of the distinction between internal and external listeners is the
- // following. Internal listeners are listeners that MediaDataManager depends on, and external
- // listeners are listeners that depend on MediaDataManager.
- // TODO(b/159539991#comment5): Move internal listeners to separate package.
- private val internalListeners: MutableSet<Listener> = mutableSetOf()
- private val mediaEntries: LinkedHashMap<String, MediaData> = LinkedHashMap()
- // There should ONLY be at most one Smartspace media recommendation.
- var smartspaceMediaData: SmartspaceMediaData = EMPTY_SMARTSPACE_MEDIA_DATA
- @Keep private var smartspaceSession: SmartspaceSession? = null
- private var allowMediaRecommendations = allowMediaRecommendations(context)
-
- private val artworkWidth =
- context.resources.getDimensionPixelSize(
- com.android.internal.R.dimen.config_mediaMetadataBitmapMaxSize
- )
- private val artworkHeight =
- context.resources.getDimensionPixelSize(R.dimen.qs_media_session_height_expanded)
-
- @SuppressLint("WrongConstant") // sysui allowed to call STATUS_BAR_SERVICE
- private val statusBarManager =
- context.getSystemService(Context.STATUS_BAR_SERVICE) as StatusBarManager
-
- /** Check whether this notification is an RCN */
- private fun isRemoteCastNotification(sbn: StatusBarNotification): Boolean {
- return sbn.notification.extras.containsKey(Notification.EXTRA_MEDIA_REMOTE_DEVICE)
- }
-
- @Inject
- constructor(
- context: Context,
- threadFactory: ThreadFactory,
- @Main uiExecutor: Executor,
- @Main foregroundExecutor: DelayableExecutor,
- mediaControllerFactory: MediaControllerFactory,
- dumpManager: DumpManager,
- broadcastDispatcher: BroadcastDispatcher,
- mediaTimeoutListener: MediaTimeoutListener,
- mediaResumeListener: MediaResumeListener,
- mediaSessionBasedFilter: MediaSessionBasedFilter,
- mediaDeviceManager: MediaDeviceManager,
- mediaDataCombineLatest: MediaDataCombineLatest,
- mediaDataFilter: MediaDataFilter,
- activityStarter: ActivityStarter,
- smartspaceMediaDataProvider: SmartspaceMediaDataProvider,
- clock: SystemClock,
- tunerService: TunerService,
- mediaFlags: MediaFlags,
- logger: MediaUiEventLogger,
- smartspaceManager: SmartspaceManager?,
- keyguardUpdateMonitor: KeyguardUpdateMonitor,
- ) : this(
- context,
- // Loading bitmap for UMO background can take longer time, so it cannot run on the default
- // background thread. Use a custom thread for media.
- threadFactory.buildExecutorOnNewThread(TAG),
- uiExecutor,
- foregroundExecutor,
- mediaControllerFactory,
- broadcastDispatcher,
- dumpManager,
- mediaTimeoutListener,
- mediaResumeListener,
- mediaSessionBasedFilter,
- mediaDeviceManager,
- mediaDataCombineLatest,
- mediaDataFilter,
- activityStarter,
- smartspaceMediaDataProvider,
- Utils.useMediaResumption(context),
- Utils.useQsMediaPlayer(context),
- clock,
- tunerService,
- mediaFlags,
- logger,
- smartspaceManager,
- keyguardUpdateMonitor,
- )
-
- private val appChangeReceiver =
- object : BroadcastReceiver() {
- override fun onReceive(context: Context, intent: Intent) {
- when (intent.action) {
- Intent.ACTION_PACKAGES_SUSPENDED -> {
- val packages = intent.getStringArrayExtra(Intent.EXTRA_CHANGED_PACKAGE_LIST)
- packages?.forEach { removeAllForPackage(it) }
- }
- Intent.ACTION_PACKAGE_REMOVED,
- Intent.ACTION_PACKAGE_RESTARTED -> {
- intent.data?.encodedSchemeSpecificPart?.let { removeAllForPackage(it) }
- }
- }
- }
- }
-
- init {
- dumpManager.registerDumpable(TAG, this)
-
- // Initialize the internal processing pipeline. The listeners at the front of the pipeline
- // are set as internal listeners so that they receive events. From there, events are
- // propagated through the pipeline. The end of the pipeline is currently mediaDataFilter,
- // so it is responsible for dispatching events to external listeners. To achieve this,
- // external listeners that are registered with [MediaDataManager.addListener] are actually
- // registered as listeners to mediaDataFilter.
- addInternalListener(mediaTimeoutListener)
- addInternalListener(mediaResumeListener)
- addInternalListener(mediaSessionBasedFilter)
- mediaSessionBasedFilter.addListener(mediaDeviceManager)
- mediaSessionBasedFilter.addListener(mediaDataCombineLatest)
- mediaDeviceManager.addListener(mediaDataCombineLatest)
- mediaDataCombineLatest.addListener(mediaDataFilter)
-
- // Set up links back into the pipeline for listeners that need to send events upstream.
- mediaTimeoutListener.timeoutCallback = { key: String, timedOut: Boolean ->
- setTimedOut(key, timedOut)
- }
- mediaTimeoutListener.stateCallback = { key: String, state: PlaybackState ->
- updateState(key, state)
- }
- mediaTimeoutListener.sessionCallback = { key: String -> onSessionDestroyed(key) }
- mediaResumeListener.setManager(this)
- mediaDataFilter.mediaDataManager = this
-
- val suspendFilter = IntentFilter(Intent.ACTION_PACKAGES_SUSPENDED)
- broadcastDispatcher.registerReceiver(appChangeReceiver, suspendFilter, null, UserHandle.ALL)
-
- val uninstallFilter =
- IntentFilter().apply {
- addAction(Intent.ACTION_PACKAGE_REMOVED)
- addAction(Intent.ACTION_PACKAGE_RESTARTED)
- addDataScheme("package")
- }
- // BroadcastDispatcher does not allow filters with data schemes
- context.registerReceiver(appChangeReceiver, uninstallFilter)
-
- // Register for Smartspace data updates.
- smartspaceMediaDataProvider.registerListener(this)
- smartspaceSession =
- smartspaceManager?.createSmartspaceSession(
- SmartspaceConfig.Builder(context, SMARTSPACE_UI_SURFACE_LABEL).build()
- )
- smartspaceSession?.let {
- it.addOnTargetsAvailableListener(
- // Use a main uiExecutor thread listening to Smartspace updates instead of using
- // the existing background executor.
- // SmartspaceSession has scheduled routine updates which can be unpredictable on
- // test simulators, using the backgroundExecutor makes it's hard to test the threads
- // numbers.
- uiExecutor,
- SmartspaceSession.OnTargetsAvailableListener { targets ->
- smartspaceMediaDataProvider.onTargetsAvailable(targets)
- }
- )
- }
- smartspaceSession?.let { it.requestSmartspaceUpdate() }
- tunerService.addTunable(
- object : TunerService.Tunable {
- override fun onTuningChanged(key: String?, newValue: String?) {
- allowMediaRecommendations = allowMediaRecommendations(context)
- if (!allowMediaRecommendations) {
- dismissSmartspaceRecommendation(
- key = smartspaceMediaData.targetId,
- delay = 0L
- )
- }
- }
- },
- Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION
- )
- }
-
- fun destroy() {
- smartspaceMediaDataProvider.unregisterListener(this)
- smartspaceSession?.close()
- smartspaceSession = null
- context.unregisterReceiver(appChangeReceiver)
- }
-
- fun onNotificationAdded(key: String, sbn: StatusBarNotification) {
- if (useQsMediaPlayer && isMediaNotification(sbn)) {
- var isNewlyActiveEntry = false
- Assert.isMainThread()
- val oldKey = findExistingEntry(key, sbn.packageName)
- if (oldKey == null) {
- val instanceId = logger.getNewInstanceId()
- val temp =
- LOADING.copy(
- packageName = sbn.packageName,
- instanceId = instanceId,
- createdTimestampMillis = systemClock.currentTimeMillis(),
- )
- mediaEntries.put(key, temp)
- isNewlyActiveEntry = true
- } else if (oldKey != key) {
- // Resume -> active conversion; move to new key
- val oldData = mediaEntries.remove(oldKey)!!
- isNewlyActiveEntry = true
- mediaEntries.put(key, oldData)
- }
- loadMediaData(key, sbn, oldKey, isNewlyActiveEntry)
- } else {
- onNotificationRemoved(key)
- }
- }
-
- private fun removeAllForPackage(packageName: String) {
- Assert.isMainThread()
- val toRemove = mediaEntries.filter { it.value.packageName == packageName }
- toRemove.forEach { removeEntry(it.key) }
- }
-
- fun setResumeAction(key: String, action: Runnable?) {
- mediaEntries.get(key)?.let {
- it.resumeAction = action
- it.hasCheckedForResume = true
- }
- }
-
+ /** Adds resume media data. */
fun addResumptionControls(
userId: Int,
desc: MediaDescription,
@@ -442,1184 +57,45 @@
appName: String,
appIntent: PendingIntent,
packageName: String
- ) {
- // Resume controls don't have a notification key, so store by package name instead
- if (!mediaEntries.containsKey(packageName)) {
- val instanceId = logger.getNewInstanceId()
- val appUid =
- try {
- context.packageManager.getApplicationInfo(packageName, 0)?.uid!!
- } catch (e: PackageManager.NameNotFoundException) {
- Log.w(TAG, "Could not get app UID for $packageName", e)
- Process.INVALID_UID
- }
-
- val resumeData =
- LOADING.copy(
- packageName = packageName,
- resumeAction = action,
- hasCheckedForResume = true,
- instanceId = instanceId,
- appUid = appUid,
- createdTimestampMillis = systemClock.currentTimeMillis(),
- )
- mediaEntries.put(packageName, resumeData)
- logSingleVsMultipleMediaAdded(appUid, packageName, instanceId)
- logger.logResumeMediaAdded(appUid, packageName, instanceId)
- }
- backgroundExecutor.execute {
- loadMediaDataInBgForResumption(
- userId,
- desc,
- action,
- token,
- appName,
- appIntent,
- packageName
- )
- }
- }
-
- /**
- * Check if there is an existing entry that matches the key or package name. Returns the key
- * that matches, or null if not found.
- */
- private fun findExistingEntry(key: String, packageName: String): String? {
- if (mediaEntries.containsKey(key)) {
- return key
- }
- // Check if we already had a resume player
- if (mediaEntries.containsKey(packageName)) {
- return packageName
- }
- return null
- }
-
- private fun loadMediaData(
- key: String,
- sbn: StatusBarNotification,
- oldKey: String?,
- isNewlyActiveEntry: Boolean = false,
- ) {
- backgroundExecutor.execute { loadMediaDataInBg(key, sbn, oldKey, isNewlyActiveEntry) }
- }
-
- /** Add a listener for changes in this class */
- fun addListener(listener: Listener) {
- // mediaDataFilter is the current end of the internal pipeline. Register external
- // listeners as listeners to it.
- mediaDataFilter.addListener(listener)
- }
-
- /** Remove a listener for changes in this class */
- fun removeListener(listener: Listener) {
- // Since mediaDataFilter is the current end of the internal pipelie, external listeners
- // have been registered to it. So, they need to be removed from it too.
- mediaDataFilter.removeListener(listener)
- }
-
- /** Add a listener for internal events. */
- private fun addInternalListener(listener: Listener) = internalListeners.add(listener)
-
- /**
- * Notify internal listeners of media loaded event.
- *
- * External listeners registered with [addListener] will be notified after the event propagates
- * through the internal listener pipeline.
- */
- private fun notifyMediaDataLoaded(key: String, oldKey: String?, info: MediaData) {
- internalListeners.forEach { it.onMediaDataLoaded(key, oldKey, info) }
- }
-
- /**
- * Notify internal listeners of Smartspace media loaded event.
- *
- * External listeners registered with [addListener] will be notified after the event propagates
- * through the internal listener pipeline.
- */
- private fun notifySmartspaceMediaDataLoaded(key: String, info: SmartspaceMediaData) {
- internalListeners.forEach { it.onSmartspaceMediaDataLoaded(key, info) }
- }
-
- /**
- * Notify internal listeners of media removed event.
- *
- * External listeners registered with [addListener] will be notified after the event propagates
- * through the internal listener pipeline.
- */
- private fun notifyMediaDataRemoved(key: String) {
- internalListeners.forEach { it.onMediaDataRemoved(key) }
- }
-
- /**
- * Notify internal listeners of Smartspace media removed event.
- *
- * External listeners registered with [addListener] will be notified after the event propagates
- * through the internal listener pipeline.
- *
- * @param immediately indicates should apply the UI changes immediately, otherwise wait until
- * the next refresh-round before UI becomes visible. Should only be true if the update is
- * initiated by user's interaction.
- */
- private fun notifySmartspaceMediaDataRemoved(key: String, immediately: Boolean) {
- internalListeners.forEach { it.onSmartspaceMediaDataRemoved(key, immediately) }
- }
-
- /**
- * Called whenever the player has been paused or stopped for a while, or swiped from QQS. This
- * will make the player not active anymore, hiding it from QQS and Keyguard.
- *
- * @see MediaData.active
- */
- internal fun setTimedOut(key: String, timedOut: Boolean, forceUpdate: Boolean = false) {
- mediaEntries[key]?.let {
- if (timedOut && !forceUpdate) {
- // Only log this event when media expires on its own
- logger.logMediaTimeout(it.appUid, it.packageName, it.instanceId)
- }
- if (it.active == !timedOut && !forceUpdate) {
- if (it.resumption) {
- if (DEBUG) Log.d(TAG, "timing out resume player $key")
- dismissMediaData(key, 0L /* delay */)
- }
- return
- }
- // Update last active if media was still active.
- if (it.active) {
- it.lastActive = systemClock.elapsedRealtime()
- }
- it.active = !timedOut
- if (DEBUG) Log.d(TAG, "Updating $key timedOut: $timedOut")
- onMediaDataLoaded(key, key, it)
- }
-
- if (key == smartspaceMediaData.targetId) {
- if (DEBUG) Log.d(TAG, "smartspace card expired")
- dismissSmartspaceRecommendation(key, delay = 0L)
- }
- }
-
- /** Called when the player's [PlaybackState] has been updated with new actions and/or state */
- private fun updateState(key: String, state: PlaybackState) {
- mediaEntries.get(key)?.let {
- val token = it.token
- if (token == null) {
- if (DEBUG) Log.d(TAG, "State updated, but token was null")
- return
- }
- val actions =
- createActionsFromState(
- it.packageName,
- mediaControllerFactory.create(it.token),
- UserHandle(it.userId)
- )
-
- // Control buttons
- // If flag is enabled and controller has a PlaybackState,
- // create actions from session info
- // otherwise, no need to update semantic actions.
- val data =
- if (actions != null) {
- it.copy(semanticActions = actions, isPlaying = isPlayingState(state.state))
- } else {
- it.copy(isPlaying = isPlayingState(state.state))
- }
- if (DEBUG) Log.d(TAG, "State updated outside of notification")
- onMediaDataLoaded(key, key, data)
- }
- }
-
- private fun removeEntry(key: String, logEvent: Boolean = true) {
- mediaEntries.remove(key)?.let {
- if (logEvent) {
- logger.logMediaRemoved(it.appUid, it.packageName, it.instanceId)
- }
- }
- notifyMediaDataRemoved(key)
- }
+ )
/** Dismiss a media entry. Returns false if the key was not found. */
- fun dismissMediaData(key: String, delay: Long): Boolean {
- val existed = mediaEntries[key] != null
- backgroundExecutor.execute {
- mediaEntries[key]?.let { mediaData ->
- if (mediaData.isLocalSession()) {
- mediaData.token?.let {
- val mediaController = mediaControllerFactory.create(it)
- mediaController.transportControls.stop()
- }
- }
- }
- }
- foregroundExecutor.executeDelayed({ removeEntry(key) }, delay)
- return existed
- }
+ fun dismissMediaData(key: String, delay: Long): Boolean
/**
* Called whenever the recommendation has been expired or removed by the user. This will remove
* the recommendation card entirely from the carousel.
*/
- fun dismissSmartspaceRecommendation(key: String, delay: Long) {
- if (smartspaceMediaData.targetId != key || !smartspaceMediaData.isValid()) {
- // If this doesn't match, or we've already invalidated the data, no action needed
- return
- }
-
- if (DEBUG) Log.d(TAG, "Dismissing Smartspace media target")
- if (smartspaceMediaData.isActive) {
- smartspaceMediaData =
- EMPTY_SMARTSPACE_MEDIA_DATA.copy(
- targetId = smartspaceMediaData.targetId,
- instanceId = smartspaceMediaData.instanceId
- )
- }
- foregroundExecutor.executeDelayed(
- { notifySmartspaceMediaDataRemoved(smartspaceMediaData.targetId, immediately = true) },
- delay
- )
- }
+ fun dismissSmartspaceRecommendation(key: String, delay: Long)
/** Called when the recommendation card should no longer be visible in QQS or lockscreen */
- fun setRecommendationInactive(key: String) {
- if (!mediaFlags.isPersistentSsCardEnabled()) {
- Log.e(TAG, "Only persistent recommendation can be inactive!")
- return
- }
- if (DEBUG) Log.d(TAG, "Setting smartspace recommendation inactive")
+ fun setRecommendationInactive(key: String)
- if (smartspaceMediaData.targetId != key || !smartspaceMediaData.isValid()) {
- // If this doesn't match, or we've already invalidated the data, no action needed
- return
- }
+ /** Invoked when notification is removed. */
+ fun onNotificationRemoved(key: String)
- smartspaceMediaData = smartspaceMediaData.copy(isActive = false)
- notifySmartspaceMediaDataLoaded(smartspaceMediaData.targetId, smartspaceMediaData)
- }
-
- private fun loadMediaDataInBgForResumption(
- userId: Int,
- desc: MediaDescription,
- resumeAction: Runnable,
- token: MediaSession.Token,
- appName: String,
- appIntent: PendingIntent,
- packageName: String
- ) {
- if (desc.title.isNullOrBlank()) {
- Log.e(TAG, "Description incomplete")
- // Delete the placeholder entry
- mediaEntries.remove(packageName)
- return
- }
-
- if (DEBUG) {
- Log.d(TAG, "adding track for $userId from browser: $desc")
- }
-
- val currentEntry = mediaEntries.get(packageName)
- val appUid = currentEntry?.appUid ?: Process.INVALID_UID
-
- // Album art
- var artworkBitmap = desc.iconBitmap
- if (artworkBitmap == null && desc.iconUri != null) {
- artworkBitmap = loadBitmapFromUriForUser(desc.iconUri!!, userId, appUid, packageName)
- }
- val artworkIcon =
- if (artworkBitmap != null) {
- Icon.createWithBitmap(artworkBitmap)
- } else {
- null
- }
-
- val instanceId = currentEntry?.instanceId ?: logger.getNewInstanceId()
- val isExplicit =
- desc.extras?.getLong(MediaConstants.METADATA_KEY_IS_EXPLICIT) ==
- MediaConstants.METADATA_VALUE_ATTRIBUTE_PRESENT
-
- val progress =
- if (mediaFlags.isResumeProgressEnabled()) {
- MediaDataUtils.getDescriptionProgress(desc.extras)
- } else null
-
- val mediaAction = getResumeMediaAction(resumeAction)
- val lastActive = systemClock.elapsedRealtime()
- val createdTimestampMillis = currentEntry?.createdTimestampMillis ?: 0L
- foregroundExecutor.execute {
- onMediaDataLoaded(
- packageName,
- null,
- MediaData(
- userId,
- true,
- appName,
- null,
- desc.subtitle,
- desc.title,
- artworkIcon,
- listOf(mediaAction),
- listOf(0),
- MediaButton(playOrPause = mediaAction),
- packageName,
- token,
- appIntent,
- device = null,
- active = false,
- resumeAction = resumeAction,
- resumption = true,
- notificationKey = packageName,
- hasCheckedForResume = true,
- lastActive = lastActive,
- createdTimestampMillis = createdTimestampMillis,
- instanceId = instanceId,
- appUid = appUid,
- isExplicit = isExplicit,
- resumeProgress = progress,
- )
- )
- }
- }
-
- fun loadMediaDataInBg(
- key: String,
- sbn: StatusBarNotification,
- oldKey: String?,
- isNewlyActiveEntry: Boolean = false,
- ) {
- val token =
- sbn.notification.extras.getParcelable(
- Notification.EXTRA_MEDIA_SESSION,
- MediaSession.Token::class.java
- )
- if (token == null) {
- return
- }
- val mediaController = mediaControllerFactory.create(token)
- val metadata = mediaController.metadata
- val notif: Notification = sbn.notification
-
- val appInfo =
- notif.extras.getParcelable(
- Notification.EXTRA_BUILDER_APPLICATION_INFO,
- ApplicationInfo::class.java
- )
- ?: getAppInfoFromPackage(sbn.packageName)
-
- // App name
- val appName = getAppName(sbn, appInfo)
-
- // Song name
- var song: CharSequence? = metadata?.getString(MediaMetadata.METADATA_KEY_DISPLAY_TITLE)
- if (song.isNullOrBlank()) {
- song = metadata?.getString(MediaMetadata.METADATA_KEY_TITLE)
- }
- if (song.isNullOrBlank()) {
- song = HybridGroupManager.resolveTitle(notif)
- }
- if (song.isNullOrBlank()) {
- // For apps that don't include a title, log and add a placeholder
- song = context.getString(R.string.controls_media_empty_title, appName)
- try {
- statusBarManager.logBlankMediaTitle(sbn.packageName, sbn.user.identifier)
- } catch (e: RuntimeException) {
- Log.e(TAG, "Error reporting blank media title for package ${sbn.packageName}")
- }
- }
-
- // Album art
- var artworkBitmap = metadata?.let { loadBitmapFromUri(it) }
- if (artworkBitmap == null) {
- artworkBitmap = metadata?.getBitmap(MediaMetadata.METADATA_KEY_ART)
- }
- if (artworkBitmap == null) {
- artworkBitmap = metadata?.getBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART)
- }
- val artWorkIcon =
- if (artworkBitmap == null) {
- notif.getLargeIcon()
- } else {
- Icon.createWithBitmap(artworkBitmap)
- }
-
- // App Icon
- val smallIcon = sbn.notification.smallIcon
-
- // Explicit Indicator
- var isExplicit = false
- val mediaMetadataCompat = MediaMetadataCompat.fromMediaMetadata(metadata)
- isExplicit =
- mediaMetadataCompat?.getLong(MediaConstants.METADATA_KEY_IS_EXPLICIT) ==
- MediaConstants.METADATA_VALUE_ATTRIBUTE_PRESENT
-
- // Artist name
- var artist: CharSequence? = metadata?.getString(MediaMetadata.METADATA_KEY_ARTIST)
- if (artist.isNullOrBlank()) {
- artist = HybridGroupManager.resolveText(notif)
- }
-
- // Device name (used for remote cast notifications)
- var device: MediaDeviceData? = null
- if (isRemoteCastNotification(sbn)) {
- val extras = sbn.notification.extras
- val deviceName = extras.getCharSequence(Notification.EXTRA_MEDIA_REMOTE_DEVICE, null)
- val deviceIcon = extras.getInt(Notification.EXTRA_MEDIA_REMOTE_ICON, -1)
- val deviceIntent =
- extras.getParcelable(
- Notification.EXTRA_MEDIA_REMOTE_INTENT,
- PendingIntent::class.java
- )
- Log.d(TAG, "$key is RCN for $deviceName")
-
- if (deviceName != null && deviceIcon > -1) {
- // Name and icon must be present, but intent may be null
- val enabled = deviceIntent != null && deviceIntent.isActivity
- val deviceDrawable =
- Icon.createWithResource(sbn.packageName, deviceIcon)
- .loadDrawable(sbn.getPackageContext(context))
- device =
- MediaDeviceData(
- enabled,
- deviceDrawable,
- deviceName,
- deviceIntent,
- showBroadcastButton = false
- )
- }
- }
-
- // Control buttons
- // If flag is enabled and controller has a PlaybackState, create actions from session info
- // Otherwise, use the notification actions
- var actionIcons: List<MediaAction> = emptyList()
- var actionsToShowCollapsed: List<Int> = emptyList()
- val semanticActions = createActionsFromState(sbn.packageName, mediaController, sbn.user)
- if (semanticActions == null) {
- val actions = createActionsFromNotification(sbn)
- actionIcons = actions.first
- actionsToShowCollapsed = actions.second
- }
-
- val playbackLocation =
- if (isRemoteCastNotification(sbn)) MediaData.PLAYBACK_CAST_REMOTE
- else if (
- mediaController.playbackInfo?.playbackType ==
- MediaController.PlaybackInfo.PLAYBACK_TYPE_LOCAL
- )
- MediaData.PLAYBACK_LOCAL
- else MediaData.PLAYBACK_CAST_LOCAL
- val isPlaying = mediaController.playbackState?.let { isPlayingState(it.state) } ?: null
-
- val currentEntry = mediaEntries.get(key)
- val instanceId = currentEntry?.instanceId ?: logger.getNewInstanceId()
- val appUid = appInfo?.uid ?: Process.INVALID_UID
-
- if (isNewlyActiveEntry) {
- logSingleVsMultipleMediaAdded(appUid, sbn.packageName, instanceId)
- logger.logActiveMediaAdded(appUid, sbn.packageName, instanceId, playbackLocation)
- } else if (playbackLocation != currentEntry?.playbackLocation) {
- logger.logPlaybackLocationChange(appUid, sbn.packageName, instanceId, playbackLocation)
- }
-
- val lastActive = systemClock.elapsedRealtime()
- val createdTimestampMillis = currentEntry?.createdTimestampMillis ?: 0L
- foregroundExecutor.execute {
- val resumeAction: Runnable? = mediaEntries[key]?.resumeAction
- val hasCheckedForResume = mediaEntries[key]?.hasCheckedForResume == true
- val active = mediaEntries[key]?.active ?: true
- onMediaDataLoaded(
- key,
- oldKey,
- MediaData(
- sbn.normalizedUserId,
- true,
- appName,
- smallIcon,
- artist,
- song,
- artWorkIcon,
- actionIcons,
- actionsToShowCollapsed,
- semanticActions,
- sbn.packageName,
- token,
- notif.contentIntent,
- device,
- active,
- resumeAction = resumeAction,
- playbackLocation = playbackLocation,
- notificationKey = key,
- hasCheckedForResume = hasCheckedForResume,
- isPlaying = isPlaying,
- isClearable = !sbn.isOngoing,
- lastActive = lastActive,
- createdTimestampMillis = createdTimestampMillis,
- instanceId = instanceId,
- appUid = appUid,
- isExplicit = isExplicit,
- )
- )
- }
- }
-
- private fun logSingleVsMultipleMediaAdded(
- appUid: Int,
- packageName: String,
- instanceId: InstanceId
- ) {
- if (mediaEntries.size == 1) {
- logger.logSingleMediaPlayerInCarousel(appUid, packageName, instanceId)
- } else if (mediaEntries.size == 2) {
- // Since this method is only called when there is a new media session added.
- // logging needed once there is more than one media session in carousel.
- logger.logMultipleMediaPlayersInCarousel(appUid, packageName, instanceId)
- }
- }
-
- private fun getAppInfoFromPackage(packageName: String): ApplicationInfo? {
- try {
- return context.packageManager.getApplicationInfo(packageName, 0)
- } catch (e: PackageManager.NameNotFoundException) {
- Log.w(TAG, "Could not get app info for $packageName", e)
- }
- return null
- }
-
- private fun getAppName(sbn: StatusBarNotification, appInfo: ApplicationInfo?): String {
- val name = sbn.notification.extras.getString(EXTRA_SUBSTITUTE_APP_NAME)
- if (name != null) {
- return name
- }
-
- return if (appInfo != null) {
- context.packageManager.getApplicationLabel(appInfo).toString()
- } else {
- sbn.packageName
- }
- }
-
- /** Generate action buttons based on notification actions */
- private fun createActionsFromNotification(
- sbn: StatusBarNotification
- ): Pair<List<MediaAction>, List<Int>> {
- val notif = sbn.notification
- val actionIcons: MutableList<MediaAction> = ArrayList()
- val actions = notif.actions
- var actionsToShowCollapsed =
- notif.extras.getIntArray(Notification.EXTRA_COMPACT_ACTIONS)?.toMutableList()
- ?: mutableListOf()
- if (actionsToShowCollapsed.size > MAX_COMPACT_ACTIONS) {
- Log.e(
- TAG,
- "Too many compact actions for ${sbn.key}," +
- "limiting to first $MAX_COMPACT_ACTIONS"
- )
- actionsToShowCollapsed = actionsToShowCollapsed.subList(0, MAX_COMPACT_ACTIONS)
- }
-
- if (actions != null) {
- for ((index, action) in actions.withIndex()) {
- if (index == MAX_NOTIFICATION_ACTIONS) {
- Log.w(
- TAG,
- "Too many notification actions for ${sbn.key}," +
- " limiting to first $MAX_NOTIFICATION_ACTIONS"
- )
- break
- }
- if (action.getIcon() == null) {
- if (DEBUG) Log.i(TAG, "No icon for action $index ${action.title}")
- actionsToShowCollapsed.remove(index)
- continue
- }
- val runnable =
- if (action.actionIntent != null) {
- Runnable {
- if (action.actionIntent.isActivity) {
- activityStarter.startPendingIntentDismissingKeyguard(
- action.actionIntent
- )
- } else if (action.isAuthenticationRequired()) {
- activityStarter.dismissKeyguardThenExecute(
- {
- var result = sendPendingIntent(action.actionIntent)
- result
- },
- {},
- true
- )
- } else {
- sendPendingIntent(action.actionIntent)
- }
- }
- } else {
- null
- }
- val mediaActionIcon =
- if (action.getIcon()?.getType() == Icon.TYPE_RESOURCE) {
- Icon.createWithResource(sbn.packageName, action.getIcon()!!.getResId())
- } else {
- action.getIcon()
- }
- .setTint(themeText)
- .loadDrawable(context)
- val mediaAction = MediaAction(mediaActionIcon, runnable, action.title, null)
- actionIcons.add(mediaAction)
- }
- }
- return Pair(actionIcons, actionsToShowCollapsed)
- }
-
- /**
- * Generates action button info for this media session based on the PlaybackState
- *
- * @param packageName Package name for the media app
- * @param controller MediaController for the current session
- * @return a Pair consisting of a list of media actions, and a list of ints representing which
- *
- * ```
- * of those actions should be shown in the compact player
- * ```
- */
- private fun createActionsFromState(
- packageName: String,
- controller: MediaController,
- user: UserHandle
- ): MediaButton? {
- val state = controller.playbackState
- if (state == null || !mediaFlags.areMediaSessionActionsEnabled(packageName, user)) {
- return null
- }
-
- // First, check for standard actions
- val playOrPause =
- if (isConnectingState(state.state)) {
- // Spinner needs to be animating to render anything. Start it here.
- val drawable =
- context.getDrawable(com.android.internal.R.drawable.progress_small_material)
- (drawable as Animatable).start()
- MediaAction(
- drawable,
- null, // no action to perform when clicked
- context.getString(R.string.controls_media_button_connecting),
- context.getDrawable(R.drawable.ic_media_connecting_container),
- // Specify a rebind id to prevent the spinner from restarting on later binds.
- com.android.internal.R.drawable.progress_small_material
- )
- } else if (isPlayingState(state.state)) {
- getStandardAction(controller, state.actions, PlaybackState.ACTION_PAUSE)
- } else {
- getStandardAction(controller, state.actions, PlaybackState.ACTION_PLAY)
- }
- val prevButton =
- getStandardAction(controller, state.actions, PlaybackState.ACTION_SKIP_TO_PREVIOUS)
- val nextButton =
- getStandardAction(controller, state.actions, PlaybackState.ACTION_SKIP_TO_NEXT)
-
- // Then, create a way to build any custom actions that will be needed
- val customActions =
- state.customActions
- .asSequence()
- .filterNotNull()
- .map { getCustomAction(state, packageName, controller, it) }
- .iterator()
- fun nextCustomAction() = if (customActions.hasNext()) customActions.next() else null
-
- // Finally, assign the remaining button slots: play/pause A B C D
- // A = previous, else custom action (if not reserved)
- // B = next, else custom action (if not reserved)
- // C and D are always custom actions
- val reservePrev =
- controller.extras?.getBoolean(
- MediaConstants.SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_PREV
- ) == true
- val reserveNext =
- controller.extras?.getBoolean(
- MediaConstants.SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_NEXT
- ) == true
-
- val prevOrCustom =
- if (prevButton != null) {
- prevButton
- } else if (!reservePrev) {
- nextCustomAction()
- } else {
- null
- }
-
- val nextOrCustom =
- if (nextButton != null) {
- nextButton
- } else if (!reserveNext) {
- nextCustomAction()
- } else {
- null
- }
-
- return MediaButton(
- playOrPause,
- nextOrCustom,
- prevOrCustom,
- nextCustomAction(),
- nextCustomAction(),
- reserveNext,
- reservePrev
- )
- }
-
- /**
- * Create a [MediaAction] for a given action and media session
- *
- * @param controller MediaController for the session
- * @param stateActions The actions included with the session's [PlaybackState]
- * @param action A [PlaybackState.Actions] value representing what action to generate. One of:
- * ```
- * [PlaybackState.ACTION_PLAY]
- * [PlaybackState.ACTION_PAUSE]
- * [PlaybackState.ACTION_SKIP_TO_PREVIOUS]
- * [PlaybackState.ACTION_SKIP_TO_NEXT]
- * @return
- * ```
- *
- * A [MediaAction] with correct values set, or null if the state doesn't support it
- */
- private fun getStandardAction(
- controller: MediaController,
- stateActions: Long,
- @PlaybackState.Actions action: Long
- ): MediaAction? {
- if (!includesAction(stateActions, action)) {
- return null
- }
-
- return when (action) {
- PlaybackState.ACTION_PLAY -> {
- MediaAction(
- context.getDrawable(R.drawable.ic_media_play),
- { controller.transportControls.play() },
- context.getString(R.string.controls_media_button_play),
- context.getDrawable(R.drawable.ic_media_play_container)
- )
- }
- PlaybackState.ACTION_PAUSE -> {
- MediaAction(
- context.getDrawable(R.drawable.ic_media_pause),
- { controller.transportControls.pause() },
- context.getString(R.string.controls_media_button_pause),
- context.getDrawable(R.drawable.ic_media_pause_container)
- )
- }
- PlaybackState.ACTION_SKIP_TO_PREVIOUS -> {
- MediaAction(
- context.getDrawable(R.drawable.ic_media_prev),
- { controller.transportControls.skipToPrevious() },
- context.getString(R.string.controls_media_button_prev),
- null
- )
- }
- PlaybackState.ACTION_SKIP_TO_NEXT -> {
- MediaAction(
- context.getDrawable(R.drawable.ic_media_next),
- { controller.transportControls.skipToNext() },
- context.getString(R.string.controls_media_button_next),
- null
- )
- }
- else -> null
- }
- }
-
- /** Check whether the actions from a [PlaybackState] include a specific action */
- private fun includesAction(stateActions: Long, @PlaybackState.Actions action: Long): Boolean {
- if (
- (action == PlaybackState.ACTION_PLAY || action == PlaybackState.ACTION_PAUSE) &&
- (stateActions and PlaybackState.ACTION_PLAY_PAUSE > 0L)
- ) {
- return true
- }
- return (stateActions and action != 0L)
- }
-
- /** Get a [MediaAction] representing a [PlaybackState.CustomAction] */
- private fun getCustomAction(
- state: PlaybackState,
- packageName: String,
- controller: MediaController,
- customAction: PlaybackState.CustomAction
- ): MediaAction {
- return MediaAction(
- Icon.createWithResource(packageName, customAction.icon).loadDrawable(context),
- { controller.transportControls.sendCustomAction(customAction, customAction.extras) },
- customAction.name,
- null
- )
- }
-
- /** Load a bitmap from the various Art metadata URIs */
- private fun loadBitmapFromUri(metadata: MediaMetadata): Bitmap? {
- for (uri in ART_URIS) {
- val uriString = metadata.getString(uri)
- if (!TextUtils.isEmpty(uriString)) {
- val albumArt = loadBitmapFromUri(Uri.parse(uriString))
- if (albumArt != null) {
- if (DEBUG) Log.d(TAG, "loaded art from $uri")
- return albumArt
- }
- }
- }
- return null
- }
-
- private fun sendPendingIntent(intent: PendingIntent): Boolean {
- return try {
- val options = BroadcastOptions.makeBasic()
- options.setInteractive(true)
- options.setPendingIntentBackgroundActivityStartMode(
- ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED
- )
- intent.send(options.toBundle())
- true
- } catch (e: PendingIntent.CanceledException) {
- Log.d(TAG, "Intent canceled", e)
- false
- }
- }
-
- /** Returns a bitmap if the user can access the given URI, else null */
- private fun loadBitmapFromUriForUser(
- uri: Uri,
- userId: Int,
- appUid: Int,
- packageName: String,
- ): Bitmap? {
- try {
- val ugm = UriGrantsManager.getService()
- ugm.checkGrantUriPermission_ignoreNonSystem(
- appUid,
- packageName,
- ContentProvider.getUriWithoutUserId(uri),
- Intent.FLAG_GRANT_READ_URI_PERMISSION,
- ContentProvider.getUserIdFromUri(uri, userId)
- )
- return loadBitmapFromUri(uri)
- } catch (e: SecurityException) {
- Log.e(TAG, "Failed to get URI permission: $e")
- }
- return null
- }
-
- /**
- * Load a bitmap from a URI
- *
- * @param uri the uri to load
- * @return bitmap, or null if couldn't be loaded
- */
- private fun loadBitmapFromUri(uri: Uri): Bitmap? {
- // ImageDecoder requires a scheme of the following types
- if (uri.scheme == null) {
- return null
- }
-
- if (
- !uri.scheme.equals(ContentResolver.SCHEME_CONTENT) &&
- !uri.scheme.equals(ContentResolver.SCHEME_ANDROID_RESOURCE) &&
- !uri.scheme.equals(ContentResolver.SCHEME_FILE)
- ) {
- return null
- }
-
- val source = ImageDecoder.createSource(context.contentResolver, uri)
- return try {
- ImageDecoder.decodeBitmap(source) { decoder, info, _ ->
- val width = info.size.width
- val height = info.size.height
- val scale =
- MediaDataUtils.getScaleFactor(
- APair(width, height),
- APair(artworkWidth, artworkHeight)
- )
-
- // Downscale if needed
- if (scale != 0f && scale < 1) {
- decoder.setTargetSize((scale * width).toInt(), (scale * height).toInt())
- }
- decoder.allocator = ImageDecoder.ALLOCATOR_SOFTWARE
- }
- } catch (e: IOException) {
- Log.e(TAG, "Unable to load bitmap", e)
- null
- } catch (e: RuntimeException) {
- Log.e(TAG, "Unable to load bitmap", e)
- null
- }
- }
-
- private fun getResumeMediaAction(action: Runnable): MediaAction {
- return MediaAction(
- Icon.createWithResource(context, R.drawable.ic_media_play)
- .setTint(themeText)
- .loadDrawable(context),
- action,
- context.getString(R.string.controls_media_resume),
- context.getDrawable(R.drawable.ic_media_play_container)
- )
- }
-
- fun onMediaDataLoaded(key: String, oldKey: String?, data: MediaData) =
- traceSection("MediaDataManager#onMediaDataLoaded") {
- Assert.isMainThread()
- if (mediaEntries.containsKey(key)) {
- // Otherwise this was removed already
- mediaEntries.put(key, data)
- notifyMediaDataLoaded(key, oldKey, data)
- }
- }
-
- override fun onSmartspaceTargetsUpdated(targets: List<Parcelable>) {
- if (!allowMediaRecommendations) {
- if (DEBUG) Log.d(TAG, "Smartspace recommendation is disabled in Settings.")
- return
- }
-
- val mediaTargets = targets.filterIsInstance<SmartspaceTarget>()
- when (mediaTargets.size) {
- 0 -> {
- if (!smartspaceMediaData.isActive) {
- return
- }
- if (DEBUG) {
- Log.d(TAG, "Set Smartspace media to be inactive for the data update")
- }
- if (mediaFlags.isPersistentSsCardEnabled()) {
- // Smartspace uses this signal to hide the card (e.g. when it expires or user
- // disconnects headphones), so treat as setting inactive when flag is on
- smartspaceMediaData = smartspaceMediaData.copy(isActive = false)
- notifySmartspaceMediaDataLoaded(
- smartspaceMediaData.targetId,
- smartspaceMediaData,
- )
- } else {
- smartspaceMediaData =
- EMPTY_SMARTSPACE_MEDIA_DATA.copy(
- targetId = smartspaceMediaData.targetId,
- instanceId = smartspaceMediaData.instanceId,
- )
- notifySmartspaceMediaDataRemoved(
- smartspaceMediaData.targetId,
- immediately = false,
- )
- }
- }
- 1 -> {
- val newMediaTarget = mediaTargets.get(0)
- if (smartspaceMediaData.targetId == newMediaTarget.smartspaceTargetId) {
- // The same Smartspace updates can be received. Skip the duplicate updates.
- return
- }
- if (DEBUG) Log.d(TAG, "Forwarding Smartspace media update.")
- smartspaceMediaData = toSmartspaceMediaData(newMediaTarget)
- notifySmartspaceMediaDataLoaded(smartspaceMediaData.targetId, smartspaceMediaData)
- }
- else -> {
- // There should NOT be more than 1 Smartspace media update. When it happens, it
- // indicates a bad state or an error. Reset the status accordingly.
- Log.wtf(TAG, "More than 1 Smartspace Media Update. Resetting the status...")
- notifySmartspaceMediaDataRemoved(
- smartspaceMediaData.targetId,
- immediately = false,
- )
- smartspaceMediaData = EMPTY_SMARTSPACE_MEDIA_DATA
- }
- }
- }
-
- fun onNotificationRemoved(key: String) {
- Assert.isMainThread()
- val removed = mediaEntries.remove(key) ?: return
- if (keyguardUpdateMonitor.isUserInLockdown(removed.userId)) {
- logger.logMediaRemoved(removed.appUid, removed.packageName, removed.instanceId)
- } else if (isAbleToResume(removed)) {
- convertToResumePlayer(key, removed)
- } else if (mediaFlags.isRetainingPlayersEnabled()) {
- handlePossibleRemoval(key, removed, notificationRemoved = true)
- } else {
- notifyMediaDataRemoved(key)
- logger.logMediaRemoved(removed.appUid, removed.packageName, removed.instanceId)
- }
- }
-
- private fun onSessionDestroyed(key: String) {
- if (DEBUG) Log.d(TAG, "session destroyed for $key")
- val entry = mediaEntries.remove(key) ?: return
- // Clear token since the session is no longer valid
- val updated = entry.copy(token = null)
- handlePossibleRemoval(key, updated)
- }
-
- private fun isAbleToResume(data: MediaData): Boolean {
- val isEligibleForResume =
- data.isLocalSession() ||
- (mediaFlags.isRemoteResumeAllowed() &&
- data.playbackLocation != MediaData.PLAYBACK_CAST_REMOTE)
- return useMediaResumption && data.resumeAction != null && isEligibleForResume
- }
-
- /**
- * Convert to resume state if the player is no longer valid and active, then notify listeners
- * that the data was updated. Does not convert to resume state if the player is still valid, or
- * if it was removed before becoming inactive. (Assumes that [removed] was removed from
- * [mediaEntries] before this function was called)
- */
- private fun handlePossibleRemoval(
- key: String,
- removed: MediaData,
- notificationRemoved: Boolean = false
- ) {
- val hasSession = removed.token != null
- if (hasSession && removed.semanticActions != null) {
- // The app was using session actions, and the session is still valid: keep player
- if (DEBUG) Log.d(TAG, "Notification removed but using session actions $key")
- mediaEntries.put(key, removed)
- notifyMediaDataLoaded(key, key, removed)
- } else if (!notificationRemoved && removed.semanticActions == null) {
- // The app was using notification actions, and notif wasn't removed yet: keep player
- if (DEBUG) Log.d(TAG, "Session destroyed but using notification actions $key")
- mediaEntries.put(key, removed)
- notifyMediaDataLoaded(key, key, removed)
- } else if (removed.active && !isAbleToResume(removed)) {
- // This player was still active - it didn't last long enough to time out,
- // and its app doesn't normally support resume: remove
- if (DEBUG) Log.d(TAG, "Removing still-active player $key")
- notifyMediaDataRemoved(key)
- logger.logMediaRemoved(removed.appUid, removed.packageName, removed.instanceId)
- } else if (mediaFlags.isRetainingPlayersEnabled() || isAbleToResume(removed)) {
- // Convert to resume
- if (DEBUG) {
- Log.d(
- TAG,
- "Notification ($notificationRemoved) and/or session " +
- "($hasSession) gone for inactive player $key"
- )
- }
- convertToResumePlayer(key, removed)
- } else {
- // Retaining players flag is off and app doesn't support resume: remove player.
- if (DEBUG) Log.d(TAG, "Removing player $key")
- notifyMediaDataRemoved(key)
- logger.logMediaRemoved(removed.appUid, removed.packageName, removed.instanceId)
- }
- }
-
- /** Set the given [MediaData] as a resume state player and notify listeners */
- private fun convertToResumePlayer(key: String, data: MediaData) {
- if (DEBUG) Log.d(TAG, "Converting $key to resume")
- // Resumption controls must have a title.
- if (data.song.isNullOrBlank()) {
- Log.e(TAG, "Description incomplete")
- notifyMediaDataRemoved(key)
- logger.logMediaRemoved(data.appUid, data.packageName, data.instanceId)
- return
- }
- // Move to resume key (aka package name) if that key doesn't already exist.
- val resumeAction = data.resumeAction?.let { getResumeMediaAction(it) }
- val actions = resumeAction?.let { listOf(resumeAction) } ?: emptyList()
- val launcherIntent =
- context.packageManager.getLaunchIntentForPackage(data.packageName)?.let {
- PendingIntent.getActivity(context, 0, it, PendingIntent.FLAG_IMMUTABLE)
- }
- val lastActive =
- if (data.active) {
- systemClock.elapsedRealtime()
- } else {
- data.lastActive
- }
- val updated =
- data.copy(
- token = null,
- actions = actions,
- semanticActions = MediaButton(playOrPause = resumeAction),
- actionsToShowInCompact = listOf(0),
- active = false,
- resumption = true,
- isPlaying = false,
- isClearable = true,
- clickIntent = launcherIntent,
- lastActive = lastActive,
- )
- val pkg = data.packageName
- val migrate = mediaEntries.put(pkg, updated) == null
- // Notify listeners of "new" controls when migrating or removed and update when not
- Log.d(TAG, "migrating? $migrate from $key -> $pkg")
- if (migrate) {
- notifyMediaDataLoaded(key = pkg, oldKey = key, info = updated)
- } else {
- // Since packageName is used for the key of the resumption controls, it is
- // possible that another notification has already been reused for the resumption
- // controls of this package. In this case, rather than renaming this player as
- // packageName, just remove it and then send a update to the existing resumption
- // controls.
- notifyMediaDataRemoved(key)
- notifyMediaDataLoaded(key = pkg, oldKey = pkg, info = updated)
- }
- logger.logActiveConvertedToResume(updated.appUid, pkg, updated.instanceId)
-
- // Limit total number of resume controls
- val resumeEntries = mediaEntries.filter { (key, data) -> data.resumption }
- val numResume = resumeEntries.size
- if (numResume > ResumeMediaBrowser.MAX_RESUMPTION_CONTROLS) {
- resumeEntries
- .toList()
- .sortedBy { (key, data) -> data.lastActive }
- .subList(0, numResume - ResumeMediaBrowser.MAX_RESUMPTION_CONTROLS)
- .forEach { (key, data) ->
- Log.d(TAG, "Removing excess control $key")
- mediaEntries.remove(key)
- notifyMediaDataRemoved(key)
- logger.logMediaRemoved(data.appUid, data.packageName, data.instanceId)
- }
- }
- }
-
- fun setMediaResumptionEnabled(isEnabled: Boolean) {
- if (useMediaResumption == isEnabled) {
- return
- }
-
- useMediaResumption = isEnabled
-
- if (!useMediaResumption) {
- // Remove any existing resume controls
- val filtered = mediaEntries.filter { !it.value.active }
- filtered.forEach {
- mediaEntries.remove(it.key)
- notifyMediaDataRemoved(it.key)
- logger.logMediaRemoved(it.value.appUid, it.value.packageName, it.value.instanceId)
- }
- }
- }
+ fun setMediaResumptionEnabled(isEnabled: Boolean)
/** Invoked when the user has dismissed the media carousel */
- fun onSwipeToDismiss() = mediaDataFilter.onSwipeToDismiss()
+ fun onSwipeToDismiss()
/** Are there any media notifications active, including the recommendations? */
- fun hasActiveMediaOrRecommendation() = mediaDataFilter.hasActiveMediaOrRecommendation()
+ fun hasActiveMediaOrRecommendation(): Boolean
- /**
- * Are there any media entries we should display, including the recommendations?
- * - If resumption is enabled, this will include inactive players
- * - If resumption is disabled, we only want to show active players
- */
- fun hasAnyMediaOrRecommendation() = mediaDataFilter.hasAnyMediaOrRecommendation()
+ /** Are there any media entries we should display, including the recommendations? */
+ fun hasAnyMediaOrRecommendation(): Boolean
/** Are there any resume media notifications active, excluding the recommendations? */
- fun hasActiveMedia() = mediaDataFilter.hasActiveMedia()
+ fun hasActiveMedia(): Boolean
- /**
- * Are there any resume media notifications active, excluding the recommendations?
- * - If resumption is enabled, this will include inactive players
- * - If resumption is disabled, we only want to show active players
- */
- fun hasAnyMedia() = mediaDataFilter.hasAnyMedia()
+ /** Are there any resume media notifications active, excluding the recommendations? */
+ fun hasAnyMedia(): Boolean
- interface Listener {
+ /** Is recommendation card active? */
+ fun isRecommendationActive(): Boolean
+
+ // Uses [MediaDataProcessor.Listener] in order to link the new logic code with UI layer.
+ interface Listener : MediaDataProcessor.Listener {
/**
* Called whenever there's new MediaData Loaded for the consumption in views.
@@ -1637,13 +113,13 @@
* @param isSsReactivated indicates resume media card is reactivated by Smartspace
* recommendation signal
*/
- fun onMediaDataLoaded(
+ override fun onMediaDataLoaded(
key: String,
oldKey: String?,
data: MediaData,
- immediately: Boolean = true,
- receivedSmartspaceCardLatency: Int = 0,
- isSsReactivated: Boolean = false
+ immediately: Boolean,
+ receivedSmartspaceCardLatency: Int,
+ isSsReactivated: Boolean,
) {}
/**
@@ -1653,14 +129,14 @@
* it will be prioritized as the first card. Otherwise, it will show up as the last card
* as default.
*/
- fun onSmartspaceMediaDataLoaded(
+ override fun onSmartspaceMediaDataLoaded(
key: String,
data: SmartspaceMediaData,
- shouldPrioritize: Boolean = false
+ shouldPrioritize: Boolean,
) {}
/** Called whenever a previously existing Media notification was removed. */
- fun onMediaDataRemoved(key: String) {}
+ override fun onMediaDataRemoved(key: String) {}
/**
* Called whenever a previously existing Smartspace media data was removed.
@@ -1669,78 +145,14 @@
* until the next refresh-round before UI becomes visible. True by default to take in
* place immediately.
*/
- fun onSmartspaceMediaDataRemoved(key: String, immediately: Boolean = true) {}
+ override fun onSmartspaceMediaDataRemoved(key: String, immediately: Boolean) {}
}
- /**
- * Converts the pass-in SmartspaceTarget to SmartspaceMediaData
- *
- * @return An empty SmartspaceMediaData with the valid target Id is returned if the
- * SmartspaceTarget's data is invalid.
- */
- private fun toSmartspaceMediaData(target: SmartspaceTarget): SmartspaceMediaData {
- val baseAction: SmartspaceAction? = target.baseAction
- val dismissIntent =
- baseAction?.extras?.getParcelable(EXTRAS_SMARTSPACE_DISMISS_INTENT_KEY) as Intent?
+ companion object {
- val isActive =
- when {
- !mediaFlags.isPersistentSsCardEnabled() -> true
- baseAction == null -> true
- else -> {
- val triggerSource = baseAction.extras?.getString(EXTRA_KEY_TRIGGER_SOURCE)
- triggerSource != EXTRA_VALUE_TRIGGER_PERIODIC
- }
- }
-
- packageName(target)?.let {
- return SmartspaceMediaData(
- targetId = target.smartspaceTargetId,
- isActive = isActive,
- packageName = it,
- cardAction = target.baseAction,
- recommendations = target.iconGrid,
- dismissIntent = dismissIntent,
- headphoneConnectionTimeMillis = target.creationTimeMillis,
- instanceId = logger.getNewInstanceId(),
- expiryTimeMs = target.expiryTimeMillis,
- )
- }
- return EMPTY_SMARTSPACE_MEDIA_DATA.copy(
- targetId = target.smartspaceTargetId,
- isActive = isActive,
- dismissIntent = dismissIntent,
- headphoneConnectionTimeMillis = target.creationTimeMillis,
- instanceId = logger.getNewInstanceId(),
- expiryTimeMs = target.expiryTimeMillis,
- )
- }
-
- private fun packageName(target: SmartspaceTarget): String? {
- val recommendationList = target.iconGrid
- if (recommendationList == null || recommendationList.isEmpty()) {
- Log.w(TAG, "Empty or null media recommendation list.")
- return null
- }
- for (recommendation in recommendationList) {
- val extras = recommendation.extras
- extras?.let {
- it.getString(EXTRAS_MEDIA_SOURCE_PACKAGE_NAME)?.let { packageName ->
- return packageName
- }
- }
- }
- Log.w(TAG, "No valid package name is provided.")
- return null
- }
-
- override fun dump(pw: PrintWriter, args: Array<out String>) {
- pw.apply {
- println("internalListeners: $internalListeners")
- println("externalListeners: ${mediaDataFilter.listeners}")
- println("mediaEntries: $mediaEntries")
- println("useMediaResumption: $useMediaResumption")
- println("allowMediaRecommendations: $allowMediaRecommendations")
+ @JvmStatic
+ fun isMediaNotification(sbn: StatusBarNotification): Boolean {
+ return sbn.notification.isMediaNotification()
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessor.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessor.kt
new file mode 100644
index 0000000..7412290
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessor.kt
@@ -0,0 +1,1654 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS 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.media.controls.domain.pipeline
+
+import android.annotation.SuppressLint
+import android.app.ActivityOptions
+import android.app.BroadcastOptions
+import android.app.Notification
+import android.app.Notification.EXTRA_SUBSTITUTE_APP_NAME
+import android.app.PendingIntent
+import android.app.StatusBarManager
+import android.app.UriGrantsManager
+import android.app.smartspace.SmartspaceAction
+import android.app.smartspace.SmartspaceConfig
+import android.app.smartspace.SmartspaceManager
+import android.app.smartspace.SmartspaceSession
+import android.app.smartspace.SmartspaceTarget
+import android.content.BroadcastReceiver
+import android.content.ContentProvider
+import android.content.ContentResolver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.content.pm.ApplicationInfo
+import android.content.pm.PackageManager
+import android.graphics.Bitmap
+import android.graphics.ImageDecoder
+import android.graphics.drawable.Animatable
+import android.graphics.drawable.Icon
+import android.media.MediaDescription
+import android.media.MediaMetadata
+import android.media.session.MediaController
+import android.media.session.MediaSession
+import android.media.session.PlaybackState
+import android.net.Uri
+import android.os.Handler
+import android.os.Parcelable
+import android.os.Process
+import android.os.UserHandle
+import android.provider.Settings
+import android.service.notification.StatusBarNotification
+import android.support.v4.media.MediaMetadataCompat
+import android.text.TextUtils
+import android.util.Log
+import android.util.Pair as APair
+import androidx.media.utils.MediaConstants
+import com.android.app.tracing.traceSection
+import com.android.internal.annotations.Keep
+import com.android.internal.logging.InstanceId
+import com.android.keyguard.KeyguardUpdateMonitor
+import com.android.systemui.CoreStartable
+import com.android.systemui.broadcast.BroadcastDispatcher
+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.dump.DumpManager
+import com.android.systemui.media.controls.data.repository.MediaDataRepository
+import com.android.systemui.media.controls.domain.pipeline.MediaDataManager.Companion.isMediaNotification
+import com.android.systemui.media.controls.domain.pipeline.interactor.MediaCarouselInteractor
+import com.android.systemui.media.controls.domain.resume.ResumeMediaBrowser
+import com.android.systemui.media.controls.shared.model.EXTRA_KEY_TRIGGER_SOURCE
+import com.android.systemui.media.controls.shared.model.EXTRA_VALUE_TRIGGER_PERIODIC
+import com.android.systemui.media.controls.shared.model.MediaAction
+import com.android.systemui.media.controls.shared.model.MediaButton
+import com.android.systemui.media.controls.shared.model.MediaData
+import com.android.systemui.media.controls.shared.model.MediaDeviceData
+import com.android.systemui.media.controls.shared.model.SmartspaceMediaData
+import com.android.systemui.media.controls.shared.model.SmartspaceMediaDataProvider
+import com.android.systemui.media.controls.ui.view.MediaViewHolder
+import com.android.systemui.media.controls.util.MediaControllerFactory
+import com.android.systemui.media.controls.util.MediaDataUtils
+import com.android.systemui.media.controls.util.MediaFlags
+import com.android.systemui.media.controls.util.MediaUiEventLogger
+import com.android.systemui.plugins.ActivityStarter
+import com.android.systemui.plugins.BcSmartspaceDataPlugin
+import com.android.systemui.res.R
+import com.android.systemui.statusbar.NotificationMediaManager.isConnectingState
+import com.android.systemui.statusbar.NotificationMediaManager.isPlayingState
+import com.android.systemui.statusbar.notification.row.HybridGroupManager
+import com.android.systemui.util.Assert
+import com.android.systemui.util.Utils
+import com.android.systemui.util.concurrency.DelayableExecutor
+import com.android.systemui.util.concurrency.ThreadFactory
+import com.android.systemui.util.settings.SecureSettings
+import com.android.systemui.util.settings.SettingsProxyExt.observerFlow
+import com.android.systemui.util.time.SystemClock
+import java.io.IOException
+import java.io.PrintWriter
+import java.util.concurrent.Executor
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onStart
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+
+// URI fields to try loading album art from
+private val ART_URIS =
+ arrayOf(
+ MediaMetadata.METADATA_KEY_ALBUM_ART_URI,
+ MediaMetadata.METADATA_KEY_ART_URI,
+ MediaMetadata.METADATA_KEY_DISPLAY_ICON_URI
+ )
+
+private const val TAG = "MediaDataProcessor"
+private const val DEBUG = true
+private const val EXTRAS_SMARTSPACE_DISMISS_INTENT_KEY = "dismiss_intent"
+
+/** Processes all media data fields and encapsulates logic for managing media data entries. */
+@SysUISingleton
+class MediaDataProcessor(
+ private val context: Context,
+ @Application private val applicationScope: CoroutineScope,
+ @Background private val backgroundDispatcher: CoroutineDispatcher,
+ @Background private val backgroundExecutor: Executor,
+ @Main private val uiExecutor: Executor,
+ @Main private val foregroundExecutor: DelayableExecutor,
+ @Main private val handler: Handler,
+ private val mediaControllerFactory: MediaControllerFactory,
+ private val broadcastDispatcher: BroadcastDispatcher,
+ private val dumpManager: DumpManager,
+ private val activityStarter: ActivityStarter,
+ private val smartspaceMediaDataProvider: SmartspaceMediaDataProvider,
+ private var useMediaResumption: Boolean,
+ private val useQsMediaPlayer: Boolean,
+ private val systemClock: SystemClock,
+ private val secureSettings: SecureSettings,
+ private val mediaFlags: MediaFlags,
+ private val logger: MediaUiEventLogger,
+ private val smartspaceManager: SmartspaceManager?,
+ private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
+ private val mediaDataRepository: MediaDataRepository,
+) : CoreStartable, BcSmartspaceDataPlugin.SmartspaceTargetListener {
+
+ companion object {
+ /**
+ * UI surface label for subscribing Smartspace updates. String must match with
+ * [BcSmartspaceDataPlugin.UI_SURFACE_MEDIA]
+ */
+ @JvmField val SMARTSPACE_UI_SURFACE_LABEL = "media_data_manager"
+
+ // Smartspace package name's extra key.
+ @JvmField val EXTRAS_MEDIA_SOURCE_PACKAGE_NAME = "package_name"
+
+ // Maximum number of actions allowed in compact view
+ @JvmField val MAX_COMPACT_ACTIONS = 3
+
+ /**
+ * Maximum number of actions allowed in expanded view. Number must match with the size of
+ * [MediaViewHolder.genericButtonIds]
+ */
+ @JvmField val MAX_NOTIFICATION_ACTIONS = 5
+ }
+
+ private val themeText =
+ com.android.settingslib.Utils.getColorAttr(
+ context,
+ com.android.internal.R.attr.textColorPrimary
+ )
+ .defaultColor
+
+ // Internal listeners are part of the internal pipeline. External listeners (those registered
+ // with [MediaDeviceManager.addListener]) receive events after they have propagated through
+ // the internal pipeline.
+ // Another way to think of the distinction between internal and external listeners is the
+ // following. Internal listeners are listeners that MediaDataProcessor depends on, and external
+ // listeners are listeners that depend on MediaDataProcessor.
+ private val internalListeners: MutableSet<Listener> = mutableSetOf()
+
+ // There should ONLY be at most one Smartspace media recommendation.
+ @Keep private var smartspaceSession: SmartspaceSession? = null
+ private var allowMediaRecommendations = false
+
+ private val artworkWidth =
+ context.resources.getDimensionPixelSize(
+ com.android.internal.R.dimen.config_mediaMetadataBitmapMaxSize
+ )
+ private val artworkHeight =
+ context.resources.getDimensionPixelSize(R.dimen.qs_media_session_height_expanded)
+
+ @SuppressLint("WrongConstant") // sysui allowed to call STATUS_BAR_SERVICE
+ private val statusBarManager =
+ context.getSystemService(Context.STATUS_BAR_SERVICE) as StatusBarManager
+
+ /** Check whether this notification is an RCN */
+ private fun isRemoteCastNotification(sbn: StatusBarNotification): Boolean {
+ return sbn.notification.extras.containsKey(Notification.EXTRA_MEDIA_REMOTE_DEVICE)
+ }
+
+ @Inject
+ constructor(
+ context: Context,
+ @Application applicationScope: CoroutineScope,
+ @Background backgroundDispatcher: CoroutineDispatcher,
+ threadFactory: ThreadFactory,
+ @Main uiExecutor: Executor,
+ @Main foregroundExecutor: DelayableExecutor,
+ @Main handler: Handler,
+ mediaControllerFactory: MediaControllerFactory,
+ dumpManager: DumpManager,
+ broadcastDispatcher: BroadcastDispatcher,
+ activityStarter: ActivityStarter,
+ smartspaceMediaDataProvider: SmartspaceMediaDataProvider,
+ clock: SystemClock,
+ secureSettings: SecureSettings,
+ mediaFlags: MediaFlags,
+ logger: MediaUiEventLogger,
+ smartspaceManager: SmartspaceManager?,
+ keyguardUpdateMonitor: KeyguardUpdateMonitor,
+ mediaDataRepository: MediaDataRepository,
+ ) : this(
+ context,
+ applicationScope,
+ backgroundDispatcher,
+ // Loading bitmap for UMO background can take longer time, so it cannot run on the default
+ // background thread. Use a custom thread for media.
+ threadFactory.buildExecutorOnNewThread(TAG),
+ uiExecutor,
+ foregroundExecutor,
+ handler,
+ mediaControllerFactory,
+ broadcastDispatcher,
+ dumpManager,
+ activityStarter,
+ smartspaceMediaDataProvider,
+ Utils.useMediaResumption(context),
+ Utils.useQsMediaPlayer(context),
+ clock,
+ secureSettings,
+ mediaFlags,
+ logger,
+ smartspaceManager,
+ keyguardUpdateMonitor,
+ mediaDataRepository,
+ )
+
+ private val appChangeReceiver =
+ object : BroadcastReceiver() {
+ override fun onReceive(context: Context, intent: Intent) {
+ when (intent.action) {
+ Intent.ACTION_PACKAGES_SUSPENDED -> {
+ val packages = intent.getStringArrayExtra(Intent.EXTRA_CHANGED_PACKAGE_LIST)
+ packages?.forEach { removeAllForPackage(it) }
+ }
+ Intent.ACTION_PACKAGE_REMOVED,
+ Intent.ACTION_PACKAGE_RESTARTED -> {
+ intent.data?.encodedSchemeSpecificPart?.let { removeAllForPackage(it) }
+ }
+ }
+ }
+ }
+
+ override fun start() {
+ if (!mediaFlags.isMediaControlsRefactorEnabled()) {
+ return
+ }
+
+ dumpManager.registerNormalDumpable(TAG, this)
+
+ val suspendFilter = IntentFilter(Intent.ACTION_PACKAGES_SUSPENDED)
+ broadcastDispatcher.registerReceiver(appChangeReceiver, suspendFilter, null, UserHandle.ALL)
+
+ val uninstallFilter =
+ IntentFilter().apply {
+ addAction(Intent.ACTION_PACKAGE_REMOVED)
+ addAction(Intent.ACTION_PACKAGE_RESTARTED)
+ addDataScheme("package")
+ }
+ // BroadcastDispatcher does not allow filters with data schemes
+ context.registerReceiver(appChangeReceiver, uninstallFilter)
+
+ // Register for Smartspace data updates.
+ smartspaceMediaDataProvider.registerListener(this)
+ smartspaceSession =
+ smartspaceManager?.createSmartspaceSession(
+ SmartspaceConfig.Builder(context, SMARTSPACE_UI_SURFACE_LABEL).build()
+ )
+ smartspaceSession?.let {
+ it.addOnTargetsAvailableListener(
+ // Use a main uiExecutor thread listening to Smartspace updates instead of using
+ // the existing background executor.
+ // SmartspaceSession has scheduled routine updates which can be unpredictable on
+ // test simulators, using the backgroundExecutor makes it's hard to test the threads
+ // numbers.
+ uiExecutor
+ ) { targets ->
+ smartspaceMediaDataProvider.onTargetsAvailable(targets)
+ }
+ }
+ smartspaceSession?.requestSmartspaceUpdate()
+
+ // Track media controls recommendation setting.
+ applicationScope.launch { trackMediaControlsRecommendationSetting() }
+ }
+
+ fun destroy() {
+ smartspaceMediaDataProvider.unregisterListener(this)
+ smartspaceSession?.close()
+ smartspaceSession = null
+ context.unregisterReceiver(appChangeReceiver)
+ internalListeners.clear()
+ }
+
+ fun onNotificationAdded(key: String, sbn: StatusBarNotification) {
+ if (useQsMediaPlayer && isMediaNotification(sbn)) {
+ var isNewlyActiveEntry = false
+ Assert.isMainThread()
+ val oldKey = findExistingEntry(key, sbn.packageName)
+ if (oldKey == null) {
+ val instanceId = logger.getNewInstanceId()
+ val temp =
+ MediaData()
+ .copy(
+ packageName = sbn.packageName,
+ instanceId = instanceId,
+ createdTimestampMillis = systemClock.currentTimeMillis(),
+ )
+ mediaDataRepository.addMediaEntry(key, temp)
+ isNewlyActiveEntry = true
+ } else if (oldKey != key) {
+ // Resume -> active conversion; move to new key
+ val oldData = mediaDataRepository.removeMediaEntry(oldKey)!!
+ isNewlyActiveEntry = true
+ mediaDataRepository.addMediaEntry(key, oldData)
+ }
+ loadMediaData(key, sbn, oldKey, isNewlyActiveEntry)
+ } else {
+ onNotificationRemoved(key)
+ }
+ }
+
+ /**
+ * Allow recommendations from smartspace to show in media controls. Requires
+ * [Utils.useQsMediaPlayer] to be enabled. On by default, but can be disabled by setting to 0
+ */
+ private suspend fun allowMediaRecommendations(): Boolean {
+ return withContext(backgroundDispatcher) {
+ val flag =
+ secureSettings.getBoolForUser(
+ Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION,
+ true,
+ UserHandle.USER_CURRENT
+ )
+
+ useQsMediaPlayer && flag
+ }
+ }
+
+ private suspend fun trackMediaControlsRecommendationSetting() {
+ secureSettings
+ .observerFlow(UserHandle.USER_ALL, Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION)
+ // perform a query at the beginning.
+ .onStart { emit(Unit) }
+ .map { allowMediaRecommendations() }
+ .distinctUntilChanged()
+ // only track the most recent emission
+ .collectLatest {
+ allowMediaRecommendations = it
+ if (!allowMediaRecommendations) {
+ dismissSmartspaceRecommendation(
+ key = mediaDataRepository.smartspaceMediaData.value.targetId,
+ delay = 0L
+ )
+ }
+ }
+ }
+
+ private fun removeAllForPackage(packageName: String) {
+ Assert.isMainThread()
+ val toRemove =
+ mediaDataRepository.mediaEntries.value.filter { it.value.packageName == packageName }
+ toRemove.forEach { removeEntry(it.key) }
+ }
+
+ fun setResumeAction(key: String, action: Runnable?) {
+ mediaDataRepository.mediaEntries.value.get(key)?.let {
+ it.resumeAction = action
+ it.hasCheckedForResume = true
+ }
+ }
+
+ fun addResumptionControls(
+ userId: Int,
+ desc: MediaDescription,
+ action: Runnable,
+ token: MediaSession.Token,
+ appName: String,
+ appIntent: PendingIntent,
+ packageName: String
+ ) {
+ // Resume controls don't have a notification key, so store by package name instead
+ if (!mediaDataRepository.mediaEntries.value.containsKey(packageName)) {
+ val instanceId = logger.getNewInstanceId()
+ val appUid =
+ try {
+ context.packageManager.getApplicationInfo(packageName, 0).uid
+ } catch (e: PackageManager.NameNotFoundException) {
+ Log.w(TAG, "Could not get app UID for $packageName", e)
+ Process.INVALID_UID
+ }
+
+ val resumeData =
+ MediaData()
+ .copy(
+ packageName = packageName,
+ resumeAction = action,
+ hasCheckedForResume = true,
+ instanceId = instanceId,
+ appUid = appUid,
+ createdTimestampMillis = systemClock.currentTimeMillis(),
+ )
+ mediaDataRepository.addMediaEntry(packageName, resumeData)
+ logSingleVsMultipleMediaAdded(appUid, packageName, instanceId)
+ logger.logResumeMediaAdded(appUid, packageName, instanceId)
+ }
+ backgroundExecutor.execute {
+ loadMediaDataInBgForResumption(
+ userId,
+ desc,
+ action,
+ token,
+ appName,
+ appIntent,
+ packageName
+ )
+ }
+ }
+
+ /**
+ * Check if there is an existing entry that matches the key or package name. Returns the key
+ * that matches, or null if not found.
+ */
+ private fun findExistingEntry(key: String, packageName: String): String? {
+ val mediaEntries = mediaDataRepository.mediaEntries.value
+ if (mediaEntries.containsKey(key)) {
+ return key
+ }
+ // Check if we already had a resume player
+ if (mediaEntries.containsKey(packageName)) {
+ return packageName
+ }
+ return null
+ }
+
+ private fun loadMediaData(
+ key: String,
+ sbn: StatusBarNotification,
+ oldKey: String?,
+ isNewlyActiveEntry: Boolean = false,
+ ) {
+ backgroundExecutor.execute { loadMediaDataInBg(key, sbn, oldKey, isNewlyActiveEntry) }
+ }
+
+ /** Add a listener for internal events. */
+ fun addInternalListener(listener: Listener) = internalListeners.add(listener)
+
+ /**
+ * Notify internal listeners of media loaded event.
+ *
+ * External listeners registered with [MediaCarouselInteractor.addListener] will be notified
+ * after the event propagates through the internal listener pipeline.
+ */
+ private fun notifyMediaDataLoaded(key: String, oldKey: String?, info: MediaData) {
+ internalListeners.forEach { it.onMediaDataLoaded(key, oldKey, info) }
+ }
+
+ /**
+ * Notify internal listeners of Smartspace media loaded event.
+ *
+ * External listeners registered with [MediaCarouselInteractor.addListener] will be notified
+ * after the event propagates through the internal listener pipeline.
+ */
+ private fun notifySmartspaceMediaDataLoaded(key: String, info: SmartspaceMediaData) {
+ internalListeners.forEach { it.onSmartspaceMediaDataLoaded(key, info) }
+ }
+
+ /**
+ * Notify internal listeners of media removed event.
+ *
+ * External listeners registered with [MediaCarouselInteractor.addListener] will be notified
+ * after the event propagates through the internal listener pipeline.
+ */
+ private fun notifyMediaDataRemoved(key: String) {
+ internalListeners.forEach { it.onMediaDataRemoved(key) }
+ }
+
+ /**
+ * Notify internal listeners of Smartspace media removed event.
+ *
+ * External listeners registered with [MediaCarouselInteractor.addListener] will be notified
+ * after the event propagates through the internal listener pipeline.
+ *
+ * @param immediately indicates should apply the UI changes immediately, otherwise wait until
+ * the next refresh-round before UI becomes visible. Should only be true if the update is
+ * initiated by user's interaction.
+ */
+ private fun notifySmartspaceMediaDataRemoved(key: String, immediately: Boolean) {
+ internalListeners.forEach { it.onSmartspaceMediaDataRemoved(key, immediately) }
+ }
+
+ /**
+ * Called whenever the player has been paused or stopped for a while, or swiped from QQS. This
+ * will make the player not active anymore, hiding it from QQS and Keyguard.
+ *
+ * @see MediaData.active
+ */
+ fun setInactive(key: String, timedOut: Boolean, forceUpdate: Boolean = false) {
+ mediaDataRepository.mediaEntries.value[key]?.let {
+ if (timedOut && !forceUpdate) {
+ // Only log this event when media expires on its own
+ logger.logMediaTimeout(it.appUid, it.packageName, it.instanceId)
+ }
+ if (it.active == !timedOut && !forceUpdate) {
+ if (it.resumption) {
+ if (DEBUG) Log.d(TAG, "timing out resume player $key")
+ dismissMediaData(key, 0L /* delay */)
+ }
+ return
+ }
+ // Update last active if media was still active.
+ if (it.active) {
+ it.lastActive = systemClock.elapsedRealtime()
+ }
+ it.active = !timedOut
+ if (DEBUG) Log.d(TAG, "Updating $key timedOut: $timedOut")
+ onMediaDataLoaded(key, key, it)
+ }
+
+ if (key == mediaDataRepository.smartspaceMediaData.value.targetId) {
+ if (DEBUG) Log.d(TAG, "smartspace card expired")
+ dismissSmartspaceRecommendation(key, delay = 0L)
+ }
+ }
+
+ /** Called when the player's [PlaybackState] has been updated with new actions and/or state */
+ internal fun updateState(key: String, state: PlaybackState) {
+ mediaDataRepository.mediaEntries.value.get(key)?.let {
+ val token = it.token
+ if (token == null) {
+ if (DEBUG) Log.d(TAG, "State updated, but token was null")
+ return
+ }
+ val actions =
+ createActionsFromState(
+ it.packageName,
+ mediaControllerFactory.create(it.token),
+ UserHandle(it.userId)
+ )
+
+ // Control buttons
+ // If flag is enabled and controller has a PlaybackState,
+ // create actions from session info
+ // otherwise, no need to update semantic actions.
+ val data =
+ if (actions != null) {
+ it.copy(semanticActions = actions, isPlaying = isPlayingState(state.state))
+ } else {
+ it.copy(isPlaying = isPlayingState(state.state))
+ }
+ if (DEBUG) Log.d(TAG, "State updated outside of notification")
+ onMediaDataLoaded(key, key, data)
+ }
+ }
+
+ private fun removeEntry(key: String, logEvent: Boolean = true) {
+ mediaDataRepository.removeMediaEntry(key)?.let {
+ if (logEvent) {
+ logger.logMediaRemoved(it.appUid, it.packageName, it.instanceId)
+ }
+ }
+ notifyMediaDataRemoved(key)
+ }
+
+ /** Dismiss a media entry. Returns false if the key was not found. */
+ fun dismissMediaData(key: String, delay: Long): Boolean {
+ val existed = mediaDataRepository.mediaEntries.value[key] != null
+ backgroundExecutor.execute {
+ mediaDataRepository.mediaEntries.value[key]?.let { mediaData ->
+ if (mediaData.isLocalSession()) {
+ mediaData.token?.let {
+ val mediaController = mediaControllerFactory.create(it)
+ mediaController.transportControls.stop()
+ }
+ }
+ }
+ }
+ foregroundExecutor.executeDelayed({ removeEntry(key) }, delay)
+ return existed
+ }
+
+ /**
+ * Called whenever the recommendation has been expired or removed by the user. This will remove
+ * the recommendation card entirely from the carousel.
+ */
+ fun dismissSmartspaceRecommendation(key: String, delay: Long) {
+ if (mediaDataRepository.dismissSmartspaceRecommendation(key)) {
+ foregroundExecutor.executeDelayed(
+ { notifySmartspaceMediaDataRemoved(key, immediately = true) },
+ delay
+ )
+ }
+ }
+
+ /** Called when the recommendation card should no longer be visible in QQS or lockscreen */
+ fun setRecommendationInactive(key: String) {
+ if (mediaDataRepository.setRecommendationInactive(key)) {
+ val recommendation = mediaDataRepository.smartspaceMediaData.value
+ notifySmartspaceMediaDataLoaded(recommendation.targetId, recommendation)
+ }
+ }
+
+ private fun loadMediaDataInBgForResumption(
+ userId: Int,
+ desc: MediaDescription,
+ resumeAction: Runnable,
+ token: MediaSession.Token,
+ appName: String,
+ appIntent: PendingIntent,
+ packageName: String
+ ) {
+ if (desc.title.isNullOrBlank()) {
+ Log.e(TAG, "Description incomplete")
+ // Delete the placeholder entry
+ mediaDataRepository.removeMediaEntry(packageName)
+ return
+ }
+
+ if (DEBUG) {
+ Log.d(TAG, "adding track for $userId from browser: $desc")
+ }
+
+ val currentEntry = mediaDataRepository.mediaEntries.value.get(packageName)
+ val appUid = currentEntry?.appUid ?: Process.INVALID_UID
+
+ // Album art
+ var artworkBitmap = desc.iconBitmap
+ if (artworkBitmap == null && desc.iconUri != null) {
+ artworkBitmap = loadBitmapFromUriForUser(desc.iconUri!!, userId, appUid, packageName)
+ }
+ val artworkIcon =
+ if (artworkBitmap != null) {
+ Icon.createWithBitmap(artworkBitmap)
+ } else {
+ null
+ }
+
+ val instanceId = currentEntry?.instanceId ?: logger.getNewInstanceId()
+ val isExplicit =
+ desc.extras?.getLong(MediaConstants.METADATA_KEY_IS_EXPLICIT) ==
+ MediaConstants.METADATA_VALUE_ATTRIBUTE_PRESENT
+
+ val progress =
+ if (mediaFlags.isResumeProgressEnabled()) {
+ MediaDataUtils.getDescriptionProgress(desc.extras)
+ } else null
+
+ val mediaAction = getResumeMediaAction(resumeAction)
+ val lastActive = systemClock.elapsedRealtime()
+ val createdTimestampMillis = currentEntry?.createdTimestampMillis ?: 0L
+ foregroundExecutor.execute {
+ onMediaDataLoaded(
+ packageName,
+ null,
+ MediaData(
+ userId,
+ true,
+ appName,
+ null,
+ desc.subtitle,
+ desc.title,
+ artworkIcon,
+ listOf(mediaAction),
+ listOf(0),
+ MediaButton(playOrPause = mediaAction),
+ packageName,
+ token,
+ appIntent,
+ device = null,
+ active = false,
+ resumeAction = resumeAction,
+ resumption = true,
+ notificationKey = packageName,
+ hasCheckedForResume = true,
+ lastActive = lastActive,
+ createdTimestampMillis = createdTimestampMillis,
+ instanceId = instanceId,
+ appUid = appUid,
+ isExplicit = isExplicit,
+ resumeProgress = progress,
+ )
+ )
+ }
+ }
+
+ fun loadMediaDataInBg(
+ key: String,
+ sbn: StatusBarNotification,
+ oldKey: String?,
+ isNewlyActiveEntry: Boolean = false,
+ ) {
+ val token =
+ sbn.notification.extras.getParcelable(
+ Notification.EXTRA_MEDIA_SESSION,
+ MediaSession.Token::class.java
+ )
+ if (token == null) {
+ return
+ }
+ val mediaController = mediaControllerFactory.create(token)
+ val metadata = mediaController.metadata
+ val notif: Notification = sbn.notification
+
+ val appInfo =
+ notif.extras.getParcelable(
+ Notification.EXTRA_BUILDER_APPLICATION_INFO,
+ ApplicationInfo::class.java
+ )
+ ?: getAppInfoFromPackage(sbn.packageName)
+
+ // App name
+ val appName = getAppName(sbn, appInfo)
+
+ // Song name
+ var song: CharSequence? = metadata?.getString(MediaMetadata.METADATA_KEY_DISPLAY_TITLE)
+ if (song.isNullOrBlank()) {
+ song = metadata?.getString(MediaMetadata.METADATA_KEY_TITLE)
+ }
+ if (song.isNullOrBlank()) {
+ song = HybridGroupManager.resolveTitle(notif)
+ }
+ if (song.isNullOrBlank()) {
+ // For apps that don't include a title, log and add a placeholder
+ song = context.getString(R.string.controls_media_empty_title, appName)
+ try {
+ statusBarManager.logBlankMediaTitle(sbn.packageName, sbn.user.identifier)
+ } catch (e: RuntimeException) {
+ Log.e(TAG, "Error reporting blank media title for package ${sbn.packageName}")
+ }
+ }
+
+ // Album art
+ var artworkBitmap = metadata?.let { loadBitmapFromUri(it) }
+ if (artworkBitmap == null) {
+ artworkBitmap = metadata?.getBitmap(MediaMetadata.METADATA_KEY_ART)
+ }
+ if (artworkBitmap == null) {
+ artworkBitmap = metadata?.getBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART)
+ }
+ val artWorkIcon =
+ if (artworkBitmap == null) {
+ notif.getLargeIcon()
+ } else {
+ Icon.createWithBitmap(artworkBitmap)
+ }
+
+ // App Icon
+ val smallIcon = sbn.notification.smallIcon
+
+ // Explicit Indicator
+ val isExplicit: Boolean
+ val mediaMetadataCompat = MediaMetadataCompat.fromMediaMetadata(metadata)
+ isExplicit =
+ mediaMetadataCompat?.getLong(MediaConstants.METADATA_KEY_IS_EXPLICIT) ==
+ MediaConstants.METADATA_VALUE_ATTRIBUTE_PRESENT
+
+ // Artist name
+ var artist: CharSequence? = metadata?.getString(MediaMetadata.METADATA_KEY_ARTIST)
+ if (artist.isNullOrBlank()) {
+ artist = HybridGroupManager.resolveText(notif)
+ }
+
+ // Device name (used for remote cast notifications)
+ var device: MediaDeviceData? = null
+ if (isRemoteCastNotification(sbn)) {
+ val extras = sbn.notification.extras
+ val deviceName = extras.getCharSequence(Notification.EXTRA_MEDIA_REMOTE_DEVICE, null)
+ val deviceIcon = extras.getInt(Notification.EXTRA_MEDIA_REMOTE_ICON, -1)
+ val deviceIntent =
+ extras.getParcelable(
+ Notification.EXTRA_MEDIA_REMOTE_INTENT,
+ PendingIntent::class.java
+ )
+ Log.d(TAG, "$key is RCN for $deviceName")
+
+ if (deviceName != null && deviceIcon > -1) {
+ // Name and icon must be present, but intent may be null
+ val enabled = deviceIntent != null && deviceIntent.isActivity
+ val deviceDrawable =
+ Icon.createWithResource(sbn.packageName, deviceIcon)
+ .loadDrawable(sbn.getPackageContext(context))
+ device =
+ MediaDeviceData(
+ enabled,
+ deviceDrawable,
+ deviceName,
+ deviceIntent,
+ showBroadcastButton = false
+ )
+ }
+ }
+
+ // Control buttons
+ // If flag is enabled and controller has a PlaybackState, create actions from session info
+ // Otherwise, use the notification actions
+ var actionIcons: List<MediaAction> = emptyList()
+ var actionsToShowCollapsed: List<Int> = emptyList()
+ val semanticActions = createActionsFromState(sbn.packageName, mediaController, sbn.user)
+ if (semanticActions == null) {
+ val actions = createActionsFromNotification(sbn)
+ actionIcons = actions.first
+ actionsToShowCollapsed = actions.second
+ }
+
+ val playbackLocation =
+ if (isRemoteCastNotification(sbn)) MediaData.PLAYBACK_CAST_REMOTE
+ else if (
+ mediaController.playbackInfo?.playbackType ==
+ MediaController.PlaybackInfo.PLAYBACK_TYPE_LOCAL
+ )
+ MediaData.PLAYBACK_LOCAL
+ else MediaData.PLAYBACK_CAST_LOCAL
+ val isPlaying = mediaController.playbackState?.let { isPlayingState(it.state) }
+
+ val currentEntry = mediaDataRepository.mediaEntries.value.get(key)
+ val instanceId = currentEntry?.instanceId ?: logger.getNewInstanceId()
+ val appUid = appInfo?.uid ?: Process.INVALID_UID
+
+ if (isNewlyActiveEntry) {
+ logSingleVsMultipleMediaAdded(appUid, sbn.packageName, instanceId)
+ logger.logActiveMediaAdded(appUid, sbn.packageName, instanceId, playbackLocation)
+ } else if (playbackLocation != currentEntry?.playbackLocation) {
+ logger.logPlaybackLocationChange(appUid, sbn.packageName, instanceId, playbackLocation)
+ }
+
+ val lastActive = systemClock.elapsedRealtime()
+ val createdTimestampMillis = currentEntry?.createdTimestampMillis ?: 0L
+ foregroundExecutor.execute {
+ val resumeAction: Runnable? = mediaDataRepository.mediaEntries.value[key]?.resumeAction
+ val hasCheckedForResume =
+ mediaDataRepository.mediaEntries.value[key]?.hasCheckedForResume == true
+ val active = mediaDataRepository.mediaEntries.value[key]?.active ?: true
+ onMediaDataLoaded(
+ key,
+ oldKey,
+ MediaData(
+ sbn.normalizedUserId,
+ true,
+ appName,
+ smallIcon,
+ artist,
+ song,
+ artWorkIcon,
+ actionIcons,
+ actionsToShowCollapsed,
+ semanticActions,
+ sbn.packageName,
+ token,
+ notif.contentIntent,
+ device,
+ active,
+ resumeAction = resumeAction,
+ playbackLocation = playbackLocation,
+ notificationKey = key,
+ hasCheckedForResume = hasCheckedForResume,
+ isPlaying = isPlaying,
+ isClearable = !sbn.isOngoing,
+ lastActive = lastActive,
+ createdTimestampMillis = createdTimestampMillis,
+ instanceId = instanceId,
+ appUid = appUid,
+ isExplicit = isExplicit,
+ )
+ )
+ }
+ }
+
+ private fun logSingleVsMultipleMediaAdded(
+ appUid: Int,
+ packageName: String,
+ instanceId: InstanceId
+ ) {
+ if (mediaDataRepository.mediaEntries.value.size == 1) {
+ logger.logSingleMediaPlayerInCarousel(appUid, packageName, instanceId)
+ } else if (mediaDataRepository.mediaEntries.value.size == 2) {
+ // Since this method is only called when there is a new media session added.
+ // logging needed once there is more than one media session in carousel.
+ logger.logMultipleMediaPlayersInCarousel(appUid, packageName, instanceId)
+ }
+ }
+
+ private fun getAppInfoFromPackage(packageName: String): ApplicationInfo? {
+ try {
+ return context.packageManager.getApplicationInfo(packageName, 0)
+ } catch (e: PackageManager.NameNotFoundException) {
+ Log.w(TAG, "Could not get app info for $packageName", e)
+ }
+ return null
+ }
+
+ private fun getAppName(sbn: StatusBarNotification, appInfo: ApplicationInfo?): String {
+ val name = sbn.notification.extras.getString(EXTRA_SUBSTITUTE_APP_NAME)
+ if (name != null) {
+ return name
+ }
+
+ return if (appInfo != null) {
+ context.packageManager.getApplicationLabel(appInfo).toString()
+ } else {
+ sbn.packageName
+ }
+ }
+
+ /** Generate action buttons based on notification actions */
+ private fun createActionsFromNotification(
+ sbn: StatusBarNotification
+ ): Pair<List<MediaAction>, List<Int>> {
+ val notif = sbn.notification
+ val actionIcons: MutableList<MediaAction> = ArrayList()
+ val actions = notif.actions
+ var actionsToShowCollapsed =
+ notif.extras.getIntArray(Notification.EXTRA_COMPACT_ACTIONS)?.toMutableList()
+ ?: mutableListOf()
+ if (actionsToShowCollapsed.size > MAX_COMPACT_ACTIONS) {
+ Log.e(
+ TAG,
+ "Too many compact actions for ${sbn.key}," +
+ "limiting to first $MAX_COMPACT_ACTIONS"
+ )
+ actionsToShowCollapsed = actionsToShowCollapsed.subList(0, MAX_COMPACT_ACTIONS)
+ }
+
+ if (actions != null) {
+ for ((index, action) in actions.withIndex()) {
+ if (index == MAX_NOTIFICATION_ACTIONS) {
+ Log.w(
+ TAG,
+ "Too many notification actions for ${sbn.key}," +
+ " limiting to first $MAX_NOTIFICATION_ACTIONS"
+ )
+ break
+ }
+ if (action.getIcon() == null) {
+ if (DEBUG) Log.i(TAG, "No icon for action $index ${action.title}")
+ actionsToShowCollapsed.remove(index)
+ continue
+ }
+ val runnable =
+ if (action.actionIntent != null) {
+ Runnable {
+ if (action.actionIntent.isActivity) {
+ activityStarter.startPendingIntentDismissingKeyguard(
+ action.actionIntent
+ )
+ } else if (action.isAuthenticationRequired()) {
+ activityStarter.dismissKeyguardThenExecute(
+ {
+ var result = sendPendingIntent(action.actionIntent)
+ result
+ },
+ {},
+ true
+ )
+ } else {
+ sendPendingIntent(action.actionIntent)
+ }
+ }
+ } else {
+ null
+ }
+ val mediaActionIcon =
+ if (action.getIcon()?.getType() == Icon.TYPE_RESOURCE) {
+ Icon.createWithResource(sbn.packageName, action.getIcon()!!.getResId())
+ } else {
+ action.getIcon()
+ }
+ .setTint(themeText)
+ .loadDrawable(context)
+ val mediaAction = MediaAction(mediaActionIcon, runnable, action.title, null)
+ actionIcons.add(mediaAction)
+ }
+ }
+ return Pair(actionIcons, actionsToShowCollapsed)
+ }
+
+ /**
+ * Generates action button info for this media session based on the PlaybackState
+ *
+ * @param packageName Package name for the media app
+ * @param controller MediaController for the current session
+ * @return a Pair consisting of a list of media actions, and a list of ints representing which
+ *
+ * ```
+ * of those actions should be shown in the compact player
+ * ```
+ */
+ private fun createActionsFromState(
+ packageName: String,
+ controller: MediaController,
+ user: UserHandle
+ ): MediaButton? {
+ val state = controller.playbackState
+ if (state == null || !mediaFlags.areMediaSessionActionsEnabled(packageName, user)) {
+ return null
+ }
+
+ // First, check for standard actions
+ val playOrPause =
+ if (isConnectingState(state.state)) {
+ // Spinner needs to be animating to render anything. Start it here.
+ val drawable =
+ context.getDrawable(com.android.internal.R.drawable.progress_small_material)
+ (drawable as Animatable).start()
+ MediaAction(
+ drawable,
+ null, // no action to perform when clicked
+ context.getString(R.string.controls_media_button_connecting),
+ context.getDrawable(R.drawable.ic_media_connecting_container),
+ // Specify a rebind id to prevent the spinner from restarting on later binds.
+ com.android.internal.R.drawable.progress_small_material
+ )
+ } else if (isPlayingState(state.state)) {
+ getStandardAction(controller, state.actions, PlaybackState.ACTION_PAUSE)
+ } else {
+ getStandardAction(controller, state.actions, PlaybackState.ACTION_PLAY)
+ }
+ val prevButton =
+ getStandardAction(controller, state.actions, PlaybackState.ACTION_SKIP_TO_PREVIOUS)
+ val nextButton =
+ getStandardAction(controller, state.actions, PlaybackState.ACTION_SKIP_TO_NEXT)
+
+ // Then, create a way to build any custom actions that will be needed
+ val customActions =
+ state.customActions
+ .asSequence()
+ .filterNotNull()
+ .map { getCustomAction(packageName, controller, it) }
+ .iterator()
+ fun nextCustomAction() = if (customActions.hasNext()) customActions.next() else null
+
+ // Finally, assign the remaining button slots: play/pause A B C D
+ // A = previous, else custom action (if not reserved)
+ // B = next, else custom action (if not reserved)
+ // C and D are always custom actions
+ val reservePrev =
+ controller.extras?.getBoolean(
+ MediaConstants.SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_PREV
+ ) == true
+ val reserveNext =
+ controller.extras?.getBoolean(
+ MediaConstants.SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_NEXT
+ ) == true
+
+ val prevOrCustom =
+ if (prevButton != null) {
+ prevButton
+ } else if (!reservePrev) {
+ nextCustomAction()
+ } else {
+ null
+ }
+
+ val nextOrCustom =
+ if (nextButton != null) {
+ nextButton
+ } else if (!reserveNext) {
+ nextCustomAction()
+ } else {
+ null
+ }
+
+ return MediaButton(
+ playOrPause,
+ nextOrCustom,
+ prevOrCustom,
+ nextCustomAction(),
+ nextCustomAction(),
+ reserveNext,
+ reservePrev
+ )
+ }
+
+ /**
+ * Create a [MediaAction] for a given action and media session
+ *
+ * @param controller MediaController for the session
+ * @param stateActions The actions included with the session's [PlaybackState]
+ * @param action A [PlaybackState.Actions] value representing what action to generate. One of:
+ * ```
+ * [PlaybackState.ACTION_PLAY]
+ * [PlaybackState.ACTION_PAUSE]
+ * [PlaybackState.ACTION_SKIP_TO_PREVIOUS]
+ * [PlaybackState.ACTION_SKIP_TO_NEXT]
+ * @return
+ * ```
+ *
+ * A [MediaAction] with correct values set, or null if the state doesn't support it
+ */
+ private fun getStandardAction(
+ controller: MediaController,
+ stateActions: Long,
+ @PlaybackState.Actions action: Long
+ ): MediaAction? {
+ if (!includesAction(stateActions, action)) {
+ return null
+ }
+
+ return when (action) {
+ PlaybackState.ACTION_PLAY -> {
+ MediaAction(
+ context.getDrawable(R.drawable.ic_media_play),
+ { controller.transportControls.play() },
+ context.getString(R.string.controls_media_button_play),
+ context.getDrawable(R.drawable.ic_media_play_container)
+ )
+ }
+ PlaybackState.ACTION_PAUSE -> {
+ MediaAction(
+ context.getDrawable(R.drawable.ic_media_pause),
+ { controller.transportControls.pause() },
+ context.getString(R.string.controls_media_button_pause),
+ context.getDrawable(R.drawable.ic_media_pause_container)
+ )
+ }
+ PlaybackState.ACTION_SKIP_TO_PREVIOUS -> {
+ MediaAction(
+ context.getDrawable(R.drawable.ic_media_prev),
+ { controller.transportControls.skipToPrevious() },
+ context.getString(R.string.controls_media_button_prev),
+ null
+ )
+ }
+ PlaybackState.ACTION_SKIP_TO_NEXT -> {
+ MediaAction(
+ context.getDrawable(R.drawable.ic_media_next),
+ { controller.transportControls.skipToNext() },
+ context.getString(R.string.controls_media_button_next),
+ null
+ )
+ }
+ else -> null
+ }
+ }
+
+ /** Check whether the actions from a [PlaybackState] include a specific action */
+ private fun includesAction(stateActions: Long, @PlaybackState.Actions action: Long): Boolean {
+ if (
+ (action == PlaybackState.ACTION_PLAY || action == PlaybackState.ACTION_PAUSE) &&
+ (stateActions and PlaybackState.ACTION_PLAY_PAUSE > 0L)
+ ) {
+ return true
+ }
+ return (stateActions and action != 0L)
+ }
+
+ /** Get a [MediaAction] representing a [PlaybackState.CustomAction] */
+ private fun getCustomAction(
+ packageName: String,
+ controller: MediaController,
+ customAction: PlaybackState.CustomAction
+ ): MediaAction {
+ return MediaAction(
+ Icon.createWithResource(packageName, customAction.icon).loadDrawable(context),
+ { controller.transportControls.sendCustomAction(customAction, customAction.extras) },
+ customAction.name,
+ null
+ )
+ }
+
+ /** Load a bitmap from the various Art metadata URIs */
+ private fun loadBitmapFromUri(metadata: MediaMetadata): Bitmap? {
+ for (uri in ART_URIS) {
+ val uriString = metadata.getString(uri)
+ if (!TextUtils.isEmpty(uriString)) {
+ val albumArt = loadBitmapFromUri(Uri.parse(uriString))
+ if (albumArt != null) {
+ if (DEBUG) Log.d(TAG, "loaded art from $uri")
+ return albumArt
+ }
+ }
+ }
+ return null
+ }
+
+ private fun sendPendingIntent(intent: PendingIntent): Boolean {
+ return try {
+ val options = BroadcastOptions.makeBasic()
+ options.setInteractive(true)
+ options.setPendingIntentBackgroundActivityStartMode(
+ ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED
+ )
+ intent.send(options.toBundle())
+ true
+ } catch (e: PendingIntent.CanceledException) {
+ Log.d(TAG, "Intent canceled", e)
+ false
+ }
+ }
+
+ /** Returns a bitmap if the user can access the given URI, else null */
+ private fun loadBitmapFromUriForUser(
+ uri: Uri,
+ userId: Int,
+ appUid: Int,
+ packageName: String,
+ ): Bitmap? {
+ try {
+ val ugm = UriGrantsManager.getService()
+ ugm.checkGrantUriPermission_ignoreNonSystem(
+ appUid,
+ packageName,
+ ContentProvider.getUriWithoutUserId(uri),
+ Intent.FLAG_GRANT_READ_URI_PERMISSION,
+ ContentProvider.getUserIdFromUri(uri, userId)
+ )
+ return loadBitmapFromUri(uri)
+ } catch (e: SecurityException) {
+ Log.e(TAG, "Failed to get URI permission: $e")
+ }
+ return null
+ }
+
+ /**
+ * Load a bitmap from a URI
+ *
+ * @param uri the uri to load
+ * @return bitmap, or null if couldn't be loaded
+ */
+ private fun loadBitmapFromUri(uri: Uri): Bitmap? {
+ // ImageDecoder requires a scheme of the following types
+ if (uri.scheme == null) {
+ return null
+ }
+
+ if (
+ !uri.scheme.equals(ContentResolver.SCHEME_CONTENT) &&
+ !uri.scheme.equals(ContentResolver.SCHEME_ANDROID_RESOURCE) &&
+ !uri.scheme.equals(ContentResolver.SCHEME_FILE)
+ ) {
+ return null
+ }
+
+ val source = ImageDecoder.createSource(context.contentResolver, uri)
+ return try {
+ ImageDecoder.decodeBitmap(source) { decoder, info, _ ->
+ val width = info.size.width
+ val height = info.size.height
+ val scale =
+ MediaDataUtils.getScaleFactor(
+ APair(width, height),
+ APair(artworkWidth, artworkHeight)
+ )
+
+ // Downscale if needed
+ if (scale != 0f && scale < 1) {
+ decoder.setTargetSize((scale * width).toInt(), (scale * height).toInt())
+ }
+ decoder.allocator = ImageDecoder.ALLOCATOR_SOFTWARE
+ }
+ } catch (e: IOException) {
+ Log.e(TAG, "Unable to load bitmap", e)
+ null
+ } catch (e: RuntimeException) {
+ Log.e(TAG, "Unable to load bitmap", e)
+ null
+ }
+ }
+
+ private fun getResumeMediaAction(action: Runnable): MediaAction {
+ return MediaAction(
+ Icon.createWithResource(context, R.drawable.ic_media_play)
+ .setTint(themeText)
+ .loadDrawable(context),
+ action,
+ context.getString(R.string.controls_media_resume),
+ context.getDrawable(R.drawable.ic_media_play_container)
+ )
+ }
+
+ fun onMediaDataLoaded(key: String, oldKey: String?, data: MediaData) =
+ traceSection("MediaDataProcessor#onMediaDataLoaded") {
+ Assert.isMainThread()
+ if (mediaDataRepository.mediaEntries.value.containsKey(key)) {
+ // Otherwise this was removed already
+ mediaDataRepository.addMediaEntry(key, data)
+ notifyMediaDataLoaded(key, oldKey, data)
+ }
+ }
+
+ override fun onSmartspaceTargetsUpdated(targets: List<Parcelable>) {
+ if (!allowMediaRecommendations) {
+ if (DEBUG) Log.d(TAG, "Smartspace recommendation is disabled in Settings.")
+ return
+ }
+
+ val mediaTargets = targets.filterIsInstance<SmartspaceTarget>()
+ val smartspaceMediaData = mediaDataRepository.smartspaceMediaData.value
+ when (mediaTargets.size) {
+ 0 -> {
+ if (!smartspaceMediaData.isActive) {
+ return
+ }
+ if (DEBUG) {
+ Log.d(TAG, "Set Smartspace media to be inactive for the data update")
+ }
+ if (mediaFlags.isPersistentSsCardEnabled()) {
+ // Smartspace uses this signal to hide the card (e.g. when it expires or user
+ // disconnects headphones), so treat as setting inactive when flag is on
+ val recommendation = smartspaceMediaData.copy(isActive = false)
+ mediaDataRepository.setRecommendation(recommendation)
+ notifySmartspaceMediaDataLoaded(recommendation.targetId, recommendation)
+ } else {
+ notifySmartspaceMediaDataRemoved(
+ smartspaceMediaData.targetId,
+ immediately = false
+ )
+ mediaDataRepository.setRecommendation(
+ SmartspaceMediaData(
+ targetId = smartspaceMediaData.targetId,
+ instanceId = smartspaceMediaData.instanceId,
+ )
+ )
+ }
+ }
+ 1 -> {
+ val newMediaTarget = mediaTargets.get(0)
+ if (smartspaceMediaData.targetId == newMediaTarget.smartspaceTargetId) {
+ // The same Smartspace updates can be received. Skip the duplicate updates.
+ return
+ }
+ if (DEBUG) Log.d(TAG, "Forwarding Smartspace media update.")
+ val recommendation = toSmartspaceMediaData(newMediaTarget)
+ mediaDataRepository.setRecommendation(recommendation)
+ notifySmartspaceMediaDataLoaded(recommendation.targetId, recommendation)
+ }
+ else -> {
+ // There should NOT be more than 1 Smartspace media update. When it happens, it
+ // indicates a bad state or an error. Reset the status accordingly.
+ Log.wtf(TAG, "More than 1 Smartspace Media Update. Resetting the status...")
+ notifySmartspaceMediaDataRemoved(smartspaceMediaData.targetId, immediately = false)
+ mediaDataRepository.setRecommendation(SmartspaceMediaData())
+ }
+ }
+ }
+
+ fun onNotificationRemoved(key: String) {
+ Assert.isMainThread()
+ val removed = mediaDataRepository.removeMediaEntry(key) ?: return
+ if (keyguardUpdateMonitor.isUserInLockdown(removed.userId)) {
+ logger.logMediaRemoved(removed.appUid, removed.packageName, removed.instanceId)
+ } else if (isAbleToResume(removed)) {
+ convertToResumePlayer(key, removed)
+ } else if (mediaFlags.isRetainingPlayersEnabled()) {
+ handlePossibleRemoval(key, removed, notificationRemoved = true)
+ } else {
+ notifyMediaDataRemoved(key)
+ logger.logMediaRemoved(removed.appUid, removed.packageName, removed.instanceId)
+ }
+ }
+
+ internal fun onSessionDestroyed(key: String) {
+ if (DEBUG) Log.d(TAG, "session destroyed for $key")
+ val entry = mediaDataRepository.removeMediaEntry(key) ?: return
+ // Clear token since the session is no longer valid
+ val updated = entry.copy(token = null)
+ handlePossibleRemoval(key, updated)
+ }
+
+ private fun isAbleToResume(data: MediaData): Boolean {
+ val isEligibleForResume =
+ data.isLocalSession() ||
+ (mediaFlags.isRemoteResumeAllowed() &&
+ data.playbackLocation != MediaData.PLAYBACK_CAST_REMOTE)
+ return useMediaResumption && data.resumeAction != null && isEligibleForResume
+ }
+
+ /**
+ * Convert to resume state if the player is no longer valid and active, then notify listeners
+ * that the data was updated. Does not convert to resume state if the player is still valid, or
+ * if it was removed before becoming inactive. (Assumes that [removed] was removed from
+ * [mediaDataRepository.mediaEntries] state before this function was called)
+ */
+ private fun handlePossibleRemoval(
+ key: String,
+ removed: MediaData,
+ notificationRemoved: Boolean = false
+ ) {
+ val hasSession = removed.token != null
+ if (hasSession && removed.semanticActions != null) {
+ // The app was using session actions, and the session is still valid: keep player
+ if (DEBUG) Log.d(TAG, "Notification removed but using session actions $key")
+ mediaDataRepository.addMediaEntry(key, removed)
+ notifyMediaDataLoaded(key, key, removed)
+ } else if (!notificationRemoved && removed.semanticActions == null) {
+ // The app was using notification actions, and notif wasn't removed yet: keep player
+ if (DEBUG) Log.d(TAG, "Session destroyed but using notification actions $key")
+ mediaDataRepository.addMediaEntry(key, removed)
+ notifyMediaDataLoaded(key, key, removed)
+ } else if (removed.active && !isAbleToResume(removed)) {
+ // This player was still active - it didn't last long enough to time out,
+ // and its app doesn't normally support resume: remove
+ if (DEBUG) Log.d(TAG, "Removing still-active player $key")
+ notifyMediaDataRemoved(key)
+ logger.logMediaRemoved(removed.appUid, removed.packageName, removed.instanceId)
+ } else if (mediaFlags.isRetainingPlayersEnabled() || isAbleToResume(removed)) {
+ // Convert to resume
+ if (DEBUG) {
+ Log.d(
+ TAG,
+ "Notification ($notificationRemoved) and/or session " +
+ "($hasSession) gone for inactive player $key"
+ )
+ }
+ convertToResumePlayer(key, removed)
+ } else {
+ // Retaining players flag is off and app doesn't support resume: remove player.
+ if (DEBUG) Log.d(TAG, "Removing player $key")
+ notifyMediaDataRemoved(key)
+ logger.logMediaRemoved(removed.appUid, removed.packageName, removed.instanceId)
+ }
+ }
+
+ /** Set the given [MediaData] as a resume state player and notify listeners */
+ private fun convertToResumePlayer(key: String, data: MediaData) {
+ if (DEBUG) Log.d(TAG, "Converting $key to resume")
+ // Resumption controls must have a title.
+ if (data.song.isNullOrBlank()) {
+ Log.e(TAG, "Description incomplete")
+ notifyMediaDataRemoved(key)
+ logger.logMediaRemoved(data.appUid, data.packageName, data.instanceId)
+ return
+ }
+ // Move to resume key (aka package name) if that key doesn't already exist.
+ val resumeAction = data.resumeAction?.let { getResumeMediaAction(it) }
+ val actions = resumeAction?.let { listOf(resumeAction) } ?: emptyList()
+ val launcherIntent =
+ context.packageManager.getLaunchIntentForPackage(data.packageName)?.let {
+ PendingIntent.getActivity(context, 0, it, PendingIntent.FLAG_IMMUTABLE)
+ }
+ val lastActive =
+ if (data.active) {
+ systemClock.elapsedRealtime()
+ } else {
+ data.lastActive
+ }
+ val updated =
+ data.copy(
+ token = null,
+ actions = actions,
+ semanticActions = MediaButton(playOrPause = resumeAction),
+ actionsToShowInCompact = listOf(0),
+ active = false,
+ resumption = true,
+ isPlaying = false,
+ isClearable = true,
+ clickIntent = launcherIntent,
+ lastActive = lastActive,
+ )
+ val pkg = data.packageName
+ val migrate = mediaDataRepository.addMediaEntry(pkg, updated) == null
+ // Notify listeners of "new" controls when migrating or removed and update when not
+ Log.d(TAG, "migrating? $migrate from $key -> $pkg")
+ if (migrate) {
+ notifyMediaDataLoaded(key = pkg, oldKey = key, info = updated)
+ } else {
+ // Since packageName is used for the key of the resumption controls, it is
+ // possible that another notification has already been reused for the resumption
+ // controls of this package. In this case, rather than renaming this player as
+ // packageName, just remove it and then send a update to the existing resumption
+ // controls.
+ notifyMediaDataRemoved(key)
+ notifyMediaDataLoaded(key = pkg, oldKey = pkg, info = updated)
+ }
+ logger.logActiveConvertedToResume(updated.appUid, pkg, updated.instanceId)
+
+ // Limit total number of resume controls
+ val resumeEntries =
+ mediaDataRepository.mediaEntries.value.filter { (_, data) -> data.resumption }
+ val numResume = resumeEntries.size
+ if (numResume > ResumeMediaBrowser.MAX_RESUMPTION_CONTROLS) {
+ resumeEntries
+ .toList()
+ .sortedBy { (_, data) -> data.lastActive }
+ .subList(0, numResume - ResumeMediaBrowser.MAX_RESUMPTION_CONTROLS)
+ .forEach { (key, data) ->
+ Log.d(TAG, "Removing excess control $key")
+ mediaDataRepository.removeMediaEntry(key)
+ notifyMediaDataRemoved(key)
+ logger.logMediaRemoved(data.appUid, data.packageName, data.instanceId)
+ }
+ }
+ }
+
+ fun setMediaResumptionEnabled(isEnabled: Boolean) {
+ if (useMediaResumption == isEnabled) {
+ return
+ }
+
+ useMediaResumption = isEnabled
+
+ if (!useMediaResumption) {
+ // Remove any existing resume controls
+ val filtered = mediaDataRepository.mediaEntries.value.filter { !it.value.active }
+ filtered.forEach {
+ mediaDataRepository.removeMediaEntry(it.key)
+ notifyMediaDataRemoved(it.key)
+ logger.logMediaRemoved(it.value.appUid, it.value.packageName, it.value.instanceId)
+ }
+ }
+ }
+
+ /** Listener to data changes. */
+ interface Listener {
+
+ /**
+ * Called whenever there's new MediaData Loaded for the consumption in views.
+ *
+ * oldKey is provided to check whether the view has changed keys, which can happen when a
+ * player has gone from resume state (key is package name) to active state (key is
+ * notification key) or vice versa.
+ *
+ * @param immediately indicates should apply the UI changes immediately, otherwise wait
+ * until the next refresh-round before UI becomes visible. True by default to take in
+ * place immediately.
+ * @param receivedSmartspaceCardLatency is the latency between headphone connects and sysUI
+ * displays Smartspace media targets. Will be 0 if the data is not activated by Smartspace
+ * signal.
+ * @param isSsReactivated indicates resume media card is reactivated by Smartspace
+ * recommendation signal
+ */
+ fun onMediaDataLoaded(
+ key: String,
+ oldKey: String?,
+ data: MediaData,
+ immediately: Boolean = true,
+ receivedSmartspaceCardLatency: Int = 0,
+ isSsReactivated: Boolean = false
+ ) {}
+
+ /**
+ * Called whenever there's new Smartspace media data loaded.
+ *
+ * @param shouldPrioritize indicates the sorting priority of the Smartspace card. If true,
+ * it will be prioritized as the first card. Otherwise, it will show up as the last card
+ * as default.
+ */
+ fun onSmartspaceMediaDataLoaded(
+ key: String,
+ data: SmartspaceMediaData,
+ shouldPrioritize: Boolean = false
+ ) {}
+
+ /** Called whenever a previously existing Media notification was removed. */
+ fun onMediaDataRemoved(key: String) {}
+
+ /**
+ * Called whenever a previously existing Smartspace media data was removed.
+ *
+ * @param immediately indicates should apply the UI changes immediately, otherwise wait
+ * until the next refresh-round before UI becomes visible. True by default to take in
+ * place immediately.
+ */
+ fun onSmartspaceMediaDataRemoved(key: String, immediately: Boolean = true) {}
+ }
+
+ /**
+ * Converts the pass-in SmartspaceTarget to SmartspaceMediaData
+ *
+ * @return An empty SmartspaceMediaData with the valid target Id is returned if the
+ * SmartspaceTarget's data is invalid.
+ */
+ private fun toSmartspaceMediaData(target: SmartspaceTarget): SmartspaceMediaData {
+ val baseAction: SmartspaceAction? = target.baseAction
+ val dismissIntent =
+ baseAction
+ ?.extras
+ ?.getParcelable(EXTRAS_SMARTSPACE_DISMISS_INTENT_KEY, Intent::class.java)
+
+ val isActive =
+ when {
+ !mediaFlags.isPersistentSsCardEnabled() -> true
+ baseAction == null -> true
+ else -> {
+ val triggerSource = baseAction.extras?.getString(EXTRA_KEY_TRIGGER_SOURCE)
+ triggerSource != EXTRA_VALUE_TRIGGER_PERIODIC
+ }
+ }
+
+ packageName(target)?.let {
+ return SmartspaceMediaData(
+ targetId = target.smartspaceTargetId,
+ isActive = isActive,
+ packageName = it,
+ cardAction = target.baseAction,
+ recommendations = target.iconGrid,
+ dismissIntent = dismissIntent,
+ headphoneConnectionTimeMillis = target.creationTimeMillis,
+ instanceId = logger.getNewInstanceId(),
+ expiryTimeMs = target.expiryTimeMillis,
+ )
+ }
+ return SmartspaceMediaData(
+ targetId = target.smartspaceTargetId,
+ isActive = isActive,
+ dismissIntent = dismissIntent,
+ headphoneConnectionTimeMillis = target.creationTimeMillis,
+ instanceId = logger.getNewInstanceId(),
+ expiryTimeMs = target.expiryTimeMillis,
+ )
+ }
+
+ private fun packageName(target: SmartspaceTarget): String? {
+ val recommendationList: MutableList<SmartspaceAction> = target.iconGrid
+ if (recommendationList.isEmpty()) {
+ Log.w(TAG, "Empty or null media recommendation list.")
+ return null
+ }
+ for (recommendation in recommendationList) {
+ val extras = recommendation.extras
+ extras?.let {
+ it.getString(EXTRAS_MEDIA_SOURCE_PACKAGE_NAME)?.let { packageName ->
+ return packageName
+ }
+ }
+ }
+ Log.w(TAG, "No valid package name is provided.")
+ return null
+ }
+
+ override fun dump(pw: PrintWriter, args: Array<out String>) {
+ pw.apply {
+ println("internalListeners: $internalListeners")
+ println("useMediaResumption: $useMediaResumption")
+ println("allowMediaRecommendations: $allowMediaRecommendations")
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceManager.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceManager.kt
index f4d70a5..c7cfb0b 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceManager.kt
@@ -35,10 +35,8 @@
import com.android.settingslib.media.LocalMediaManager
import com.android.settingslib.media.MediaDevice
import com.android.settingslib.media.PhoneMediaDevice
-import com.android.systemui.Dumpable
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.dagger.qualifiers.Main
-import com.android.systemui.dump.DumpManager
import com.android.systemui.media.controls.shared.model.MediaData
import com.android.systemui.media.controls.shared.model.MediaDeviceData
import com.android.systemui.media.controls.util.LocalMediaManagerFactory
@@ -70,16 +68,11 @@
private val localBluetoothManager: Lazy<LocalBluetoothManager?>,
@Main private val fgExecutor: Executor,
@Background private val bgExecutor: Executor,
- dumpManager: DumpManager,
-) : MediaDataManager.Listener, Dumpable {
+) : MediaDataManager.Listener {
private val listeners: MutableSet<Listener> = mutableSetOf()
private val entries: MutableMap<String, Entry> = mutableMapOf()
- init {
- dumpManager.registerDumpable(this)
- }
-
/** Add a listener for changes to the media route (ie. device). */
fun addListener(listener: Listener) = listeners.add(listener)
@@ -123,7 +116,7 @@
token?.let { listeners.forEach { it.onKeyRemoved(key) } }
}
- override fun dump(pw: PrintWriter, args: Array<String>) {
+ fun dump(pw: PrintWriter) {
with(pw) {
println("MediaDeviceManager state:")
entries.forEach { (key, entry) ->
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/interactor/MediaCarouselInteractor.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/interactor/MediaCarouselInteractor.kt
new file mode 100644
index 0000000..4a92b71
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/interactor/MediaCarouselInteractor.kt
@@ -0,0 +1,234 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.media.controls.domain.pipeline.interactor
+
+import android.app.PendingIntent
+import android.media.MediaDescription
+import android.media.session.MediaSession
+import android.media.session.PlaybackState
+import android.service.notification.StatusBarNotification
+import com.android.systemui.CoreStartable
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.media.controls.data.repository.MediaDataRepository
+import com.android.systemui.media.controls.data.repository.MediaFilterRepository
+import com.android.systemui.media.controls.domain.pipeline.MediaDataCombineLatest
+import com.android.systemui.media.controls.domain.pipeline.MediaDataFilterImpl
+import com.android.systemui.media.controls.domain.pipeline.MediaDataManager
+import com.android.systemui.media.controls.domain.pipeline.MediaDataProcessor
+import com.android.systemui.media.controls.domain.pipeline.MediaDeviceManager
+import com.android.systemui.media.controls.domain.pipeline.MediaSessionBasedFilter
+import com.android.systemui.media.controls.domain.pipeline.MediaTimeoutListener
+import com.android.systemui.media.controls.domain.resume.MediaResumeListener
+import com.android.systemui.media.controls.util.MediaFlags
+import java.io.PrintWriter
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.mapLatest
+import kotlinx.coroutines.flow.stateIn
+
+/** Encapsulates business logic for media pipeline. */
+@OptIn(ExperimentalCoroutinesApi::class)
+@SysUISingleton
+class MediaCarouselInteractor
+@Inject
+constructor(
+ @Application applicationScope: CoroutineScope,
+ private val mediaDataRepository: MediaDataRepository,
+ private val mediaDataProcessor: MediaDataProcessor,
+ private val mediaTimeoutListener: MediaTimeoutListener,
+ private val mediaResumeListener: MediaResumeListener,
+ private val mediaSessionBasedFilter: MediaSessionBasedFilter,
+ private val mediaDeviceManager: MediaDeviceManager,
+ private val mediaDataCombineLatest: MediaDataCombineLatest,
+ private val mediaDataFilter: MediaDataFilterImpl,
+ mediaFilterRepository: MediaFilterRepository,
+ private val mediaFlags: MediaFlags,
+) : MediaDataManager, CoreStartable {
+
+ /** Are there any media notifications active, including the recommendations? */
+ val hasActiveMediaOrRecommendation: StateFlow<Boolean> =
+ combine(
+ mediaFilterRepository.selectedUserEntries,
+ mediaFilterRepository.smartspaceMediaData,
+ mediaFilterRepository.reactivatedKey
+ ) { entries, smartspaceMediaData, reactivatedKey ->
+ entries.any { it.value.active } ||
+ (smartspaceMediaData.isActive &&
+ (smartspaceMediaData.isValid() || reactivatedKey != null))
+ }
+ .distinctUntilChanged()
+ .stateIn(applicationScope, SharingStarted.WhileSubscribed(), false)
+
+ /** Are there any media entries we should display, including the recommendations? */
+ val hasAnyMediaOrRecommendation: StateFlow<Boolean> =
+ combine(
+ mediaFilterRepository.selectedUserEntries,
+ mediaFilterRepository.smartspaceMediaData
+ ) { entries, smartspaceMediaData ->
+ entries.isNotEmpty() ||
+ (if (mediaFlags.isPersistentSsCardEnabled()) {
+ smartspaceMediaData.isValid()
+ } else {
+ smartspaceMediaData.isActive && smartspaceMediaData.isValid()
+ })
+ }
+ .distinctUntilChanged()
+ .stateIn(applicationScope, SharingStarted.WhileSubscribed(), false)
+
+ /** Are there any media notifications active, excluding the recommendations? */
+ val hasActiveMedia: StateFlow<Boolean> =
+ mediaFilterRepository.selectedUserEntries
+ .mapLatest { entries -> entries.any { it.value.active } }
+ .distinctUntilChanged()
+ .stateIn(applicationScope, SharingStarted.WhileSubscribed(), false)
+
+ /** Are there any media notifications, excluding the recommendations? */
+ val hasAnyMedia: StateFlow<Boolean> =
+ mediaFilterRepository.selectedUserEntries
+ .mapLatest { entries -> entries.isNotEmpty() }
+ .distinctUntilChanged()
+ .stateIn(applicationScope, SharingStarted.WhileSubscribed(), false)
+
+ override fun start() {
+ if (!mediaFlags.isMediaControlsRefactorEnabled()) {
+ return
+ }
+
+ // Initialize the internal processing pipeline. The listeners at the front of the pipeline
+ // are set as internal listeners so that they receive events. From there, events are
+ // propagated through the pipeline. The end of the pipeline is currently mediaDataFilter,
+ // so it is responsible for dispatching events to external listeners. To achieve this,
+ // external listeners that are registered with [MediaDataManager.addListener] are actually
+ // registered as listeners to mediaDataFilter.
+ addInternalListener(mediaTimeoutListener)
+ addInternalListener(mediaResumeListener)
+ addInternalListener(mediaSessionBasedFilter)
+ mediaSessionBasedFilter.addListener(mediaDeviceManager)
+ mediaSessionBasedFilter.addListener(mediaDataCombineLatest)
+ mediaDeviceManager.addListener(mediaDataCombineLatest)
+ mediaDataCombineLatest.addListener(mediaDataFilter)
+
+ // Set up links back into the pipeline for listeners that need to send events upstream.
+ mediaTimeoutListener.timeoutCallback = { key: String, timedOut: Boolean ->
+ setInactive(key, timedOut)
+ }
+ mediaTimeoutListener.stateCallback = { key: String, state: PlaybackState ->
+ mediaDataProcessor.updateState(key, state)
+ }
+ mediaTimeoutListener.sessionCallback = { key: String ->
+ mediaDataProcessor.onSessionDestroyed(key)
+ }
+ mediaResumeListener.setManager(this)
+ mediaDataFilter.mediaDataManager = this
+ }
+
+ override fun addListener(listener: MediaDataManager.Listener) {
+ mediaDataFilter.addListener(listener)
+ }
+
+ override fun removeListener(listener: MediaDataManager.Listener) {
+ mediaDataFilter.removeListener(listener)
+ }
+
+ override fun setInactive(key: String, timedOut: Boolean, forceUpdate: Boolean) {
+ mediaDataProcessor.setInactive(key, timedOut, forceUpdate)
+ }
+
+ override fun onNotificationAdded(key: String, sbn: StatusBarNotification) {
+ mediaDataProcessor.onNotificationAdded(key, sbn)
+ }
+
+ override fun destroy() {
+ mediaSessionBasedFilter.removeListener(mediaDeviceManager)
+ mediaSessionBasedFilter.removeListener(mediaDataCombineLatest)
+ mediaDeviceManager.removeListener(mediaDataCombineLatest)
+ mediaDataCombineLatest.removeListener(mediaDataFilter)
+ mediaDataProcessor.destroy()
+ }
+
+ override fun setResumeAction(key: String, action: Runnable?) {
+ mediaDataProcessor.setResumeAction(key, action)
+ }
+
+ override fun addResumptionControls(
+ userId: Int,
+ desc: MediaDescription,
+ action: Runnable,
+ token: MediaSession.Token,
+ appName: String,
+ appIntent: PendingIntent,
+ packageName: String
+ ) {
+ mediaDataProcessor.addResumptionControls(
+ userId,
+ desc,
+ action,
+ token,
+ appName,
+ appIntent,
+ packageName
+ )
+ }
+
+ override fun dismissMediaData(key: String, delay: Long): Boolean {
+ return mediaDataProcessor.dismissMediaData(key, delay)
+ }
+
+ override fun dismissSmartspaceRecommendation(key: String, delay: Long) {
+ return mediaDataProcessor.dismissSmartspaceRecommendation(key, delay)
+ }
+
+ override fun setRecommendationInactive(key: String) {
+ mediaDataProcessor.setRecommendationInactive(key)
+ }
+
+ override fun onNotificationRemoved(key: String) {
+ mediaDataProcessor.onNotificationRemoved(key)
+ }
+
+ override fun setMediaResumptionEnabled(isEnabled: Boolean) {
+ mediaDataProcessor.setMediaResumptionEnabled(isEnabled)
+ }
+
+ override fun onSwipeToDismiss() {
+ mediaDataFilter.onSwipeToDismiss()
+ }
+
+ override fun hasActiveMediaOrRecommendation() = hasActiveMediaOrRecommendation.value
+
+ override fun hasAnyMediaOrRecommendation() = hasAnyMediaOrRecommendation.value
+
+ override fun hasActiveMedia() = hasActiveMedia.value
+
+ override fun hasAnyMedia() = hasAnyMedia.value
+
+ override fun isRecommendationActive() = mediaDataRepository.smartspaceMediaData.value.isActive
+
+ /** Add a listener for internal events. */
+ private fun addInternalListener(listener: MediaDataManager.Listener) =
+ mediaDataProcessor.addInternalListener(listener)
+
+ override fun dump(pw: PrintWriter, args: Array<out String>) {
+ mediaDeviceManager.dump(pw)
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/shared/model/MediaData.kt b/packages/SystemUI/src/com/android/systemui/media/controls/shared/model/MediaData.kt
index 4fa7cb5..11a5629 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/shared/model/MediaData.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/shared/model/MediaData.kt
@@ -20,48 +20,49 @@
import android.graphics.drawable.Drawable
import android.graphics.drawable.Icon
import android.media.session.MediaSession
+import android.os.Process
import com.android.internal.logging.InstanceId
import com.android.systemui.res.R
/** State of a media view. */
data class MediaData(
- val userId: Int,
+ val userId: Int = -1,
val initialized: Boolean = false,
/** App name that will be displayed on the player. */
- val app: String?,
+ val app: String? = null,
/** App icon shown on player. */
- val appIcon: Icon?,
+ val appIcon: Icon? = null,
/** Artist name. */
- val artist: CharSequence?,
+ val artist: CharSequence? = null,
/** Song name. */
- val song: CharSequence?,
+ val song: CharSequence? = null,
/** Album artwork. */
- val artwork: Icon?,
+ val artwork: Icon? = null,
/** List of generic action buttons for the media player, based on notification actions */
- val actions: List<MediaAction>,
+ val actions: List<MediaAction> = emptyList(),
/** Same as above, but shown on smaller versions of the player, like in QQS or keyguard. */
- val actionsToShowInCompact: List<Int>,
+ val actionsToShowInCompact: List<Int> = emptyList(),
/**
* Semantic actions buttons, based on the PlaybackState of the media session. If present, these
* actions will be preferred in the UI over [actions]
*/
val semanticActions: MediaButton? = null,
/** Package name of the app that's posting the media. */
- val packageName: String,
+ val packageName: String = "INVALID",
/** Unique media session identifier. */
- val token: MediaSession.Token?,
+ val token: MediaSession.Token? = null,
/** Action to perform when the player is tapped. This is unrelated to {@link #actions}. */
- val clickIntent: PendingIntent?,
+ val clickIntent: PendingIntent? = null,
/** Where the media is playing: phone, headphones, ear buds, remote session. */
- val device: MediaDeviceData?,
+ val device: MediaDeviceData? = null,
/**
* When active, a player will be displayed on keyguard and quick-quick settings. This is
* unrelated to the stream being playing or not, a player will not be active if timed out, or in
* resumption mode.
*/
- var active: Boolean,
+ var active: Boolean = true,
/** Action that should be performed to restart a non active session. */
- var resumeAction: Runnable?,
+ var resumeAction: Runnable? = null,
/** Playback location: one of PLAYBACK_LOCAL, PLAYBACK_CAST_LOCAL, or PLAYBACK_CAST_REMOTE */
var playbackLocation: Int = PLAYBACK_LOCAL,
/**
@@ -88,10 +89,10 @@
var createdTimestampMillis: Long = 0L,
/** Instance ID for logging purposes */
- val instanceId: InstanceId,
+ val instanceId: InstanceId = InstanceId.fakeInstanceId(-1),
/** The UID of the app, used for logging */
- val appUid: Int,
+ val appUid: Int = Process.INVALID_UID,
/** Whether explicit indicator exists */
val isExplicit: Boolean = false,
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/shared/model/SmartspaceMediaData.kt b/packages/SystemUI/src/com/android/systemui/media/controls/shared/model/SmartspaceMediaData.kt
index 52c605f..b446585 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/shared/model/SmartspaceMediaData.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/shared/model/SmartspaceMediaData.kt
@@ -30,23 +30,23 @@
/** State of a Smartspace media recommendations view. */
data class SmartspaceMediaData(
/** Unique id of a Smartspace media target. */
- val targetId: String,
+ val targetId: String = "INVALID",
/** Indicates if the status is active. */
- val isActive: Boolean,
+ val isActive: Boolean = false,
/** Package name of the media recommendations' provider-app. */
- val packageName: String,
+ val packageName: String = "INVALID",
/** Action to perform when the card is tapped. Also contains the target's extra info. */
- val cardAction: SmartspaceAction?,
+ val cardAction: SmartspaceAction? = null,
/** List of media recommendations. */
- val recommendations: List<SmartspaceAction>,
+ val recommendations: List<SmartspaceAction> = emptyList(),
/** Intent for the user's initiated dismissal. */
- val dismissIntent: Intent?,
+ val dismissIntent: Intent? = null,
/** The timestamp in milliseconds that the card was generated */
- val headphoneConnectionTimeMillis: Long,
+ val headphoneConnectionTimeMillis: Long = 0L,
/** Instance ID for [MediaUiEventLogger] */
- val instanceId: InstanceId,
+ val instanceId: InstanceId? = null,
/** The timestamp in milliseconds indicating when the card should be removed */
- val expiryTimeMs: Long,
+ val expiryTimeMs: Long = 0L,
) {
/**
* Indicates if all the data is valid.
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/KeyguardMediaController.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/KeyguardMediaController.kt
index ba7d410..89a9ba7 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/KeyguardMediaController.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/KeyguardMediaController.kt
@@ -27,10 +27,10 @@
import android.view.ViewGroup
import androidx.annotation.VisibleForTesting
import com.android.systemui.Dumpable
-import com.android.systemui.Flags.migrateClocksToBlueprint
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.dump.DumpManager
+import com.android.systemui.keyguard.MigrateClocksToBlueprint
import com.android.systemui.media.controls.ui.view.MediaHost
import com.android.systemui.media.controls.ui.view.MediaHostState
import com.android.systemui.media.dagger.MediaModule.KEYGUARD
@@ -185,7 +185,7 @@
refreshMediaPosition(reason = "onMediaHostVisibilityChanged")
if (visible) {
- if (migrateClocksToBlueprint() && useSplitShade) {
+ if (MigrateClocksToBlueprint.isEnabled && useSplitShade) {
return
}
mediaHost.hostView.layoutParams.apply {
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaCarouselController.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaCarouselController.kt
index b721236..655e6a5 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaCarouselController.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaCarouselController.kt
@@ -1163,7 +1163,7 @@
// Only log media resume card when Smartspace data is available
if (
!mediaControlKey.isSsMediaRec &&
- !mediaManager.smartspaceMediaData.isActive &&
+ !mediaManager.isRecommendationActive() &&
MediaPlayerData.smartspaceMediaData == null
) {
return
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaUiEventLogger.kt b/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaUiEventLogger.kt
index f8c816c..2c25fe2 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaUiEventLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaUiEventLogger.kt
@@ -161,7 +161,7 @@
logger.log(event)
}
- fun logRecommendationAdded(packageName: String, instanceId: InstanceId) {
+ fun logRecommendationAdded(packageName: String, instanceId: InstanceId?) {
logger.logWithInstanceId(
MediaUiEvent.MEDIA_RECOMMENDATION_ADDED,
0,
@@ -170,7 +170,7 @@
)
}
- fun logRecommendationRemoved(packageName: String, instanceId: InstanceId) {
+ fun logRecommendationRemoved(packageName: String, instanceId: InstanceId?) {
logger.logWithInstanceId(
MediaUiEvent.MEDIA_RECOMMENDATION_REMOVED,
0,
diff --git a/packages/SystemUI/src/com/android/systemui/media/dagger/MediaModule.java b/packages/SystemUI/src/com/android/systemui/media/dagger/MediaModule.java
index d84e5dd..0fa3605 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dagger/MediaModule.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dagger/MediaModule.java
@@ -19,6 +19,7 @@
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.log.LogBuffer;
import com.android.systemui.log.LogBufferFactory;
+import com.android.systemui.media.controls.domain.MediaDomainModule;
import com.android.systemui.media.controls.domain.pipeline.MediaDataManager;
import com.android.systemui.media.controls.ui.controller.MediaHierarchyManager;
import com.android.systemui.media.controls.ui.controller.MediaHostStatesManager;
@@ -38,7 +39,11 @@
import javax.inject.Named;
/** Dagger module for the media package. */
-@Module(subcomponents = {
+@Module(
+ includes = {
+ MediaDomainModule.class
+ },
+ subcomponents = {
MediaComplicationComponent.class,
})
public interface MediaModule {
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/NavBarHelper.java b/packages/SystemUI/src/com/android/systemui/navigationbar/NavBarHelper.java
index dfe41eb..d49a513 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/NavBarHelper.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/NavBarHelper.java
@@ -243,7 +243,7 @@
Settings.Secure.getUriFor(Settings.Secure.ASSIST_LONG_PRESS_HOME_ENABLED),
false, mAssistContentObserver, UserHandle.USER_ALL);
mContentResolver.registerContentObserver(
- Settings.Secure.getUriFor(Secure.SEARCH_LONG_PRESS_HOME_ENABLED),
+ Settings.Secure.getUriFor(Secure.SEARCH_ALL_ENTRYPOINTS_ENABLED),
false, mAssistContentObserver, UserHandle.USER_ALL);
mContentResolver.registerContentObserver(
Settings.Secure.getUriFor(Settings.Secure.ASSIST_TOUCH_GESTURE_ENABLED),
@@ -443,10 +443,10 @@
boolean overrideLongPressHome = mAssistManagerLazy.get()
.shouldOverrideAssist(AssistManager.INVOCATION_TYPE_HOME_BUTTON_LONG_PRESS);
boolean longPressDefault = mContext.getResources().getBoolean(overrideLongPressHome
- ? com.android.internal.R.bool.config_searchLongPressHomeEnabledDefault
+ ? com.android.internal.R.bool.config_searchAllEntrypointsEnabledDefault
: com.android.internal.R.bool.config_assistLongPressHomeEnabledDefault);
mLongPressHomeEnabled = Settings.Secure.getIntForUser(mContentResolver,
- overrideLongPressHome ? Secure.SEARCH_LONG_PRESS_HOME_ENABLED
+ overrideLongPressHome ? Secure.SEARCH_ALL_ENTRYPOINTS_ENABLED
: Settings.Secure.ASSIST_LONG_PRESS_HOME_ENABLED, longPressDefault ? 1 : 0,
mUserTracker.getUserId()) != 0;
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java
index 768bb8e..4fe3a11 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java
@@ -934,48 +934,51 @@
private void orientSecondaryHomeHandle() {
if (!canShowSecondaryHandle()) {
- if (mStartingQuickSwitchRotation == -1) {
- resetSecondaryHandle();
- }
return;
}
- int deltaRotation = deltaRotation(mCurrentRotation, mStartingQuickSwitchRotation);
- if (mStartingQuickSwitchRotation == -1 || deltaRotation == -1) {
- // Curious if starting quickswitch can change between the if check and our delta
- Log.d(TAG, "secondary nav delta rotation: " + deltaRotation
- + " current: " + mCurrentRotation
- + " starting: " + mStartingQuickSwitchRotation);
- }
- int height = 0;
- int width = 0;
- Rect dispSize = mWindowManager.getCurrentWindowMetrics().getBounds();
- mOrientationHandle.setDeltaRotation(deltaRotation);
- switch (deltaRotation) {
- case Surface.ROTATION_90, Surface.ROTATION_270:
- height = dispSize.height();
- width = mView.getHeight();
- break;
- case Surface.ROTATION_180, Surface.ROTATION_0:
- // TODO(b/152683657): Need to determine best UX for this
- if (!mShowOrientedHandleForImmersiveMode) {
- resetSecondaryHandle();
- return;
- }
- width = dispSize.width();
- height = mView.getHeight();
- break;
- }
+ if (mStartingQuickSwitchRotation == -1) {
+ resetSecondaryHandle();
+ } else {
+ int deltaRotation = deltaRotation(mCurrentRotation, mStartingQuickSwitchRotation);
+ if (mStartingQuickSwitchRotation == -1 || deltaRotation == -1) {
+ // Curious if starting quickswitch can change between the if check and our delta
+ Log.d(TAG, "secondary nav delta rotation: " + deltaRotation
+ + " current: " + mCurrentRotation
+ + " starting: " + mStartingQuickSwitchRotation);
+ }
+ int height = 0;
+ int width = 0;
+ Rect dispSize = mWindowManager.getCurrentWindowMetrics().getBounds();
+ mOrientationHandle.setDeltaRotation(deltaRotation);
+ switch (deltaRotation) {
+ case Surface.ROTATION_90:
+ case Surface.ROTATION_270:
+ height = dispSize.height();
+ width = mView.getHeight();
+ break;
+ case Surface.ROTATION_180:
+ case Surface.ROTATION_0:
+ // TODO(b/152683657): Need to determine best UX for this
+ if (!mShowOrientedHandleForImmersiveMode) {
+ resetSecondaryHandle();
+ return;
+ }
+ width = dispSize.width();
+ height = mView.getHeight();
+ break;
+ }
- mOrientationParams.gravity =
- deltaRotation == Surface.ROTATION_0 ? Gravity.BOTTOM :
- (deltaRotation == Surface.ROTATION_90 ? Gravity.LEFT : Gravity.RIGHT);
- mOrientationParams.height = height;
- mOrientationParams.width = width;
- mWindowManager.updateViewLayout(mOrientationHandle, mOrientationParams);
- mView.setVisibility(View.GONE);
- mOrientationHandle.setVisibility(View.VISIBLE);
- logNavbarOrientation("orientSecondaryHomeHandle");
+ mOrientationParams.gravity =
+ deltaRotation == Surface.ROTATION_0 ? Gravity.BOTTOM :
+ (deltaRotation == Surface.ROTATION_90 ? Gravity.LEFT : Gravity.RIGHT);
+ mOrientationParams.height = height;
+ mOrientationParams.width = width;
+ mWindowManager.updateViewLayout(mOrientationHandle, mOrientationParams);
+ mView.setVisibility(View.GONE);
+ mOrientationHandle.setVisibility(View.VISIBLE);
+ logNavbarOrientation("orientSecondaryHomeHandle");
+ }
}
private void resetSecondaryHandle() {
@@ -1789,8 +1792,7 @@
}
private boolean canShowSecondaryHandle() {
- return mNavBarMode == NAV_BAR_MODE_GESTURAL && mOrientationHandle != null
- && mStartingQuickSwitchRotation != -1;
+ return mNavBarMode == NAV_BAR_MODE_GESTURAL && mOrientationHandle != null;
}
private final UserTracker.Callback mUserChangedCallback =
diff --git a/packages/SystemUI/src/com/android/systemui/people/widget/PeopleSpaceWidgetManager.java b/packages/SystemUI/src/com/android/systemui/people/widget/PeopleSpaceWidgetManager.java
index 1d820a1..0a880293 100644
--- a/packages/SystemUI/src/com/android/systemui/people/widget/PeopleSpaceWidgetManager.java
+++ b/packages/SystemUI/src/com/android/systemui/people/widget/PeopleSpaceWidgetManager.java
@@ -21,6 +21,9 @@
import static android.app.NotificationManager.INTERRUPTION_FILTER_ALL;
import static android.app.NotificationManager.INTERRUPTION_FILTER_NONE;
import static android.app.NotificationManager.INTERRUPTION_FILTER_PRIORITY;
+import static android.appwidget.AppWidgetProviderInfo.WIDGET_CATEGORY_HOME_SCREEN;
+import static android.appwidget.AppWidgetProviderInfo.WIDGET_CATEGORY_KEYGUARD;
+import static android.appwidget.flags.Flags.generatedPreviews;
import static android.content.Intent.ACTION_BOOT_COMPLETED;
import static android.content.Intent.ACTION_PACKAGE_ADDED;
import static android.content.Intent.ACTION_PACKAGE_REMOVED;
@@ -56,6 +59,7 @@
import android.app.people.PeopleManager;
import android.app.people.PeopleSpaceTile;
import android.appwidget.AppWidgetManager;
+import android.appwidget.AppWidgetProviderInfo;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
@@ -80,12 +84,15 @@
import android.service.notification.ZenModeConfig;
import android.text.TextUtils;
import android.util.Log;
+import android.util.SparseBooleanArray;
import android.widget.RemoteViews;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.logging.UiEventLogger;
import com.android.internal.logging.UiEventLoggerImpl;
+import com.android.keyguard.KeyguardUpdateMonitor;
+import com.android.keyguard.KeyguardUpdateMonitorCallback;
import com.android.systemui.Dumpable;
import com.android.systemui.broadcast.BroadcastDispatcher;
import com.android.systemui.dagger.SysUISingleton;
@@ -96,6 +103,8 @@
import com.android.systemui.people.PeopleSpaceUtils;
import com.android.systemui.people.PeopleTileViewHelper;
import com.android.systemui.people.SharedPreferencesHelper;
+import com.android.systemui.res.R;
+import com.android.systemui.settings.UserTracker;
import com.android.systemui.statusbar.NotificationListener;
import com.android.systemui.statusbar.NotificationListener.NotificationHandler;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
@@ -160,13 +169,27 @@
@GuardedBy("mLock")
public static Map<Integer, PeopleSpaceTile> mTiles = new HashMap<>();
+ @NonNull private final UserTracker mUserTracker;
+ @NonNull private final SparseBooleanArray mUpdatedPreviews = new SparseBooleanArray();
+ @NonNull private final KeyguardUpdateMonitorCallback mKeyguardUpdateMonitorCallback =
+ new KeyguardUpdateMonitorCallback() {
+ @Override
+ public void onUserUnlocked() {
+ if (DEBUG) {
+ Log.d(TAG, "onUserUnlocked " + mUserTracker.getUserId());
+ }
+ updateGeneratedPreviewForUser(mUserTracker.getUserHandle());
+ }
+ };
+
@Inject
public PeopleSpaceWidgetManager(Context context, LauncherApps launcherApps,
CommonNotifCollection notifCollection,
PackageManager packageManager, Optional<Bubbles> bubblesOptional,
UserManager userManager, NotificationManager notificationManager,
BroadcastDispatcher broadcastDispatcher, @Background Executor bgExecutor,
- DumpManager dumpManager) {
+ DumpManager dumpManager, @NonNull UserTracker userTracker,
+ @NonNull KeyguardUpdateMonitor keyguardUpdateMonitor) {
if (DEBUG) Log.d(TAG, "constructor");
mContext = context;
mAppWidgetManager = AppWidgetManager.getInstance(context);
@@ -187,6 +210,8 @@
mBroadcastDispatcher = broadcastDispatcher;
mBgExecutor = bgExecutor;
dumpManager.registerNormalDumpable(TAG, this);
+ mUserTracker = userTracker;
+ keyguardUpdateMonitor.registerCallback(mKeyguardUpdateMonitorCallback);
}
/** Initializes {@PeopleSpaceWidgetManager}. */
@@ -246,7 +271,7 @@
CommonNotifCollection notifCollection, PackageManager packageManager,
Optional<Bubbles> bubblesOptional, UserManager userManager, BackupManager backupManager,
INotificationManager iNotificationManager, NotificationManager notificationManager,
- @Background Executor executor) {
+ @Background Executor executor, UserTracker userTracker) {
mContext = context;
mAppWidgetManager = appWidgetManager;
mIPeopleManager = iPeopleManager;
@@ -262,6 +287,7 @@
mManager = this;
mSharedPrefs = PreferenceManager.getDefaultSharedPreferences(context);
mBgExecutor = executor;
+ mUserTracker = userTracker;
}
/**
@@ -1407,4 +1433,32 @@
Trace.traceEnd(Trace.TRACE_TAG_APP);
}
+
+ @VisibleForTesting
+ void updateGeneratedPreviewForUser(UserHandle user) {
+ if (!generatedPreviews() || mUpdatedPreviews.get(user.getIdentifier())
+ || !mUserManager.isUserUnlocked(user)) {
+ return;
+ }
+
+ // The widget provider may be disabled on SystemUI implementers, e.g. TvSystemUI.
+ ComponentName provider = new ComponentName(mContext, PeopleSpaceWidgetProvider.class);
+ List<AppWidgetProviderInfo> infos = mAppWidgetManager.getInstalledProvidersForPackage(
+ mContext.getPackageName(), user);
+ if (infos.stream().noneMatch(info -> info.provider.equals(provider))) {
+ return;
+ }
+
+ if (DEBUG) {
+ Log.d(TAG, "Updating People Space widget preview for user " + user.getIdentifier());
+ }
+ boolean success = mAppWidgetManager.setWidgetPreview(
+ provider, WIDGET_CATEGORY_HOME_SCREEN | WIDGET_CATEGORY_KEYGUARD,
+ new RemoteViews(mContext.getPackageName(),
+ R.layout.people_space_placeholder_layout));
+ if (DEBUG && !success) {
+ Log.d(TAG, "Failed to update generated preview for user " + user.getIdentifier());
+ }
+ mUpdatedPreviews.put(user.getIdentifier(), success);
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java b/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java
index 5d2aeef..b34b370 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java
@@ -432,6 +432,9 @@
for (int i = 0; i < NP; i++) {
mPages.get(i).removeAllViews();
}
+ if (mPageIndicator != null) {
+ mPageIndicator.setNumPages(numPages);
+ }
if (NP == numPages) {
return;
}
@@ -443,7 +446,6 @@
mLogger.d("Removing page");
mPages.remove(mPages.size() - 1);
}
- mPageIndicator.setNumPages(mPages.size());
setAdapter(mAdapter);
mAdapter.notifyDataSetChanged();
if (mPageToRestore != NO_PAGE) {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java
index 7a7ee59..00757b7 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java
@@ -127,8 +127,9 @@
}
- void initialize(QSLogger qsLogger) {
+ void initialize(QSLogger qsLogger, boolean usingMediaPlayer) {
mQsLogger = qsLogger;
+ mUsingMediaPlayer = usingMediaPlayer;
mTileLayout = getOrCreateTileLayout();
if (mUsingMediaPlayer) {
@@ -163,22 +164,25 @@
}
protected void setHorizontalContentContainerClipping() {
- mHorizontalContentContainer.setClipChildren(true);
- mHorizontalContentContainer.setClipToPadding(false);
- // Don't clip on the top, that way, secondary pages tiles can animate up
- // Clipping coordinates should be relative to this view, not absolute (parent coordinates)
- mHorizontalContentContainer.addOnLayoutChangeListener(
- (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
- if ((right - left) != (oldRight - oldLeft)
- || ((bottom - top) != (oldBottom - oldTop))) {
- mClippingRect.right = right - left;
- mClippingRect.bottom = bottom - top;
- mHorizontalContentContainer.setClipBounds(mClippingRect);
- }
- });
- mClippingRect.left = 0;
- mClippingRect.top = -1000;
- mHorizontalContentContainer.setClipBounds(mClippingRect);
+ if (mHorizontalContentContainer != null) {
+ mHorizontalContentContainer.setClipChildren(true);
+ mHorizontalContentContainer.setClipToPadding(false);
+ // Don't clip on the top, that way, secondary pages tiles can animate up
+ // Clipping coordinates should be relative to this view, not absolute
+ // (parent coordinates)
+ mHorizontalContentContainer.addOnLayoutChangeListener(
+ (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
+ if ((right - left) != (oldRight - oldLeft)
+ || ((bottom - top) != (oldBottom - oldTop))) {
+ mClippingRect.right = right - left;
+ mClippingRect.bottom = bottom - top;
+ mHorizontalContentContainer.setClipBounds(mClippingRect);
+ }
+ });
+ mClippingRect.left = 0;
+ mClippingRect.top = -1000;
+ mHorizontalContentContainer.setClipBounds(mClippingRect);
+ }
}
/**
@@ -412,7 +416,7 @@
}
private void updateHorizontalLinearLayoutMargins() {
- if (mHorizontalLinearLayout != null && !displayMediaMarginsOnMedia()) {
+ if (mUsingMediaPlayer && mHorizontalLinearLayout != null && !displayMediaMarginsOnMedia()) {
LayoutParams lp = (LayoutParams) mHorizontalLinearLayout.getLayoutParams();
lp.bottomMargin = Math.max(mMediaTotalBottomMargin - getPaddingBottom(), 0);
mHorizontalLinearLayout.setLayoutParams(lp);
@@ -461,6 +465,11 @@
/** Call when orientation has changed and MediaHost needs to be adjusted. */
private void reAttachMediaHost(ViewGroup hostView, boolean horizontal) {
if (!mUsingMediaPlayer) {
+ // If the host view was attached, detach it.
+ ViewGroup parent = (ViewGroup) hostView.getParent();
+ if (parent != null) {
+ parent.removeView(hostView);
+ }
return;
}
mMediaHostView = hostView;
@@ -492,8 +501,10 @@
public void setExpanded(boolean expanded) {
if (mExpanded == expanded) return;
mExpanded = expanded;
- if (!mExpanded && mTileLayout instanceof PagedTileLayout) {
- ((PagedTileLayout) mTileLayout).setCurrentItem(0, false);
+ if (!mExpanded && mTileLayout instanceof PagedTileLayout tilesLayout) {
+ // Use post, so it will wait until the view is attached. If the view is not attached,
+ // it will not populate corresponding views (and will not do it later when attached).
+ tilesLayout.post(() -> tilesLayout.setCurrentItem(0, false));
}
}
@@ -616,7 +627,10 @@
if (horizontal != mUsingHorizontalLayout || force) {
Log.d(getDumpableTag(), "setUsingHorizontalLayout: " + horizontal + ", " + force);
mUsingHorizontalLayout = horizontal;
- ViewGroup newParent = horizontal ? mHorizontalContentContainer : this;
+ // The tile layout should be reparented if horizontal and we are using media. If not
+ // using media, the parent should always be this.
+ ViewGroup newParent =
+ horizontal && mUsingMediaPlayer ? mHorizontalContentContainer : this;
switchAllContentToParent(newParent, mTileLayout);
reAttachMediaHost(mediaHostView, horizontal);
if (needsDynamicRowsAndColumns()) {
@@ -624,7 +638,9 @@
mTileLayout.setMaxColumns(horizontal ? 2 : 4);
}
updateMargins(mediaHostView);
- mHorizontalLinearLayout.setVisibility(horizontal ? View.VISIBLE : View.GONE);
+ if (mHorizontalLinearLayout != null) {
+ mHorizontalLinearLayout.setVisibility(horizontal ? View.VISIBLE : View.GONE);
+ }
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java
index 5e12b9d..d8e8187 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java
@@ -167,7 +167,7 @@
@Override
protected void onInit() {
- mView.initialize(mQSLogger);
+ mView.initialize(mQSLogger, mUsingMediaPlayer);
mQSLogger.logAllTilesChangeListening(mView.isListening(), mView.getDumpableTag(), "");
mHost.addCallback(mQSHostCallback);
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/BluetoothTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/BluetoothTile.java
index 18d2f30..b0707db 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/BluetoothTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/BluetoothTile.java
@@ -111,7 +111,7 @@
@Override
protected void handleClick(@Nullable View view) {
if (mFeatureFlags.isEnabled(Flags.BLUETOOTH_QS_TILE_DIALOG)) {
- mDialogViewModel.showDialog(mContext, view);
+ mDialogViewModel.showDialog(view);
} else {
// Secondary clicks are header clicks, just toggle.
final boolean isEnabled = mState.value;
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/RecordIssueTile.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/RecordIssueTile.kt
index d82b175..b418a17 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/RecordIssueTile.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/RecordIssueTile.kt
@@ -44,6 +44,7 @@
import com.android.systemui.qs.pipeline.domain.interactor.PanelInteractor
import com.android.systemui.qs.tileimpl.QSTileImpl
import com.android.systemui.recordissue.IssueRecordingService
+import com.android.systemui.recordissue.IssueRecordingState
import com.android.systemui.recordissue.RecordIssueDialogDelegate
import com.android.systemui.res.R
import com.android.systemui.screenrecord.RecordingService
@@ -69,6 +70,7 @@
private val dialogTransitionAnimator: DialogTransitionAnimator,
private val panelInteractor: PanelInteractor,
private val userContextProvider: UserContextProvider,
+ private val issueRecordingState: IssueRecordingState,
private val delegateFactory: RecordIssueDialogDelegate.Factory,
) :
QSTileImpl<QSTile.BooleanState>(
@@ -83,7 +85,16 @@
qsLogger
) {
- @VisibleForTesting var isRecording: Boolean = false
+ private val onRecordingChangeListener = Runnable { refreshState() }
+
+ override fun handleSetListening(listening: Boolean) {
+ super.handleSetListening(listening)
+ if (listening) {
+ issueRecordingState.addListener(onRecordingChangeListener)
+ } else {
+ issueRecordingState.removeListener(onRecordingChangeListener)
+ }
+ }
override fun getTileLabel(): CharSequence = mContext.getString(R.string.qs_record_issue_label)
@@ -103,13 +114,11 @@
@VisibleForTesting
public override fun handleClick(view: View?) {
- if (isRecording) {
- isRecording = false
+ if (issueRecordingState.isRecording) {
stopIssueRecordingService()
} else {
mUiHandler.post { showPrompt(view) }
}
- refreshState()
}
private fun startIssueRecordingService(screenRecord: Boolean, winscopeTracing: Boolean) =
@@ -138,11 +147,9 @@
val dialog: AlertDialog =
delegateFactory
.create {
- isRecording = true
startIssueRecordingService(it.screenRecord, it.winscopeTracing)
dialogTransitionAnimator.disableAllCurrentDialogsExitAnimations()
panelInteractor.collapsePanels()
- refreshState()
}
.createDialog()
val dismissAction =
@@ -168,7 +175,7 @@
@VisibleForTesting
public override fun handleUpdateState(qsTileState: QSTile.BooleanState, arg: Any?) {
qsTileState.apply {
- if (isRecording) {
+ if (issueRecordingState.isRecording) {
value = true
state = Tile.STATE_ACTIVE
forceExpandIcon = false
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothAutoOnInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothAutoOnInteractor.kt
index 1247854..59fc81c 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothAutoOnInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothAutoOnInteractor.kt
@@ -19,8 +19,6 @@
import android.util.Log
import com.android.systemui.dagger.SysUISingleton
import javax.inject.Inject
-import kotlinx.coroutines.flow.distinctUntilChanged
-import kotlinx.coroutines.flow.map
/** Interactor class responsible for interacting with the Bluetooth Auto-On feature. */
@SysUISingleton
@@ -30,14 +28,10 @@
private val bluetoothAutoOnRepository: BluetoothAutoOnRepository,
) {
- val isEnabled = bluetoothAutoOnRepository.isAutoOn.map { it == ENABLED }.distinctUntilChanged()
+ val isEnabled = bluetoothAutoOnRepository.isAutoOn
- /**
- * Checks if the auto on value is present in the repository.
- *
- * @return `true` if a value is present (i.e, the feature is enabled by the Bluetooth server).
- */
- suspend fun isValuePresent(): Boolean = bluetoothAutoOnRepository.isValuePresent()
+ /** Checks if the auto on feature is supported. */
+ suspend fun isAutoOnSupported(): Boolean = bluetoothAutoOnRepository.isAutoOnSupported()
/**
* Sets enabled or disabled based on the provided value.
@@ -45,17 +39,14 @@
* @param value `true` to enable the feature, `false` to disable it.
*/
suspend fun setEnabled(value: Boolean) {
- if (!isValuePresent()) {
+ if (!isAutoOnSupported()) {
Log.e(TAG, "Trying to set toggle value while feature not available.")
} else {
- val newValue = if (value) ENABLED else DISABLED
- bluetoothAutoOnRepository.setAutoOn(newValue)
+ bluetoothAutoOnRepository.setAutoOn(value)
}
}
companion object {
private const val TAG = "BluetoothAutoOnInteractor"
- const val DISABLED = 0
- const val ENABLED = 1
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothAutoOnRepository.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothAutoOnRepository.kt
index f97fc38..9ee582a 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothAutoOnRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothAutoOnRepository.kt
@@ -16,22 +16,23 @@
package com.android.systemui.qs.tiles.dialog.bluetooth
+import android.bluetooth.BluetoothAdapter
+import android.util.Log
+import com.android.settingslib.bluetooth.BluetoothCallback
+import com.android.settingslib.bluetooth.LocalBluetoothManager
+import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
+import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Background
-import com.android.systemui.user.data.repository.UserRepository
-import com.android.systemui.util.settings.SecureSettings
-import com.android.systemui.util.settings.SettingsProxyExt.observerFlow
import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
-import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.distinctUntilChanged
-import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.flowOn
-import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.withContext
@@ -44,61 +45,87 @@
class BluetoothAutoOnRepository
@Inject
constructor(
- private val secureSettings: SecureSettings,
- private val userRepository: UserRepository,
+ localBluetoothManager: LocalBluetoothManager?,
+ private val bluetoothAdapter: BluetoothAdapter?,
@Application private val coroutineScope: CoroutineScope,
@Background private val backgroundDispatcher: CoroutineDispatcher,
) {
- // Flow representing the auto on setting value for the current user
- @OptIn(ExperimentalCoroutinesApi::class)
- internal val isAutoOn: StateFlow<Int> =
- userRepository.selectedUserInfo
- .flatMapLatest { userInfo ->
- secureSettings
- .observerFlow(userInfo.id, SETTING_NAME)
- .onStart { emit(Unit) }
- .map { secureSettings.getIntForUser(SETTING_NAME, UNSET, userInfo.id) }
- }
- .distinctUntilChanged()
- .flowOn(backgroundDispatcher)
- .stateIn(
- coroutineScope,
- SharingStarted.WhileSubscribed(replayExpirationMillis = 0),
- UNSET
- )
+ // Flow representing the auto on state for the current user
+ internal val isAutoOn: Flow<Boolean> =
+ localBluetoothManager?.eventManager?.let { eventManager ->
+ conflatedCallbackFlow {
+ val listener =
+ object : BluetoothCallback {
+ override fun onAutoOnStateChanged(autoOnState: Int) {
+ super.onAutoOnStateChanged(autoOnState)
+ if (
+ autoOnState == BluetoothAdapter.AUTO_ON_STATE_ENABLED ||
+ autoOnState == BluetoothAdapter.AUTO_ON_STATE_DISABLED
+ ) {
+ trySendWithFailureLogging(
+ autoOnState == BluetoothAdapter.AUTO_ON_STATE_ENABLED,
+ TAG,
+ "onAutoOnStateChanged"
+ )
+ }
+ }
+ }
+ eventManager.registerCallback(listener)
+ awaitClose { eventManager.unregisterCallback(listener) }
+ }
+ .onStart { emit(isAutoOnEnabled()) }
+ .flowOn(backgroundDispatcher)
+ .stateIn(
+ coroutineScope,
+ SharingStarted.WhileSubscribed(replayExpirationMillis = 0),
+ initialValue = false
+ )
+ }
+ ?: flowOf(false)
/**
- * Checks if the auto on setting value is ever set for the current user.
+ * Checks if the auto on feature is supported for the current user.
*
- * @return `true` if the setting value is not UNSET, `false` otherwise.
+ * @throws Exception if an error occurs while checking auto-on support.
*/
- suspend fun isValuePresent(): Boolean =
+ suspend fun isAutoOnSupported(): Boolean =
withContext(backgroundDispatcher) {
- secureSettings.getIntForUser(
- SETTING_NAME,
- UNSET,
- userRepository.getSelectedUserInfo().id
- ) != UNSET
+ try {
+ bluetoothAdapter?.isAutoOnSupported ?: false
+ } catch (e: Exception) {
+ // Server could throw TimeoutException, InterruptedException or ExecutionException
+ Log.e(TAG, "Error calling isAutoOnSupported", e)
+ false
+ }
}
- /**
- * Sets the Bluetooth Auto-On setting value for the current user.
- *
- * @param value The new setting value to be applied.
- */
- suspend fun setAutoOn(value: Int) {
+ /** Sets the Bluetooth Auto-On for the current user. */
+ suspend fun setAutoOn(value: Boolean) {
withContext(backgroundDispatcher) {
- secureSettings.putIntForUser(
- SETTING_NAME,
- value,
- userRepository.getSelectedUserInfo().id
- )
+ try {
+ bluetoothAdapter?.setAutoOnEnabled(value)
+ } catch (e: Exception) {
+ // Server could throw IllegalStateException, TimeoutException, InterruptedException
+ // or ExecutionException
+ Log.e(TAG, "Error calling setAutoOnEnabled", e)
+ }
}
}
- companion object {
- const val SETTING_NAME = "bluetooth_automatic_turn_on"
- const val UNSET = -1
+ private suspend fun isAutoOnEnabled() =
+ withContext(backgroundDispatcher) {
+ try {
+ bluetoothAdapter?.isAutoOnEnabled ?: false
+ } catch (e: Exception) {
+ // Server could throw IllegalStateException, TimeoutException, InterruptedException
+ // or ExecutionException
+ Log.e(TAG, "Error calling isAutoOnEnabled", e)
+ false
+ }
+ }
+
+ private companion object {
+ const val TAG = "BluetoothAutoOnRepository"
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogDelegate.kt
index 9d53703..a8d9e78 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogDelegate.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogDelegate.kt
@@ -16,7 +16,6 @@
package com.android.systemui.qs.tiles.dialog.bluetooth
-import android.content.Context
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
@@ -58,7 +57,6 @@
class BluetoothTileDialogDelegate
@AssistedInject
internal constructor(
- @Assisted private val context: Context,
@Assisted private val initialUiProperties: BluetoothTileDialogViewModel.UiProperties,
@Assisted private val cachedContentHeight: Int,
@Assisted private val bluetoothToggleInitialValue: Boolean,
@@ -69,11 +67,8 @@
private val uiEventLogger: UiEventLogger,
private val logger: BluetoothTileDialogLogger,
private val systemuiDialogFactory: SystemUIDialog.Factory,
- mainLayoutInflater: LayoutInflater,
) : SystemUIDialog.Delegate {
- private val layoutInflater = mainLayoutInflater.cloneInContext(context)
-
private val mutableBluetoothStateToggle: MutableStateFlow<Boolean> =
MutableStateFlow(bluetoothToggleInitialValue)
internal val bluetoothStateToggle
@@ -102,7 +97,6 @@
@AssistedFactory
internal interface Factory {
fun create(
- context: Context,
initialUiProperties: BluetoothTileDialogViewModel.UiProperties,
cachedContentHeight: Int,
bluetoothEnabled: Boolean,
@@ -112,16 +106,15 @@
}
override fun createDialog(): SystemUIDialog {
- val dialog = systemuiDialogFactory.create(this, context)
-
- return dialog
+ return systemuiDialogFactory.create(this)
}
override fun onCreate(dialog: SystemUIDialog, savedInstanceState: Bundle?) {
SystemUIDialog.registerDismissListener(dialog, dismissListener)
uiEventLogger.log(BluetoothTileDialogUiEvent.BLUETOOTH_TILE_DIALOG_SHOWN)
+ val context = dialog.context
- layoutInflater.inflate(R.layout.bluetooth_tile_dialog, null).apply {
+ LayoutInflater.from(context).inflate(R.layout.bluetooth_tile_dialog, null).apply {
accessibilityPaneTitle = context.getText(R.string.accessibility_desc_quick_settings)
dialog.setContentView(this)
}
@@ -201,7 +194,7 @@
setEnabled(true)
alpha = ENABLED_ALPHA
}
- getSubtitleTextView(dialog).text = context.getString(uiProperties.subTitleResId)
+ getSubtitleTextView(dialog).text = dialog.context.getString(uiProperties.subTitleResId)
getAutoOnToggleView(dialog).visibility = uiProperties.autoOnToggleVisibility
}
@@ -215,7 +208,7 @@
setEnabled(true)
alpha = ENABLED_ALPHA
}
- getAutoOnToggleInfoTextView(dialog).text = context.getString(infoResId)
+ getAutoOnToggleInfoTextView(dialog).text = dialog.context.getString(infoResId)
}
private fun setupToggle(dialog: SystemUIDialog) {
@@ -288,7 +281,7 @@
private fun setupRecyclerView(dialog: SystemUIDialog) {
getDeviceListView(dialog).apply {
- layoutManager = LinearLayoutManager(context)
+ layoutManager = LinearLayoutManager(dialog.context)
adapter = deviceItemAdapter
}
}
@@ -343,7 +336,9 @@
private val asyncListDiffer = AsyncListDiffer(this, diffUtilCallback)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DeviceItemViewHolder {
- val view = layoutInflater.inflate(R.layout.bluetooth_device_item, parent, false)
+ val view =
+ LayoutInflater.from(parent.context)
+ .inflate(R.layout.bluetooth_device_item, parent, false)
return DeviceItemViewHolder(view)
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogViewModel.kt
index e4f3c19..fd624d2 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogViewModel.kt
@@ -16,7 +16,6 @@
package com.android.systemui.qs.tiles.dialog.bluetooth
-import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import android.os.Bundle
@@ -29,7 +28,6 @@
import androidx.annotation.VisibleForTesting
import com.android.internal.jank.InteractionJankMonitor
import com.android.internal.logging.UiEventLogger
-import com.android.settingslib.flags.Flags.bluetoothQsTileDialogAutoOnToggle
import com.android.systemui.Prefs
import com.android.systemui.animation.DialogCuj
import com.android.systemui.animation.DialogTransitionAnimator
@@ -78,19 +76,19 @@
/**
* Shows the dialog.
*
- * @param context The context in which the dialog is displayed.
* @param view The view from which the dialog is shown.
*/
@kotlinx.coroutines.ExperimentalCoroutinesApi
- fun showDialog(context: Context, view: View?) {
+ fun showDialog(view: View?) {
cancelJob()
job =
coroutineScope.launch(mainDispatcher) {
var updateDeviceItemJob: Job?
var updateDialogUiJob: Job? = null
- val dialogDelegate = createBluetoothTileDialog(context)
+ val dialogDelegate = createBluetoothTileDialog()
val dialog = dialogDelegate.createDialog()
+ val context = dialog.context
view?.let {
dialogTransitionAnimator.showFromView(
@@ -213,7 +211,7 @@
}
}
- private suspend fun createBluetoothTileDialog(context: Context): BluetoothTileDialogDelegate {
+ private suspend fun createBluetoothTileDialog(): BluetoothTileDialogDelegate {
val cachedContentHeight =
withContext(backgroundDispatcher) {
sharedPreferences.getInt(
@@ -223,7 +221,6 @@
}
return bluetoothDialogDelegateFactory.create(
- context,
UiProperties.build(
bluetoothStateInteractor.isBluetoothEnabled,
isAutoOnToggleFeatureAvailable()
@@ -277,7 +274,7 @@
@VisibleForTesting
internal suspend fun isAutoOnToggleFeatureAvailable() =
- bluetoothQsTileDialogAutoOnToggle() && bluetoothAutoOnInteractor.isValuePresent()
+ bluetoothAutoOnInteractor.isAutoOnSupported()
companion object {
private const val INTERACTION_JANK_TAG = "bluetooth_tile_dialog"
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/alarm/domain/AlarmTileMapper.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/alarm/domain/AlarmTileMapper.kt
index 2b8c335..c0fc52e 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/alarm/domain/AlarmTileMapper.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/alarm/domain/AlarmTileMapper.kt
@@ -83,6 +83,7 @@
}
}
+ sideViewIcon = QSTileState.SideViewIcon.Chevron
contentDescription = label
supportedActions = setOf(QSTileState.UserAction.CLICK)
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/saver/domain/DataSaverDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/saver/domain/DataSaverDialogDelegate.kt
index fc42ba4..b25c61c 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/saver/domain/DataSaverDialogDelegate.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/saver/domain/DataSaverDialogDelegate.kt
@@ -39,7 +39,7 @@
return sysuiDialogFactory.create(this, context)
}
- override fun onCreate(dialog: SystemUIDialog, savedInstanceState: Bundle?) {
+ override fun beforeCreate(dialog: SystemUIDialog, savedInstanceState: Bundle?) {
with(dialog) {
setTitle(R.string.data_saver_enable_title)
setMessage(R.string.data_saver_description)
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/work/domain/interactor/WorkModeTileDataInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/work/domain/interactor/WorkModeTileDataInteractor.kt
new file mode 100644
index 0000000..a2a9e87a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/work/domain/interactor/WorkModeTileDataInteractor.kt
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.tiles.impl.work.domain.interactor
+
+import android.os.UserHandle
+import com.android.systemui.qs.tiles.base.interactor.DataUpdateTrigger
+import com.android.systemui.qs.tiles.base.interactor.QSTileDataInteractor
+import com.android.systemui.qs.tiles.impl.work.domain.model.WorkModeTileModel
+import com.android.systemui.statusbar.phone.ManagedProfileController
+import com.android.systemui.util.kotlin.hasActiveWorkProfile
+import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.map
+
+/** Observes data saver state changes providing the [WorkModeTileModel]. */
+class WorkModeTileDataInteractor
+@Inject
+constructor(
+ private val profileController: ManagedProfileController,
+) : QSTileDataInteractor<WorkModeTileModel> {
+ override fun tileData(
+ user: UserHandle,
+ triggers: Flow<DataUpdateTrigger>
+ ): Flow<WorkModeTileModel> =
+ profileController.hasActiveWorkProfile.map { hasActiveWorkProfile: Boolean ->
+ if (hasActiveWorkProfile) {
+ WorkModeTileModel.HasActiveProfile(profileController.isWorkModeEnabled)
+ } else {
+ WorkModeTileModel.NoActiveProfile
+ }
+ }
+
+ override fun availability(user: UserHandle): Flow<Boolean> =
+ profileController.hasActiveWorkProfile
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/work/domain/interactor/WorkModeTileUserActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/work/domain/interactor/WorkModeTileUserActionInteractor.kt
new file mode 100644
index 0000000..f765f8b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/work/domain/interactor/WorkModeTileUserActionInteractor.kt
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.tiles.impl.work.domain.interactor
+
+import android.content.Intent
+import android.provider.Settings
+import com.android.systemui.qs.tiles.base.actions.QSTileIntentUserInputHandler
+import com.android.systemui.qs.tiles.base.interactor.QSTileInput
+import com.android.systemui.qs.tiles.base.interactor.QSTileUserActionInteractor
+import com.android.systemui.qs.tiles.impl.work.domain.model.WorkModeTileModel
+import com.android.systemui.qs.tiles.viewmodel.QSTileUserAction
+import com.android.systemui.statusbar.phone.ManagedProfileController
+import javax.inject.Inject
+
+/** Handles airplane mode tile clicks and long clicks. */
+class WorkModeTileUserActionInteractor
+@Inject
+constructor(
+ private val profileController: ManagedProfileController,
+ private val qsTileIntentUserActionHandler: QSTileIntentUserInputHandler,
+) : QSTileUserActionInteractor<WorkModeTileModel> {
+ override suspend fun handleInput(input: QSTileInput<WorkModeTileModel>) =
+ with(input) {
+ when (action) {
+ is QSTileUserAction.Click -> {
+ if (data is WorkModeTileModel.HasActiveProfile) {
+ profileController.setWorkModeEnabled(!data.isEnabled)
+ }
+ }
+ is QSTileUserAction.LongClick -> {
+ if (data is WorkModeTileModel.HasActiveProfile) {
+ qsTileIntentUserActionHandler.handle(
+ action.view,
+ Intent(Settings.ACTION_MANAGED_PROFILE_SETTINGS)
+ )
+ }
+ }
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/work/domain/model/WorkModeTileModel.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/work/domain/model/WorkModeTileModel.kt
new file mode 100644
index 0000000..ae8382d
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/work/domain/model/WorkModeTileModel.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.tiles.impl.work.domain.model
+
+/** Work mode tile model. */
+sealed interface WorkModeTileModel {
+ /** @param isEnabled is true when the work mode is enabled */
+ data class HasActiveProfile(val isEnabled: Boolean) : WorkModeTileModel
+ data object NoActiveProfile : WorkModeTileModel
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/work/ui/WorkModeTileMapper.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/work/ui/WorkModeTileMapper.kt
new file mode 100644
index 0000000..55445bb
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/work/ui/WorkModeTileMapper.kt
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.tiles.impl.work.ui
+
+import android.app.admin.DevicePolicyManager
+import android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_WORK_PROFILE_LABEL
+import android.content.res.Resources
+import android.service.quicksettings.Tile
+import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.qs.tiles.base.interactor.QSTileDataToStateMapper
+import com.android.systemui.qs.tiles.impl.work.domain.model.WorkModeTileModel
+import com.android.systemui.qs.tiles.viewmodel.QSTileConfig
+import com.android.systemui.qs.tiles.viewmodel.QSTileState
+import com.android.systemui.res.R
+import javax.inject.Inject
+
+/** Maps [WorkModeTileModel] to [QSTileState]. */
+class WorkModeTileMapper
+@Inject
+constructor(
+ @Main private val resources: Resources,
+ private val theme: Resources.Theme,
+ private val devicePolicyManager: DevicePolicyManager,
+) : QSTileDataToStateMapper<WorkModeTileModel> {
+ override fun map(config: QSTileConfig, data: WorkModeTileModel): QSTileState =
+ QSTileState.build(resources, theme, config.uiConfig) {
+ label = getTileLabel()!!
+ contentDescription = label
+
+ icon = {
+ Icon.Loaded(
+ resources.getDrawable(
+ com.android.internal.R.drawable.stat_sys_managed_profile_status,
+ theme
+ ),
+ contentDescription = null
+ )
+ }
+
+ when (data) {
+ is WorkModeTileModel.HasActiveProfile -> {
+ if (data.isEnabled) {
+ activationState = QSTileState.ActivationState.ACTIVE
+ secondaryLabel = ""
+ } else {
+ activationState = QSTileState.ActivationState.INACTIVE
+ secondaryLabel =
+ resources.getString(R.string.quick_settings_work_mode_paused_state)
+ }
+ supportedActions =
+ setOf(QSTileState.UserAction.CLICK, QSTileState.UserAction.LONG_CLICK)
+ }
+ is WorkModeTileModel.NoActiveProfile -> {
+ activationState = QSTileState.ActivationState.UNAVAILABLE
+ secondaryLabel =
+ resources.getStringArray(R.array.tile_states_work)[Tile.STATE_UNAVAILABLE]
+ supportedActions = setOf()
+ }
+ }
+
+ sideViewIcon = QSTileState.SideViewIcon.None
+ }
+
+ private fun getTileLabel(): CharSequence? {
+ return devicePolicyManager.resources.getString(QS_WORK_PROFILE_LABEL) {
+ resources.getString(R.string.quick_settings_work_mode_label)
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/ui/adapter/QSSceneAdapter.kt b/packages/SystemUI/src/com/android/systemui/qs/ui/adapter/QSSceneAdapter.kt
index c1b2037..6710504 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/ui/adapter/QSSceneAdapter.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/ui/adapter/QSSceneAdapter.kt
@@ -23,16 +23,21 @@
import androidx.annotation.VisibleForTesting
import androidx.asynclayoutinflater.view.AsyncLayoutInflater
import com.android.settingslib.applications.InterestingConfigChanges
+import com.android.systemui.Dumpable
import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.dump.DumpManager
import com.android.systemui.plugins.qs.QSContainerController
import com.android.systemui.qs.QSContainerImpl
import com.android.systemui.qs.QSImpl
import com.android.systemui.qs.dagger.QSSceneComponent
import com.android.systemui.res.R
+import com.android.systemui.shade.domain.interactor.ShadeInteractor
+import com.android.systemui.shade.shared.model.ShadeMode
import com.android.systemui.util.kotlin.sample
+import java.io.PrintWriter
import javax.inject.Inject
import javax.inject.Provider
import kotlin.coroutines.resume
@@ -107,11 +112,17 @@
}
/** State for appearing QQS from Lockscreen or Gone */
- data class Unsquishing(override val squishiness: Float) : State {
+ data class UnsquishingQQS(override val squishiness: Float) : State {
override val isVisible = true
override val expansion = 0f
}
+ /** State for appearing QS from Lockscreen or Gone, used in Split shade */
+ data class UnsquishingQS(override val squishiness: Float) : State {
+ override val isVisible = true
+ override val expansion = 1f
+ }
+
companion object {
// These are special cases of the expansion.
val QQS = Expanding(0f)
@@ -129,22 +140,28 @@
constructor(
private val qsSceneComponentFactory: QSSceneComponent.Factory,
private val qsImplProvider: Provider<QSImpl>,
+ shadeInteractor: ShadeInteractor,
+ dumpManager: DumpManager,
@Main private val mainDispatcher: CoroutineDispatcher,
@Application applicationScope: CoroutineScope,
private val configurationInteractor: ConfigurationInteractor,
private val asyncLayoutInflaterFactory: (Context) -> AsyncLayoutInflater,
-) : QSContainerController, QSSceneAdapter {
+) : QSContainerController, QSSceneAdapter, Dumpable {
@Inject
constructor(
qsSceneComponentFactory: QSSceneComponent.Factory,
qsImplProvider: Provider<QSImpl>,
+ shadeInteractor: ShadeInteractor,
+ dumpManager: DumpManager,
@Main dispatcher: CoroutineDispatcher,
@Application scope: CoroutineScope,
configurationInteractor: ConfigurationInteractor,
) : this(
qsSceneComponentFactory,
qsImplProvider,
+ shadeInteractor,
+ dumpManager,
dispatcher,
scope,
configurationInteractor,
@@ -182,6 +199,7 @@
)
init {
+ dumpManager.registerDumpable(this)
applicationScope.launch {
launch {
state.sample(_isCustomizing, ::Pair).collect { (state, customizing) ->
@@ -210,6 +228,11 @@
it.second.applyBottomNavBarToCustomizerPadding(it.first)
}
}
+ launch {
+ shadeInteractor.shadeMode.collect {
+ qsImpl.value?.setInSplitShade(it == ShadeMode.Split)
+ }
+ }
}
}
@@ -256,9 +279,17 @@
private fun QSImpl.applyState(state: QSSceneAdapter.State) {
setQsVisible(state.isVisible)
- setExpanded(state.isVisible)
+ setExpanded(state.isVisible && state.expansion > 0f)
setListening(state.isVisible)
setQsExpansion(state.expansion, 1f, 0f, state.squishiness)
- setTransitionToFullShadeProgress(false, 1f, state.squishiness)
+ }
+
+ override fun dump(pw: PrintWriter, args: Array<out String>) {
+ pw.apply {
+ println("Last state: ${state.value}")
+ println("Customizing: ${isCustomizing.value}")
+ println("QQS height: $qqsHeight")
+ println("QS height: $qsHeight")
+ }
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModel.kt
index 34f66b8..c695d4c 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModel.kt
@@ -48,6 +48,8 @@
qsSceneAdapter.isCustomizing.map { customizing ->
if (customizing) {
mapOf<UserAction, UserActionResult>(Back to UserActionResult(Scenes.QuickSettings))
+ // TODO(b/330200163) Add an Up from Bottom to be able to collapse the shade
+ // while customizing
} else {
mapOf(
Back to UserActionResult(Scenes.Shade),
diff --git a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
index d0ff338..7c1a2c0 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
@@ -86,7 +86,6 @@
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.dump.DumpManager;
-import com.android.systemui.flags.FeatureFlags;
import com.android.systemui.keyguard.KeyguardUnlockAnimationController;
import com.android.systemui.keyguard.KeyguardWmStateRefactor;
import com.android.systemui.keyguard.WakefulnessLifecycle;
@@ -146,7 +145,6 @@
private static final long MAX_BACKOFF_MILLIS = 10 * 60 * 1000;
private final Context mContext;
- private final FeatureFlags mFeatureFlags;
private final SceneContainerFlags mSceneContainerFlags;
private final Executor mMainExecutor;
private final ShellInterface mShellInterface;
@@ -209,8 +207,10 @@
@Override
public void onStatusBarTouchEvent(MotionEvent event) {
verifyCallerAndClearCallingIdentity("onStatusBarTouchEvent", () -> {
- // TODO move this logic to message queue
- if (event.getActionMasked() == ACTION_DOWN) {
+ if (mSceneContainerFlags.isEnabled()) {
+ //TODO(b/329863123) implement latency tracking for shade scene
+ Log.i(TAG_OPS, "Scene container enabled. Latency tracking not started.");
+ } else if (event.getActionMasked() == ACTION_DOWN) {
mShadeViewControllerLazy.get().startExpandLatencyTracking();
}
mHandler.post(() -> {
@@ -600,7 +600,6 @@
KeyguardUnlockAnimationController sysuiUnlockAnimationController,
InWindowLauncherUnlockAnimationManager inWindowLauncherUnlockAnimationManager,
AssistUtils assistUtils,
- FeatureFlags featureFlags,
SceneContainerFlags sceneContainerFlags,
DumpManager dumpManager,
Optional<UnfoldTransitionProgressForwarder> unfoldTransitionProgressForwarder,
@@ -613,7 +612,6 @@
}
mContext = context;
- mFeatureFlags = featureFlags;
mSceneContainerFlags = sceneContainerFlags;
mMainExecutor = mainExecutor;
mShellInterface = shellInterface;
diff --git a/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingService.kt b/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingService.kt
index 7009816..5e4919d 100644
--- a/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingService.kt
+++ b/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingService.kt
@@ -59,6 +59,7 @@
keyguardDismissUtil: KeyguardDismissUtil,
private val dialogTransitionAnimator: DialogTransitionAnimator,
private val panelInteractor: PanelInteractor,
+ private val issueRecordingState: IssueRecordingState,
) :
RecordingService(
controller,
@@ -90,6 +91,7 @@
DEFAULT_MAX_TRACE_SIZE,
DEFAULT_MAX_TRACE_DURATION_IN_MINUTES
)
+ issueRecordingState.isRecording = true
if (!intent.getBooleanExtra(EXTRA_SCREEN_RECORD, false)) {
// If we don't want to record the screen, the ACTION_SHOW_START_NOTIF action
// will circumvent the RecordingService's screen recording start code.
@@ -103,6 +105,7 @@
// this line should be removed.
getSystemService(LauncherApps::class.java)?.saveViewCaptureData()
TraceUtils.traceStop(contentResolver)
+ issueRecordingState.isRecording = false
}
ACTION_SHARE -> {
shareRecording(intent)
diff --git a/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingState.kt b/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingState.kt
new file mode 100644
index 0000000..394c5c2
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingState.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.recordissue
+
+import com.android.systemui.dagger.SysUISingleton
+import java.util.concurrent.CopyOnWriteArrayList
+import javax.inject.Inject
+
+@SysUISingleton
+class IssueRecordingState @Inject constructor() {
+
+ private val listeners = CopyOnWriteArrayList<Runnable>()
+
+ var isRecording = false
+ set(value) {
+ field = value
+ listeners.forEach(Runnable::run)
+ }
+
+ fun addListener(listener: Runnable) {
+ listeners.add(listener)
+ }
+
+ fun removeListener(listener: Runnable) {
+ listeners.remove(listener)
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/recordissue/RecordIssueDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/recordissue/RecordIssueDialogDelegate.kt
index 7313a49..832fc3f 100644
--- a/packages/SystemUI/src/com/android/systemui/recordissue/RecordIssueDialogDelegate.kt
+++ b/packages/SystemUI/src/com/android/systemui/recordissue/RecordIssueDialogDelegate.kt
@@ -17,6 +17,7 @@
package com.android.systemui.recordissue
import android.annotation.SuppressLint
+import android.app.AlertDialog
import android.content.Context
import android.content.res.ColorStateList
import android.graphics.Color
@@ -74,7 +75,6 @@
@SuppressLint("UseSwitchCompatOrMaterialCode") private lateinit var screenRecordSwitch: Switch
private lateinit var issueTypeButton: Button
- private var hasSelectedIssueType: Boolean = false
@MainThread
override fun beforeCreate(dialog: SystemUIDialog, savedInstanceState: Bundle?) {
@@ -86,15 +86,13 @@
setPositiveButton(
R.string.qs_record_issue_start,
{ _, _ ->
- if (hasSelectedIssueType) {
- onStarted.accept(
- IssueRecordingConfig(
- screenRecordSwitch.isChecked,
- true /* TODO: Base this on issueType selected */
- )
+ onStarted.accept(
+ IssueRecordingConfig(
+ screenRecordSwitch.isChecked,
+ true /* TODO: Base this on issueType selected */
)
- dismiss()
- }
+ )
+ dismiss()
},
false
)
@@ -115,8 +113,12 @@
bgExecutor.execute { onScreenRecordSwitchClicked() }
}
}
+ val startButton = dialog.getButton(AlertDialog.BUTTON_POSITIVE)
issueTypeButton = requireViewById(R.id.issue_type_button)
- issueTypeButton.setOnClickListener { onIssueTypeClicked(context) }
+ issueTypeButton.setOnClickListener {
+ onIssueTypeClicked(context) { startButton.isEnabled = true }
+ }
+ startButton.isEnabled = false
}
}
@@ -159,7 +161,7 @@
}
@MainThread
- private fun onIssueTypeClicked(context: Context) {
+ private fun onIssueTypeClicked(context: Context, onIssueTypeSelected: Runnable) {
val selectedCategory = issueTypeButton.text.toString()
val popupMenu = PopupMenu(context, issueTypeButton)
@@ -174,11 +176,11 @@
popupMenu.apply {
setOnMenuItemClickListener {
issueTypeButton.text = it.title
+ onIssueTypeSelected.run()
true
}
setForceShowIcon(true)
show()
}
- hasSelectedIssueType = true
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/scene/shared/flag/SceneContainerFlags.kt b/packages/SystemUI/src/com/android/systemui/scene/shared/flag/SceneContainerFlags.kt
index 467089d..54ec398 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/shared/flag/SceneContainerFlags.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/shared/flag/SceneContainerFlags.kt
@@ -18,18 +18,15 @@
package com.android.systemui.scene.shared.flag
-import com.android.systemui.Flags.FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR
-import com.android.systemui.Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR
-import com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT
import com.android.systemui.Flags.FLAG_SCENE_CONTAINER
-import com.android.systemui.Flags.keyguardBottomAreaRefactor
-import com.android.systemui.Flags.keyguardWmStateRefactor
-import com.android.systemui.Flags.migrateClocksToBlueprint
import com.android.systemui.Flags.sceneContainer
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.flags.FlagToken
import com.android.systemui.flags.Flags.SCENE_CONTAINER_ENABLED
import com.android.systemui.flags.RefactorFlagUtils
+import com.android.systemui.keyguard.KeyguardBottomAreaRefactor
+import com.android.systemui.keyguard.KeyguardWmStateRefactor
+import com.android.systemui.keyguard.MigrateClocksToBlueprint
import com.android.systemui.keyguard.shared.ComposeLockscreen
import com.android.systemui.media.controls.util.MediaInSceneContainerFlag
import dagger.Module
@@ -45,11 +42,11 @@
get() =
SCENE_CONTAINER_ENABLED && // mainStaticFlag
sceneContainer() && // mainAconfigFlag
- keyguardBottomAreaRefactor() &&
- migrateClocksToBlueprint() &&
+ KeyguardBottomAreaRefactor.isEnabled &&
+ MigrateClocksToBlueprint.isEnabled &&
ComposeLockscreen.isEnabled &&
MediaInSceneContainerFlag.isEnabled &&
- keyguardWmStateRefactor()
+ KeyguardWmStateRefactor.isEnabled
// NOTE: Changes should also be made in getSecondaryFlags and @EnableSceneContainer
/**
@@ -66,9 +63,9 @@
/** The set of secondary flags which must be enabled for scene container to work properly */
inline fun getSecondaryFlags(): Sequence<FlagToken> =
sequenceOf(
- FlagToken(FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR, keyguardBottomAreaRefactor()),
- FlagToken(FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT, migrateClocksToBlueprint()),
- FlagToken(FLAG_KEYGUARD_WM_STATE_REFACTOR, keyguardWmStateRefactor()),
+ KeyguardBottomAreaRefactor.token,
+ MigrateClocksToBlueprint.token,
+ KeyguardWmStateRefactor.token,
ComposeLockscreen.token,
MediaInSceneContainerFlag.token,
// NOTE: Changes should also be made in isEnabled and @EnableSceneContainer
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotActionsProvider.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotActionsProvider.kt
new file mode 100644
index 0000000..abdbd68
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotActionsProvider.kt
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.screenshot
+
+import android.content.Context
+import android.content.Intent
+import android.graphics.drawable.Drawable
+import android.net.Uri
+import android.os.UserHandle
+import androidx.appcompat.content.res.AppCompatResources
+import com.android.systemui.res.R
+import javax.inject.Inject
+
+/**
+ * Provides static actions for screenshots. This class can be overridden by a vendor-specific SysUI
+ * implementation.
+ */
+interface ScreenshotActionsProvider {
+ data class ScreenshotAction(
+ val icon: Drawable?,
+ val text: String?,
+ val overrideTransition: Boolean,
+ val retrieveIntent: (Uri) -> Intent
+ )
+
+ fun getPreviewAction(context: Context, uri: Uri, user: UserHandle): Intent
+ fun getActions(context: Context, user: UserHandle): List<ScreenshotAction>
+}
+
+class DefaultScreenshotActionsProvider @Inject constructor() : ScreenshotActionsProvider {
+ override fun getPreviewAction(context: Context, uri: Uri, user: UserHandle): Intent {
+ return ActionIntentCreator.createEdit(uri, context)
+ }
+
+ override fun getActions(
+ context: Context,
+ user: UserHandle
+ ): List<ScreenshotActionsProvider.ScreenshotAction> {
+ val editAction =
+ ScreenshotActionsProvider.ScreenshotAction(
+ AppCompatResources.getDrawable(context, R.drawable.ic_screenshot_edit),
+ context.resources.getString(R.string.screenshot_edit_label),
+ true
+ ) { uri ->
+ ActionIntentCreator.createEdit(uri, context)
+ }
+ val shareAction =
+ ScreenshotActionsProvider.ScreenshotAction(
+ AppCompatResources.getDrawable(context, R.drawable.ic_screenshot_share),
+ context.resources.getString(R.string.screenshot_share_label),
+ false
+ ) { uri ->
+ ActionIntentCreator.createShare(uri)
+ }
+ return listOf(editAction, shareAction)
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotShelfViewProxy.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotShelfViewProxy.kt
new file mode 100644
index 0000000..9354fd2
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotShelfViewProxy.kt
@@ -0,0 +1,226 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS 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.screenshot
+
+import android.animation.Animator
+import android.animation.AnimatorListenerAdapter
+import android.app.Notification
+import android.content.Context
+import android.graphics.Bitmap
+import android.graphics.Rect
+import android.view.KeyEvent
+import android.view.LayoutInflater
+import android.view.ScrollCaptureResponse
+import android.view.View
+import android.view.ViewTreeObserver
+import android.view.WindowInsets
+import android.window.OnBackInvokedCallback
+import android.window.OnBackInvokedDispatcher
+import com.android.internal.logging.UiEventLogger
+import com.android.systemui.log.DebugLogger.debugLog
+import com.android.systemui.res.R
+import com.android.systemui.screenshot.LogConfig.DEBUG_ACTIONS
+import com.android.systemui.screenshot.LogConfig.DEBUG_DISMISS
+import com.android.systemui.screenshot.LogConfig.DEBUG_INPUT
+import com.android.systemui.screenshot.LogConfig.DEBUG_WINDOW
+import com.android.systemui.screenshot.ScreenshotEvent.SCREENSHOT_DISMISSED_OTHER
+import com.android.systemui.screenshot.scroll.ScrollCaptureController
+import com.android.systemui.screenshot.ui.ScreenshotAnimationController
+import com.android.systemui.screenshot.ui.ScreenshotShelfView
+import com.android.systemui.screenshot.ui.binder.ScreenshotShelfViewBinder
+import com.android.systemui.screenshot.ui.viewmodel.ActionButtonViewModel
+import com.android.systemui.screenshot.ui.viewmodel.ScreenshotViewModel
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+
+/** Controls the screenshot view and viewModel. */
+class ScreenshotShelfViewProxy
+@AssistedInject
+constructor(
+ private val logger: UiEventLogger,
+ private val viewModel: ScreenshotViewModel,
+ private val staticActionsProvider: ScreenshotActionsProvider,
+ @Assisted private val context: Context,
+ @Assisted private val displayId: Int
+) : ScreenshotViewProxy {
+ override val view: ScreenshotShelfView =
+ LayoutInflater.from(context).inflate(R.layout.screenshot_shelf, null) as ScreenshotShelfView
+ override val screenshotPreview: View
+ override var packageName: String = ""
+ override var callbacks: ScreenshotView.ScreenshotViewCallback? = null
+ override var screenshot: ScreenshotData? = null
+ set(value) {
+ viewModel.setScreenshotBitmap(value?.bitmap)
+ field = value
+ }
+
+ override val isAttachedToWindow
+ get() = view.isAttachedToWindow
+ override var isDismissing = false
+ override var isPendingSharedTransition = false
+
+ private val animationController = ScreenshotAnimationController(view)
+
+ init {
+ ScreenshotShelfViewBinder.bind(view, viewModel, LayoutInflater.from(context))
+ addPredictiveBackListener { requestDismissal(SCREENSHOT_DISMISSED_OTHER) }
+ setOnKeyListener { requestDismissal(SCREENSHOT_DISMISSED_OTHER) }
+ debugLog(DEBUG_WINDOW) { "adding OnComputeInternalInsetsListener" }
+ screenshotPreview = view.screenshotPreview
+ }
+
+ override fun reset() {
+ animationController.cancel()
+ isPendingSharedTransition = false
+ viewModel.setScreenshotBitmap(null)
+ viewModel.setActions(listOf())
+ }
+ override fun updateInsets(insets: WindowInsets) {}
+ override fun updateOrientation(insets: WindowInsets) {}
+
+ override fun createScreenshotDropInAnimation(screenRect: Rect, showFlash: Boolean): Animator {
+ return animationController.getEntranceAnimation()
+ }
+
+ override fun addQuickShareChip(quickShareAction: Notification.Action) {}
+
+ override fun setChipIntents(imageData: ScreenshotController.SavedImageData) {
+ val staticActions =
+ staticActionsProvider.getActions(context, imageData.owner).map {
+ ActionButtonViewModel(it.icon, it.text) {
+ val intent = it.retrieveIntent(imageData.uri)
+ debugLog(DEBUG_ACTIONS) { "Action tapped: $intent" }
+ isPendingSharedTransition = true
+ callbacks?.onAction(intent, imageData.owner, it.overrideTransition)
+ }
+ }
+
+ viewModel.setActions(staticActions)
+ }
+
+ override fun requestDismissal(event: ScreenshotEvent) {
+ debugLog(DEBUG_DISMISS) { "screenshot dismissal requested: $event" }
+
+ // If we're already animating out, don't restart the animation
+ if (isDismissing) {
+ debugLog(DEBUG_DISMISS) { "Already dismissing, ignoring duplicate command $event" }
+ return
+ }
+ logger.log(event, 0, packageName)
+ val animator = animationController.getExitAnimation()
+ animator.addListener(
+ object : AnimatorListenerAdapter() {
+ override fun onAnimationStart(animator: Animator) {
+ isDismissing = true
+ }
+ override fun onAnimationEnd(animator: Animator) {
+ isDismissing = false
+ callbacks?.onDismiss()
+ }
+ }
+ )
+ animator.start()
+ }
+
+ override fun showScrollChip(packageName: String, onClick: Runnable) {}
+
+ override fun hideScrollChip() {}
+
+ override fun prepareScrollingTransition(
+ response: ScrollCaptureResponse,
+ screenBitmap: Bitmap,
+ newScreenshot: Bitmap,
+ screenshotTakenInPortrait: Boolean,
+ onTransitionPrepared: Runnable,
+ ) {}
+
+ override fun startLongScreenshotTransition(
+ transitionDestination: Rect,
+ onTransitionEnd: Runnable,
+ longScreenshot: ScrollCaptureController.LongScreenshot
+ ) {}
+
+ override fun restoreNonScrollingUi() {}
+
+ override fun stopInputListening() {}
+
+ override fun requestFocus() {
+ view.requestFocus()
+ }
+
+ override fun announceForAccessibility(string: String) = view.announceForAccessibility(string)
+
+ override fun prepareEntranceAnimation(runnable: Runnable) {
+ view.viewTreeObserver.addOnPreDrawListener(
+ object : ViewTreeObserver.OnPreDrawListener {
+ override fun onPreDraw(): Boolean {
+ debugLog(DEBUG_WINDOW) { "onPreDraw: startAnimation" }
+ view.viewTreeObserver.removeOnPreDrawListener(this)
+ runnable.run()
+ return true
+ }
+ }
+ )
+ }
+
+ private fun addPredictiveBackListener(onDismissRequested: (ScreenshotEvent) -> Unit) {
+ val onBackInvokedCallback = OnBackInvokedCallback {
+ debugLog(DEBUG_INPUT) { "Predictive Back callback dispatched" }
+ onDismissRequested.invoke(SCREENSHOT_DISMISSED_OTHER)
+ }
+ view.addOnAttachStateChangeListener(
+ object : View.OnAttachStateChangeListener {
+ override fun onViewAttachedToWindow(v: View) {
+ debugLog(DEBUG_INPUT) { "Registering Predictive Back callback" }
+ view
+ .findOnBackInvokedDispatcher()
+ ?.registerOnBackInvokedCallback(
+ OnBackInvokedDispatcher.PRIORITY_DEFAULT,
+ onBackInvokedCallback
+ )
+ }
+
+ override fun onViewDetachedFromWindow(view: View) {
+ debugLog(DEBUG_INPUT) { "Unregistering Predictive Back callback" }
+ view
+ .findOnBackInvokedDispatcher()
+ ?.unregisterOnBackInvokedCallback(onBackInvokedCallback)
+ }
+ }
+ )
+ }
+ private fun setOnKeyListener(onDismissRequested: (ScreenshotEvent) -> Unit) {
+ view.setOnKeyListener(
+ object : View.OnKeyListener {
+ override fun onKey(view: View, keyCode: Int, event: KeyEvent): Boolean {
+ if (keyCode == KeyEvent.KEYCODE_BACK || keyCode == KeyEvent.KEYCODE_ESCAPE) {
+ debugLog(DEBUG_INPUT) { "onKeyEvent: $keyCode" }
+ onDismissRequested.invoke(SCREENSHOT_DISMISSED_OTHER)
+ return true
+ }
+ return false
+ }
+ }
+ )
+ }
+
+ @AssistedFactory
+ interface Factory : ScreenshotViewProxy.Factory {
+ override fun getProxy(context: Context, displayId: Int): ScreenshotShelfViewProxy
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsService.java b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsService.java
index 06c0b8b..c89b476 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsService.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsService.java
@@ -33,6 +33,7 @@
import android.content.Intent.CaptureContentForNoteStatusCodes;
import android.content.res.Resources;
import android.os.IBinder;
+import android.util.Log;
import androidx.annotation.Nullable;
@@ -58,6 +59,8 @@
*/
public class AppClipsService extends Service {
+ private static final String TAG = AppClipsService.class.getSimpleName();
+
@Application private final Context mContext;
private final FeatureFlags mFeatureFlags;
private final Optional<Bubbles> mOptionalBubbles;
@@ -77,14 +80,22 @@
private boolean checkIndependentVariables() {
if (!mFeatureFlags.isEnabled(SCREENSHOT_APP_CLIPS)) {
+ Log.d(TAG, "Feature flag disabled");
return false;
}
if (mOptionalBubbles.isEmpty()) {
+ Log.d(TAG, "Bubbles not available");
return false;
}
- return isComponentValid();
+ if (isComponentValid()) {
+ Log.d(TAG, "checkIndependentVariables returned true");
+ return true;
+ }
+
+ Log.d(TAG, "checkIndependentVariables returned false");
+ return false;
}
private boolean isComponentValid() {
@@ -93,12 +104,27 @@
componentName = ComponentName.unflattenFromString(
mContext.getString(R.string.config_screenshotAppClipsActivityComponent));
} catch (Resources.NotFoundException e) {
+ Log.d(TAG, "AppClips activity component resource not defined");
return false;
}
- return componentName != null
- && !componentName.getPackageName().isEmpty()
- && !componentName.getClassName().isEmpty();
+ if (componentName == null) {
+ Log.d(TAG, "AppClips component name not defined");
+ return false;
+ }
+
+ if (componentName.getPackageName().isEmpty()) {
+ Log.d(TAG, "AppClips component package name is empty");
+ return false;
+ }
+
+ if (componentName.getClassName().isEmpty()) {
+ Log.d(TAG, "AppClips component class name is empty");
+ return false;
+ }
+
+ Log.d(TAG, "isComponentValid returned true");
+ return true;
}
@Nullable
@@ -107,24 +133,39 @@
return new IAppClipsService.Stub() {
@Override
public boolean canLaunchCaptureContentActivityForNote(int taskId) {
- return canLaunchCaptureContentActivityForNoteInternal(taskId)
- == CAPTURE_CONTENT_FOR_NOTE_SUCCESS;
+ if (canLaunchCaptureContentActivityForNoteInternal(taskId)
+ == CAPTURE_CONTENT_FOR_NOTE_SUCCESS) {
+ Log.d(TAG, String.format("Can launch AppClips returned true for %d", taskId));
+ return true;
+ }
+
+ Log.d(TAG, String.format("Can launch AppClips returned false for %d", taskId));
+ return false;
}
@Override
@CaptureContentForNoteStatusCodes
public int canLaunchCaptureContentActivityForNoteInternal(int taskId) {
if (!mAreTaskAndTimeIndependentPrerequisitesMet) {
+ Log.d(TAG,
+ String.format("Task (%d) and time independent prereqs not met", taskId));
return CAPTURE_CONTENT_FOR_NOTE_FAILED;
}
if (!mOptionalBubbles.get().isAppBubbleTaskId(taskId)) {
+ Log.d(TAG, String.format("Taskid %d is not app bubble task", taskId));
return CAPTURE_CONTENT_FOR_NOTE_WINDOW_MODE_UNSUPPORTED;
}
- return mDevicePolicyManager.getScreenCaptureDisabled(null)
- ? CAPTURE_CONTENT_FOR_NOTE_BLOCKED_BY_ADMIN
- : CAPTURE_CONTENT_FOR_NOTE_SUCCESS;
+ if (mDevicePolicyManager.getScreenCaptureDisabled(null)) {
+ Log.d(TAG,
+ String.format("Screen capture disabled by admin, taskId %d", taskId));
+ return CAPTURE_CONTENT_FOR_NOTE_BLOCKED_BY_ADMIN;
+ }
+
+ Log.d(TAG,
+ String.format("Can launch AppClips (internal) successful for %d", taskId));
+ return CAPTURE_CONTENT_FOR_NOTE_SUCCESS;
}
};
}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/dagger/ScreenshotModule.java b/packages/SystemUI/src/com/android/systemui/screenshot/dagger/ScreenshotModule.java
index cdb9abb..9118ee1 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/dagger/ScreenshotModule.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/dagger/ScreenshotModule.java
@@ -16,16 +16,23 @@
package com.android.systemui.screenshot.dagger;
-import android.app.Service;
+import static com.android.systemui.Flags.screenshotShelfUi;
+import android.app.Service;
+import android.view.accessibility.AccessibilityManager;
+
+import com.android.systemui.dagger.SysUISingleton;
+import com.android.systemui.screenshot.DefaultScreenshotActionsProvider;
import com.android.systemui.screenshot.ImageCapture;
import com.android.systemui.screenshot.ImageCaptureImpl;
import com.android.systemui.screenshot.LegacyScreenshotViewProxy;
import com.android.systemui.screenshot.RequestProcessor;
+import com.android.systemui.screenshot.ScreenshotActionsProvider;
import com.android.systemui.screenshot.ScreenshotPolicy;
import com.android.systemui.screenshot.ScreenshotPolicyImpl;
import com.android.systemui.screenshot.ScreenshotProxyService;
import com.android.systemui.screenshot.ScreenshotRequestProcessor;
+import com.android.systemui.screenshot.ScreenshotShelfViewProxy;
import com.android.systemui.screenshot.ScreenshotSoundController;
import com.android.systemui.screenshot.ScreenshotSoundControllerImpl;
import com.android.systemui.screenshot.ScreenshotSoundProvider;
@@ -34,6 +41,7 @@
import com.android.systemui.screenshot.TakeScreenshotService;
import com.android.systemui.screenshot.appclips.AppClipsScreenshotHelperService;
import com.android.systemui.screenshot.appclips.AppClipsService;
+import com.android.systemui.screenshot.ui.viewmodel.ScreenshotViewModel;
import dagger.Binds;
import dagger.Module;
@@ -85,9 +93,25 @@
abstract ScreenshotSoundController bindScreenshotSoundController(
ScreenshotSoundControllerImpl screenshotSoundProviderImpl);
+ @Binds
+ abstract ScreenshotActionsProvider bindScreenshotActionsProvider(
+ DefaultScreenshotActionsProvider defaultScreenshotActionsProvider);
+
+ @Provides
+ @SysUISingleton
+ static ScreenshotViewModel providesScreenshotViewModel(
+ AccessibilityManager accessibilityManager) {
+ return new ScreenshotViewModel(accessibilityManager);
+ }
+
@Provides
static ScreenshotViewProxy.Factory providesScreenshotViewProxyFactory(
+ ScreenshotShelfViewProxy.Factory shelfScreenshotViewProxyFactory,
LegacyScreenshotViewProxy.Factory legacyScreenshotViewProxyFactory) {
- return legacyScreenshotViewProxyFactory;
+ if (screenshotShelfUi()) {
+ return shelfScreenshotViewProxyFactory;
+ } else {
+ return legacyScreenshotViewProxyFactory;
+ }
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ui/ScreenshotAnimationController.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ui/ScreenshotAnimationController.kt
new file mode 100644
index 0000000..2c17873
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ui/ScreenshotAnimationController.kt
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.screenshot.ui
+
+import android.animation.Animator
+import android.animation.AnimatorListenerAdapter
+import android.animation.ValueAnimator
+import android.view.View
+
+class ScreenshotAnimationController(private val view: View) {
+ private var animator: Animator? = null
+
+ fun getEntranceAnimation(): Animator {
+ val animator = ValueAnimator.ofFloat(0f, 1f)
+ animator.addUpdateListener { view.alpha = it.animatedFraction }
+ animator.addListener(
+ object : AnimatorListenerAdapter() {
+ override fun onAnimationStart(animator: Animator) {
+ view.alpha = 0f
+ }
+ override fun onAnimationEnd(animator: Animator) {
+ view.alpha = 1f
+ }
+ }
+ )
+ this.animator = animator
+ return animator
+ }
+
+ fun getExitAnimation(): Animator {
+ val animator = ValueAnimator.ofFloat(1f, 0f)
+ animator.addUpdateListener { view.alpha = it.animatedValue as Float }
+ animator.addListener(
+ object : AnimatorListenerAdapter() {
+ override fun onAnimationStart(animator: Animator) {
+ view.alpha = 1f
+ }
+ override fun onAnimationEnd(animator: Animator) {
+ view.alpha = 0f
+ }
+ }
+ )
+ this.animator = animator
+ return animator
+ }
+
+ fun cancel() {
+ animator?.cancel()
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ui/ScreenshotShelfView.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ui/ScreenshotShelfView.kt
new file mode 100644
index 0000000..747ad4f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ui/ScreenshotShelfView.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.screenshot.ui
+
+import android.content.Context
+import android.util.AttributeSet
+import android.widget.ImageView
+import androidx.constraintlayout.widget.ConstraintLayout
+import com.android.systemui.res.R
+
+class ScreenshotShelfView(context: Context, attrs: AttributeSet? = null) :
+ ConstraintLayout(context, attrs) {
+ lateinit var screenshotPreview: ImageView
+
+ override fun onFinishInflate() {
+ super.onFinishInflate()
+ screenshotPreview = requireViewById(R.id.screenshot_preview)
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ui/binder/ActionButtonViewBinder.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ui/binder/ActionButtonViewBinder.kt
new file mode 100644
index 0000000..a5825b5
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ui/binder/ActionButtonViewBinder.kt
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.screenshot.ui.binder
+
+import android.view.View
+import android.widget.ImageView
+import android.widget.LinearLayout
+import android.widget.TextView
+import com.android.systemui.res.R
+import com.android.systemui.screenshot.ui.viewmodel.ActionButtonViewModel
+
+object ActionButtonViewBinder {
+ /** Binds the given view to the given view-model */
+ fun bind(view: View, viewModel: ActionButtonViewModel) {
+ val iconView = view.requireViewById<ImageView>(R.id.overlay_action_chip_icon)
+ val textView = view.requireViewById<TextView>(R.id.overlay_action_chip_text)
+ iconView.setImageDrawable(viewModel.icon)
+ textView.text = viewModel.name
+ setMargins(iconView, textView, viewModel.name?.isNotEmpty() ?: false)
+ if (viewModel.onClicked != null) {
+ view.setOnClickListener { viewModel.onClicked.invoke() }
+ } else {
+ view.setOnClickListener(null)
+ }
+ view.visibility = View.VISIBLE
+ view.alpha = 1f
+ }
+
+ private fun setMargins(iconView: View, textView: View, hasText: Boolean) {
+ val iconParams = iconView.layoutParams as LinearLayout.LayoutParams
+ val textParams = textView.layoutParams as LinearLayout.LayoutParams
+ if (hasText) {
+ iconParams.marginStart = iconView.dpToPx(R.dimen.overlay_action_chip_padding_start)
+ iconParams.marginEnd = iconView.dpToPx(R.dimen.overlay_action_chip_spacing)
+ textParams.marginStart = 0
+ textParams.marginEnd = textView.dpToPx(R.dimen.overlay_action_chip_padding_end)
+ } else {
+ val paddingHorizontal =
+ iconView.dpToPx(R.dimen.overlay_action_chip_icon_only_padding_horizontal)
+ iconParams.marginStart = paddingHorizontal
+ iconParams.marginEnd = paddingHorizontal
+ }
+ iconView.layoutParams = iconParams
+ textView.layoutParams = textParams
+ }
+
+ private fun View.dpToPx(dimenId: Int): Int {
+ return this.resources.getDimensionPixelSize(dimenId)
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ui/binder/ScreenshotShelfViewBinder.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ui/binder/ScreenshotShelfViewBinder.kt
new file mode 100644
index 0000000..3bcd52c
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ui/binder/ScreenshotShelfViewBinder.kt
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.screenshot.ui.binder
+
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.ImageView
+import android.widget.LinearLayout
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.repeatOnLifecycle
+import com.android.systemui.lifecycle.repeatWhenAttached
+import com.android.systemui.res.R
+import com.android.systemui.screenshot.ui.viewmodel.ScreenshotViewModel
+import com.android.systemui.util.children
+import kotlinx.coroutines.launch
+
+object ScreenshotShelfViewBinder {
+ fun bind(
+ view: ViewGroup,
+ viewModel: ScreenshotViewModel,
+ layoutInflater: LayoutInflater,
+ ) {
+ val previewView: ImageView = view.requireViewById(R.id.screenshot_preview)
+ val previewBorder = view.requireViewById<View>(R.id.screenshot_preview_border)
+ previewView.clipToOutline = true
+ val actionsContainer: LinearLayout = view.requireViewById(R.id.screenshot_actions)
+ view.requireViewById<View>(R.id.screenshot_dismiss_button).visibility =
+ if (viewModel.showDismissButton) View.VISIBLE else View.GONE
+
+ view.repeatWhenAttached {
+ lifecycleScope.launch {
+ repeatOnLifecycle(Lifecycle.State.STARTED) {
+ launch {
+ viewModel.preview.collect { bitmap ->
+ if (bitmap != null) {
+ previewView.setImageBitmap(bitmap)
+ previewView.visibility = View.VISIBLE
+ previewBorder.visibility = View.VISIBLE
+ } else {
+ previewView.visibility = View.GONE
+ previewBorder.visibility = View.GONE
+ }
+ }
+ }
+ launch {
+ viewModel.actions.collect { actions ->
+ if (actions.isNotEmpty()) {
+ view
+ .requireViewById<View>(R.id.actions_container_background)
+ .visibility = View.VISIBLE
+ }
+ val viewPool = actionsContainer.children.toList()
+ actionsContainer.removeAllViews()
+ val actionButtons =
+ List(actions.size) {
+ viewPool.getOrElse(it) {
+ layoutInflater.inflate(
+ R.layout.overlay_action_chip,
+ actionsContainer,
+ false
+ )
+ }
+ }
+ actionButtons.zip(actions).forEach {
+ actionsContainer.addView(it.first)
+ ActionButtonViewBinder.bind(it.first, it.second)
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ui/viewmodel/ActionButtonViewModel.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ui/viewmodel/ActionButtonViewModel.kt
new file mode 100644
index 0000000..6ee9705
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ui/viewmodel/ActionButtonViewModel.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.screenshot.ui.viewmodel
+
+import android.graphics.drawable.Drawable
+
+data class ActionButtonViewModel(
+ val icon: Drawable?,
+ val name: String?,
+ val onClicked: (() -> Unit)?
+)
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ui/viewmodel/ScreenshotViewModel.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ui/viewmodel/ScreenshotViewModel.kt
new file mode 100644
index 0000000..3a652d9
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ui/viewmodel/ScreenshotViewModel.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.screenshot.ui.viewmodel
+
+import android.graphics.Bitmap
+import android.view.accessibility.AccessibilityManager
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+
+class ScreenshotViewModel(private val accessibilityManager: AccessibilityManager) {
+ private val _preview = MutableStateFlow<Bitmap?>(null)
+ val preview: StateFlow<Bitmap?> = _preview
+ private val _actions = MutableStateFlow(emptyList<ActionButtonViewModel>())
+ val actions: StateFlow<List<ActionButtonViewModel>> = _actions
+ val showDismissButton: Boolean
+ get() = accessibilityManager.isEnabled
+
+ fun setScreenshotBitmap(bitmap: Bitmap?) {
+ _preview.value = bitmap
+ }
+
+ fun setActions(actions: List<ActionButtonViewModel>) {
+ _actions.value = actions
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
index 3a2a081..f928ccc 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
@@ -24,8 +24,6 @@
import static com.android.app.animation.Interpolators.EMPHASIZED_DECELERATE;
import static com.android.keyguard.KeyguardClockSwitch.LARGE;
import static com.android.keyguard.KeyguardClockSwitch.SMALL;
-import static com.android.systemui.Flags.keyguardBottomAreaRefactor;
-import static com.android.systemui.Flags.migrateClocksToBlueprint;
import static com.android.systemui.Flags.predictiveBackAnimateShade;
import static com.android.systemui.Flags.smartspaceRelocateToBottom;
import static com.android.systemui.classifier.Classifier.BOUNCER_UNLOCK;
@@ -129,8 +127,10 @@
import com.android.systemui.flags.FeatureFlags;
import com.android.systemui.flags.Flags;
import com.android.systemui.fragments.FragmentService;
+import com.android.systemui.keyguard.KeyguardBottomAreaRefactor;
import com.android.systemui.keyguard.KeyguardUnlockAnimationController;
import com.android.systemui.keyguard.KeyguardViewConfigurator;
+import com.android.systemui.keyguard.MigrateClocksToBlueprint;
import com.android.systemui.keyguard.domain.interactor.KeyguardBottomAreaInteractor;
import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor;
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor;
@@ -193,6 +193,7 @@
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
import com.android.systemui.statusbar.notification.row.ExpandableView;
import com.android.systemui.statusbar.notification.row.NotificationGutsManager;
+import com.android.systemui.statusbar.notification.shared.NotificationsHeadsUpRefactor;
import com.android.systemui.statusbar.notification.stack.AmbientState;
import com.android.systemui.statusbar.notification.stack.AnimationProperties;
import com.android.systemui.statusbar.notification.stack.NotificationListContainer;
@@ -1018,7 +1019,7 @@
instantCollapse();
} else {
mView.animate().cancel();
- if (!migrateClocksToBlueprint()) {
+ if (!MigrateClocksToBlueprint.isEnabled()) {
mView.animate()
.alpha(0f)
.setStartDelay(0)
@@ -1075,7 +1076,7 @@
mQsController.init();
mShadeHeadsUpTracker.addTrackingHeadsUpListener(
mNotificationStackScrollLayoutController::setTrackingHeadsUp);
- if (!keyguardBottomAreaRefactor()) {
+ if (!KeyguardBottomAreaRefactor.isEnabled()) {
setKeyguardBottomArea(mView.findViewById(R.id.keyguard_bottom_area));
}
@@ -1154,7 +1155,7 @@
// Occluded->Lockscreen
collectFlow(mView, mKeyguardTransitionInteractor.getOccludedToLockscreenTransition(),
mOccludedToLockscreenTransition, mMainDispatcher);
- if (!migrateClocksToBlueprint()) {
+ if (!MigrateClocksToBlueprint.isEnabled()) {
collectFlow(mView, mOccludedToLockscreenTransitionViewModel.getLockscreenAlpha(),
setTransitionAlpha(mNotificationStackScrollLayoutController), mMainDispatcher);
collectFlow(mView,
@@ -1165,7 +1166,7 @@
// Lockscreen->Dreaming
collectFlow(mView, mKeyguardTransitionInteractor.getLockscreenToDreamingTransition(),
mLockscreenToDreamingTransition, mMainDispatcher);
- if (!migrateClocksToBlueprint()) {
+ if (!MigrateClocksToBlueprint.isEnabled()) {
collectFlow(mView, mLockscreenToDreamingTransitionViewModel.getLockscreenAlpha(),
setDreamLockscreenTransitionAlpha(mNotificationStackScrollLayoutController),
mMainDispatcher);
@@ -1177,7 +1178,7 @@
// Gone->Dreaming
collectFlow(mView, mKeyguardTransitionInteractor.getGoneToDreamingTransition(),
mGoneToDreamingTransition, mMainDispatcher);
- if (!migrateClocksToBlueprint()) {
+ if (!MigrateClocksToBlueprint.isEnabled()) {
collectFlow(mView, mGoneToDreamingTransitionViewModel.getLockscreenAlpha(),
setTransitionAlpha(mNotificationStackScrollLayoutController), mMainDispatcher);
}
@@ -1188,7 +1189,7 @@
// Lockscreen->Occluded
collectFlow(mView, mKeyguardTransitionInteractor.getLockscreenToOccludedTransition(),
mLockscreenToOccludedTransition, mMainDispatcher);
- if (!migrateClocksToBlueprint()) {
+ if (!MigrateClocksToBlueprint.isEnabled()) {
collectFlow(mView, mLockscreenToOccludedTransitionViewModel.getLockscreenAlpha(),
setTransitionAlpha(mNotificationStackScrollLayoutController), mMainDispatcher);
collectFlow(mView, mLockscreenToOccludedTransitionViewModel.getLockscreenTranslationY(),
@@ -1196,7 +1197,7 @@
}
// Primary bouncer->Gone (ensures lockscreen content is not visible on successful auth)
- if (!migrateClocksToBlueprint()) {
+ if (!MigrateClocksToBlueprint.isEnabled()) {
collectFlow(mView, mPrimaryBouncerToGoneTransitionViewModel.getLockscreenAlpha(),
setTransitionAlpha(mNotificationStackScrollLayoutController,
/* excludeNotifications=*/ true), mMainDispatcher);
@@ -1280,7 +1281,7 @@
mKeyguardStatusViewController.onDestroy();
}
- if (migrateClocksToBlueprint()) {
+ if (MigrateClocksToBlueprint.isEnabled()) {
// Need a shared controller until mKeyguardStatusViewController can be removed from
// here, due to important state being set in that controller. Rebind in order to pick
// up config changes
@@ -1332,13 +1333,13 @@
private void onSplitShadeEnabledChanged() {
mShadeLog.logSplitShadeChanged(mSplitShadeEnabled);
- if (!migrateClocksToBlueprint()) {
+ if (!MigrateClocksToBlueprint.isEnabled()) {
mKeyguardStatusViewController.setSplitShadeEnabled(mSplitShadeEnabled);
}
// Reset any left over overscroll state. It is a rare corner case but can happen.
mQsController.setOverScrollAmount(0);
mScrimController.setNotificationsOverScrollAmount(0);
- if (!migrateClocksToBlueprint()) {
+ if (!MigrateClocksToBlueprint.isEnabled()) {
mNotificationStackScrollLayoutController.setOverExpansion(0);
mNotificationStackScrollLayoutController.setOverScrollAmount(0);
}
@@ -1359,7 +1360,7 @@
}
updateClockAppearance();
mQsController.updateQsState();
- if (!migrateClocksToBlueprint() && !FooterViewRefactor.isEnabled()) {
+ if (!MigrateClocksToBlueprint.isEnabled() && !FooterViewRefactor.isEnabled()) {
mNotificationStackScrollLayoutController.updateFooter();
}
}
@@ -1391,7 +1392,7 @@
void reInflateViews() {
debugLog("reInflateViews");
// Re-inflate the status view group.
- if (!migrateClocksToBlueprint()) {
+ if (!MigrateClocksToBlueprint.isEnabled()) {
KeyguardStatusView keyguardStatusView =
mNotificationContainerParent.findViewById(R.id.keyguard_status_view);
int statusIndex = mNotificationContainerParent.indexOfChild(keyguardStatusView);
@@ -1430,7 +1431,7 @@
updateViewControllers(userAvatarView, keyguardUserSwitcherView);
- if (!keyguardBottomAreaRefactor()) {
+ if (!KeyguardBottomAreaRefactor.isEnabled()) {
// Update keyguard bottom area
int index = mView.indexOfChild(mKeyguardBottomArea);
mView.removeView(mKeyguardBottomArea);
@@ -1449,7 +1450,7 @@
mStatusBarStateListener.onDozeAmountChanged(mStatusBarStateController.getDozeAmount(),
mStatusBarStateController.getInterpolatedDozeAmount());
- if (!migrateClocksToBlueprint()) {
+ if (!MigrateClocksToBlueprint.isEnabled()) {
mKeyguardStatusViewController.setKeyguardStatusViewVisibility(
mBarState,
false,
@@ -1471,7 +1472,7 @@
mBarState);
}
- if (!keyguardBottomAreaRefactor()) {
+ if (!KeyguardBottomAreaRefactor.isEnabled()) {
setKeyguardBottomAreaVisibility(mBarState, false);
}
@@ -1480,14 +1481,14 @@
}
private void attachSplitShadeMediaPlayerContainer(FrameLayout container) {
- if (migrateClocksToBlueprint()) {
+ if (MigrateClocksToBlueprint.isEnabled()) {
return;
}
mKeyguardMediaController.attachSplitShadeContainer(container);
}
private void initBottomArea() {
- if (!keyguardBottomAreaRefactor()) {
+ if (!KeyguardBottomAreaRefactor.isEnabled()) {
mKeyguardBottomArea.init(
mKeyguardBottomAreaViewModel,
mFalsingManager,
@@ -1513,7 +1514,7 @@
}
private void updateMaxDisplayedNotifications(boolean recompute) {
- if (migrateClocksToBlueprint()) {
+ if (MigrateClocksToBlueprint.isEnabled()) {
return;
}
@@ -1630,7 +1631,7 @@
int userSwitcherPreferredY = mStatusBarHeaderHeightKeyguard;
boolean bypassEnabled = mKeyguardBypassController.getBypassEnabled();
boolean shouldAnimateClockChange = mScreenOffAnimationController.shouldAnimateClockChange();
- if (migrateClocksToBlueprint()) {
+ if (MigrateClocksToBlueprint.isEnabled()) {
mKeyguardClockInteractor.setClockSize(computeDesiredClockSize());
} else {
mKeyguardStatusViewController.displayClock(computeDesiredClockSize(),
@@ -1671,11 +1672,11 @@
mKeyguardStatusViewController.getClockBottom(mStatusBarHeaderHeightKeyguard),
mKeyguardStatusViewController.isClockTopAligned());
mClockPositionAlgorithm.run(mClockPositionResult);
- if (!migrateClocksToBlueprint()) {
+ if (!MigrateClocksToBlueprint.isEnabled()) {
mKeyguardStatusViewController.setLockscreenClockY(
mClockPositionAlgorithm.getExpandedPreferredClockY());
}
- if (!(migrateClocksToBlueprint() || keyguardBottomAreaRefactor())) {
+ if (!(MigrateClocksToBlueprint.isEnabled() || KeyguardBottomAreaRefactor.isEnabled())) {
mKeyguardBottomAreaInteractor.setClockPosition(
mClockPositionResult.clockX, mClockPositionResult.clockY);
}
@@ -1683,7 +1684,7 @@
boolean animate = mNotificationStackScrollLayoutController.isAddOrRemoveAnimationPending();
boolean animateClock = (animate || mAnimateNextPositionUpdate) && shouldAnimateClockChange;
- if (!migrateClocksToBlueprint()) {
+ if (!MigrateClocksToBlueprint.isEnabled()) {
mKeyguardStatusViewController.updatePosition(
mClockPositionResult.clockX, mClockPositionResult.clockY,
mClockPositionResult.clockScale, animateClock);
@@ -1740,7 +1741,7 @@
// To prevent the weather clock from overlapping with the notification shelf on AOD, we use
// the small clock here
// With migrateClocksToBlueprint, weather clock will have behaviors similar to other clocks
- if (!migrateClocksToBlueprint()) {
+ if (!MigrateClocksToBlueprint.isEnabled()) {
if (mKeyguardStatusViewController.isLargeClockBlockingNotificationShelf()
&& hasVisibleNotifications() && isOnAod()) {
return SMALL;
@@ -1758,7 +1759,7 @@
private void updateKeyguardStatusViewAlignment(boolean animate) {
boolean shouldBeCentered = shouldKeyguardStatusViewBeCentered();
- if (migrateClocksToBlueprint()) {
+ if (MigrateClocksToBlueprint.isEnabled()) {
mKeyguardInteractor.setClockShouldBeCentered(shouldBeCentered);
return;
}
@@ -1941,7 +1942,7 @@
}
float alpha = mClockPositionResult.clockAlpha * mKeyguardOnlyContentAlpha;
mKeyguardStatusViewController.setAlpha(alpha);
- if (migrateClocksToBlueprint()) {
+ if (MigrateClocksToBlueprint.isEnabled()) {
// TODO (b/296373478) This is for split shade media movement.
} else {
mKeyguardStatusViewController
@@ -2498,7 +2499,7 @@
}
if (!mKeyguardBypassController.getBypassEnabled()) {
- if (migrateClocksToBlueprint() && !mSplitShadeEnabled) {
+ if (MigrateClocksToBlueprint.isEnabled() && !mSplitShadeEnabled) {
return (int) mKeyguardInteractor.getNotificationContainerBounds()
.getValue().getTop();
}
@@ -2531,7 +2532,7 @@
void requestScrollerTopPaddingUpdate(boolean animate) {
float padding = mQsController.calculateNotificationsTopPadding(mIsExpandingOrCollapsing,
getKeyguardNotificationStaticPadding(), mExpandedFraction);
- if (migrateClocksToBlueprint()) {
+ if (MigrateClocksToBlueprint.isEnabled()) {
mSharedNotificationContainerInteractor.setTopPosition(padding);
} else {
mNotificationStackScrollLayoutController.updateTopPadding(padding, animate);
@@ -2712,7 +2713,7 @@
return;
}
- if (!migrateClocksToBlueprint()) {
+ if (!MigrateClocksToBlueprint.isEnabled()) {
float alpha = 1f;
if (mClosingWithAlphaFadeOut && !mExpandingFromHeadsUp
&& !mHeadsUpManager.hasPinnedHeadsUp()) {
@@ -2748,7 +2749,7 @@
}
private void updateKeyguardBottomAreaAlpha() {
- if (migrateClocksToBlueprint()) {
+ if (MigrateClocksToBlueprint.isEnabled()) {
return;
}
if (mIsOcclusionTransitionRunning) {
@@ -2766,7 +2767,7 @@
float alpha = Math.min(expansionAlpha, 1 - mQsController.computeExpansionFraction());
alpha *= mBottomAreaShadeAlpha;
- if (keyguardBottomAreaRefactor()) {
+ if (KeyguardBottomAreaRefactor.isEnabled()) {
mKeyguardInteractor.setAlpha(alpha);
} else {
mKeyguardBottomAreaInteractor.setAlpha(alpha);
@@ -2978,7 +2979,7 @@
}
private void updateDozingVisibilities(boolean animate) {
- if (keyguardBottomAreaRefactor()) {
+ if (KeyguardBottomAreaRefactor.isEnabled()) {
mKeyguardInteractor.setAnimateDozingTransitions(animate);
} else {
mKeyguardBottomAreaInteractor.setAnimateDozingTransitions(animate);
@@ -2990,7 +2991,7 @@
@Override
public void onScreenTurningOn() {
- if (!migrateClocksToBlueprint()) {
+ if (!MigrateClocksToBlueprint.isEnabled()) {
mKeyguardStatusViewController.dozeTimeTick();
}
}
@@ -3189,7 +3190,7 @@
mDozing = dozing;
// TODO (b/) make listeners for this
mNotificationStackScrollLayoutController.setDozing(mDozing, animate);
- if (keyguardBottomAreaRefactor()) {
+ if (KeyguardBottomAreaRefactor.isEnabled()) {
mKeyguardInteractor.setAnimateDozingTransitions(animate);
} else {
mKeyguardBottomAreaInteractor.setAnimateDozingTransitions(animate);
@@ -3245,7 +3246,7 @@
public void dozeTimeTick() {
mLockIconViewController.dozeTimeTick();
- if (!migrateClocksToBlueprint()) {
+ if (!MigrateClocksToBlueprint.isEnabled()) {
mKeyguardStatusViewController.dozeTimeTick();
}
if (mInterpolatedDarkAmount > 0) {
@@ -3257,7 +3258,6 @@
mKeyguardStatusViewController.setStatusAccessibilityImportance(mode);
}
- @Override
public void performHapticFeedback(int constant) {
mVibratorHelper.performHapticFeedback(mView, constant);
}
@@ -3325,7 +3325,7 @@
/** Updates the views to the initial state for the fold to AOD animation. */
@Override
public void prepareFoldToAodAnimation() {
- if (migrateClocksToBlueprint()) {
+ if (MigrateClocksToBlueprint.isEnabled()) {
return;
}
// Force show AOD UI even if we are not locked
@@ -3349,7 +3349,7 @@
@Override
public void startFoldToAodAnimation(Runnable startAction, Runnable endAction,
Runnable cancelAction) {
- if (migrateClocksToBlueprint()) {
+ if (MigrateClocksToBlueprint.isEnabled()) {
return;
}
final ViewPropertyAnimator viewAnimator = mView.animate();
@@ -3387,7 +3387,7 @@
/** Cancels fold to AOD transition and resets view state. */
@Override
public void cancelFoldToAodAnimation() {
- if (migrateClocksToBlueprint()) {
+ if (MigrateClocksToBlueprint.isEnabled()) {
return;
}
cancelAnimation();
@@ -4382,6 +4382,10 @@
@Override
public void onHeadsUpPinned(NotificationEntry entry) {
+ if (NotificationsHeadsUpRefactor.isEnabled()) {
+ return;
+ }
+
if (!isKeyguardShowing()) {
mNotificationStackScrollLayoutController.generateHeadsUpAnimation(entry, true);
}
@@ -4389,6 +4393,9 @@
@Override
public void onHeadsUpUnPinned(NotificationEntry entry) {
+ if (NotificationsHeadsUpRefactor.isEnabled()) {
+ return;
+ }
// When we're unpinning the notification via active edge they remain heads-upped,
// we need to make sure that an animation happens in this case, otherwise the
@@ -4461,7 +4468,7 @@
&& statusBarState == KEYGUARD) {
// This means we're doing the screen off animation - position the keyguard status
// view where it'll be on AOD, so we can animate it in.
- if (!migrateClocksToBlueprint()) {
+ if (!MigrateClocksToBlueprint.isEnabled()) {
mKeyguardStatusViewController.updatePosition(
mClockPositionResult.clockX,
mClockPositionResult.clockYFullyDozing,
@@ -4470,7 +4477,7 @@
}
}
- if (!migrateClocksToBlueprint()) {
+ if (!MigrateClocksToBlueprint.isEnabled()) {
mKeyguardStatusViewController.setKeyguardStatusViewVisibility(
statusBarState,
keyguardFadingAway,
@@ -4478,7 +4485,7 @@
mBarState);
}
- if (!keyguardBottomAreaRefactor()) {
+ if (!KeyguardBottomAreaRefactor.isEnabled()) {
setKeyguardBottomAreaVisibility(statusBarState, goingToFullShade);
}
@@ -4583,7 +4590,7 @@
setDozing(true /* dozing */, false /* animate */);
mStatusBarStateController.setUpcomingState(KEYGUARD);
- if (migrateClocksToBlueprint()) {
+ if (MigrateClocksToBlueprint.isEnabled()) {
mStatusBarStateController.setState(KEYGUARD);
} else {
mStatusBarStateListener.onStateChanged(KEYGUARD);
@@ -4646,7 +4653,7 @@
setIsFullWidth(mNotificationStackScrollLayoutController.getWidth() == mView.getWidth());
// Update Clock Pivot (used by anti-burnin transformations)
- if (!migrateClocksToBlueprint()) {
+ if (!MigrateClocksToBlueprint.isEnabled()) {
mKeyguardStatusViewController.updatePivot(mView.getWidth(), mView.getHeight());
}
@@ -4747,7 +4754,7 @@
stackScroller.setMaxAlphaForKeyguard(alpha, "NPVC.setTransitionAlpha()");
}
- if (keyguardBottomAreaRefactor()) {
+ if (KeyguardBottomAreaRefactor.isEnabled()) {
mKeyguardInteractor.setAlpha(alpha);
} else {
mKeyguardBottomAreaInteractor.setAlpha(alpha);
@@ -4766,7 +4773,7 @@
private Consumer<Float> setTransitionY(
NotificationStackScrollLayoutController stackScroller) {
return (Float translationY) -> {
- if (!migrateClocksToBlueprint()) {
+ if (!MigrateClocksToBlueprint.isEnabled()) {
mKeyguardStatusViewController.setTranslationY(translationY,
/* excludeMedia= */false);
stackScroller.setTranslationY(translationY);
@@ -4808,7 +4815,7 @@
*/
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
- if (migrateClocksToBlueprint() && !mUseExternalTouch) {
+ if (MigrateClocksToBlueprint.isEnabled() && !mUseExternalTouch) {
return false;
}
@@ -4879,7 +4886,7 @@
switch (event.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
- if (!migrateClocksToBlueprint()) {
+ if (!MigrateClocksToBlueprint.isEnabled()) {
mCentralSurfaces.userActivity();
}
mAnimatingOnDown = mHeightAnimator != null && !mIsSpringBackAnimation;
@@ -4980,7 +4987,7 @@
*/
@Override
public boolean onTouchEvent(MotionEvent event) {
- if (migrateClocksToBlueprint() && !mUseExternalTouch) {
+ if (MigrateClocksToBlueprint.isEnabled() && !mUseExternalTouch) {
return false;
}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java
index e577178..e8e629c 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java
@@ -16,7 +16,6 @@
package com.android.systemui.shade;
-import static com.android.systemui.Flags.migrateClocksToBlueprint;
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;
@@ -48,6 +47,7 @@
import com.android.systemui.flags.Flags;
import com.android.systemui.keyevent.domain.interactor.SysUIKeyEventHandler;
import com.android.systemui.keyguard.KeyguardUnlockAnimationController;
+import com.android.systemui.keyguard.MigrateClocksToBlueprint;
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor;
import com.android.systemui.keyguard.shared.model.TransitionState;
import com.android.systemui.keyguard.shared.model.TransitionStep;
@@ -320,7 +320,7 @@
mTouchActive = true;
mTouchCancelled = false;
mDownEvent = ev;
- if (migrateClocksToBlueprint()) {
+ if (MigrateClocksToBlueprint.isEnabled()) {
mService.userActivity();
}
} else if (ev.getActionMasked() == MotionEvent.ACTION_UP
@@ -475,7 +475,7 @@
&& !bouncerShowing
&& !mStatusBarStateController.isDozing()) {
if (mDragDownHelper.isDragDownEnabled()) {
- if (migrateClocksToBlueprint()) {
+ if (MigrateClocksToBlueprint.isEnabled()) {
// When on lockscreen, if the touch originates at the top of the screen
// go directly to QS and not the shade
if (mStatusBarStateController.getState() == KEYGUARD
@@ -488,7 +488,7 @@
// This handles drag down over lockscreen
boolean result = mDragDownHelper.onInterceptTouchEvent(ev);
- if (migrateClocksToBlueprint()) {
+ if (MigrateClocksToBlueprint.isEnabled()) {
if (result) {
mLastInterceptWasDragDownHelper = true;
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
@@ -511,7 +511,7 @@
return true;
}
}
- } else if (migrateClocksToBlueprint()) {
+ } else if (MigrateClocksToBlueprint.isEnabled()) {
// This final check handles swipes on HUNs and when Pulsing
if (!bouncerShowing && didNotificationPanelInterceptEvent(ev)) {
mShadeLogger.d("NSWVC: intercepted for HUN/PULSING");
@@ -526,7 +526,7 @@
MotionEvent cancellation = MotionEvent.obtain(ev);
cancellation.setAction(MotionEvent.ACTION_CANCEL);
mStackScrollLayout.onInterceptTouchEvent(cancellation);
- if (!migrateClocksToBlueprint()) {
+ if (!MigrateClocksToBlueprint.isEnabled()) {
mNotificationPanelViewController.handleExternalInterceptTouch(cancellation);
}
cancellation.recycle();
@@ -541,7 +541,7 @@
if (mStatusBarKeyguardViewManager.onTouch(ev)) {
return true;
}
- if (migrateClocksToBlueprint()) {
+ if (MigrateClocksToBlueprint.isEnabled()) {
if (mLastInterceptWasDragDownHelper && (mDragDownHelper.isDraggingDown())) {
// we still want to finish our drag down gesture when locking the screen
handled |= mDragDownHelper.onTouchEvent(ev) || handled;
@@ -631,7 +631,7 @@
}
private boolean didNotificationPanelInterceptEvent(MotionEvent ev) {
- if (migrateClocksToBlueprint()) {
+ if (MigrateClocksToBlueprint.isEnabled()) {
// Since NotificationStackScrollLayout is now a sibling of notification_panel, we need
// to also ask NotificationPanelViewController directly, in order to process swipe up
// events originating from notifications
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationsQSContainerController.kt b/packages/SystemUI/src/com/android/systemui/shade/NotificationsQSContainerController.kt
index 29de688..8b88da1 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationsQSContainerController.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationsQSContainerController.kt
@@ -28,10 +28,10 @@
import androidx.constraintlayout.widget.ConstraintSet.TOP
import androidx.lifecycle.lifecycleScope
import com.android.systemui.Flags.centralizedStatusBarHeightFix
-import com.android.systemui.Flags.migrateClocksToBlueprint
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.fragments.FragmentService
+import com.android.systemui.keyguard.MigrateClocksToBlueprint
import com.android.systemui.lifecycle.repeatWhenAttached
import com.android.systemui.navigationbar.NavigationModeController
import com.android.systemui.plugins.qs.QS
@@ -52,11 +52,12 @@
import kotlin.reflect.KMutableProperty0
import kotlinx.coroutines.launch
-@VisibleForTesting
-internal const val INSET_DEBOUNCE_MILLIS = 500L
+@VisibleForTesting internal const val INSET_DEBOUNCE_MILLIS = 500L
@SysUISingleton
-class NotificationsQSContainerController @Inject constructor(
+class NotificationsQSContainerController
+@Inject
+constructor(
view: NotificationsQuickSettingsContainer,
private val navigationModeController: NavigationModeController,
private val overviewProxyService: OverviewProxyService,
@@ -64,8 +65,7 @@
private val shadeInteractor: ShadeInteractor,
private val fragmentService: FragmentService,
@Main private val delayableExecutor: DelayableExecutor,
- private val
- notificationStackScrollLayoutController: NotificationStackScrollLayoutController,
+ private val notificationStackScrollLayoutController: NotificationStackScrollLayoutController,
private val splitShadeStateController: SplitShadeStateController,
private val largeScreenHeaderHelperLazy: Lazy<LargeScreenHeaderHelper>,
) : ViewController<NotificationsQuickSettingsContainer>(view), QSContainerController {
@@ -88,45 +88,48 @@
private var isGestureNavigation = true
private var taskbarVisible = false
- private val taskbarVisibilityListener: OverviewProxyListener = object : OverviewProxyListener {
- override fun onTaskbarStatusUpdated(visible: Boolean, stashed: Boolean) {
- taskbarVisible = visible
+ private val taskbarVisibilityListener: OverviewProxyListener =
+ object : OverviewProxyListener {
+ override fun onTaskbarStatusUpdated(visible: Boolean, stashed: Boolean) {
+ taskbarVisible = visible
+ }
}
- }
// With certain configuration changes (like light/dark changes), the nav bar will disappear
// for a bit, causing `bottomStableInsets` to be unstable for some time. Debounce the value
// for 500ms.
// All interactions with this object happen in the main thread.
- private val delayedInsetSetter = object : Runnable, Consumer<WindowInsets> {
- private var canceller: Runnable? = null
- private var stableInsets = 0
- private var cutoutInsets = 0
+ private val delayedInsetSetter =
+ object : Runnable, Consumer<WindowInsets> {
+ private var canceller: Runnable? = null
+ private var stableInsets = 0
+ private var cutoutInsets = 0
- override fun accept(insets: WindowInsets) {
- // when taskbar is visible, stableInsetBottom will include its height
- stableInsets = insets.stableInsetBottom
- cutoutInsets = insets.displayCutout?.safeInsetBottom ?: 0
- canceller?.run()
- canceller = delayableExecutor.executeDelayed(this, INSET_DEBOUNCE_MILLIS)
- }
+ override fun accept(insets: WindowInsets) {
+ // when taskbar is visible, stableInsetBottom will include its height
+ stableInsets = insets.stableInsetBottom
+ cutoutInsets = insets.displayCutout?.safeInsetBottom ?: 0
+ canceller?.run()
+ canceller = delayableExecutor.executeDelayed(this, INSET_DEBOUNCE_MILLIS)
+ }
- override fun run() {
- bottomStableInsets = stableInsets
- bottomCutoutInsets = cutoutInsets
- updateBottomSpacing()
+ override fun run() {
+ bottomStableInsets = stableInsets
+ bottomCutoutInsets = cutoutInsets
+ updateBottomSpacing()
+ }
}
- }
override fun onInit() {
mView.repeatWhenAttached {
lifecycleScope.launch {
- shadeInteractor.isQsExpanded.collect{ _ -> mView.invalidate() }
+ shadeInteractor.isQsExpanded.collect { _ -> mView.invalidate() }
}
}
- val currentMode: Int = navigationModeController.addListener { mode: Int ->
- isGestureNavigation = QuickStepContract.isGesturalMode(mode)
- }
+ val currentMode: Int =
+ navigationModeController.addListener { mode: Int ->
+ isGestureNavigation = QuickStepContract.isGesturalMode(mode)
+ }
isGestureNavigation = QuickStepContract.isGesturalMode(currentMode)
mView.setStackScroller(notificationStackScrollLayoutController.getView())
@@ -151,30 +154,35 @@
fun updateResources() {
val newSplitShadeEnabled =
- splitShadeStateController.shouldUseSplitNotificationShade(resources)
+ splitShadeStateController.shouldUseSplitNotificationShade(resources)
val splitShadeEnabledChanged = newSplitShadeEnabled != splitShadeEnabled
splitShadeEnabled = newSplitShadeEnabled
largeScreenShadeHeaderActive = LargeScreenUtils.shouldUseLargeScreenShadeHeader(resources)
- notificationsBottomMargin = resources.getDimensionPixelSize(
- R.dimen.notification_panel_margin_bottom)
+ notificationsBottomMargin =
+ resources.getDimensionPixelSize(R.dimen.notification_panel_margin_bottom)
largeScreenShadeHeaderHeight = calculateLargeShadeHeaderHeight()
shadeHeaderHeight = calculateShadeHeaderHeight()
- panelMarginHorizontal = resources.getDimensionPixelSize(
- R.dimen.notification_panel_margin_horizontal)
- topMargin = if (largeScreenShadeHeaderActive) {
- largeScreenShadeHeaderHeight
- } else {
- resources.getDimensionPixelSize(R.dimen.notification_panel_margin_top)
- }
+ panelMarginHorizontal =
+ resources.getDimensionPixelSize(R.dimen.notification_panel_margin_horizontal)
+ topMargin =
+ if (largeScreenShadeHeaderActive) {
+ largeScreenShadeHeaderHeight
+ } else {
+ resources.getDimensionPixelSize(R.dimen.notification_panel_margin_top)
+ }
updateConstraints()
- val scrimMarginChanged = ::scrimShadeBottomMargin.setAndReportChange(
- resources.getDimensionPixelSize(R.dimen.split_shade_notifications_scrim_margin_bottom)
- )
- val footerOffsetChanged = ::footerActionsOffset.setAndReportChange(
- resources.getDimensionPixelSize(R.dimen.qs_footer_action_inset) +
- resources.getDimensionPixelSize(R.dimen.qs_footer_actions_bottom_padding)
- )
+ val scrimMarginChanged =
+ ::scrimShadeBottomMargin.setAndReportChange(
+ resources.getDimensionPixelSize(
+ R.dimen.split_shade_notifications_scrim_margin_bottom
+ )
+ )
+ val footerOffsetChanged =
+ ::footerActionsOffset.setAndReportChange(
+ resources.getDimensionPixelSize(R.dimen.qs_footer_action_inset) +
+ resources.getDimensionPixelSize(R.dimen.qs_footer_actions_bottom_padding)
+ )
val dimensChanged = scrimMarginChanged || footerOffsetChanged
if (splitShadeEnabledChanged || dimensChanged) {
@@ -198,7 +206,7 @@
// 2. carrier_group height (R.dimen.large_screen_shade_header_min_height)
// 3. date height (R.dimen.new_qs_header_non_clickable_element_height)
val estimatedHeight =
- 2 * resources.getDimensionPixelSize(R.dimen.large_screen_shade_header_min_height) +
+ 2 * resources.getDimensionPixelSize(R.dimen.large_screen_shade_header_min_height) +
resources.getDimensionPixelSize(R.dimen.new_qs_header_non_clickable_element_height)
return estimatedHeight.coerceAtLeast(minHeight)
}
@@ -250,16 +258,17 @@
containerPadding = 0
stackScrollMargin = bottomStableInsets + notificationsBottomMargin
}
- val qsContainerPadding = if (!isQSDetailShowing) {
- // We also want this padding in the bottom in these cases
- if (splitShadeEnabled) {
- stackScrollMargin - scrimShadeBottomMargin - footerActionsOffset
+ val qsContainerPadding =
+ if (!isQSDetailShowing) {
+ // We also want this padding in the bottom in these cases
+ if (splitShadeEnabled) {
+ stackScrollMargin - scrimShadeBottomMargin - footerActionsOffset
+ } else {
+ bottomStableInsets
+ }
} else {
- bottomStableInsets
+ 0
}
- } else {
- 0
- }
return Paddings(containerPadding, stackScrollMargin, qsContainerPadding)
}
@@ -284,7 +293,7 @@
}
private fun setNotificationsConstraints(constraintSet: ConstraintSet) {
- if (migrateClocksToBlueprint()) {
+ if (MigrateClocksToBlueprint.isEnabled) {
return
}
val startConstraintId = if (splitShadeEnabled) R.id.qs_edge_guideline else PARENT_ID
@@ -309,8 +318,8 @@
}
private fun setKeyguardStatusViewConstraints(constraintSet: ConstraintSet) {
- val statusViewMarginHorizontal = resources.getDimensionPixelSize(
- R.dimen.status_view_margin_horizontal)
+ val statusViewMarginHorizontal =
+ resources.getDimensionPixelSize(R.dimen.status_view_margin_horizontal)
constraintSet.apply {
setMargin(R.id.keyguard_status_view, START, statusViewMarginHorizontal)
setMargin(R.id.keyguard_status_view, END, statusViewMarginHorizontal)
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationsQuickSettingsContainer.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationsQuickSettingsContainer.java
index e82f2d3..1333055 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationsQuickSettingsContainer.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationsQuickSettingsContainer.java
@@ -18,8 +18,6 @@
import static androidx.constraintlayout.core.widgets.Optimizer.OPTIMIZATION_GRAPH;
-import static com.android.systemui.Flags.migrateClocksToBlueprint;
-
import android.app.Fragment;
import android.content.Context;
import android.content.res.Configuration;
@@ -35,6 +33,7 @@
import androidx.constraintlayout.widget.ConstraintSet;
import com.android.systemui.fragments.FragmentHostManager.FragmentListener;
+import com.android.systemui.keyguard.MigrateClocksToBlueprint;
import com.android.systemui.plugins.qs.QS;
import com.android.systemui.res.R;
import com.android.systemui.statusbar.notification.AboveShelfObserver;
@@ -190,7 +189,7 @@
@Override
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
- if (migrateClocksToBlueprint()) {
+ if (MigrateClocksToBlueprint.isEnabled()) {
return super.drawChild(canvas, child, drawingTime);
}
int layoutIndex = mLayoutDrawingOrder.indexOf(child);
diff --git a/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerImpl.java b/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerImpl.java
index 8ba0544..3a0e167 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerImpl.java
@@ -21,7 +21,6 @@
import static com.android.internal.jank.InteractionJankMonitor.CUJ_NOTIFICATION_SHADE_QS_EXPAND_COLLAPSE;
import static com.android.systemui.Flags.centralizedStatusBarHeightFix;
-import static com.android.systemui.Flags.migrateClocksToBlueprint;
import static com.android.systemui.classifier.Classifier.QS_COLLAPSE;
import static com.android.systemui.shade.NotificationPanelViewController.COUNTER_PANEL_OPEN_QS;
import static com.android.systemui.shade.NotificationPanelViewController.FLING_COLLAPSE;
@@ -71,6 +70,7 @@
import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFaceAuthInteractor;
import com.android.systemui.dump.DumpManager;
import com.android.systemui.fragments.FragmentHostManager;
+import com.android.systemui.keyguard.MigrateClocksToBlueprint;
import com.android.systemui.media.controls.domain.pipeline.MediaDataManager;
import com.android.systemui.media.controls.ui.controller.MediaHierarchyManager;
import com.android.systemui.plugins.FalsingManager;
@@ -1280,18 +1280,20 @@
mScrimController.setScrimCornerRadius(radius);
- // Convert global clipping coordinates to local ones,
- // relative to NotificationStackScrollLayout
- int nsslLeft = calculateNsslLeft(left);
- int nsslRight = calculateNsslRight(right);
- int nsslTop = getNotificationsClippingTopBounds(top);
- int nsslBottom = bottom - mNotificationStackScrollLayoutController.getTop();
- int bottomRadius = mSplitShadeEnabled ? radius : 0;
- // TODO (b/265193930): remove dependency on NPVC
- int topRadius = mSplitShadeEnabled
- && mPanelViewControllerLazy.get().isExpandingFromHeadsUp() ? 0 : radius;
- mNotificationStackScrollLayoutController.setRoundedClippingBounds(
- nsslLeft, nsslTop, nsslRight, nsslBottom, topRadius, bottomRadius);
+ if (!SceneContainerFlag.isEnabled()) {
+ // Convert global clipping coordinates to local ones,
+ // relative to NotificationStackScrollLayout
+ int nsslLeft = calculateNsslLeft(left);
+ int nsslRight = calculateNsslRight(right);
+ int nsslTop = getNotificationsClippingTopBounds(top);
+ int nsslBottom = bottom - mNotificationStackScrollLayoutController.getTop();
+ int bottomRadius = mSplitShadeEnabled ? radius : 0;
+ // TODO (b/265193930): remove dependency on NPVC
+ int topRadius = mSplitShadeEnabled
+ && mPanelViewControllerLazy.get().isExpandingFromHeadsUp() ? 0 : radius;
+ mNotificationStackScrollLayoutController.setRoundedClippingBounds(
+ nsslLeft, nsslTop, nsslRight, nsslBottom, topRadius, bottomRadius);
+ }
}
/**
@@ -1776,7 +1778,7 @@
// Dragging down on the lockscreen statusbar should prohibit other interactions
// immediately, otherwise we'll wait on the touchslop. This is to allow
// dragging down to expanded quick settings directly on the lockscreen.
- if (!migrateClocksToBlueprint()) {
+ if (!MigrateClocksToBlueprint.isEnabled()) {
mPanelView.getParent().requestDisallowInterceptTouchEvent(true);
}
}
@@ -1821,7 +1823,7 @@
&& Math.abs(h) > Math.abs(x - mInitialTouchX)
&& shouldQuickSettingsIntercept(
mInitialTouchX, mInitialTouchY, h)) {
- if (!migrateClocksToBlueprint()) {
+ if (!MigrateClocksToBlueprint.isEnabled()) {
mPanelView.getParent().requestDisallowInterceptTouchEvent(true);
}
mShadeLog.onQsInterceptMoveQsTrackingEnabled(h);
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeController.java b/packages/SystemUI/src/com/android/systemui/shade/ShadeController.java
index 0a57b64..813df11 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeController.java
@@ -232,6 +232,13 @@
/** Called when a launch animation ends. */
void onLaunchAnimationEnd(boolean launchIsFullScreen);
+ /**
+ * Performs haptic feedback from a view with a haptic feedback constant.
+ *
+ * @param constant One of android.view.HapticFeedbackConstants
+ */
+ void performHapticFeedback(int constant);
+
/** Sets the listener for when the visibility of the shade changes. */
default void setVisibilityListener(ShadeVisibilityListener listener) {}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerEmptyImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerEmptyImpl.kt
index 093690f..d703a27 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerEmptyImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerEmptyImpl.kt
@@ -63,4 +63,5 @@
override fun onStatusBarTouch(event: MotionEvent?) {}
override fun onLaunchAnimationCancelled(isLaunchForActivity: Boolean) {}
override fun onLaunchAnimationEnd(launchIsFullScreen: Boolean) {}
+ override fun performHapticFeedback(constant: Int) {}
}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerImpl.java b/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerImpl.java
index d99d607..5f5e5ce 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerImpl.java
@@ -271,6 +271,11 @@
}
@Override
+ public void performHapticFeedback(int constant) {
+ getNpvc().performHapticFeedback(constant);
+ }
+
+ @Override
public void instantCollapseShade() {
getNpvc().instantCollapse();
runPostCollapseActions();
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt
index 177c3db..6bb1df7 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt
@@ -33,6 +33,7 @@
import com.android.systemui.shade.domain.interactor.ShadeInteractor
import com.android.systemui.statusbar.CommandQueue
import com.android.systemui.statusbar.NotificationShadeWindowController
+import com.android.systemui.statusbar.VibratorHelper
import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout
import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager
import dagger.Lazy
@@ -62,6 +63,7 @@
private val deviceEntryInteractor: DeviceEntryInteractor,
private val notificationStackScrollLayout: NotificationStackScrollLayout,
@ShadeTouchLog private val touchLog: LogBuffer,
+ private val vibratorHelper: VibratorHelper,
commandQueue: CommandQueue,
statusBarKeyguardViewManager: StatusBarKeyguardViewManager,
notificationShadeWindowController: NotificationShadeWindowController,
@@ -246,7 +248,11 @@
}
override fun onStatusBarTouch(event: MotionEvent) {
- // The only call to this doesn't happen with migrateClocksToBlueprint() enabled
+ // The only call to this doesn't happen with MigrateClocksToBlueprint.isEnabled enabled
throw UnsupportedOperationException()
}
+
+ override fun performHapticFeedback(constant: Int) {
+ vibratorHelper.performHapticFeedback(notificationStackScrollLayout, constant)
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeViewController.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeViewController.kt
index d90bb0b..9902a32 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeViewController.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeViewController.kt
@@ -44,6 +44,7 @@
fun disableHeader(state1: Int, state2: Int, animated: Boolean)
/** If the latency tracker is enabled, begins tracking expand latency. */
+ @Deprecated("No longer supported. Do not add new calls to this.")
fun startExpandLatencyTracking()
/** Sets the alpha value of the shade to a value between 0 and 255. */
@@ -57,13 +58,14 @@
fun setAlphaChangeAnimationEndAction(r: Runnable)
/** Sets Qs ScrimEnabled and updates QS state. */
+ @Deprecated("Does nothing when scene container is enabled.")
fun setQsScrimEnabled(qsScrimEnabled: Boolean)
/** Sets the top spacing for the ambient indicator. */
fun setAmbientIndicationTop(ambientIndicationTop: Int, ambientTextVisible: Boolean)
/** Updates notification panel-specific flags on [SysUiState]. */
- fun updateSystemUiStateFlags()
+ @Deprecated("Does nothing when scene container is enabled.") fun updateSystemUiStateFlags()
/** Ensures that the touchable region is updated. */
fun updateTouchableRegion()
@@ -105,16 +107,6 @@
@Deprecated("No longer supported. Do not add new calls to this.")
fun finishInputFocusTransfer(velocity: Float)
- /**
- * Performs haptic feedback from a view with a haptic feedback constant.
- *
- * The implementation of this method should use the [android.view.View.performHapticFeedback]
- * method with the provided constant.
- *
- * @param[constant] One of [android.view.HapticFeedbackConstants]
- */
- fun performHapticFeedback(constant: Int)
-
/** Returns the ShadeHeadsUpTracker. */
val shadeHeadsUpTracker: ShadeHeadsUpTracker
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeViewControllerEmptyImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeViewControllerEmptyImpl.kt
index 69849e8..93c3772 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeViewControllerEmptyImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeViewControllerEmptyImpl.kt
@@ -84,8 +84,6 @@
override fun startInputFocusTransfer() {}
override fun cancelInputFocusTransfer() {}
override fun finishInputFocusTransfer(velocity: Float) {}
- override fun performHapticFeedback(constant: Int) {}
-
override val shadeHeadsUpTracker = ShadeHeadsUpTrackerEmptyImpl()
override val shadeFoldAnimator = ShadeFoldAnimatorEmptyImpl()
@Deprecated("Use SceneInteractor.currentScene instead.")
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModel.kt b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModel.kt
index ea549f2..24b7533 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModel.kt
@@ -66,11 +66,13 @@
deviceEntryInteractor.isUnlocked,
deviceEntryInteractor.canSwipeToEnter,
shadeInteractor.shadeMode,
- ) { isUnlocked, canSwipeToDismiss, shadeMode ->
+ qsSceneAdapter.isCustomizing
+ ) { isUnlocked, canSwipeToDismiss, shadeMode, isCustomizing ->
destinationScenes(
isUnlocked = isUnlocked,
canSwipeToDismiss = canSwipeToDismiss,
shadeMode = shadeMode,
+ isCustomizing = isCustomizing
)
}
.stateIn(
@@ -81,6 +83,7 @@
isUnlocked = deviceEntryInteractor.isUnlocked.value,
canSwipeToDismiss = deviceEntryInteractor.canSwipeToEnter.value,
shadeMode = shadeInteractor.shadeMode.value,
+ isCustomizing = qsSceneAdapter.isCustomizing.value,
),
)
@@ -120,6 +123,7 @@
isUnlocked: Boolean,
canSwipeToDismiss: Boolean?,
shadeMode: ShadeMode,
+ isCustomizing: Boolean,
): Map<UserAction, UserActionResult> {
val up =
when {
@@ -131,7 +135,9 @@
val down = Scenes.QuickSettings.takeIf { shadeMode is ShadeMode.Single }
return buildMap {
- this[Swipe(SwipeDirection.Up)] = UserActionResult(up)
+ if (!isCustomizing) {
+ this[Swipe(SwipeDirection.Up)] = UserActionResult(up)
+ } // TODO(b/330200163) Add an else to be able to collapse the shade while customizing
down?.let { this[Swipe(SwipeDirection.Down)] = UserActionResult(down) }
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java
index 4406813..e7b159a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java
@@ -529,9 +529,9 @@
default void immersiveModeChanged(int rootDisplayAreaId, boolean isImmersiveMode) {}
/**
- * @see IStatusBar#enterDesktop(int)
+ * @see IStatusBar#moveFocusedTaskToDesktop(int)
*/
- default void enterDesktop(int displayId) {}
+ default void moveFocusedTaskToDesktop(int displayId) {}
}
@VisibleForTesting
@@ -1444,7 +1444,7 @@
}
@Override
- public void enterDesktop(int displayId) {
+ public void moveFocusedTaskToDesktop(int displayId) {
SomeArgs args = SomeArgs.obtain();
args.arg1 = displayId;
mHandler.obtainMessage(MSG_ENTER_DESKTOP, args).sendToTarget();
@@ -1960,7 +1960,7 @@
args = (SomeArgs) msg.obj;
int displayId = args.argi1;
for (int i = 0; i < mCallbacks.size(); i++) {
- mCallbacks.get(i).enterDesktop(displayId);
+ mCallbacks.get(i).moveFocusedTaskToDesktop(displayId);
}
break;
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutListSearch.java b/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutListSearch.java
index a12b970..d6858ca 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutListSearch.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutListSearch.java
@@ -560,11 +560,6 @@
Pair.create(
KeyEvent.KEYCODE_TAB,
KeyEvent.META_SHIFT_ON | KeyEvent.META_ALT_ON))),
- /* Hide and (re)show taskbar: Meta + T */
- new ShortcutKeyGroupMultiMappingInfo(
- context.getString(R.string.group_system_hide_reshow_taskbar),
- Arrays.asList(
- Pair.create(KeyEvent.KEYCODE_T, KeyEvent.META_META_ON))),
/* Access notification shade: Meta + N */
new ShortcutKeyGroupMultiMappingInfo(
context.getString(R.string.group_system_access_notification_shade),
@@ -636,34 +631,41 @@
// Enter Split screen with current app to RHS: Meta + Ctrl + Right arrow
// Enter Split screen with current app to LHS: Meta + Ctrl + Left arrow
// Switch from Split screen to full screen: Meta + Ctrl + Up arrow
- String[] shortcutLabels = {
- context.getString(R.string.system_multitasking_rhs),
- context.getString(R.string.system_multitasking_lhs),
- context.getString(R.string.system_multitasking_full_screen),
- };
- int[] keyCodes = {
- KeyEvent.KEYCODE_DPAD_RIGHT,
- KeyEvent.KEYCODE_DPAD_LEFT,
- KeyEvent.KEYCODE_DPAD_UP,
- };
-
- for (int i = 0; i < shortcutLabels.length; i++) {
- List<ShortcutKeyGroup> shortcutKeyGroups = Arrays.asList(new ShortcutKeyGroup(
- new KeyboardShortcutInfo(
- shortcutLabels[i],
- keyCodes[i],
- KeyEvent.META_META_ON | KeyEvent.META_CTRL_ON),
- null));
- ShortcutMultiMappingInfo shortcutMultiMappingInfo =
- new ShortcutMultiMappingInfo(
- shortcutLabels[i],
- null,
- shortcutKeyGroups);
- systemMultitaskingGroup.addItem(shortcutMultiMappingInfo);
- }
+ // Change split screen focus to RHS: Meta + Alt + Right arrow
+ // Change split screen focus to LHS: Meta + Alt + Left arrow
+ systemMultitaskingGroup.addItem(
+ getMultitaskingShortcut(context.getString(R.string.system_multitasking_rhs),
+ KeyEvent.KEYCODE_DPAD_RIGHT,
+ KeyEvent.META_META_ON | KeyEvent.META_CTRL_ON));
+ systemMultitaskingGroup.addItem(
+ getMultitaskingShortcut(context.getString(R.string.system_multitasking_lhs),
+ KeyEvent.KEYCODE_DPAD_LEFT,
+ KeyEvent.META_META_ON | KeyEvent.META_CTRL_ON));
+ systemMultitaskingGroup.addItem(
+ getMultitaskingShortcut(context.getString(R.string.system_multitasking_full_screen),
+ KeyEvent.KEYCODE_DPAD_UP,
+ KeyEvent.META_META_ON | KeyEvent.META_CTRL_ON));
+ systemMultitaskingGroup.addItem(
+ getMultitaskingShortcut(
+ context.getString(R.string.system_multitasking_splitscreen_focus_rhs),
+ KeyEvent.KEYCODE_DPAD_RIGHT,
+ KeyEvent.META_META_ON | KeyEvent.META_ALT_ON));
+ systemMultitaskingGroup.addItem(
+ getMultitaskingShortcut(
+ context.getString(R.string.system_multitasking_splitscreen_focus_lhs),
+ KeyEvent.KEYCODE_DPAD_LEFT,
+ KeyEvent.META_META_ON | KeyEvent.META_ALT_ON));
return systemMultitaskingGroup;
}
+ private static ShortcutMultiMappingInfo getMultitaskingShortcut(String shortcutLabel,
+ int keycode, int modifiers) {
+ List<ShortcutKeyGroup> shortcutKeyGroups = Arrays.asList(
+ new ShortcutKeyGroup(new KeyboardShortcutInfo(shortcutLabel, keycode, modifiers),
+ null));
+ return new ShortcutMultiMappingInfo(shortcutLabel, null, shortcutKeyGroups);
+ }
+
private static KeyboardShortcutMultiMappingGroup getMultiMappingInputShortcuts(
Context context) {
List<ShortcutMultiMappingInfo> shortcutMultiMappingInfoList = Arrays.asList(
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java
index c904621..815236e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java
@@ -757,8 +757,8 @@
mRotateTextViewController.updateIndication(
INDICATION_TYPE_ADAPTIVE_AUTH,
new KeyguardIndication.Builder()
- .setMessage(mContext
- .getString(R.string.kg_prompt_after_adaptive_auth_lock))
+ .setMessage(mContext.getString(
+ R.string.keyguard_indication_after_adaptive_auth_lock))
.setTextColor(mInitialTextColorState)
.build(),
true);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt
index 4b16126..d974bc44 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt
@@ -15,13 +15,13 @@
import com.android.systemui.Dumpable
import com.android.systemui.ExpandHelper
import com.android.systemui.Flags.nsslFalsingFix
-import com.android.systemui.Flags.migrateClocksToBlueprint
import com.android.systemui.Gefingerpoken
import com.android.systemui.biometrics.UdfpsKeyguardViewControllerLegacy
import com.android.systemui.classifier.Classifier
import com.android.systemui.classifier.FalsingCollector
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dump.DumpManager
+import com.android.systemui.keyguard.MigrateClocksToBlueprint
import com.android.systemui.keyguard.WakefulnessLifecycle
import com.android.systemui.keyguard.domain.interactor.NaturalScrollingSettingObserver
import com.android.systemui.media.controls.ui.controller.MediaHierarchyManager
@@ -69,7 +69,7 @@
private val mediaHierarchyManager: MediaHierarchyManager,
private val scrimTransitionController: LockscreenShadeScrimTransitionController,
private val keyguardTransitionControllerFactory:
- LockscreenShadeKeyguardTransitionController.Factory,
+ LockscreenShadeKeyguardTransitionController.Factory,
private val depthController: NotificationShadeDepthController,
private val context: Context,
private val splitShadeOverScrollerFactory: SplitShadeLockScreenOverScroller.Factory,
@@ -292,8 +292,7 @@
/** @return true if the interaction is accepted, false if it should be cancelled */
internal fun canDragDown(): Boolean {
return (statusBarStateController.state == StatusBarState.KEYGUARD ||
- nsslController.isInLockedDownShade()) &&
- (isQsFullyCollapsed || useSplitShade)
+ nsslController.isInLockedDownShade()) && (isQsFullyCollapsed || useSplitShade)
}
/** Called by the touch helper when when a gesture has completed all the way and released. */
@@ -885,7 +884,7 @@
isDraggingDown = false
isTrackpadReverseScroll = false
shadeRepository.setLegacyLockscreenShadeTracking(false)
- if (nsslFalsingFix() || migrateClocksToBlueprint()) {
+ if (nsslFalsingFix() || MigrateClocksToBlueprint.isEnabled) {
return true
}
} else {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java
index 5171a5c..9a82ecf 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java
@@ -863,7 +863,7 @@
boolean isAppearing = row.isDrawingAppearAnimation() && !row.isInShelf();
iconState.hidden = isAppearing
|| (view instanceof ExpandableNotificationRow
- && ((ExpandableNotificationRow) view).isLowPriority()
+ && ((ExpandableNotificationRow) view).isMinimized()
&& mShelfIcons.areIconsOverflowing())
|| (transitionAmount == 0.0f && !iconState.isAnimating(icon))
|| row.isAboveShelf()
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java
index e111525..8cdf60b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java
@@ -42,7 +42,6 @@
import android.app.RemoteInput;
import android.app.RemoteInputHistoryItem;
import android.content.Context;
-import android.content.pm.ShortcutInfo;
import android.net.Uri;
import android.os.Bundle;
import android.os.Parcelable;
@@ -133,7 +132,6 @@
public Uri remoteInputUri;
public ContentInfo remoteInputAttachment;
private Notification.BubbleMetadata mBubbleMetadata;
- private ShortcutInfo mShortcutInfo;
/**
* If {@link RemoteInput#getEditChoicesBeforeSending} is enabled, and the user is
@@ -168,10 +166,8 @@
private ListenerSet<OnSensitivityChangedListener> mOnSensitivityChangedListeners =
new ListenerSet<>();
- private boolean mAutoHeadsUp;
private boolean mPulseSupressed;
private int mBucket = BUCKET_ALERTING;
- @Nullable private Long mPendingAnimationDuration;
private boolean mIsMarkedForUserTriggeredMovement;
private boolean mIsHeadsUpEntry;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinator.kt
index 0c69a65..8531eaa 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinator.kt
@@ -22,6 +22,7 @@
import android.provider.Settings
import androidx.annotation.VisibleForTesting
import com.android.systemui.Dumpable
+import com.android.systemui.Flags.notificationMinimalismPrototype
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.dump.DumpManager
@@ -59,6 +60,7 @@
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.conflate
import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
@@ -260,8 +262,11 @@
}
}
- private suspend fun trackUnseenFilterSettingChanges() {
- secureSettings
+ private fun unseenFeatureEnabled(): Flow<Boolean> {
+ if (notificationMinimalismPrototype()) {
+ return flowOf(true)
+ }
+ return secureSettings
// emit whenever the setting has changed
.observerFlow(
UserHandle.USER_ALL,
@@ -283,17 +288,20 @@
// only track the most recent emission, if events are happening faster than they can be
// consumed
.conflate()
- .collectLatest { setting ->
- // update local field and invalidate if necessary
- if (setting != unseenFilterEnabled) {
- unseenFilterEnabled = setting
- unseenNotifFilter.invalidateList("unseen setting changed")
- }
- // if the setting is enabled, then start tracking and filtering unseen notifications
- if (setting) {
- trackSeenNotifications()
- }
+ }
+
+ private suspend fun trackUnseenFilterSettingChanges() {
+ unseenFeatureEnabled().collectLatest { setting ->
+ // update local field and invalidate if necessary
+ if (setting != unseenFilterEnabled) {
+ unseenFilterEnabled = setting
+ unseenNotifFilter.invalidateList("unseen setting changed")
}
+ // if the setting is enabled, then start tracking and filtering unseen notifications
+ if (setting) {
+ trackSeenNotifications()
+ }
+ }
}
private val collectionListener =
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/MediaCoordinator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/MediaCoordinator.java
index dcfccd8..0bbde21 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/MediaCoordinator.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/MediaCoordinator.java
@@ -16,7 +16,7 @@
package com.android.systemui.statusbar.notification.collection.coordinator;
-import static com.android.systemui.media.controls.domain.pipeline.MediaDataManagerKt.isMediaNotification;
+import static com.android.systemui.media.controls.domain.pipeline.MediaDataManager.isMediaNotification;
import android.os.RemoteException;
import android.service.notification.StatusBarNotification;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinator.java
index dfb0f9b..7a7b184 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinator.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinator.java
@@ -363,7 +363,7 @@
NotifInflater.Params getInflaterParams(NotifUiAdjustment adjustment, String reason) {
return new NotifInflater.Params(
- /* isLowPriority = */ adjustment.isMinimized(),
+ /* isMinimized = */ adjustment.isMinimized(),
/* reason = */ reason,
/* showSnooze = */ adjustment.isSnoozeEnabled(),
/* isChildInGroup = */ adjustment.isChildInGroup(),
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotifInflater.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotifInflater.kt
index 7b8a062..ff72888 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotifInflater.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotifInflater.kt
@@ -56,7 +56,7 @@
/** A class holding parameters used when inflating the notification row */
class Params(
- val isLowPriority: Boolean,
+ val isMinimized: Boolean,
val reason: String,
val showSnooze: Boolean,
val isChildInGroup: Boolean = false,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinderImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinderImpl.java
index 4bbe035..4a895c0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinderImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinderImpl.java
@@ -243,7 +243,7 @@
@Nullable NotificationRowContentBinder.InflationCallback inflationCallback) {
final boolean useIncreasedCollapsedHeight =
mMessagingUtil.isImportantMessaging(entry.getSbn(), entry.getImportance());
- final boolean isLowPriority = inflaterParams.isLowPriority();
+ final boolean isMinimized = inflaterParams.isMinimized();
// Set show snooze action
row.setShowSnooze(inflaterParams.getShowSnooze());
@@ -252,7 +252,7 @@
params.requireContentViews(FLAG_CONTENT_VIEW_CONTRACTED);
params.requireContentViews(FLAG_CONTENT_VIEW_EXPANDED);
params.setUseIncreasedCollapsedHeight(useIncreasedCollapsedHeight);
- params.setUseLowPriority(isLowPriority);
+ params.setUseMinimized(isMinimized);
if (screenshareNotificationHiding()
? inflaterParams.getNeedsRedaction()
@@ -275,7 +275,7 @@
if (AsyncGroupHeaderViewInflation.isEnabled()) {
if (inflaterParams.isGroupSummary()) {
params.requireContentViews(FLAG_GROUP_SUMMARY_HEADER);
- if (isLowPriority) {
+ if (isMinimized) {
params.requireContentViews(FLAG_LOW_PRIORITY_GROUP_SUMMARY_HEADER);
}
} else {
@@ -288,7 +288,7 @@
mRowContentBindStage.requestRebind(entry, en -> {
mLogger.logRebindComplete(entry);
row.setUsesIncreasedCollapsedHeight(useIncreasedCollapsedHeight);
- row.setIsLowPriority(isLowPriority);
+ row.setIsMinimized(isMinimized);
if (inflationCallback != null) {
inflationCallback.onAsyncInflationFinished(en);
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/NotificationDataLayerModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/NotificationDataLayerModule.kt
index e5e5292..2b0d2aa 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/NotificationDataLayerModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/NotificationDataLayerModule.kt
@@ -15,8 +15,8 @@
*/
package com.android.systemui.statusbar.notification.data
-import com.android.systemui.statusbar.notification.data.repository.HeadsUpNotificationRepository
-import com.android.systemui.statusbar.notification.data.repository.HeadsUpNotificationRepositoryImpl
+import com.android.systemui.statusbar.notification.data.repository.HeadsUpRepository
+import com.android.systemui.statusbar.phone.HeadsUpManagerPhone
import dagger.Binds
import dagger.Module
@@ -27,8 +27,5 @@
]
)
interface NotificationDataLayerModule {
- @Binds
- fun bindHeadsUpNotificationRepository(
- impl: HeadsUpNotificationRepositoryImpl
- ): HeadsUpNotificationRepository
+ @Binds fun bindHeadsUpNotificationRepository(impl: HeadsUpManagerPhone): HeadsUpRepository
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/repository/HeadsUpNotificationRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/repository/HeadsUpNotificationRepository.kt
deleted file mode 100644
index d60ee98..0000000
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/repository/HeadsUpNotificationRepository.kt
+++ /dev/null
@@ -1,59 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.statusbar.notification.data.repository
-
-import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
-import com.android.systemui.statusbar.notification.collection.NotificationEntry
-import com.android.systemui.statusbar.policy.HeadsUpManager
-import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener
-import javax.inject.Inject
-import kotlinx.coroutines.channels.awaitClose
-import kotlinx.coroutines.flow.Flow
-
-class HeadsUpNotificationRepositoryImpl
-@Inject
-constructor(
- headsUpManager: HeadsUpManager,
-) : HeadsUpNotificationRepository {
- override val hasPinnedHeadsUp: Flow<Boolean> = conflatedCallbackFlow {
- val listener =
- object : OnHeadsUpChangedListener {
- override fun onHeadsUpPinnedModeChanged(inPinnedMode: Boolean) {
- trySend(headsUpManager.hasPinnedHeadsUp())
- }
-
- override fun onHeadsUpPinned(entry: NotificationEntry?) {
- trySend(headsUpManager.hasPinnedHeadsUp())
- }
-
- override fun onHeadsUpUnPinned(entry: NotificationEntry?) {
- trySend(headsUpManager.hasPinnedHeadsUp())
- }
-
- override fun onHeadsUpStateChanged(entry: NotificationEntry, isHeadsUp: Boolean) {
- trySend(headsUpManager.hasPinnedHeadsUp())
- }
- }
- trySend(headsUpManager.hasPinnedHeadsUp())
- headsUpManager.addListener(listener)
- awaitClose { headsUpManager.removeListener(listener) }
- }
-}
-
-interface HeadsUpNotificationRepository {
- val hasPinnedHeadsUp: Flow<Boolean>
-}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/repository/HeadsUpRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/repository/HeadsUpRepository.kt
new file mode 100644
index 0000000..ed8c056
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/repository/HeadsUpRepository.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.statusbar.notification.data.repository
+
+import kotlinx.coroutines.flow.Flow
+
+/**
+ * A repository of currently displayed heads up notifications.
+ *
+ * This repository serves as a boundary between the
+ * [com.android.systemui.statusbar.policy.HeadsUpManager] and the modern notifications presentation
+ * codebase.
+ */
+interface HeadsUpRepository {
+
+ /**
+ * True if we are exiting the headsUp pinned mode, and some notifications might still be
+ * animating out. This is used to keep the touchable regions in a reasonable state.
+ */
+ val headsUpAnimatingAway: Flow<Boolean>
+
+ /** The heads up row that should be displayed on top. */
+ val topHeadsUpRow: Flow<HeadsUpRowRepository?>
+
+ /** Set of currently active top-level heads up rows to be displayed. */
+ val activeHeadsUpRows: Flow<Set<HeadsUpRowRepository>>
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/repository/HeadsUpRowRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/repository/HeadsUpRowRepository.kt
new file mode 100644
index 0000000..7b40812
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/repository/HeadsUpRowRepository.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.data.repository
+
+import com.android.systemui.statusbar.notification.shared.HeadsUpRowKey
+import kotlinx.coroutines.flow.StateFlow
+
+/** Representation of a top-level heads up row. */
+interface HeadsUpRowRepository : HeadsUpRowKey {
+ /**
+ * The key for this notification. Guaranteed to be immutable and unique.
+ *
+ * @see com.android.systemui.statusbar.notification.collection.NotificationEntry.getKey
+ */
+ val key: String
+
+ /** A key to identify this row in the view hierarchy. */
+ val elementKey: Any
+
+ /** Whether this notification is "pinned", meaning that it should stay on top of the screen. */
+ val isPinned: StateFlow<Boolean>
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractor.kt
index 5c8f354..d1dd7b5 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractor.kt
@@ -14,14 +14,59 @@
* limitations under the License.
*/
+@file:OptIn(ExperimentalCoroutinesApi::class)
+
package com.android.systemui.statusbar.notification.domain.interactor
-import com.android.systemui.statusbar.notification.data.repository.HeadsUpNotificationRepository
+import com.android.systemui.statusbar.notification.data.repository.HeadsUpRepository
+import com.android.systemui.statusbar.notification.data.repository.HeadsUpRowRepository
+import com.android.systemui.statusbar.notification.shared.HeadsUpRowKey
import javax.inject.Inject
+import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.map
-class HeadsUpNotificationInteractor @Inject constructor(repository: HeadsUpNotificationRepository) {
+class HeadsUpNotificationInteractor @Inject constructor(repository: HeadsUpRepository) {
+
+ val topHeadsUpRow: Flow<HeadsUpRowKey?> = repository.topHeadsUpRow
+
+ /** Set of currently pinned top-level heads up rows to be displayed. */
+ val pinnedHeadsUpRows: Flow<Set<HeadsUpRowKey>> =
+ repository.activeHeadsUpRows.flatMapLatest { repositories ->
+ if (repositories.isNotEmpty()) {
+ val toCombine: List<Flow<Pair<HeadsUpRowRepository, Boolean>>> =
+ repositories.map { repo -> repo.isPinned.map { isPinned -> repo to isPinned } }
+ combine(toCombine) { pairs ->
+ pairs.filter { (_, isPinned) -> isPinned }.map { (repo, _) -> repo }.toSet()
+ }
+ } else {
+ // if the set is empty, there are no flows to combine
+ flowOf(emptySet())
+ }
+ }
+
+ /** Are there any pinned heads up rows to display? */
+ val hasPinnedRows: Flow<Boolean> =
+ repository.activeHeadsUpRows.flatMapLatest { rows ->
+ if (rows.isNotEmpty()) {
+ combine(rows.map { it.isPinned }) { pins -> pins.any { it } }
+ } else {
+ // if the set is empty, there are no flows to combine
+ flowOf(false)
+ }
+ }
+
val isHeadsUpOrAnimatingAway: Flow<Boolean> =
- // TODO(b/296118689): Needs to include the animating away state.
- repository.hasPinnedHeadsUp
+ combine(hasPinnedRows, repository.headsUpAnimatingAway) { hasPinnedRows, animatingAway ->
+ hasPinnedRows || animatingAway
+ }
+
+ fun headsUpRow(key: HeadsUpRowKey): HeadsUpRowInteractor =
+ HeadsUpRowInteractor(key as HeadsUpRowRepository)
+ fun elementKeyFor(key: HeadsUpRowKey) = (key as HeadsUpRowRepository).elementKey
}
+
+class HeadsUpRowInteractor(repository: HeadsUpRowRepository)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/view/FooterView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/view/FooterView.java
index f792898..adcbbfb 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/view/FooterView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/view/FooterView.java
@@ -58,6 +58,7 @@
private FooterViewButton mClearAllButton;
private FooterViewButton mManageOrHistoryButton;
+ private boolean mShouldBeHidden;
private boolean mShowHistory;
// String cache, for performance reasons.
// Reading them from a Resources object can be quite slow sometimes.
@@ -110,6 +111,20 @@
setSecondaryVisible(visible, animate, onAnimationEnded);
}
+ /** See {@link this#setShouldBeHidden} below. */
+ public boolean shouldBeHidden() {
+ return mShouldBeHidden;
+ }
+
+ /**
+ * Whether this view's visibility should be set to INVISIBLE. Note that this is different from
+ * the {@link StackScrollerDecorView#setVisible} method, which in turn handles visibility
+ * transitions between VISIBLE and GONE.
+ */
+ public void setShouldBeHidden(boolean hide) {
+ mShouldBeHidden = hide;
+ }
+
@Override
public void dump(PrintWriter pwOriginal, String[] args) {
IndentingPrintWriter pw = DumpUtilsKt.asIndenting(pwOriginal);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
index c05c3c3..eb6c7b5 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
@@ -327,7 +327,7 @@
private OnClickListener mExpandClickListener = new OnClickListener() {
@Override
public void onClick(View v) {
- if (!shouldShowPublic() && (!mIsLowPriority || isExpanded())
+ if (!shouldShowPublic() && (!mIsMinimized || isExpanded())
&& mGroupMembershipManager.isGroupSummary(mEntry)) {
mGroupExpansionChanging = true;
final boolean wasExpanded = mGroupExpansionManager.isGroupExpanded(mEntry);
@@ -382,7 +382,7 @@
private boolean mAboveShelf;
private OnUserInteractionCallback mOnUserInteractionCallback;
private NotificationGutsManager mNotificationGutsManager;
- private boolean mIsLowPriority;
+ private boolean mIsMinimized;
private boolean mUseIncreasedCollapsedHeight;
private boolean mUseIncreasedHeadsUpHeight;
private float mTranslationWhenRemoved;
@@ -467,7 +467,8 @@
if (viewWrapper != null) {
setIconAnimationRunningForChild(running, viewWrapper.getIcon());
}
- NotificationViewWrapper lowPriWrapper = mChildrenContainer.getLowPriorityViewWrapper();
+ NotificationViewWrapper lowPriWrapper = mChildrenContainer
+ .getMinimizedGroupHeaderWrapper();
if (lowPriWrapper != null) {
setIconAnimationRunningForChild(running, lowPriWrapper.getIcon());
}
@@ -680,7 +681,7 @@
if (color != Notification.COLOR_INVALID) {
return color;
} else {
- return mEntry.getContrastedColor(mContext, mIsLowPriority && !isExpanded(),
+ return mEntry.getContrastedColor(mContext, mIsMinimized && !isExpanded(),
getBackgroundColorWithoutTint());
}
}
@@ -1545,7 +1546,7 @@
* Set the low-priority group notification header view
* @param headerView header view to set
*/
- public void setLowPriorityGroupHeader(NotificationHeaderView headerView) {
+ public void setMinimizedGroupHeader(NotificationHeaderView headerView) {
NotificationChildrenContainer childrenContainer = getChildrenContainerNonNull();
childrenContainer.setLowPriorityGroupHeader(
/* headerViewLowPriority= */ headerView,
@@ -1664,16 +1665,19 @@
}
}
- public void setIsLowPriority(boolean isLowPriority) {
- mIsLowPriority = isLowPriority;
- mPrivateLayout.setIsLowPriority(isLowPriority);
+ /**
+ * Set if the row is minimized.
+ */
+ public void setIsMinimized(boolean isMinimized) {
+ mIsMinimized = isMinimized;
+ mPrivateLayout.setIsLowPriority(isMinimized);
if (mChildrenContainer != null) {
- mChildrenContainer.setIsLowPriority(isLowPriority);
+ mChildrenContainer.setIsMinimized(isMinimized);
}
}
- public boolean isLowPriority() {
- return mIsLowPriority;
+ public boolean isMinimized() {
+ return mIsMinimized;
}
public void setUsesIncreasedCollapsedHeight(boolean use) {
@@ -1763,9 +1767,7 @@
*/
public ExpandableNotificationRow(Context context, AttributeSet attrs) {
this(context, attrs, context);
- if (com.android.systemui.Flags.notificationRowUserContext()) {
- Log.wtf(TAG, "This constructor shouldn't be called");
- }
+ Log.wtf(TAG, "This constructor shouldn't be called");
}
/**
@@ -2050,7 +2052,7 @@
mChildrenContainerStub = findViewById(R.id.child_container_stub);
mChildrenContainerStub.setOnInflateListener((stub, inflated) -> {
mChildrenContainer = (NotificationChildrenContainer) inflated;
- mChildrenContainer.setIsLowPriority(mIsLowPriority);
+ mChildrenContainer.setIsMinimized(mIsMinimized);
mChildrenContainer.setContainingNotification(ExpandableNotificationRow.this);
mChildrenContainer.onNotificationUpdated();
mChildrenContainer.setLogger(mChildrenContainerLogger);
@@ -3435,7 +3437,7 @@
private void onExpansionChanged(boolean userAction, boolean wasExpanded) {
boolean nowExpanded = isExpanded();
- if (mIsSummaryWithChildren && (!mIsLowPriority || wasExpanded)) {
+ if (mIsSummaryWithChildren && (!mIsMinimized || wasExpanded)) {
nowExpanded = mGroupExpansionManager.isGroupExpanded(mEntry);
}
if (nowExpanded != wasExpanded) {
@@ -3492,7 +3494,7 @@
if (!expandable) {
if (mIsSummaryWithChildren) {
expandable = true;
- if (!mIsLowPriority || isExpanded()) {
+ if (!mIsMinimized || isExpanded()) {
isExpanded = isGroupExpanded();
}
} else {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/HybridConversationNotificationView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/HybridConversationNotificationView.java
index 6bc2b2f..ba1cfcc 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/HybridConversationNotificationView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/HybridConversationNotificationView.java
@@ -30,14 +30,21 @@
import android.widget.TextView;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.widget.ConversationAvatarData;
+import com.android.internal.widget.ConversationAvatarData.GroupConversationAvatarData;
+import com.android.internal.widget.ConversationAvatarData.OneToOneConversationAvatarData;
+import com.android.internal.widget.ConversationHeaderData;
import com.android.internal.widget.ConversationLayout;
import com.android.systemui.res.R;
import com.android.systemui.statusbar.notification.NotificationFadeAware;
import com.android.systemui.statusbar.notification.row.shared.AsyncHybridViewInflation;
+import com.android.systemui.statusbar.notification.row.shared.ConversationStyleSetAvatarAsync;
import com.android.systemui.statusbar.notification.row.ui.viewmodel.ConversationAvatar;
import com.android.systemui.statusbar.notification.row.ui.viewmodel.FacePile;
import com.android.systemui.statusbar.notification.row.ui.viewmodel.SingleIcon;
+import java.util.Objects;
+
/**
* A hybrid view which may contain information about one ore more conversations.
*/
@@ -103,7 +110,7 @@
@Override
public void bind(@Nullable CharSequence title, @Nullable CharSequence text,
- @Nullable View contentView) {
+ @Nullable View contentView) {
AsyncHybridViewInflation.assertInLegacyMode();
if (!(contentView instanceof ConversationLayout)) {
super.bind(title, text, contentView);
@@ -111,34 +118,7 @@
}
ConversationLayout conversationLayout = (ConversationLayout) contentView;
- Icon conversationIcon = conversationLayout.getConversationIcon();
- if (conversationIcon != null) {
- mConversationFacePile.setVisibility(GONE);
- mConversationIconView.setVisibility(VISIBLE);
- mConversationIconView.setImageIcon(conversationIcon);
- setSize(mConversationIconView, mSingleAvatarSize);
- } else {
- // If there isn't an icon, generate a "face pile" based on the sender avatars
- mConversationIconView.setVisibility(GONE);
- mConversationFacePile.setVisibility(VISIBLE);
-
- mConversationFacePile =
- requireViewById(com.android.internal.R.id.conversation_face_pile);
- ImageView facePileBottomBg = mConversationFacePile.requireViewById(
- com.android.internal.R.id.conversation_face_pile_bottom_background);
- ImageView facePileBottom = mConversationFacePile.requireViewById(
- com.android.internal.R.id.conversation_face_pile_bottom);
- ImageView facePileTop = mConversationFacePile.requireViewById(
- com.android.internal.R.id.conversation_face_pile_top);
- conversationLayout.bindFacePile(facePileBottomBg, facePileBottom, facePileTop);
- setSize(mConversationFacePile, mFacePileSize);
- setSize(facePileBottom, mFacePileAvatarSize);
- setSize(facePileTop, mFacePileAvatarSize);
- setSize(facePileBottomBg, mFacePileAvatarSize + 2 * mFacePileProtectionWidth);
- mTransformationHelper.addViewTransformingToSimilar(facePileTop);
- mTransformationHelper.addViewTransformingToSimilar(facePileBottom);
- mTransformationHelper.addViewTransformingToSimilar(facePileBottomBg);
- }
+ loadConversationAvatar(conversationLayout);
CharSequence conversationTitle = conversationLayout.getConversationTitle();
if (TextUtils.isEmpty(conversationTitle)) {
conversationTitle = title;
@@ -156,6 +136,90 @@
super.bind(conversationTitle, conversationText, conversationLayout);
}
+ private void loadConversationAvatar(ConversationLayout conversationLayout) {
+ AsyncHybridViewInflation.assertInLegacyMode();
+ if (ConversationStyleSetAvatarAsync.isEnabled()) {
+ loadConversationAvatarWithDrawable(conversationLayout);
+ } else {
+ loadConversationAvatarWithIcon(conversationLayout);
+ }
+ }
+
+ @Deprecated
+ private void loadConversationAvatarWithIcon(ConversationLayout conversationLayout) {
+ ConversationStyleSetAvatarAsync.assertInLegacyMode();
+ AsyncHybridViewInflation.assertInLegacyMode();
+ final Icon conversationIcon = conversationLayout.getConversationIcon();
+ if (conversationIcon != null) {
+ mConversationFacePile.setVisibility(GONE);
+ mConversationIconView.setVisibility(VISIBLE);
+ mConversationIconView.setImageIcon(conversationIcon);
+ setSize(mConversationIconView, mSingleAvatarSize);
+ } else {
+ // If there isn't an icon, generate a "face pile" based on the sender avatars
+ mConversationIconView.setVisibility(GONE);
+ mConversationFacePile.setVisibility(VISIBLE);
+
+ mConversationFacePile =
+ requireViewById(com.android.internal.R.id.conversation_face_pile);
+ final ImageView facePileBottomBg = mConversationFacePile.requireViewById(
+ com.android.internal.R.id.conversation_face_pile_bottom_background);
+ final ImageView facePileBottom = mConversationFacePile.requireViewById(
+ com.android.internal.R.id.conversation_face_pile_bottom);
+ final ImageView facePileTop = mConversationFacePile.requireViewById(
+ com.android.internal.R.id.conversation_face_pile_top);
+ conversationLayout.bindFacePile(facePileBottomBg, facePileBottom, facePileTop);
+ setSize(mConversationFacePile, mFacePileSize);
+ setSize(facePileBottom, mFacePileAvatarSize);
+ setSize(facePileTop, mFacePileAvatarSize);
+ setSize(facePileBottomBg, mFacePileAvatarSize + 2 * mFacePileProtectionWidth);
+ mTransformationHelper.addViewTransformingToSimilar(facePileTop);
+ mTransformationHelper.addViewTransformingToSimilar(facePileBottom);
+ mTransformationHelper.addViewTransformingToSimilar(facePileBottomBg);
+ }
+ }
+
+ private void loadConversationAvatarWithDrawable(ConversationLayout conversationLayout) {
+ AsyncHybridViewInflation.assertInLegacyMode();
+ final ConversationHeaderData conversationHeaderData = Objects.requireNonNull(
+ conversationLayout.getConversationHeaderData(),
+ /* message = */ "conversationHeaderData should not be null");
+ final ConversationAvatarData conversationAvatar =
+ Objects.requireNonNull(conversationHeaderData.getConversationAvatar(),
+ /* message = */"conversationAvatar should not be null");
+
+ if (conversationAvatar instanceof OneToOneConversationAvatarData oneToOneAvatar) {
+ mConversationFacePile.setVisibility(GONE);
+ mConversationIconView.setVisibility(VISIBLE);
+ mConversationIconView.setImageDrawable(oneToOneAvatar.mDrawable);
+ setSize(mConversationIconView, mSingleAvatarSize);
+ } else {
+ // If there isn't an icon, generate a "face pile" based on the sender avatars
+ mConversationIconView.setVisibility(GONE);
+ mConversationFacePile.setVisibility(VISIBLE);
+
+ final GroupConversationAvatarData groupAvatar =
+ (GroupConversationAvatarData) conversationAvatar;
+ mConversationFacePile =
+ requireViewById(com.android.internal.R.id.conversation_face_pile);
+ final ImageView facePileBottomBg = mConversationFacePile.requireViewById(
+ com.android.internal.R.id.conversation_face_pile_bottom_background);
+ final ImageView facePileBottom = mConversationFacePile.requireViewById(
+ com.android.internal.R.id.conversation_face_pile_bottom);
+ final ImageView facePileTop = mConversationFacePile.requireViewById(
+ com.android.internal.R.id.conversation_face_pile_top);
+ conversationLayout.bindFacePileWithDrawable(facePileBottomBg, facePileBottom,
+ facePileTop, groupAvatar);
+ setSize(mConversationFacePile, mFacePileSize);
+ setSize(facePileBottom, mFacePileAvatarSize);
+ setSize(facePileTop, mFacePileAvatarSize);
+ setSize(facePileBottomBg, mFacePileAvatarSize + 2 * mFacePileProtectionWidth);
+ mTransformationHelper.addViewTransformingToSimilar(facePileTop);
+ mTransformationHelper.addViewTransformingToSimilar(facePileBottom);
+ mTransformationHelper.addViewTransformingToSimilar(facePileBottomBg);
+ }
+ }
+
/**
* Set the avatar using ConversationAvatar from SingleLineViewModel
*
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java
index f835cca..ded635c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java
@@ -150,7 +150,7 @@
entry,
mConversationProcessor,
row,
- bindParams.isLowPriority,
+ bindParams.isMinimized,
bindParams.usesIncreasedHeight,
bindParams.usesIncreasedHeadsUpHeight,
callback,
@@ -178,7 +178,7 @@
SmartReplyStateInflater smartRepliesInflater) {
InflationProgress result = createRemoteViews(reInflateFlags,
builder,
- bindParams.isLowPriority,
+ bindParams.isMinimized,
bindParams.usesIncreasedHeight,
bindParams.usesIncreasedHeadsUpHeight,
packageContext,
@@ -215,6 +215,7 @@
apply(
mInflationExecutor,
inflateSynchronously,
+ bindParams.isMinimized,
result,
reInflateFlags,
mRemoteViewCache,
@@ -365,7 +366,7 @@
}
private static InflationProgress createRemoteViews(@InflationFlag int reInflateFlags,
- Notification.Builder builder, boolean isLowPriority, boolean usesIncreasedHeight,
+ Notification.Builder builder, boolean isMinimized, boolean usesIncreasedHeight,
boolean usesIncreasedHeadsUpHeight, Context packageContext,
ExpandableNotificationRow row,
NotifLayoutInflaterFactory.Provider notifLayoutInflaterFactoryProvider,
@@ -376,13 +377,13 @@
if ((reInflateFlags & FLAG_CONTENT_VIEW_CONTRACTED) != 0) {
logger.logAsyncTaskProgress(entryForLogging, "creating contracted remote view");
- result.newContentView = createContentView(builder, isLowPriority,
+ result.newContentView = createContentView(builder, isMinimized,
usesIncreasedHeight);
}
if ((reInflateFlags & FLAG_CONTENT_VIEW_EXPANDED) != 0) {
logger.logAsyncTaskProgress(entryForLogging, "creating expanded remote view");
- result.newExpandedView = createExpandedView(builder, isLowPriority);
+ result.newExpandedView = createExpandedView(builder, isMinimized);
}
if ((reInflateFlags & FLAG_CONTENT_VIEW_HEADS_UP) != 0) {
@@ -393,7 +394,7 @@
if ((reInflateFlags & FLAG_CONTENT_VIEW_PUBLIC) != 0) {
logger.logAsyncTaskProgress(entryForLogging, "creating public remote view");
- result.newPublicView = builder.makePublicContentView(isLowPriority);
+ result.newPublicView = builder.makePublicContentView(isMinimized);
}
if (AsyncGroupHeaderViewInflation.isEnabled()) {
@@ -406,7 +407,7 @@
if ((reInflateFlags & FLAG_LOW_PRIORITY_GROUP_SUMMARY_HEADER) != 0) {
logger.logAsyncTaskProgress(entryForLogging,
"creating low-priority group summary remote view");
- result.mNewLowPriorityGroupHeaderView =
+ result.mNewMinimizedGroupHeaderView =
builder.makeLowPriorityContentView(true /* useRegularSubtext */);
}
}
@@ -444,6 +445,7 @@
private static CancellationSignal apply(
Executor inflationExecutor,
boolean inflateSynchronously,
+ boolean isMinimized,
InflationProgress result,
@InflationFlag int reInflateFlags,
NotifRemoteViewCache remoteViewCache,
@@ -475,7 +477,8 @@
}
};
logger.logAsyncTaskProgress(entry, "applying contracted view");
- applyRemoteView(inflationExecutor, inflateSynchronously, result, reInflateFlags, flag,
+ applyRemoteView(inflationExecutor, inflateSynchronously, isMinimized, result,
+ reInflateFlags, flag,
remoteViewCache, entry, row, isNewView, remoteViewClickHandler, callback,
privateLayout, privateLayout.getContractedChild(),
privateLayout.getVisibleWrapper(
@@ -502,7 +505,8 @@
}
};
logger.logAsyncTaskProgress(entry, "applying expanded view");
- applyRemoteView(inflationExecutor, inflateSynchronously, result, reInflateFlags,
+ applyRemoteView(inflationExecutor, inflateSynchronously, isMinimized, result,
+ reInflateFlags,
flag, remoteViewCache, entry, row, isNewView, remoteViewClickHandler,
callback, privateLayout, privateLayout.getExpandedChild(),
privateLayout.getVisibleWrapper(VISIBLE_TYPE_EXPANDED), runningInflations,
@@ -529,7 +533,8 @@
}
};
logger.logAsyncTaskProgress(entry, "applying heads up view");
- applyRemoteView(inflationExecutor, inflateSynchronously, result, reInflateFlags,
+ applyRemoteView(inflationExecutor, inflateSynchronously, isMinimized,
+ result, reInflateFlags,
flag, remoteViewCache, entry, row, isNewView, remoteViewClickHandler,
callback, privateLayout, privateLayout.getHeadsUpChild(),
privateLayout.getVisibleWrapper(VISIBLE_TYPE_HEADSUP), runningInflations,
@@ -555,7 +560,8 @@
}
};
logger.logAsyncTaskProgress(entry, "applying public view");
- applyRemoteView(inflationExecutor, inflateSynchronously, result, reInflateFlags, flag,
+ applyRemoteView(inflationExecutor, inflateSynchronously, isMinimized,
+ result, reInflateFlags, flag,
remoteViewCache, entry, row, isNewView, remoteViewClickHandler, callback,
publicLayout, publicLayout.getContractedChild(),
publicLayout.getVisibleWrapper(NotificationContentView.VISIBLE_TYPE_CONTRACTED),
@@ -583,11 +589,12 @@
}
};
logger.logAsyncTaskProgress(entry, "applying group header view");
- applyRemoteView(inflationExecutor, inflateSynchronously, result, reInflateFlags,
+ applyRemoteView(inflationExecutor, inflateSynchronously, isMinimized,
+ result, reInflateFlags,
/* inflationId = */ FLAG_GROUP_SUMMARY_HEADER,
remoteViewCache, entry, row, isNewView, remoteViewClickHandler, callback,
/* parentLayout = */ childrenContainer,
- /* existingView = */ childrenContainer.getNotificationHeader(),
+ /* existingView = */ childrenContainer.getGroupHeader(),
/* existingWrapper = */ childrenContainer.getNotificationHeaderWrapper(),
runningInflations, applyCallback, logger);
}
@@ -595,7 +602,7 @@
if ((reInflateFlags & FLAG_LOW_PRIORITY_GROUP_SUMMARY_HEADER) != 0) {
boolean isNewView =
!canReapplyRemoteView(
- /* newView = */ result.mNewLowPriorityGroupHeaderView,
+ /* newView = */ result.mNewMinimizedGroupHeaderView,
/* oldView = */ remoteViewCache.getCachedView(
entry, FLAG_LOW_PRIORITY_GROUP_SUMMARY_HEADER));
ApplyCallback applyCallback = new ApplyCallback() {
@@ -603,29 +610,30 @@
public void setResultView(View v) {
logger.logAsyncTaskProgress(entry,
"low-priority group header view applied");
- result.mInflatedLowPriorityGroupHeaderView = (NotificationHeaderView) v;
+ result.mInflatedMinimizedGroupHeaderView = (NotificationHeaderView) v;
}
@Override
public RemoteViews getRemoteView() {
- return result.mNewLowPriorityGroupHeaderView;
+ return result.mNewMinimizedGroupHeaderView;
}
};
logger.logAsyncTaskProgress(entry, "applying low priority group header view");
- applyRemoteView(inflationExecutor, inflateSynchronously, result, reInflateFlags,
+ applyRemoteView(inflationExecutor, inflateSynchronously, isMinimized,
+ result, reInflateFlags,
/* inflationId = */ FLAG_LOW_PRIORITY_GROUP_SUMMARY_HEADER,
remoteViewCache, entry, row, isNewView, remoteViewClickHandler, callback,
/* parentLayout = */ childrenContainer,
- /* existingView = */ childrenContainer.getNotificationHeaderLowPriority(),
+ /* existingView = */ childrenContainer.getMinimizedNotificationHeader(),
/* existingWrapper = */ childrenContainer
- .getLowPriorityViewWrapper(),
+ .getMinimizedGroupHeaderWrapper(),
runningInflations, applyCallback, logger);
}
}
// Let's try to finish, maybe nobody is even inflating anything
- finishIfDone(result, reInflateFlags, remoteViewCache, runningInflations, callback, entry,
- row, logger);
+ finishIfDone(result, isMinimized, reInflateFlags, remoteViewCache, runningInflations,
+ callback, entry, row, logger);
CancellationSignal cancellationSignal = new CancellationSignal();
cancellationSignal.setOnCancelListener(
() -> {
@@ -641,6 +649,7 @@
static void applyRemoteView(
Executor inflationExecutor,
boolean inflateSynchronously,
+ boolean isMinimized,
final InflationProgress result,
final @InflationFlag int reInflateFlags,
@InflationFlag int inflationId,
@@ -707,7 +716,8 @@
existingWrapper.onReinflated();
}
runningInflations.remove(inflationId);
- finishIfDone(result, reInflateFlags, remoteViewCache, runningInflations,
+ finishIfDone(result, isMinimized,
+ reInflateFlags, remoteViewCache, runningInflations,
callback, entry, row, logger);
}
@@ -838,6 +848,7 @@
* @return true if the inflation was finished
*/
private static boolean finishIfDone(InflationProgress result,
+ boolean isMinimized,
@InflationFlag int reInflateFlags, NotifRemoteViewCache remoteViewCache,
HashMap<Integer, CancellationSignal> runningInflations,
@Nullable InflationCallback endListener, NotificationEntry entry,
@@ -944,7 +955,9 @@
if (AsyncGroupHeaderViewInflation.isEnabled()) {
if ((reInflateFlags & FLAG_GROUP_SUMMARY_HEADER) != 0) {
if (result.mInflatedGroupHeaderView != null) {
- row.setIsLowPriority(false);
+ // We need to set if the row is minimized before setting the group header to
+ // make sure the setting of header view works correctly
+ row.setIsMinimized(isMinimized);
row.setGroupHeader(/* headerView= */ result.mInflatedGroupHeaderView);
remoteViewCache.putCachedView(entry, FLAG_GROUP_SUMMARY_HEADER,
result.mNewGroupHeaderView);
@@ -957,13 +970,14 @@
}
if ((reInflateFlags & FLAG_LOW_PRIORITY_GROUP_SUMMARY_HEADER) != 0) {
- if (result.mInflatedLowPriorityGroupHeaderView != null) {
- // New view case, set row to low priority
- row.setIsLowPriority(true);
- row.setLowPriorityGroupHeader(
- /* headerView= */ result.mInflatedLowPriorityGroupHeaderView);
+ if (result.mInflatedMinimizedGroupHeaderView != null) {
+ // We need to set if the row is minimized before setting the group header to
+ // make sure the setting of header view works correctly
+ row.setIsMinimized(isMinimized);
+ row.setMinimizedGroupHeader(
+ /* headerView= */ result.mInflatedMinimizedGroupHeaderView);
remoteViewCache.putCachedView(entry, FLAG_LOW_PRIORITY_GROUP_SUMMARY_HEADER,
- result.mNewLowPriorityGroupHeaderView);
+ result.mNewMinimizedGroupHeaderView);
} else if (remoteViewCache.hasCachedView(entry,
FLAG_LOW_PRIORITY_GROUP_SUMMARY_HEADER)) {
// Re-inflation case. Only update if it's still cached (i.e. view has not
@@ -984,12 +998,12 @@
}
private static RemoteViews createExpandedView(Notification.Builder builder,
- boolean isLowPriority) {
+ boolean isMinimized) {
RemoteViews bigContentView = builder.createBigContentView();
if (bigContentView != null) {
return bigContentView;
}
- if (isLowPriority) {
+ if (isMinimized) {
RemoteViews contentView = builder.createContentView();
Notification.Builder.makeHeaderExpanded(contentView);
return contentView;
@@ -998,8 +1012,8 @@
}
private static RemoteViews createContentView(Notification.Builder builder,
- boolean isLowPriority, boolean useLarge) {
- if (isLowPriority) {
+ boolean isMinimized, boolean useLarge) {
+ if (isMinimized) {
return builder.makeLowPriorityContentView(false /* useRegularSubtext */);
}
return builder.createContentView(useLarge);
@@ -1038,7 +1052,7 @@
private final NotificationEntry mEntry;
private final Context mContext;
private final boolean mInflateSynchronously;
- private final boolean mIsLowPriority;
+ private final boolean mIsMinimized;
private final boolean mUsesIncreasedHeight;
private final InflationCallback mCallback;
private final boolean mUsesIncreasedHeadsUpHeight;
@@ -1063,7 +1077,7 @@
NotificationEntry entry,
ConversationNotificationProcessor conversationProcessor,
ExpandableNotificationRow row,
- boolean isLowPriority,
+ boolean isMinimized,
boolean usesIncreasedHeight,
boolean usesIncreasedHeadsUpHeight,
InflationCallback callback,
@@ -1080,7 +1094,7 @@
mRemoteViewCache = cache;
mSmartRepliesInflater = smartRepliesInflater;
mContext = mRow.getContext();
- mIsLowPriority = isLowPriority;
+ mIsMinimized = isMinimized;
mUsesIncreasedHeight = usesIncreasedHeight;
mUsesIncreasedHeadsUpHeight = usesIncreasedHeadsUpHeight;
mRemoteViewClickHandler = remoteViewClickHandler;
@@ -1150,7 +1164,7 @@
mEntry, recoveredBuilder, mLogger);
}
InflationProgress inflationProgress = createRemoteViews(mReInflateFlags,
- recoveredBuilder, mIsLowPriority, mUsesIncreasedHeight,
+ recoveredBuilder, mIsMinimized, mUsesIncreasedHeight,
mUsesIncreasedHeadsUpHeight, packageContext, mRow,
mNotifLayoutInflaterFactoryProvider, mLogger);
@@ -1209,6 +1223,7 @@
mCancellationSignal = apply(
mInflationExecutor,
mInflateSynchronously,
+ mIsMinimized,
result,
mReInflateFlags,
mRemoteViewCache,
@@ -1295,7 +1310,7 @@
private RemoteViews newExpandedView;
private RemoteViews newPublicView;
private RemoteViews mNewGroupHeaderView;
- private RemoteViews mNewLowPriorityGroupHeaderView;
+ private RemoteViews mNewMinimizedGroupHeaderView;
@VisibleForTesting
Context packageContext;
@@ -1305,7 +1320,7 @@
private View inflatedExpandedView;
private View inflatedPublicView;
private NotificationHeaderView mInflatedGroupHeaderView;
- private NotificationHeaderView mInflatedLowPriorityGroupHeaderView;
+ private NotificationHeaderView mInflatedMinimizedGroupHeaderView;
private CharSequence headsUpStatusBarText;
private CharSequence headsUpStatusBarTextPublic;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java
index 8a3e7e8..6f00d96 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java
@@ -1514,7 +1514,7 @@
}
ImageView bubbleButton = layout.findViewById(com.android.internal.R.id.bubble_button);
View actionContainer = layout.findViewById(com.android.internal.R.id.actions_container);
- LinearLayout actionListMarginTarget = layout.findViewById(
+ ViewGroup actionListMarginTarget = layout.findViewById(
com.android.internal.R.id.notification_action_list_margin_target);
if (bubbleButton == null || actionContainer == null) {
return;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationInlineImageResolver.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationInlineImageResolver.java
index 609b15e..3e932aa 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationInlineImageResolver.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationInlineImageResolver.java
@@ -31,7 +31,6 @@
import com.android.internal.widget.ImageResolver;
import com.android.internal.widget.LocalImageResolver;
import com.android.internal.widget.MessagingMessage;
-import com.android.systemui.Flags;
import java.util.HashSet;
import java.util.List;
@@ -67,11 +66,7 @@
* @param imageCache The implementation of internal cache.
*/
public NotificationInlineImageResolver(Context context, ImageCache imageCache) {
- if (Flags.notificationRowUserContext()) {
- mContext = context;
- } else {
- mContext = context.getApplicationContext();
- }
+ mContext = context;
mImageCache = imageCache;
if (mImageCache != null) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinder.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinder.java
index b0fd475..33339a7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinder.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinder.java
@@ -128,9 +128,9 @@
class BindParams {
/**
- * Bind a low priority version of the content views.
+ * Bind a minimized version of the content views.
*/
- public boolean isLowPriority;
+ public boolean isMinimized;
/**
* Use increased height when binding contracted view.
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindParams.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindParams.java
index 1494c27..bae89fb 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindParams.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindParams.java
@@ -26,7 +26,7 @@
* Parameters for {@link RowContentBindStage}.
*/
public final class RowContentBindParams {
- private boolean mUseLowPriority;
+ private boolean mUseMinimized;
private boolean mUseIncreasedHeight;
private boolean mUseIncreasedHeadsUpHeight;
private boolean mViewsNeedReinflation;
@@ -41,17 +41,20 @@
private @InflationFlag int mDirtyContentViews = mContentViews;
/**
- * Set whether content should use a low priority version of its content views.
+ * Set whether content should use a minimized version of its content views.
*/
- public void setUseLowPriority(boolean useLowPriority) {
- if (mUseLowPriority != useLowPriority) {
+ public void setUseMinimized(boolean useMinimized) {
+ if (mUseMinimized != useMinimized) {
mDirtyContentViews |= (FLAG_CONTENT_VIEW_CONTRACTED | FLAG_CONTENT_VIEW_EXPANDED);
}
- mUseLowPriority = useLowPriority;
+ mUseMinimized = useMinimized;
}
- public boolean useLowPriority() {
- return mUseLowPriority;
+ /**
+ * @return Whether the row uses the minimized style.
+ */
+ public boolean useMinimized() {
+ return mUseMinimized;
}
/**
@@ -149,9 +152,9 @@
@Override
public String toString() {
return String.format("RowContentBindParams[mContentViews=%x mDirtyContentViews=%x "
- + "mUseLowPriority=%b mUseIncreasedHeight=%b "
+ + "mUseMinimized=%b mUseIncreasedHeight=%b "
+ "mUseIncreasedHeadsUpHeight=%b mViewsNeedReinflation=%b]",
- mContentViews, mDirtyContentViews, mUseLowPriority, mUseIncreasedHeight,
+ mContentViews, mDirtyContentViews, mUseMinimized, mUseIncreasedHeight,
mUseIncreasedHeadsUpHeight, mViewsNeedReinflation);
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindStage.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindStage.java
index f4f8374..89fcda9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindStage.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindStage.java
@@ -73,7 +73,7 @@
mBinder.unbindContent(entry, row, contentToUnbind);
BindParams bindParams = new BindParams();
- bindParams.isLowPriority = params.useLowPriority();
+ bindParams.isMinimized = params.useMinimized();
bindParams.usesIncreasedHeight = params.useIncreasedHeight();
bindParams.usesIncreasedHeadsUpHeight = params.useIncreasedHeadsUpHeight();
boolean forceInflate = params.needsReinflation();
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowInflaterTask.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowInflaterTask.java
index ea3036e..5fbcebd 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowInflaterTask.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowInflaterTask.java
@@ -66,9 +66,7 @@
mInflateOrigin = new Throwable("inflate requested here");
}
mListener = listener;
- AsyncLayoutInflater inflater = com.android.systemui.Flags.notificationRowUserContext()
- ? new AsyncLayoutInflater(context, makeRowInflater(entry))
- : new AsyncLayoutInflater(context);
+ AsyncLayoutInflater inflater = new AsyncLayoutInflater(context, makeRowInflater(entry));
mEntry = entry;
entry.setInflationTask(this);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/shared/ConversationStyleSetAvatarAsync.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/shared/ConversationStyleSetAvatarAsync.kt
new file mode 100644
index 0000000..3c056c9
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/shared/ConversationStyleSetAvatarAsync.kt
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.row.shared
+
+import android.widget.flags.Flags
+import com.android.systemui.flags.FlagToken
+import com.android.systemui.flags.RefactorFlagUtils
+
+/** Helper for reading or using the conversation style set avatar async flag state. */
+@Suppress("NOTHING_TO_INLINE")
+object ConversationStyleSetAvatarAsync {
+ const val FLAG_NAME = Flags.FLAG_CONVERSATION_STYLE_SET_AVATAR_ASYNC
+
+ /** A token used for dependency declaration */
+ val token: FlagToken
+ get() = FlagToken(FLAG_NAME, isEnabled)
+
+ /** Is async hybrid (single-line) view inflation enabled */
+ @JvmStatic
+ inline val isEnabled
+ get() = Flags.conversationStyleSetAvatarAsync()
+
+ /**
+ * Called to ensure code is only run when the flag is enabled. This protects users from the
+ * unintended behaviors caused by accidentally running new logic, while also crashing on an eng
+ * build to ensure that the refactor author catches issues in testing.
+ */
+ @JvmStatic
+ inline fun isUnexpectedlyInLegacyMode() =
+ RefactorFlagUtils.isUnexpectedlyInLegacyMode(isEnabled, FLAG_NAME)
+
+ /**
+ * Called to ensure code is only run when the flag is disabled. This will throw an exception if
+ * the flag is enabled to ensure that the refactor author catches issues in testing.
+ */
+ @JvmStatic
+ inline fun assertInLegacyMode() = RefactorFlagUtils.assertInLegacyMode(isEnabled, FLAG_NAME)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/HeadsUpRowKey.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/HeadsUpRowKey.kt
new file mode 100644
index 0000000..8dc395d
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/HeadsUpRowKey.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.statusbar.notification.shared
+
+/**
+ * A unique key representing a top-level heads up notification.
+ *
+ * @see com.android.systemui.statusbar.notification.domain.interactor.HeadsUpNotificationInteractor
+ */
+interface HeadsUpRowKey
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationsHeadsUpRefactor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationsHeadsUpRefactor.kt
new file mode 100644
index 0000000..62641fe
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationsHeadsUpRefactor.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.statusbar.notification.shared
+
+import com.android.systemui.Flags
+import com.android.systemui.flags.FlagToken
+import com.android.systemui.flags.RefactorFlagUtils
+
+/** Helper for reading or using the notifications heads up refactor flag state. */
+@Suppress("NOTHING_TO_INLINE")
+object NotificationsHeadsUpRefactor {
+ /** The aconfig flag name */
+ const val FLAG_NAME = Flags.FLAG_NOTIFICATIONS_HEADS_UP_REFACTOR
+
+ /** A token used for dependency declaration */
+ val token: FlagToken
+ get() = FlagToken(FLAG_NAME, isEnabled)
+
+ /** Is the refactor enabled */
+ @JvmStatic
+ inline val isEnabled
+ get() = Flags.notificationsHeadsUpRefactor()
+
+ /**
+ * Called to ensure code is only run when the flag is enabled. This protects users from the
+ * unintended behaviors caused by accidentally running new logic, while also crashing on an eng
+ * build to ensure that the refactor author catches issues in testing.
+ */
+ @JvmStatic
+ inline fun isUnexpectedlyInLegacyMode() =
+ RefactorFlagUtils.isUnexpectedlyInLegacyMode(isEnabled, FLAG_NAME)
+
+ /**
+ * Called to ensure code is only run when the flag is disabled. This will throw an exception if
+ * the flag is enabled to ensure that the refactor author catches issues in testing.
+ */
+ @JvmStatic
+ inline fun assertInLegacyMode() = RefactorFlagUtils.assertInLegacyMode(isEnabled, FLAG_NAME)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java
index 28f874d..5dc37e0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java
@@ -110,14 +110,14 @@
*/
private boolean mEnableShadowOnChildNotifications;
- private NotificationHeaderView mNotificationHeader;
- private NotificationHeaderViewWrapper mNotificationHeaderWrapper;
- private NotificationHeaderView mNotificationHeaderLowPriority;
- private NotificationHeaderViewWrapper mNotificationHeaderWrapperLowPriority;
+ private NotificationHeaderView mGroupHeader;
+ private NotificationHeaderViewWrapper mGroupHeaderWrapper;
+ private NotificationHeaderView mMinimizedGroupHeader;
+ private NotificationHeaderViewWrapper mMinimizedGroupHeaderWrapper;
private NotificationGroupingUtil mGroupingUtil;
private ViewState mHeaderViewState;
private int mClipBottomAmount;
- private boolean mIsLowPriority;
+ private boolean mIsMinimized;
private OnClickListener mHeaderClickListener;
private ViewGroup mCurrentHeader;
private boolean mIsConversation;
@@ -217,14 +217,14 @@
int right = left + mOverflowNumber.getMeasuredWidth();
mOverflowNumber.layout(left, 0, right, mOverflowNumber.getMeasuredHeight());
}
- if (mNotificationHeader != null) {
- mNotificationHeader.layout(0, 0, mNotificationHeader.getMeasuredWidth(),
- mNotificationHeader.getMeasuredHeight());
+ if (mGroupHeader != null) {
+ mGroupHeader.layout(0, 0, mGroupHeader.getMeasuredWidth(),
+ mGroupHeader.getMeasuredHeight());
}
- if (mNotificationHeaderLowPriority != null) {
- mNotificationHeaderLowPriority.layout(0, 0,
- mNotificationHeaderLowPriority.getMeasuredWidth(),
- mNotificationHeaderLowPriority.getMeasuredHeight());
+ if (mMinimizedGroupHeader != null) {
+ mMinimizedGroupHeader.layout(0, 0,
+ mMinimizedGroupHeader.getMeasuredWidth(),
+ mMinimizedGroupHeader.getMeasuredHeight());
}
}
@@ -271,11 +271,11 @@
}
int headerHeightSpec = MeasureSpec.makeMeasureSpec(mHeaderHeight, MeasureSpec.EXACTLY);
- if (mNotificationHeader != null) {
- mNotificationHeader.measure(widthMeasureSpec, headerHeightSpec);
+ if (mGroupHeader != null) {
+ mGroupHeader.measure(widthMeasureSpec, headerHeightSpec);
}
- if (mNotificationHeaderLowPriority != null) {
- mNotificationHeaderLowPriority.measure(widthMeasureSpec, headerHeightSpec);
+ if (mMinimizedGroupHeader != null) {
+ mMinimizedGroupHeader.measure(widthMeasureSpec, headerHeightSpec);
}
setMeasuredDimension(width, height);
@@ -308,11 +308,11 @@
* appropriately.
*/
public void setNotificationGroupWhen(long whenMillis) {
- if (mNotificationHeaderWrapper != null) {
- mNotificationHeaderWrapper.setNotificationWhen(whenMillis);
+ if (mGroupHeaderWrapper != null) {
+ mGroupHeaderWrapper.setNotificationWhen(whenMillis);
}
- if (mNotificationHeaderWrapperLowPriority != null) {
- mNotificationHeaderWrapperLowPriority.setNotificationWhen(whenMillis);
+ if (mMinimizedGroupHeaderWrapper != null) {
+ mMinimizedGroupHeaderWrapper.setNotificationWhen(whenMillis);
}
}
@@ -410,28 +410,28 @@
Trace.beginSection("recreateHeader#makeNotificationGroupHeader");
RemoteViews header = builder.makeNotificationGroupHeader();
Trace.endSection();
- if (mNotificationHeader == null) {
+ if (mGroupHeader == null) {
Trace.beginSection("recreateHeader#apply");
- mNotificationHeader = (NotificationHeaderView) header.apply(getContext(), this);
+ mGroupHeader = (NotificationHeaderView) header.apply(getContext(), this);
Trace.endSection();
- mNotificationHeader.findViewById(com.android.internal.R.id.expand_button)
+ mGroupHeader.findViewById(com.android.internal.R.id.expand_button)
.setVisibility(VISIBLE);
- mNotificationHeader.setOnClickListener(mHeaderClickListener);
- mNotificationHeaderWrapper =
+ mGroupHeader.setOnClickListener(mHeaderClickListener);
+ mGroupHeaderWrapper =
(NotificationHeaderViewWrapper) NotificationViewWrapper.wrap(
getContext(),
- mNotificationHeader,
+ mGroupHeader,
mContainingNotification);
- mNotificationHeaderWrapper.setOnRoundnessChangedListener(this::invalidate);
- addView(mNotificationHeader, 0);
+ mGroupHeaderWrapper.setOnRoundnessChangedListener(this::invalidate);
+ addView(mGroupHeader, 0);
invalidate();
} else {
Trace.beginSection("recreateHeader#reapply");
- header.reapply(getContext(), mNotificationHeader);
+ header.reapply(getContext(), mGroupHeader);
Trace.endSection();
}
- mNotificationHeaderWrapper.setExpanded(mChildrenExpanded);
- mNotificationHeaderWrapper.onContentUpdated(mContainingNotification);
+ mGroupHeaderWrapper.setExpanded(mChildrenExpanded);
+ mGroupHeaderWrapper.onContentUpdated(mContainingNotification);
recreateLowPriorityHeader(builder, isConversation);
updateHeaderVisibility(false /* animate */);
updateChildrenAppearance();
@@ -439,21 +439,21 @@
}
private void removeGroupHeader() {
- if (mNotificationHeader == null) {
+ if (mGroupHeader == null) {
return;
}
- removeView(mNotificationHeader);
- mNotificationHeader = null;
- mNotificationHeaderWrapper = null;
+ removeView(mGroupHeader);
+ mGroupHeader = null;
+ mGroupHeaderWrapper = null;
}
private void removeLowPriorityGroupHeader() {
- if (mNotificationHeaderLowPriority == null) {
+ if (mMinimizedGroupHeader == null) {
return;
}
- removeView(mNotificationHeaderLowPriority);
- mNotificationHeaderLowPriority = null;
- mNotificationHeaderWrapperLowPriority = null;
+ removeView(mMinimizedGroupHeader);
+ mMinimizedGroupHeader = null;
+ mMinimizedGroupHeaderWrapper = null;
}
/**
@@ -474,21 +474,21 @@
return;
}
- mNotificationHeader = headerView;
- mNotificationHeader.findViewById(com.android.internal.R.id.expand_button)
+ mGroupHeader = headerView;
+ mGroupHeader.findViewById(com.android.internal.R.id.expand_button)
.setVisibility(VISIBLE);
- mNotificationHeader.setOnClickListener(mHeaderClickListener);
- mNotificationHeaderWrapper =
+ mGroupHeader.setOnClickListener(mHeaderClickListener);
+ mGroupHeaderWrapper =
(NotificationHeaderViewWrapper) NotificationViewWrapper.wrap(
getContext(),
- mNotificationHeader,
+ mGroupHeader,
mContainingNotification);
- mNotificationHeaderWrapper.setOnRoundnessChangedListener(this::invalidate);
- addView(mNotificationHeader, 0);
+ mGroupHeaderWrapper.setOnRoundnessChangedListener(this::invalidate);
+ addView(mGroupHeader, 0);
invalidate();
- mNotificationHeaderWrapper.setExpanded(mChildrenExpanded);
- mNotificationHeaderWrapper.onContentUpdated(mContainingNotification);
+ mGroupHeaderWrapper.setExpanded(mChildrenExpanded);
+ mGroupHeaderWrapper.onContentUpdated(mContainingNotification);
updateHeaderVisibility(false /* animate */);
updateChildrenAppearance();
@@ -511,20 +511,20 @@
return;
}
- mNotificationHeaderLowPriority = headerViewLowPriority;
- mNotificationHeaderLowPriority.findViewById(com.android.internal.R.id.expand_button)
+ mMinimizedGroupHeader = headerViewLowPriority;
+ mMinimizedGroupHeader.findViewById(com.android.internal.R.id.expand_button)
.setVisibility(VISIBLE);
- mNotificationHeaderLowPriority.setOnClickListener(onClickListener);
- mNotificationHeaderWrapperLowPriority =
+ mMinimizedGroupHeader.setOnClickListener(onClickListener);
+ mMinimizedGroupHeaderWrapper =
(NotificationHeaderViewWrapper) NotificationViewWrapper.wrap(
getContext(),
- mNotificationHeaderLowPriority,
+ mMinimizedGroupHeader,
mContainingNotification);
- mNotificationHeaderWrapperLowPriority.setOnRoundnessChangedListener(this::invalidate);
- addView(mNotificationHeaderLowPriority, 0);
+ mMinimizedGroupHeaderWrapper.setOnRoundnessChangedListener(this::invalidate);
+ addView(mMinimizedGroupHeader, 0);
invalidate();
- mNotificationHeaderWrapperLowPriority.onContentUpdated(mContainingNotification);
+ mMinimizedGroupHeaderWrapper.onContentUpdated(mContainingNotification);
updateHeaderVisibility(false /* animate */);
updateChildrenAppearance();
}
@@ -539,35 +539,35 @@
AsyncGroupHeaderViewInflation.assertInLegacyMode();
RemoteViews header;
StatusBarNotification notification = mContainingNotification.getEntry().getSbn();
- if (mIsLowPriority) {
+ if (mIsMinimized) {
if (builder == null) {
builder = Notification.Builder.recoverBuilder(getContext(),
notification.getNotification());
}
header = builder.makeLowPriorityContentView(true /* useRegularSubtext */);
- if (mNotificationHeaderLowPriority == null) {
- mNotificationHeaderLowPriority = (NotificationHeaderView) header.apply(getContext(),
+ if (mMinimizedGroupHeader == null) {
+ mMinimizedGroupHeader = (NotificationHeaderView) header.apply(getContext(),
this);
- mNotificationHeaderLowPriority.findViewById(com.android.internal.R.id.expand_button)
+ mMinimizedGroupHeader.findViewById(com.android.internal.R.id.expand_button)
.setVisibility(VISIBLE);
- mNotificationHeaderLowPriority.setOnClickListener(mHeaderClickListener);
- mNotificationHeaderWrapperLowPriority =
+ mMinimizedGroupHeader.setOnClickListener(mHeaderClickListener);
+ mMinimizedGroupHeaderWrapper =
(NotificationHeaderViewWrapper) NotificationViewWrapper.wrap(
getContext(),
- mNotificationHeaderLowPriority,
+ mMinimizedGroupHeader,
mContainingNotification);
- mNotificationHeaderWrapper.setOnRoundnessChangedListener(this::invalidate);
- addView(mNotificationHeaderLowPriority, 0);
+ mGroupHeaderWrapper.setOnRoundnessChangedListener(this::invalidate);
+ addView(mMinimizedGroupHeader, 0);
invalidate();
} else {
- header.reapply(getContext(), mNotificationHeaderLowPriority);
+ header.reapply(getContext(), mMinimizedGroupHeader);
}
- mNotificationHeaderWrapperLowPriority.onContentUpdated(mContainingNotification);
- resetHeaderVisibilityIfNeeded(mNotificationHeaderLowPriority, calculateDesiredHeader());
+ mMinimizedGroupHeaderWrapper.onContentUpdated(mContainingNotification);
+ resetHeaderVisibilityIfNeeded(mMinimizedGroupHeader, calculateDesiredHeader());
} else {
- removeView(mNotificationHeaderLowPriority);
- mNotificationHeaderLowPriority = null;
- mNotificationHeaderWrapperLowPriority = null;
+ removeView(mMinimizedGroupHeader);
+ mMinimizedGroupHeader = null;
+ mMinimizedGroupHeaderWrapper = null;
}
}
@@ -588,8 +588,8 @@
public void updateGroupOverflow() {
if (mShowGroupCountInExpander) {
- setExpandButtonNumber(mNotificationHeaderWrapper);
- setExpandButtonNumber(mNotificationHeaderWrapperLowPriority);
+ setExpandButtonNumber(mGroupHeaderWrapper);
+ setExpandButtonNumber(mMinimizedGroupHeaderWrapper);
return;
}
int maxAllowedVisibleChildren = getMaxAllowedVisibleChildren(true /* likeCollapsed */);
@@ -641,9 +641,9 @@
* @param alpha alpha value to apply to the content
*/
public void setContentAlpha(float alpha) {
- if (mNotificationHeader != null) {
- for (int i = 0; i < mNotificationHeader.getChildCount(); i++) {
- mNotificationHeader.getChildAt(i).setAlpha(alpha);
+ if (mGroupHeader != null) {
+ for (int i = 0; i < mGroupHeader.getChildCount(); i++) {
+ mGroupHeader.getChildAt(i).setAlpha(alpha);
}
}
for (ExpandableNotificationRow child : getAttachedChildren()) {
@@ -683,7 +683,7 @@
if (AsyncGroupHeaderViewInflation.isEnabled()) {
return mHeaderHeight;
} else {
- return mNotificationHeaderLowPriority.getHeight();
+ return mMinimizedGroupHeader.getHeight();
}
}
int intrinsicHeight = mNotificationHeaderMargin + mCurrentHeaderTranslation;
@@ -837,15 +837,15 @@
mGroupOverFlowState.setAlpha(0.0f);
}
}
- if (mNotificationHeader != null) {
+ if (mGroupHeader != null) {
if (mHeaderViewState == null) {
mHeaderViewState = new ViewState();
}
- mHeaderViewState.initFrom(mNotificationHeader);
+ mHeaderViewState.initFrom(mGroupHeader);
if (mContainingNotification.hasExpandingChild()) {
// Not modifying translationZ during expand animation.
- mHeaderViewState.setZTranslation(mNotificationHeader.getTranslationZ());
+ mHeaderViewState.setZTranslation(mGroupHeader.getTranslationZ());
} else if (childrenExpandedAndNotAnimating) {
mHeaderViewState.setZTranslation(parentState.getZTranslation());
} else {
@@ -898,7 +898,7 @@
&& !showingAsLowPriority()) {
return NUMBER_OF_CHILDREN_WHEN_CHILDREN_EXPANDED;
}
- if (mIsLowPriority
+ if (mIsMinimized
|| (!mContainingNotification.isOnKeyguard() && mContainingNotification.isExpanded())
|| (mContainingNotification.isHeadsUpState()
&& mContainingNotification.canShowHeadsUp())) {
@@ -946,7 +946,7 @@
mNeverAppliedGroupState = false;
}
if (mHeaderViewState != null) {
- mHeaderViewState.applyToView(mNotificationHeader);
+ mHeaderViewState.applyToView(mGroupHeader);
}
updateChildrenClipping();
}
@@ -1006,8 +1006,8 @@
}
if (child instanceof NotificationHeaderView
- && mNotificationHeaderWrapper.hasRoundedCorner()) {
- float[] radii = mNotificationHeaderWrapper.getUpdatedRadii();
+ && mGroupHeaderWrapper.hasRoundedCorner()) {
+ float[] radii = mGroupHeaderWrapper.getUpdatedRadii();
mHeaderPath.reset();
mHeaderPath.addRoundRect(
child.getLeft(),
@@ -1085,8 +1085,8 @@
}
mGroupOverFlowState.animateTo(mOverflowNumber, properties);
}
- if (mNotificationHeader != null) {
- mHeaderViewState.applyToView(mNotificationHeader);
+ if (mGroupHeader != null) {
+ mHeaderViewState.applyToView(mGroupHeader);
}
updateChildrenClipping();
}
@@ -1109,8 +1109,8 @@
public void setChildrenExpanded(boolean childrenExpanded) {
mChildrenExpanded = childrenExpanded;
updateExpansionStates();
- if (mNotificationHeaderWrapper != null) {
- mNotificationHeaderWrapper.setExpanded(childrenExpanded);
+ if (mGroupHeaderWrapper != null) {
+ mGroupHeaderWrapper.setExpanded(childrenExpanded);
}
final int count = mAttachedChildren.size();
for (int childIdx = 0; childIdx < count; childIdx++) {
@@ -1130,11 +1130,11 @@
}
public NotificationViewWrapper getNotificationViewWrapper() {
- return mNotificationHeaderWrapper;
+ return mGroupHeaderWrapper;
}
- public NotificationViewWrapper getLowPriorityViewWrapper() {
- return mNotificationHeaderWrapperLowPriority;
+ public NotificationViewWrapper getMinimizedGroupHeaderWrapper() {
+ return mMinimizedGroupHeaderWrapper;
}
@VisibleForTesting
@@ -1142,12 +1142,12 @@
return mCurrentHeader;
}
- public NotificationHeaderView getNotificationHeader() {
- return mNotificationHeader;
+ public NotificationHeaderView getGroupHeader() {
+ return mGroupHeader;
}
- public NotificationHeaderView getNotificationHeaderLowPriority() {
- return mNotificationHeaderLowPriority;
+ public NotificationHeaderView getMinimizedNotificationHeader() {
+ return mMinimizedGroupHeader;
}
private void updateHeaderVisibility(boolean animate) {
@@ -1171,7 +1171,7 @@
NotificationViewWrapper hiddenWrapper = getWrapperForView(currentHeader);
visibleWrapper.transformFrom(hiddenWrapper);
hiddenWrapper.transformTo(visibleWrapper, () -> updateHeaderVisibility(false));
- startChildAlphaAnimations(desiredHeader == mNotificationHeader);
+ startChildAlphaAnimations(desiredHeader == mGroupHeader);
} else {
animate = false;
}
@@ -1192,8 +1192,8 @@
}
}
- resetHeaderVisibilityIfNeeded(mNotificationHeader, desiredHeader);
- resetHeaderVisibilityIfNeeded(mNotificationHeaderLowPriority, desiredHeader);
+ resetHeaderVisibilityIfNeeded(mGroupHeader, desiredHeader);
+ resetHeaderVisibilityIfNeeded(mMinimizedGroupHeader, desiredHeader);
mCurrentHeader = desiredHeader;
}
@@ -1215,9 +1215,9 @@
private ViewGroup calculateDesiredHeader() {
ViewGroup desiredHeader;
if (showingAsLowPriority()) {
- desiredHeader = mNotificationHeaderLowPriority;
+ desiredHeader = mMinimizedGroupHeader;
} else {
- desiredHeader = mNotificationHeader;
+ desiredHeader = mGroupHeader;
}
return desiredHeader;
}
@@ -1244,20 +1244,20 @@
private void updateHeaderTransformation() {
if (mUserLocked && showingAsLowPriority()) {
float fraction = getGroupExpandFraction();
- mNotificationHeaderWrapper.transformFrom(mNotificationHeaderWrapperLowPriority,
+ mGroupHeaderWrapper.transformFrom(mMinimizedGroupHeaderWrapper,
fraction);
- mNotificationHeader.setVisibility(VISIBLE);
- mNotificationHeaderWrapperLowPriority.transformTo(mNotificationHeaderWrapper,
+ mGroupHeader.setVisibility(VISIBLE);
+ mMinimizedGroupHeaderWrapper.transformTo(mGroupHeaderWrapper,
fraction);
}
}
private NotificationViewWrapper getWrapperForView(View visibleHeader) {
- if (visibleHeader == mNotificationHeader) {
- return mNotificationHeaderWrapper;
+ if (visibleHeader == mGroupHeader) {
+ return mGroupHeaderWrapper;
}
- return mNotificationHeaderWrapperLowPriority;
+ return mMinimizedGroupHeaderWrapper;
}
/**
@@ -1266,13 +1266,13 @@
* @param expanded whether the group is expanded.
*/
public void updateHeaderForExpansion(boolean expanded) {
- if (mNotificationHeader != null) {
+ if (mGroupHeader != null) {
if (expanded) {
ColorDrawable cd = new ColorDrawable();
cd.setColor(mContainingNotification.calculateBgColor());
- mNotificationHeader.setHeaderBackgroundDrawable(cd);
+ mGroupHeader.setHeaderBackgroundDrawable(cd);
} else {
- mNotificationHeader.setHeaderBackgroundDrawable(null);
+ mGroupHeader.setHeaderBackgroundDrawable(null);
}
}
}
@@ -1405,11 +1405,11 @@
if (AsyncGroupHeaderViewInflation.isEnabled()) {
return mHeaderHeight;
}
- if (mNotificationHeaderLowPriority == null) {
+ if (mMinimizedGroupHeader == null) {
Log.e(TAG, "getMinHeight: low priority header is null", new Exception());
return 0;
}
- return mNotificationHeaderLowPriority.getHeight();
+ return mMinimizedGroupHeader.getHeight();
}
int minExpandHeight = mNotificationHeaderMargin + headerTranslation;
int visibleChildren = 0;
@@ -1443,20 +1443,20 @@
}
public boolean showingAsLowPriority() {
- return mIsLowPriority && !mContainingNotification.isExpanded();
+ return mIsMinimized && !mContainingNotification.isExpanded();
}
public void reInflateViews(OnClickListener listener, StatusBarNotification notification) {
if (!AsyncGroupHeaderViewInflation.isEnabled()) {
// When Async header inflation is enabled, we do not reinflate headers because they are
// inflated from the background thread
- if (mNotificationHeader != null) {
- removeView(mNotificationHeader);
- mNotificationHeader = null;
+ if (mGroupHeader != null) {
+ removeView(mGroupHeader);
+ mGroupHeader = null;
}
- if (mNotificationHeaderLowPriority != null) {
- removeView(mNotificationHeaderLowPriority);
- mNotificationHeaderLowPriority = null;
+ if (mMinimizedGroupHeader != null) {
+ removeView(mMinimizedGroupHeader);
+ mMinimizedGroupHeader = null;
}
recreateNotificationHeader(listener, mIsConversation);
}
@@ -1489,8 +1489,8 @@
}
private void updateHeaderTouchability() {
- if (mNotificationHeader != null) {
- mNotificationHeader.setAcceptAllTouches(mChildrenExpanded || mUserLocked);
+ if (mGroupHeader != null) {
+ mGroupHeader.setAcceptAllTouches(mChildrenExpanded || mUserLocked);
}
}
@@ -1534,8 +1534,11 @@
updateChildrenClipping();
}
- public void setIsLowPriority(boolean isLowPriority) {
- mIsLowPriority = isLowPriority;
+ /**
+ * Set whether the children container is minimized.
+ */
+ public void setIsMinimized(boolean isMinimized) {
+ mIsMinimized = isMinimized;
if (mContainingNotification != null) { /* we're not yet set up yet otherwise */
if (!AsyncGroupHeaderViewInflation.isEnabled()) {
recreateLowPriorityHeader(null /* existingBuilder */, mIsConversation);
@@ -1552,13 +1555,13 @@
*/
public NotificationViewWrapper getVisibleWrapper() {
if (showingAsLowPriority()) {
- return mNotificationHeaderWrapperLowPriority;
+ return mMinimizedGroupHeaderWrapper;
}
- return mNotificationHeaderWrapper;
+ return mGroupHeaderWrapper;
}
public void onExpansionChanged() {
- if (mIsLowPriority) {
+ if (mIsMinimized) {
if (mUserLocked) {
setUserLocked(mUserLocked);
}
@@ -1574,15 +1577,15 @@
@Override
public void applyRoundnessAndInvalidate() {
boolean last = true;
- if (mNotificationHeaderWrapper != null) {
- mNotificationHeaderWrapper.requestTopRoundness(
+ if (mGroupHeaderWrapper != null) {
+ mGroupHeaderWrapper.requestTopRoundness(
/* value = */ getTopRoundness(),
/* sourceType = */ FROM_PARENT,
/* animate = */ false
);
}
- if (mNotificationHeaderWrapperLowPriority != null) {
- mNotificationHeaderWrapperLowPriority.requestTopRoundness(
+ if (mMinimizedGroupHeaderWrapper != null) {
+ mMinimizedGroupHeaderWrapper.requestTopRoundness(
/* value = */ getTopRoundness(),
/* sourceType = */ FROM_PARENT,
/* animate = */ false
@@ -1612,31 +1615,31 @@
* Shows the given feedback icon, or hides the icon if null.
*/
public void setFeedbackIcon(@Nullable FeedbackIcon icon) {
- if (mNotificationHeaderWrapper != null) {
- mNotificationHeaderWrapper.setFeedbackIcon(icon);
+ if (mGroupHeaderWrapper != null) {
+ mGroupHeaderWrapper.setFeedbackIcon(icon);
}
- if (mNotificationHeaderWrapperLowPriority != null) {
- mNotificationHeaderWrapperLowPriority.setFeedbackIcon(icon);
+ if (mMinimizedGroupHeaderWrapper != null) {
+ mMinimizedGroupHeaderWrapper.setFeedbackIcon(icon);
}
}
public void setRecentlyAudiblyAlerted(boolean audiblyAlertedRecently) {
- if (mNotificationHeaderWrapper != null) {
- mNotificationHeaderWrapper.setRecentlyAudiblyAlerted(audiblyAlertedRecently);
+ if (mGroupHeaderWrapper != null) {
+ mGroupHeaderWrapper.setRecentlyAudiblyAlerted(audiblyAlertedRecently);
}
- if (mNotificationHeaderWrapperLowPriority != null) {
- mNotificationHeaderWrapperLowPriority.setRecentlyAudiblyAlerted(audiblyAlertedRecently);
+ if (mMinimizedGroupHeaderWrapper != null) {
+ mMinimizedGroupHeaderWrapper.setRecentlyAudiblyAlerted(audiblyAlertedRecently);
}
}
@Override
public void setNotificationFaded(boolean faded) {
mContainingNotificationIsFaded = faded;
- if (mNotificationHeaderWrapper != null) {
- mNotificationHeaderWrapper.setNotificationFaded(faded);
+ if (mGroupHeaderWrapper != null) {
+ mGroupHeaderWrapper.setNotificationFaded(faded);
}
- if (mNotificationHeaderWrapperLowPriority != null) {
- mNotificationHeaderWrapperLowPriority.setNotificationFaded(faded);
+ if (mMinimizedGroupHeaderWrapper != null) {
+ mMinimizedGroupHeaderWrapper.setNotificationFaded(faded);
}
for (ExpandableNotificationRow child : mAttachedChildren) {
child.setNotificationFaded(faded);
@@ -1654,7 +1657,7 @@
}
public NotificationHeaderViewWrapper getNotificationHeaderWrapper() {
- return mNotificationHeaderWrapper;
+ return mGroupHeaderWrapper;
}
public void setLogger(NotificationChildrenContainerLogger logger) {
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 9479762..fb52838 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
@@ -812,6 +812,10 @@
} else {
mDebugTextUsedYPositions.clear();
}
+
+ mDebugPaint.setColor(Color.DKGRAY);
+ canvas.drawPath(mRoundedClipPath, mDebugPaint);
+
int y = 0;
drawDebugInfo(canvas, y, Color.RED, /* label= */ "y = " + y);
@@ -843,14 +847,14 @@
drawDebugInfo(canvas, y, Color.LTGRAY,
/* label= */ "mAmbientState.getStackY() + mAmbientState.getStackHeight() = " + y);
- y = (int) mAmbientState.getStackY() + mContentHeight;
- drawDebugInfo(canvas, y, Color.MAGENTA,
- /* label= */ "mAmbientState.getStackY() + mContentHeight = " + y);
-
y = (int) (mAmbientState.getStackY() + mIntrinsicContentHeight);
drawDebugInfo(canvas, y, Color.YELLOW,
/* label= */ "mAmbientState.getStackY() + mIntrinsicContentHeight = " + y);
+ y = mContentHeight;
+ drawDebugInfo(canvas, y, Color.MAGENTA,
+ /* label= */ "mContentHeight = " + y);
+
drawDebugInfo(canvas, mRoundedRectClippingBottom, Color.DKGRAY,
/* label= */ "mRoundedRectClippingBottom) = " + y);
}
@@ -4940,6 +4944,9 @@
println(pw, "intrinsicPadding", mIntrinsicPadding);
println(pw, "topPadding", mTopPadding);
println(pw, "bottomPadding", mBottomPadding);
+ dumpRoundedRectClipping(pw);
+ println(pw, "requestedClipBounds", mRequestedClipBounds);
+ println(pw, "isClipped", mIsClipped);
println(pw, "translationX", getTranslationX());
println(pw, "translationY", getTranslationY());
println(pw, "translationZ", getTranslationZ());
@@ -4994,6 +5001,15 @@
});
}
+ private void dumpRoundedRectClipping(IndentingPrintWriter pw) {
+ pw.append("roundedRectClipping{l=").print(mRoundedRectClippingLeft);
+ pw.append(" t=").print(mRoundedRectClippingTop);
+ pw.append(" r=").print(mRoundedRectClippingRight);
+ pw.append(" b=").print(mRoundedRectClippingBottom);
+ pw.append("} topRadius=").print(mBgCornerRadii[0]);
+ pw.append(" bottomRadius=").println(mBgCornerRadii[4]);
+ }
+
private void dumpFooterViewVisibility(IndentingPrintWriter pw) {
FooterViewRefactor.assertInLegacyMode();
final boolean showDismissView = shouldShowDismissView();
@@ -5389,7 +5405,7 @@
/**
* @param topHeadsUpRow the first headsUp row in z-order.
*/
- public void setTopHeadsUpRow(ExpandableNotificationRow topHeadsUpRow) {
+ public void setTopHeadsUpRow(@Nullable ExpandableNotificationRow topHeadsUpRow) {
mTopHeadsUpRow = topHeadsUpRow;
}
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 8ed1ca2..ec111a1 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
@@ -23,7 +23,6 @@
import static com.android.internal.jank.InteractionJankMonitor.CUJ_NOTIFICATION_SHADE_SCROLL_FLING;
import static com.android.server.notification.Flags.screenshareNotificationHiding;
import static com.android.systemui.Dependency.ALLOW_NOTIFICATION_LONG_PRESS_NAME;
-import static com.android.systemui.Flags.migrateClocksToBlueprint;
import static com.android.systemui.Flags.nsslFalsingFix;
import static com.android.systemui.statusbar.StatusBarState.KEYGUARD;
import static com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.OnEmptySpaceClickListener;
@@ -71,6 +70,7 @@
import com.android.systemui.dump.DumpManager;
import com.android.systemui.flags.FeatureFlagsClassic;
import com.android.systemui.flags.Flags;
+import com.android.systemui.keyguard.MigrateClocksToBlueprint;
import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository;
import com.android.systemui.keyguard.shared.model.KeyguardState;
import com.android.systemui.keyguard.shared.model.TransitionStep;
@@ -126,6 +126,7 @@
import com.android.systemui.statusbar.notification.row.NotificationGuts;
import com.android.systemui.statusbar.notification.row.NotificationGutsManager;
import com.android.systemui.statusbar.notification.row.NotificationSnooze;
+import com.android.systemui.statusbar.notification.shared.NotificationsHeadsUpRefactor;
import com.android.systemui.statusbar.notification.stack.domain.interactor.NotificationStackAppearanceInteractor;
import com.android.systemui.statusbar.notification.stack.ui.viewbinder.NotificationListViewBinder;
import com.android.systemui.statusbar.phone.HeadsUpAppearanceController;
@@ -685,11 +686,13 @@
new OnHeadsUpChangedListener() {
@Override
public void onHeadsUpPinnedModeChanged(boolean inPinnedMode) {
+ NotificationsHeadsUpRefactor.assertInLegacyMode();
mView.setInHeadsUpPinnedMode(inPinnedMode);
}
@Override
public void onHeadsUpStateChanged(NotificationEntry entry, boolean isHeadsUp) {
+ NotificationsHeadsUpRefactor.assertInLegacyMode();
NotificationEntry topEntry = mHeadsUpManager.getTopEntry();
mView.setTopHeadsUpRow(topEntry != null ? topEntry.getRow() : null);
generateHeadsUpAnimation(entry, isHeadsUp);
@@ -870,7 +873,9 @@
});
}
- mHeadsUpManager.addListener(mOnHeadsUpChangedListener);
+ if (!NotificationsHeadsUpRefactor.isEnabled()) {
+ mHeadsUpManager.addListener(mOnHeadsUpChangedListener);
+ }
mHeadsUpManager.setAnimationStateHandler(mView::setHeadsUpGoingAwayAnimationsAllowed);
mDynamicPrivacyController.addListener(mDynamicPrivacyControllerListener);
@@ -2090,7 +2095,7 @@
}
boolean horizontalSwipeWantsIt = false;
boolean scrollerWantsIt = false;
- if (nsslFalsingFix() || migrateClocksToBlueprint()) {
+ if (nsslFalsingFix() || MigrateClocksToBlueprint.isEnabled()) {
// Reverse the order relative to the else statement. onScrollTouch will reset on an
// UP event, causing horizontalSwipeWantsIt to be set to true on vertical swipes.
if (mLongPressedView == null && !mView.isBeingDragged()
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackSizeCalculator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackSizeCalculator.kt
index 2d9c63e..1b53cbe 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackSizeCalculator.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackSizeCalculator.kt
@@ -20,6 +20,7 @@
import android.util.Log
import android.view.View.GONE
import androidx.annotation.VisibleForTesting
+import com.android.systemui.Flags.notificationMinimalismPrototype
import com.android.systemui.res.R
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Main
@@ -66,6 +67,11 @@
*/
private var maxKeyguardNotifications by notNull<Int>()
+ /**
+ * Whether [maxKeyguardNotifications] will have 1 added to it when media is shown in the stack.
+ */
+ private var maxNotificationsExcludesMedia = false
+
/** Minimum space between two notifications, see [calculateGapAndDividerHeight]. */
private var dividerHeight by notNull<Float>()
@@ -168,7 +174,11 @@
log { "\n" }
val stackHeightSequence = computeHeightPerNotificationLimit(stack, shelfHeight)
+
+ // TODO: Avoid making this split shade assumption by simply checking the stack for media
val isMediaShowing = mediaDataManager.hasActiveMediaOrRecommendation()
+ val isMediaShowingInStack = isMediaShowing && !splitShadeStateController
+ .shouldUseSplitNotificationShade(resources)
log { "\tGet maxNotifWithoutSavingSpace ---" }
val maxNotifWithoutSavingSpace =
@@ -181,12 +191,11 @@
}
// How many notifications we can show at heightWithoutLockscreenConstraints
- var minCountAtHeightWithoutConstraints =
- if (isMediaShowing && !splitShadeStateController
- .shouldUseSplitNotificationShade(resources)) 2 else 1
+ val minCountAtHeightWithoutConstraints = if (isMediaShowingInStack) 2 else 1
log {
"\t---maxNotifWithoutSavingSpace=$maxNotifWithoutSavingSpace " +
"isMediaShowing=$isMediaShowing" +
+ "isMediaShowingInStack=$isMediaShowingInStack" +
"minCountAtHeightWithoutConstraints=$minCountAtHeightWithoutConstraints"
}
log { "\n" }
@@ -223,7 +232,9 @@
}
if (onLockscreen()) {
- maxNotifications = min(maxKeyguardNotifications, maxNotifications)
+ val increaseMaxForMedia = maxNotificationsExcludesMedia && isMediaShowingInStack
+ val lockscreenMax = maxKeyguardNotifications.safeIncrementIf(increaseMaxForMedia)
+ maxNotifications = min(lockscreenMax, maxNotifications)
}
// Could be < 0 if the space available is less than the shelf size. Returns 0 in this case.
@@ -276,7 +287,7 @@
height = notifsHeight + shelfHeightWithSpaceBefore
log {
"--- computeHeight(maxNotifs=$maxNotifs, shelfHeight=$shelfHeight)" +
- " -> ${height}=($notifsHeight+$shelfHeightWithSpaceBefore)" +
+ " -> $height=($notifsHeight+$shelfHeightWithSpaceBefore)" +
" | saveSpaceOnLockscreen=$saveSpaceOnLockscreen"
}
}
@@ -367,8 +378,9 @@
}
fun updateResources() {
- maxKeyguardNotifications =
- infiniteIfNegative(resources.getInteger(R.integer.keyguard_max_notification_count))
+ maxKeyguardNotifications = if (notificationMinimalismPrototype()) 1
+ else infiniteIfNegative(resources.getInteger(R.integer.keyguard_max_notification_count))
+ maxNotificationsExcludesMedia = notificationMinimalismPrototype()
dividerHeight =
max(1f, resources.getDimensionPixelSize(R.dimen.notification_divider_height).toFloat())
@@ -486,6 +498,13 @@
v
}
+ private fun Int.safeIncrementIf(condition: Boolean): Int =
+ if (condition && this != Int.MAX_VALUE) {
+ this + 1
+ } else {
+ this
+ }
+
/** Returns the last index where [predicate] returns true, or -1 if it was always false. */
private fun <T> Sequence<T>.lastIndexWhile(predicate: (T) -> Boolean): Int =
takeWhile(predicate).count() - 1
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java
index 9b1952b..5eaccd9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java
@@ -53,9 +53,7 @@
public static final float START_FRACTION = 0.5f;
private static final String TAG = "StackScrollAlgorithm";
- private static final Boolean DEBUG = false;
private static final SourceType STACK_SCROLL_ALGO = SourceType.from("StackScrollAlgorithm");
-
private final ViewGroup mHostView;
private float mPaddingBetweenElements;
private float mGapHeight;
@@ -247,13 +245,11 @@
>= ambientState.getMaxHeadsUpTranslation();
}
- public static void log(String s) {
- if (DEBUG) {
- android.util.Log.i(TAG, s);
- }
+ public static void debugLog(String s) {
+ android.util.Log.i(TAG, s);
}
- public static void logView(View view, String s) {
+ public static void debugLogView(View view, String s) {
String viewString = "";
if (view instanceof ExpandableNotificationRow row) {
if (row.getEntry() == null) {
@@ -274,7 +270,7 @@
} else {
viewString = view.toString();
}
- log(viewString + " " + s);
+ debugLog(viewString + " " + s);
}
private void resetChildViewStates() {
@@ -598,15 +594,16 @@
);
if (view instanceof FooterView) {
if (FooterViewRefactor.isEnabled()) {
- final float footerEnd = algorithmState.mCurrentExpandedYPosition
- + view.getIntrinsicHeight();
- final boolean noSpaceForFooter = footerEnd > ambientState.getStackEndHeight();
- // TODO(b/293167744): May be able to keep only noSpaceForFooter here if we add an
- // emission when clearAllNotifications is called, and then use that in the footer
- // visibility flow.
- ((FooterView.FooterViewState) viewState).hideContent =
- noSpaceForFooter || (ambientState.isClearAllInProgress()
- && !hasNonClearableNotifs(algorithmState));
+ if (((FooterView) view).shouldBeHidden()) {
+ viewState.hidden = true;
+ } else {
+ final float footerEnd = algorithmState.mCurrentExpandedYPosition
+ + view.getIntrinsicHeight();
+ final boolean noSpaceForFooter = footerEnd > ambientState.getStackEndHeight();
+ ((FooterView.FooterViewState) viewState).hideContent =
+ noSpaceForFooter || (ambientState.isClearAllInProgress()
+ && !hasNonClearableNotifs(algorithmState));
+ }
} else {
final boolean shadeClosed = !ambientState.isShadeExpanded();
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 9efe632..79ba25e 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
@@ -17,8 +17,8 @@
package com.android.systemui.statusbar.notification.stack.data.repository
-import com.android.systemui.common.shared.model.NotificationContainerBounds
import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.statusbar.notification.stack.shared.model.StackBounds
import javax.inject.Inject
import kotlinx.coroutines.flow.MutableStateFlow
@@ -26,7 +26,7 @@
@SysUISingleton
class NotificationStackAppearanceRepository @Inject constructor() {
/** The bounds of the notification stack in the current scene. */
- val stackBounds = MutableStateFlow(NotificationContainerBounds())
+ val stackBounds = MutableStateFlow(StackBounds())
/**
* The height in px of the contents of notification stack. Depending on the number of
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 08df473..f05d017 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
@@ -17,13 +17,19 @@
package com.android.systemui.statusbar.notification.stack.domain.interactor
-import com.android.systemui.common.shared.model.NotificationContainerBounds
import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.shade.domain.interactor.ShadeInteractor
+import com.android.systemui.shade.shared.model.ShadeMode
import com.android.systemui.statusbar.notification.stack.data.repository.NotificationStackAppearanceRepository
+import com.android.systemui.statusbar.notification.stack.shared.model.StackBounds
+import com.android.systemui.statusbar.notification.stack.shared.model.StackRounding
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.flowOf
/** An interactor which controls the appearance of the NSSL */
@SysUISingleton
@@ -31,9 +37,30 @@
@Inject
constructor(
private val repository: NotificationStackAppearanceRepository,
+ shadeInteractor: ShadeInteractor,
) {
/** The bounds of the notification stack in the current scene. */
- val stackBounds: StateFlow<NotificationContainerBounds> = repository.stackBounds.asStateFlow()
+ val stackBounds: StateFlow<StackBounds> = repository.stackBounds.asStateFlow()
+
+ /**
+ * Whether the stack is expanding from GONE-with-HUN to SHADE
+ *
+ * TODO(b/296118689): implement this to match legacy QSController logic
+ */
+ private val isExpandingFromHeadsUp: Flow<Boolean> = flowOf(false)
+
+ /** The rounding of the notification stack. */
+ val stackRounding: Flow<StackRounding> =
+ combine(
+ shadeInteractor.shadeMode,
+ isExpandingFromHeadsUp,
+ ) { shadeMode, isExpandingFromHeadsUp ->
+ StackRounding(
+ roundTop = !(shadeMode == ShadeMode.Split && isExpandingFromHeadsUp),
+ roundBottom = shadeMode != ShadeMode.Single,
+ )
+ }
+ .distinctUntilChanged()
/**
* The height in px of the contents of notification stack. Depending on the number of
@@ -59,7 +86,7 @@
val syntheticScroll: Flow<Float> = repository.syntheticScroll.asStateFlow()
/** Sets the position of the notification stack in the current scene. */
- fun setStackBounds(bounds: NotificationContainerBounds) {
+ fun setStackBounds(bounds: StackBounds) {
check(bounds.top <= bounds.bottom) { "Invalid bounds: $bounds" }
repository.stackBounds.value = bounds
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/shared/model/StackBounds.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/shared/model/StackBounds.kt
new file mode 100644
index 0000000..1fc9a18
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/shared/model/StackBounds.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.stack.shared.model
+
+/** Models the bounds of the notification stack. */
+data class StackBounds(
+ /** The position of the left of the stack in its window coordinate system, in pixels. */
+ val left: Float = 0f,
+ /** The position of the top of the stack in its window coordinate system, in pixels. */
+ val top: Float = 0f,
+ /** The position of the right of the stack in its window coordinate system, in pixels. */
+ val right: Float = 0f,
+ /** The position of the bottom of the stack in its window coordinate system, in pixels. */
+ val bottom: Float = 0f,
+) {
+ /** The current height of the notification container. */
+ val height: Float = bottom - top
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/shared/model/StackClipping.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/shared/model/StackClipping.kt
new file mode 100644
index 0000000..0c92b50
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/shared/model/StackClipping.kt
@@ -0,0 +1,20 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.stack.shared.model
+
+/** Models the clipping rounded rectangle of the notification stack */
+data class StackClipping(val bounds: StackBounds, val rounding: StackRounding)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/shared/model/StackRounding.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/shared/model/StackRounding.kt
new file mode 100644
index 0000000..ddc5d7ea
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/shared/model/StackRounding.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.stack.shared.model
+
+/** Models the corner rounds of the notification stack. */
+data class StackRounding(
+ /** Whether the top corners of the notification stack should be rounded. */
+ val roundTop: Boolean = false,
+ /** Whether the bottom corners of the notification stack should be rounded. */
+ val roundBottom: Boolean = false,
+)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt
index 6b30393..18bb5119 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt
@@ -36,6 +36,7 @@
import com.android.systemui.statusbar.notification.footer.ui.viewbinder.FooterViewBinder
import com.android.systemui.statusbar.notification.footer.ui.viewmodel.FooterViewModel
import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconContainerShelfViewBinder
+import com.android.systemui.statusbar.notification.shared.NotificationsHeadsUpRefactor
import com.android.systemui.statusbar.notification.shared.NotificationsLiveDataStoreRefactor
import com.android.systemui.statusbar.notification.shelf.ui.viewbinder.NotificationShelfViewBinder
import com.android.systemui.statusbar.notification.stack.DisplaySwitchNotificationsHiderTracker
@@ -44,6 +45,7 @@
import com.android.systemui.statusbar.notification.stack.ui.view.NotificationStatsLogger
import com.android.systemui.statusbar.notification.stack.ui.viewbinder.HideNotificationsBinder.bindHideList
import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationListViewModel
+import com.android.systemui.statusbar.notification.ui.viewbinder.HeadsUpNotificationViewBinder
import com.android.systemui.statusbar.phone.NotificationIconAreaController
import com.android.systemui.util.kotlin.awaitCancellationThenDispose
import com.android.systemui.util.kotlin.getOrNull
@@ -71,6 +73,7 @@
private val hiderTracker: DisplaySwitchNotificationsHiderTracker,
private val configuration: ConfigurationState,
private val falsingManager: FalsingManager,
+ private val hunBinder: HeadsUpNotificationViewBinder,
private val iconAreaController: NotificationIconAreaController,
private val loggerOptional: Optional<NotificationStatsLogger>,
private val metricsLogger: MetricsLogger,
@@ -92,6 +95,9 @@
view.repeatWhenAttached {
lifecycleScope.launch {
+ if (NotificationsHeadsUpRefactor.isEnabled) {
+ launch { hunBinder.bindHeadsUpNotifications(view) }
+ }
launch { bindShelf(shelf) }
bindHideList(viewController, viewModel, hiderTracker)
@@ -187,13 +193,14 @@
},
)
launch {
- viewModel.shouldShowFooterView.collect { animatedVisibility ->
+ viewModel.shouldIncludeFooterView.collect { animatedVisibility ->
footerView.setVisible(
/* visible = */ animatedVisibility.value,
/* animate = */ animatedVisibility.isAnimating,
)
}
}
+ launch { viewModel.shouldHideFooterView.collect { footerView.setShouldBeHidden(it) } }
disposableHandle.awaitCancellationThenDispose()
}
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
deleted file mode 100644
index f10e5f1..0000000
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationStackAppearanceViewBinder.kt
+++ /dev/null
@@ -1,99 +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.statusbar.notification.stack.ui.viewbinder
-
-import android.content.Context
-import android.util.TypedValue
-import androidx.lifecycle.Lifecycle
-import androidx.lifecycle.repeatOnLifecycle
-import com.android.systemui.dagger.qualifiers.Main
-import com.android.systemui.lifecycle.repeatWhenAttached
-import com.android.systemui.statusbar.notification.stack.AmbientState
-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.roundToInt
-import kotlinx.coroutines.CoroutineDispatcher
-import kotlinx.coroutines.DisposableHandle
-import kotlinx.coroutines.launch
-
-/** Binds the shared notification container to its view-model. */
-object NotificationStackAppearanceViewBinder {
- const val SCRIM_CORNER_RADIUS = 32f
-
- @JvmStatic
- fun bind(
- context: Context,
- view: SharedNotificationContainer,
- viewModel: NotificationStackAppearanceViewModel,
- ambientState: AmbientState,
- controller: NotificationStackScrollLayoutController,
- @Main mainImmediateDispatcher: CoroutineDispatcher,
- ): DisposableHandle {
- return view.repeatWhenAttached(mainImmediateDispatcher) {
- repeatOnLifecycle(Lifecycle.State.CREATED) {
- launch {
- viewModel.stackBounds.collect { bounds ->
- val viewLeft = controller.view.left
- val viewTop = controller.view.top
- controller.setRoundedClippingBounds(
- bounds.left.roundToInt() - viewLeft,
- bounds.top.roundToInt() - viewTop,
- bounds.right.roundToInt() - viewLeft,
- bounds.bottom.roundToInt() - viewTop,
- SCRIM_CORNER_RADIUS.dpToPx(context),
- 0,
- )
- }
- }
-
- launch {
- viewModel.contentTop.collect {
- controller.updateTopPadding(it, controller.isAddOrRemoveAnimationPending)
- }
- }
-
- launch {
- var wasExpanding = false
- viewModel.expandFraction.collect { expandFraction ->
- val nowExpanding = expandFraction != 0f && expandFraction != 1f
- if (nowExpanding && !wasExpanding) {
- controller.onExpansionStarted()
- }
- ambientState.expansionFraction = expandFraction
- controller.expandedHeight = expandFraction * controller.view.height
- if (!nowExpanding && wasExpanding) {
- controller.onExpansionStopped()
- }
- wasExpanding = nowExpanding
- }
- }
-
- launch { viewModel.isScrollable.collect { controller.setScrollingEnabled(it) } }
- }
- }
- }
-
- private fun Float.dpToPx(context: Context): Int {
- return TypedValue.applyDimension(
- TypedValue.COMPLEX_UNIT_DIP,
- this,
- context.resources.displayMetrics
- )
- .roundToInt()
- }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationStackViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationStackViewBinder.kt
new file mode 100644
index 0000000..1a34bb4
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationStackViewBinder.kt
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.stack.ui.viewbinder
+
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.repeatOnLifecycle
+import com.android.systemui.common.ui.ConfigurationState
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.lifecycle.repeatWhenAttached
+import com.android.systemui.res.R
+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.ui.viewmodel.NotificationStackAppearanceViewModel
+import javax.inject.Inject
+import kotlin.math.roundToInt
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.DisposableHandle
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.launch
+
+/** Binds the NSSL/Controller/AmbientState to their ViewModel. */
+@SysUISingleton
+class NotificationStackViewBinder
+@Inject
+constructor(
+ @Main private val mainImmediateDispatcher: CoroutineDispatcher,
+ private val ambientState: AmbientState,
+ private val view: NotificationStackScrollLayout,
+ private val controller: NotificationStackScrollLayoutController,
+ private val viewModel: NotificationStackAppearanceViewModel,
+ private val configuration: ConfigurationState,
+) {
+
+ fun bindWhileAttached(): DisposableHandle {
+ return view.repeatWhenAttached(mainImmediateDispatcher) {
+ repeatOnLifecycle(Lifecycle.State.CREATED) { bind() }
+ }
+ }
+
+ suspend fun bind() = coroutineScope {
+ launch {
+ combine(viewModel.stackClipping, clipRadius, ::Pair).collect { (clipping, clipRadius) ->
+ val (bounds, rounding) = clipping
+ val viewLeft = controller.view.left
+ val viewTop = controller.view.top
+ controller.setRoundedClippingBounds(
+ bounds.left.roundToInt() - viewLeft,
+ bounds.top.roundToInt() - viewTop,
+ bounds.right.roundToInt() - viewLeft,
+ bounds.bottom.roundToInt() - viewTop,
+ if (rounding.roundTop) clipRadius else 0,
+ if (rounding.roundBottom) clipRadius else 0,
+ )
+ }
+ }
+
+ launch {
+ viewModel.contentTop.collect {
+ controller.updateTopPadding(it, controller.isAddOrRemoveAnimationPending)
+ }
+ }
+
+ launch {
+ var wasExpanding = false
+ viewModel.expandFraction.collect { expandFraction ->
+ val nowExpanding = expandFraction != 0f && expandFraction != 1f
+ if (nowExpanding && !wasExpanding) {
+ controller.onExpansionStarted()
+ }
+ ambientState.expansionFraction = expandFraction
+ controller.expandedHeight = expandFraction * controller.view.height
+ if (!nowExpanding && wasExpanding) {
+ controller.onExpansionStopped()
+ }
+ wasExpanding = nowExpanding
+ }
+ }
+
+ launch { viewModel.isScrollable.collect { controller.setScrollingEnabled(it) } }
+ }
+
+ private val clipRadius: Flow<Int>
+ get() = configuration.getDimensionPixelOffset(R.dimen.notification_scrim_corner_radius)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt
index 7c76ddb..ecf737a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt
@@ -20,6 +20,7 @@
import android.view.WindowInsets
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.repeatOnLifecycle
+import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.keyguard.ui.viewmodel.BurnInParameters
import com.android.systemui.keyguard.ui.viewmodel.ViewStateAccessor
@@ -30,6 +31,8 @@
import com.android.systemui.statusbar.notification.stack.NotificationStackSizeCalculator
import com.android.systemui.statusbar.notification.stack.ui.view.SharedNotificationContainer
import com.android.systemui.statusbar.notification.stack.ui.viewmodel.SharedNotificationContainerViewModel
+import com.android.systemui.util.kotlin.DisposableHandles
+import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.DisposableHandle
import kotlinx.coroutines.flow.MutableStateFlow
@@ -38,18 +41,24 @@
import kotlinx.coroutines.launch
/** Binds the shared notification container to its view-model. */
-object SharedNotificationContainerBinder {
+@SysUISingleton
+class SharedNotificationContainerBinder
+@Inject
+constructor(
+ private val sceneContainerFlags: SceneContainerFlags,
+ private val controller: NotificationStackScrollLayoutController,
+ private val notificationStackSizeCalculator: NotificationStackSizeCalculator,
+ private val notificationStackViewBinder: NotificationStackViewBinder,
+ @Main private val mainImmediateDispatcher: CoroutineDispatcher,
+) {
- @JvmStatic
fun bind(
view: SharedNotificationContainer,
viewModel: SharedNotificationContainerViewModel,
- sceneContainerFlags: SceneContainerFlags,
- controller: NotificationStackScrollLayoutController,
- notificationStackSizeCalculator: NotificationStackSizeCalculator,
- @Main mainImmediateDispatcher: CoroutineDispatcher,
): DisposableHandle {
- val disposableHandle =
+ val disposables = DisposableHandles()
+
+ disposables +=
view.repeatWhenAttached {
repeatOnLifecycle(Lifecycle.State.CREATED) {
launch {
@@ -72,24 +81,6 @@
}
}
- // Required to capture keyguard media changes and ensure the notification count is correct
- val layoutChangeListener =
- object : View.OnLayoutChangeListener {
- override fun onLayoutChange(
- view: View,
- left: Int,
- top: Int,
- right: Int,
- bottom: Int,
- oldLeft: Int,
- oldTop: Int,
- oldRight: Int,
- oldBottom: Int
- ) {
- viewModel.notificationStackChanged()
- }
- }
-
val burnInParams = MutableStateFlow(BurnInParameters())
val viewState =
ViewStateAccessor(
@@ -100,7 +91,7 @@
* For animation sensitive coroutines, immediately run just like applicationScope does
* instead of doing a post() to the main thread. This extra delay can cause visible jitter.
*/
- val disposableHandleMainImmediate =
+ disposables +=
view.repeatWhenAttached(mainImmediateDispatcher) {
repeatOnLifecycle(Lifecycle.State.CREATED) {
launch {
@@ -167,7 +158,12 @@
}
}
- controller.setOnHeightChangedRunnable(Runnable { viewModel.notificationStackChanged() })
+ if (sceneContainerFlags.isEnabled()) {
+ disposables += notificationStackViewBinder.bindWhileAttached()
+ }
+
+ controller.setOnHeightChangedRunnable { viewModel.notificationStackChanged() }
+ disposables += DisposableHandle { controller.setOnHeightChangedRunnable(null) }
view.setOnApplyWindowInsetsListener { v: View, insets: WindowInsets ->
val insetTypes = WindowInsets.Type.systemBars() or WindowInsets.Type.displayCutout()
@@ -176,16 +172,16 @@
}
insets
}
- view.addOnLayoutChangeListener(layoutChangeListener)
+ disposables += DisposableHandle { view.setOnApplyWindowInsetsListener(null) }
- return object : DisposableHandle {
- override fun dispose() {
- disposableHandle.dispose()
- disposableHandleMainImmediate.dispose()
- controller.setOnHeightChangedRunnable(null)
- view.setOnApplyWindowInsetsListener(null)
- view.removeOnLayoutChangeListener(layoutChangeListener)
+ // Required to capture keyguard media changes and ensure the notification count is correct
+ val layoutChangeListener =
+ View.OnLayoutChangeListener { _, _, _, _, _, _, _, _, _ ->
+ viewModel.notificationStackChanged()
}
- }
+ view.addOnLayoutChangeListener(layoutChangeListener)
+ disposables += DisposableHandle { view.removeOnLayoutChangeListener(layoutChangeListener) }
+
+ return disposables
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/HeadsUpRowViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/HeadsUpRowViewModel.kt
new file mode 100644
index 0000000..ec5e5be
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/HeadsUpRowViewModel.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.statusbar.notification.stack.ui.viewmodel
+
+import com.android.systemui.statusbar.notification.domain.interactor.HeadsUpRowInteractor
+
+class HeadsUpRowViewModel(headsUpRowInteractor: HeadsUpRowInteractor)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModel.kt
index 4744fcb..5a7433d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModel.kt
@@ -17,17 +17,20 @@
package com.android.systemui.statusbar.notification.stack.ui.viewmodel
import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
import com.android.systemui.shade.domain.interactor.ShadeInteractor
import com.android.systemui.statusbar.domain.interactor.RemoteInputInteractor
import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor
+import com.android.systemui.statusbar.notification.domain.interactor.HeadsUpNotificationInteractor
import com.android.systemui.statusbar.notification.domain.interactor.SeenNotificationsInteractor
import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor
import com.android.systemui.statusbar.notification.footer.ui.viewmodel.FooterViewModel
+import com.android.systemui.statusbar.notification.shared.HeadsUpRowKey
+import com.android.systemui.statusbar.notification.shared.NotificationsHeadsUpRefactor
import com.android.systemui.statusbar.notification.shelf.ui.viewmodel.NotificationShelfViewModel
import com.android.systemui.statusbar.notification.stack.domain.interactor.NotificationStackInteractor
import com.android.systemui.statusbar.policy.domain.interactor.UserSetupInteractor
import com.android.systemui.statusbar.policy.domain.interactor.ZenModeInteractor
-import com.android.systemui.util.kotlin.combine
import com.android.systemui.util.kotlin.sample
import com.android.systemui.util.ui.AnimatableEvent
import com.android.systemui.util.ui.AnimatedValue
@@ -53,6 +56,8 @@
val logger: Optional<NotificationLoggerViewModel>,
activeNotificationsInteractor: ActiveNotificationsInteractor,
notificationStackInteractor: NotificationStackInteractor,
+ private val headsUpNotificationInteractor: HeadsUpNotificationInteractor,
+ keyguardInteractor: KeyguardInteractor,
remoteInputInteractor: RemoteInputInteractor,
seenNotificationsInteractor: SeenNotificationsInteractor,
shadeInteractor: ShadeInteractor,
@@ -105,7 +110,32 @@
}
}
- val shouldShowFooterView: Flow<AnimatedValue<Boolean>> by lazy {
+ /**
+ * Whether the footer should not be visible for the user, even if it's present in the list (as
+ * per [shouldIncludeFooterView] below).
+ *
+ * This essentially corresponds to having the view set to INVISIBLE.
+ */
+ val shouldHideFooterView: Flow<Boolean> by lazy {
+ if (FooterViewRefactor.isUnexpectedlyInLegacyMode()) {
+ flowOf(false)
+ } else {
+ // When the shade is closed, the footer is still present in the list, but not visible.
+ // This prevents the footer from being shown when a HUN is present, while still allowing
+ // the footer to be counted as part of the shade for measurements.
+ shadeInteractor.shadeExpansion.map { it == 0f }.distinctUntilChanged()
+ }
+ }
+
+ /**
+ * Whether the footer should be part of the list or not, and whether the transition from one
+ * state to another should be animated. This essentially corresponds to transitioning the view
+ * visibility from VISIBLE to GONE and vice versa.
+ *
+ * Note that this value being true doesn't necessarily mean that the footer is visible. It could
+ * be hidden by another condition (see [shouldHideFooterView] above).
+ */
+ val shouldIncludeFooterView: Flow<AnimatedValue<Boolean>> by lazy {
if (FooterViewRefactor.isUnexpectedlyInLegacyMode()) {
flowOf(AnimatedValue.NotAnimating(false))
} else {
@@ -114,34 +144,30 @@
userSetupInteractor.isUserSetUp,
notificationStackInteractor.isShowingOnLockscreen,
shadeInteractor.isQsFullscreen,
- remoteInputInteractor.isRemoteInputActive,
- shadeInteractor.shadeExpansion.map { it == 0f }.distinctUntilChanged(),
+ remoteInputInteractor.isRemoteInputActive
) {
hasNotifications,
isUserSetUp,
isShowingOnLockscreen,
qsFullScreen,
- isRemoteInputActive,
- isShadeClosed ->
+ isRemoteInputActive ->
when {
- !hasNotifications -> VisibilityChange.HIDE_WITH_ANIMATION
+ !hasNotifications -> VisibilityChange.DISAPPEAR_WITH_ANIMATION
// Hide the footer until the user setup is complete, to prevent access
// to settings (b/193149550).
- !isUserSetUp -> VisibilityChange.HIDE_WITH_ANIMATION
+ !isUserSetUp -> VisibilityChange.DISAPPEAR_WITH_ANIMATION
// Do not show the footer if the lockscreen is visible (incl. AOD),
// except if the shade is opened on top. See also b/219680200.
// Do not animate, as that makes the footer appear briefly when
// transitioning between the shade and keyguard.
- isShowingOnLockscreen -> VisibilityChange.HIDE_WITHOUT_ANIMATION
+ isShowingOnLockscreen -> VisibilityChange.DISAPPEAR_WITHOUT_ANIMATION
// Do not show the footer if quick settings are fully expanded (except
// for the foldable split shade view). See b/201427195 && b/222699879.
- qsFullScreen -> VisibilityChange.HIDE_WITH_ANIMATION
+ qsFullScreen -> VisibilityChange.DISAPPEAR_WITH_ANIMATION
// Hide the footer if remote input is active (i.e. user is replying to a
// notification). See b/75984847.
- isRemoteInputActive -> VisibilityChange.HIDE_WITH_ANIMATION
- // Never show the footer if the shade is collapsed (e.g. when HUNing).
- isShadeClosed -> VisibilityChange.HIDE_WITHOUT_ANIMATION
- else -> VisibilityChange.SHOW_WITH_ANIMATION
+ isRemoteInputActive -> VisibilityChange.DISAPPEAR_WITH_ANIMATION
+ else -> VisibilityChange.APPEAR_WITH_ANIMATION
}
}
.flowOn(bgDispatcher)
@@ -174,9 +200,9 @@
}
enum class VisibilityChange(val visible: Boolean, val canAnimate: Boolean) {
- HIDE_WITHOUT_ANIMATION(visible = false, canAnimate = false),
- HIDE_WITH_ANIMATION(visible = false, canAnimate = true),
- SHOW_WITH_ANIMATION(visible = true, canAnimate = true)
+ DISAPPEAR_WITHOUT_ANIMATION(visible = false, canAnimate = false),
+ DISAPPEAR_WITH_ANIMATION(visible = false, canAnimate = true),
+ APPEAR_WITH_ANIMATION(visible = true, canAnimate = true)
}
// TODO(b/308591475): This should be tracked separately by the empty shade.
@@ -212,4 +238,41 @@
activeNotificationsInteractor.hasNonClearableSilentNotifications
}
}
+
+ val topHeadsUpRow: Flow<HeadsUpRowKey?> by lazy {
+ if (NotificationsHeadsUpRefactor.isUnexpectedlyInLegacyMode()) {
+ flowOf(null)
+ } else {
+ headsUpNotificationInteractor.topHeadsUpRow
+ }
+ }
+
+ val pinnedHeadsUpRows: Flow<Set<HeadsUpRowKey>> by lazy {
+ if (NotificationsHeadsUpRefactor.isUnexpectedlyInLegacyMode()) {
+ flowOf(emptySet())
+ } else {
+ headsUpNotificationInteractor.pinnedHeadsUpRows
+ }
+ }
+
+ val headsUpAnimationsEnabled: Flow<Boolean> by lazy {
+ combine(keyguardInteractor.isKeyguardShowing, shadeInteractor.isShadeFullyExpanded) {
+ (isKeyguardShowing, isShadeFullyExpanded) ->
+ // TODO(b/325936094) use isShadeFullyCollapsed instead
+ !isKeyguardShowing && !isShadeFullyExpanded
+ }
+ }
+
+ val hasPinnedHeadsUpRow: Flow<Boolean> by lazy {
+ if (NotificationsHeadsUpRefactor.isUnexpectedlyInLegacyMode()) {
+ flowOf(false)
+ } else {
+ headsUpNotificationInteractor.hasPinnedRows
+ }
+ }
+
+ // TODO(b/325936094) use it for the text displayed in the StatusBar
+ fun headsUpRow(key: HeadsUpRowKey): HeadsUpRowViewModel =
+ HeadsUpRowViewModel(headsUpNotificationInteractor.headsUpRow(key))
+ fun elementKeyFor(key: HeadsUpRowKey): Any = headsUpNotificationInteractor.elementKeyFor(key)
}
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 b6167e1..a7cbc33 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
@@ -18,7 +18,6 @@
package com.android.systemui.statusbar.notification.stack.ui.viewmodel
import com.android.compose.animation.scene.ObservableTransitionState
-import com.android.systemui.common.shared.model.NotificationContainerBounds
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dump.DumpManager
@@ -27,6 +26,7 @@
import com.android.systemui.scene.shared.model.Scenes.Shade
import com.android.systemui.shade.domain.interactor.ShadeInteractor
import com.android.systemui.statusbar.notification.stack.domain.interactor.NotificationStackAppearanceInteractor
+import com.android.systemui.statusbar.notification.stack.shared.model.StackClipping
import com.android.systemui.util.kotlin.FlowDumperImpl
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
@@ -83,8 +83,13 @@
.dumpWhileCollecting("expandFraction")
/** The bounds of the notification stack in the current scene. */
- val stackBounds: Flow<NotificationContainerBounds> =
- stackAppearanceInteractor.stackBounds.dumpValue("stackBounds")
+ val stackClipping: Flow<StackClipping> =
+ combine(
+ stackAppearanceInteractor.stackBounds,
+ stackAppearanceInteractor.stackRounding,
+ ::StackClipping
+ )
+ .dumpWhileCollecting("stackClipping")
/** The y-coordinate in px of top of the contents of the notification stack. */
val contentTop: StateFlow<Float> = stackAppearanceInteractor.contentTop.dumpValue("contentTop")
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt
index 9e2497d..bd83121 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
@@ -24,6 +24,8 @@
import com.android.systemui.scene.shared.flag.SceneContainerFlags
import com.android.systemui.shade.domain.interactor.ShadeInteractor
import com.android.systemui.statusbar.notification.stack.domain.interactor.NotificationStackAppearanceInteractor
+import com.android.systemui.statusbar.notification.stack.shared.model.StackBounds
+import com.android.systemui.statusbar.notification.stack.shared.model.StackRounding
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
@@ -61,12 +63,17 @@
right: Float,
bottom: Float,
) {
- val notificationContainerBounds =
- NotificationContainerBounds(top = top, bottom = bottom, left = left, right = right)
- keyguardInteractor.setNotificationContainerBounds(notificationContainerBounds)
- interactor.setStackBounds(notificationContainerBounds)
+ keyguardInteractor.setNotificationContainerBounds(
+ NotificationContainerBounds(top = top, bottom = bottom)
+ )
+ interactor.setStackBounds(
+ StackBounds(top = top, bottom = bottom, left = left, right = right)
+ )
}
+ /** Corner rounding of the stack */
+ val stackRounding: Flow<StackRounding> = interactor.stackRounding
+
/**
* The height in px of the contents of notification stack. Depending on the number of
* notifications, this can exceed the space available on screen to show notifications, at which
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt
index a38840b..ab6c148 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
@@ -386,7 +386,7 @@
// All transition view models are mututally exclusive, and safe to merge
val alphaTransitions =
merge(
- alternateBouncerToGoneTransitionViewModel.lockscreenAlpha,
+ alternateBouncerToGoneTransitionViewModel.lockscreenAlpha(viewState),
aodToLockscreenTransitionViewModel.notificationAlpha,
aodToOccludedTransitionViewModel.lockscreenAlpha(viewState),
dozingToLockscreenTransitionViewModel.lockscreenAlpha,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/ui/viewbinder/HeadsUpNotificationViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/ui/viewbinder/HeadsUpNotificationViewBinder.kt
new file mode 100644
index 0000000..cb360fe
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/ui/viewbinder/HeadsUpNotificationViewBinder.kt
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.ui.viewbinder
+
+import android.util.Log
+import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
+import com.android.systemui.statusbar.notification.shared.HeadsUpRowKey
+import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout
+import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationListViewModel
+import com.android.systemui.util.kotlin.sample
+import javax.inject.Inject
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.launch
+
+private const val TAG = "HunBinder"
+private val DEBUG = true // Compile.IS_DEBUG && Log.isLoggable(TAG, Log.DEBUG)
+
+class HeadsUpNotificationViewBinder
+@Inject
+constructor(private val viewModel: NotificationListViewModel) {
+ suspend fun bindHeadsUpNotifications(parentView: NotificationStackScrollLayout): Unit =
+ coroutineScope {
+ launch {
+ var previousKeys = emptySet<HeadsUpRowKey>()
+ viewModel.pinnedHeadsUpRows
+ .sample(viewModel.headsUpAnimationsEnabled, ::Pair)
+ .collect { (newKeys, animationsEnabled) ->
+ if (DEBUG) {
+ Log.d(TAG, "update:$newKeys")
+ }
+
+ val added = newKeys - previousKeys
+ val removed = previousKeys - newKeys
+ previousKeys = newKeys
+
+ if (animationsEnabled) {
+ added.forEach { key ->
+ parentView.generateHeadsUpAnimation(
+ obtainView(key),
+ /* isHeadsUp = */ true
+ )
+ }
+ removed.forEach { key ->
+ val row = obtainView(key)
+ parentView.generateHeadsUpAnimation(row, /* isHeadsUp = */ false)
+ row.setHeadsUpIsVisible()
+ }
+ }
+ }
+ }
+ launch {
+ viewModel.topHeadsUpRow.collect { key ->
+ parentView.setTopHeadsUpRow(key?.let(::obtainView))
+ }
+ }
+ launch {
+ viewModel.hasPinnedHeadsUpRow.collect { parentView.setInHeadsUpPinnedMode(it) }
+ }
+ }
+
+ private fun obtainView(key: HeadsUpRowKey): ExpandableNotificationRow {
+ return viewModel.elementKeyFor(key) as ExpandableNotificationRow
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterImpl.kt
index a55de25..37646ae 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterImpl.kt
@@ -21,6 +21,7 @@
import android.app.TaskStackBuilder
import android.content.Context
import android.content.Intent
+import android.os.Bundle
import android.os.RemoteException
import android.os.UserHandle
import android.provider.Settings
@@ -149,6 +150,23 @@
)
}
+ override fun startPendingIntentMaybeDismissingKeyguard(
+ intent: PendingIntent,
+ intentSentUiThreadCallback: Runnable?,
+ animationController: ActivityTransitionAnimator.Controller?,
+ fillInIntent: Intent?,
+ extraOptions: Bundle?,
+ ) {
+ activityStarterInternal.startPendingIntentDismissingKeyguard(
+ intent = intent,
+ intentSentUiThreadCallback = intentSentUiThreadCallback,
+ animationController = animationController,
+ showOverLockscreen = true,
+ fillInIntent = fillInIntent,
+ extraOptions = extraOptions,
+ )
+ }
+
/**
* TODO(b/279084380): Change callers to just call startActivityDismissingKeyguard and deprecate
* this.
@@ -554,6 +572,8 @@
associatedView: View? = null,
animationController: ActivityTransitionAnimator.Controller? = null,
showOverLockscreen: Boolean = false,
+ fillInIntent: Intent? = null,
+ extraOptions: Bundle? = null,
) {
val animationController =
if (associatedView is ExpandableNotificationRow) {
@@ -614,9 +634,10 @@
val options =
ActivityOptions(
CentralSurfaces.getActivityOptions(
- displayId,
- animationAdapter
- )
+ displayId,
+ animationAdapter
+ )
+ .apply { extraOptions?.let { putAll(it) } }
)
// TODO b/221255671: restrict this to only be set for
// notifications
@@ -625,9 +646,9 @@
ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED
)
return intent.sendAndReturnResult(
- null,
+ context,
0,
- null,
+ fillInIntent,
null,
null,
null,
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 0db5c64..665fc0a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java
@@ -537,7 +537,7 @@
@VisibleForTesting
void vibrateOnNavigationKeyDown() {
- mShadeViewController.performHapticFeedback(
+ mShadeController.performHapticFeedback(
HapticFeedbackConstants.GESTURE_START
);
}
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 d32e88b..f76de04c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
@@ -27,7 +27,6 @@
import static com.android.systemui.Dependency.TIME_TICK_HANDLER_NAME;
import static com.android.systemui.Flags.lightRevealMigration;
-import static com.android.systemui.Flags.migrateClocksToBlueprint;
import static com.android.systemui.Flags.newAodTransition;
import static com.android.systemui.Flags.predictiveBackSysui;
import static com.android.systemui.Flags.truncatedStatusBarIconsFix;
@@ -142,6 +141,7 @@
import com.android.systemui.fragments.FragmentService;
import com.android.systemui.keyguard.KeyguardUnlockAnimationController;
import com.android.systemui.keyguard.KeyguardViewMediator;
+import com.android.systemui.keyguard.MigrateClocksToBlueprint;
import com.android.systemui.keyguard.ScreenLifecycle;
import com.android.systemui.keyguard.WakefulnessLifecycle;
import com.android.systemui.keyguard.ui.binder.LightRevealScrimViewBinder;
@@ -1470,7 +1470,7 @@
return (v, event) -> {
mAutoHideController.checkUserAutoHide(event);
mRemoteInputManager.checkRemoteInputOutside(event);
- if (!migrateClocksToBlueprint()) {
+ if (!MigrateClocksToBlueprint.isEnabled()) {
mShadeController.onStatusBarTouch(event);
}
return getNotificationShadeWindowView().onTouchEvent(event);
@@ -2507,7 +2507,7 @@
mNotificationShadeWindowController.batchApplyWindowLayoutParams(()-> {
mDeviceInteractive = true;
- boolean isFlaggedOff = newAodTransition() && migrateClocksToBlueprint();
+ boolean isFlaggedOff = newAodTransition() && MigrateClocksToBlueprint.isEnabled();
if (!isFlaggedOff && shouldAnimateDozeWakeup()) {
// If this is false, the power button must be physically pressed in order to
// trigger fingerprint authentication.
@@ -3147,7 +3147,14 @@
public void onDozeAmountChanged(float linear, float eased) {
if (!lightRevealMigration()
&& !(mLightRevealScrim.getRevealEffect() instanceof CircleReveal)) {
- mLightRevealScrim.setRevealAmount(1f - linear);
+ if (DeviceEntryUdfpsRefactor.isEnabled()) {
+ // If wakeAndUnlocking, this is handled in AuthRippleInteractor
+ if (!mBiometricUnlockController.isWakeAndUnlock()) {
+ mLightRevealScrim.setRevealAmount(1f - linear);
+ }
+ } else {
+ mLightRevealScrim.setRevealAmount(1f - linear);
+ }
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java
index 24be3db..3f200d5 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java
@@ -22,6 +22,7 @@
import android.content.res.Resources;
import android.graphics.Region;
import android.os.Handler;
+import android.util.ArrayMap;
import android.util.Pools;
import androidx.collection.ArraySet;
@@ -40,7 +41,10 @@
import com.android.systemui.statusbar.notification.collection.provider.OnReorderingAllowedListener;
import com.android.systemui.statusbar.notification.collection.provider.VisualStabilityProvider;
import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManager;
+import com.android.systemui.statusbar.notification.data.repository.HeadsUpRepository;
+import com.android.systemui.statusbar.notification.data.repository.HeadsUpRowRepository;
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
+import com.android.systemui.statusbar.notification.shared.NotificationsHeadsUpRefactor;
import com.android.systemui.statusbar.policy.AccessibilityManagerWrapper;
import com.android.systemui.statusbar.policy.AnimationStateHandler;
import com.android.systemui.statusbar.policy.AvalancheController;
@@ -58,13 +62,21 @@
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
+import java.util.Objects;
+import java.util.Set;
import java.util.Stack;
import javax.inject.Inject;
+import kotlinx.coroutines.flow.Flow;
+import kotlinx.coroutines.flow.MutableStateFlow;
+import kotlinx.coroutines.flow.StateFlow;
+import kotlinx.coroutines.flow.StateFlowKt;
+
/** A implementation of HeadsUpManager for phone. */
@SysUISingleton
-public class HeadsUpManagerPhone extends BaseHeadsUpManager implements OnHeadsUpChangedListener {
+public class HeadsUpManagerPhone extends BaseHeadsUpManager implements
+ HeadsUpRepository, OnHeadsUpChangedListener {
private static final String TAG = "HeadsUpManagerPhone";
@VisibleForTesting
@@ -73,15 +85,20 @@
private final GroupMembershipManager mGroupMembershipManager;
private final List<OnHeadsUpPhoneListenerChange> mHeadsUpPhoneListeners = new ArrayList<>();
private final VisualStabilityProvider mVisualStabilityProvider;
- private boolean mReleaseOnExpandFinish;
+ // TODO(b/328393698) move the topHeadsUpRow logic to an interactor
+ private final MutableStateFlow<HeadsUpRowRepository> mTopHeadsUpRow =
+ StateFlowKt.MutableStateFlow(null);
+ private final MutableStateFlow<Set<HeadsUpRowRepository>> mHeadsUpNotificationRows =
+ StateFlowKt.MutableStateFlow(new HashSet<>());
+ private final MutableStateFlow<Boolean> mHeadsUpGoingAway = StateFlowKt.MutableStateFlow(false);
+ private boolean mReleaseOnExpandFinish;
private boolean mTrackingHeadsUp;
private final HashSet<String> mSwipedOutKeys = new HashSet<>();
private final HashSet<NotificationEntry> mEntriesToRemoveAfterExpand = new HashSet<>();
private final ArraySet<NotificationEntry> mEntriesToRemoveWhenReorderingAllowed
= new ArraySet<>();
private boolean mIsExpanded;
- private boolean mHeadsUpGoingAway;
private int mStatusBarState;
private AnimationStateHandler mAnimationStateHandler;
private int mHeadsUpInset;
@@ -94,6 +111,7 @@
@Override
public HeadsUpEntryPhone acquire() {
+ NotificationsHeadsUpRefactor.assertInLegacyMode();
if (!mPoolObjects.isEmpty()) {
return mPoolObjects.pop();
}
@@ -102,6 +120,7 @@
@Override
public boolean release(@NonNull HeadsUpEntryPhone instance) {
+ NotificationsHeadsUpRefactor.assertInLegacyMode();
mPoolObjects.push(instance);
return true;
}
@@ -245,7 +264,7 @@
if (isExpanded != mIsExpanded) {
mIsExpanded = isExpanded;
if (isExpanded) {
- mHeadsUpGoingAway = false;
+ mHeadsUpGoingAway.setValue(false);
}
}
}
@@ -256,17 +275,17 @@
*/
@Override
public void setHeadsUpGoingAway(boolean headsUpGoingAway) {
- if (headsUpGoingAway != mHeadsUpGoingAway) {
- mHeadsUpGoingAway = headsUpGoingAway;
+ if (headsUpGoingAway != mHeadsUpGoingAway.getValue()) {
for (OnHeadsUpPhoneListenerChange listener : mHeadsUpPhoneListeners) {
listener.onHeadsUpGoingAwayStateChanged(headsUpGoingAway);
}
+ mHeadsUpGoingAway.setValue(headsUpGoingAway);
}
}
@Override
public boolean isHeadsUpGoingAway() {
- return mHeadsUpGoingAway;
+ return mHeadsUpGoingAway.getValue();
}
/**
@@ -285,6 +304,7 @@
} else {
headsUpEntry.updateEntry(false /* updatePostTime */, "setRemoteInputActive(false)");
}
+ onEntryUpdated(headsUpEntry);
}
}
@@ -371,15 +391,48 @@
///////////////////////////////////////////////////////////////////////////////////////////////
// HeadsUpManager utility (protected) methods overrides:
+ @NonNull
@Override
- protected HeadsUpEntry createHeadsUpEntry() {
- return mEntryPool.acquire();
+ protected HeadsUpEntry createHeadsUpEntry(NotificationEntry entry) {
+ if (NotificationsHeadsUpRefactor.isEnabled()) {
+ return new HeadsUpEntryPhone(entry);
+ } else {
+ HeadsUpEntryPhone headsUpEntry = mEntryPool.acquire();
+ headsUpEntry.setEntry(entry);
+ return headsUpEntry;
+ }
+ }
+
+ @Override
+ protected void onEntryAdded(HeadsUpEntry headsUpEntry) {
+ super.onEntryAdded(headsUpEntry);
+ updateTopHeadsUpFlow();
+ updateHeadsUpFlow();
+ }
+
+ @Override
+ protected void onEntryUpdated(HeadsUpEntry headsUpEntry) {
+ super.onEntryUpdated(headsUpEntry);
+ // no need to update the list here
+ updateTopHeadsUpFlow();
}
@Override
protected void onEntryRemoved(HeadsUpEntry headsUpEntry) {
super.onEntryRemoved(headsUpEntry);
- mEntryPool.release((HeadsUpEntryPhone) headsUpEntry);
+ if (!NotificationsHeadsUpRefactor.isEnabled()) {
+ mEntryPool.release((HeadsUpEntryPhone) headsUpEntry);
+ }
+ updateTopHeadsUpFlow();
+ updateHeadsUpFlow();
+ }
+
+ private void updateTopHeadsUpFlow() {
+ mTopHeadsUpRow.setValue((HeadsUpRowRepository) getTopHeadsUpEntry());
+ }
+
+ private void updateHeadsUpFlow() {
+ mHeadsUpNotificationRows.setValue(new HashSet<>(getHeadsUpEntryPhoneMap().values()));
}
@Override
@@ -403,6 +456,12 @@
///////////////////////////////////////////////////////////////////////////////////////////////
// Private utility methods:
+ @NonNull
+ private ArrayMap<String, HeadsUpEntryPhone> getHeadsUpEntryPhoneMap() {
+ //noinspection unchecked
+ return (ArrayMap<String, HeadsUpEntryPhone>) ((ArrayMap) mHeadsUpEntryMap);
+ }
+
@Nullable
private HeadsUpEntryPhone getHeadsUpEntryPhone(@NonNull String key) {
return (HeadsUpEntryPhone) mHeadsUpEntryMap.get(key);
@@ -410,7 +469,11 @@
@Nullable
private HeadsUpEntryPhone getTopHeadsUpEntryPhone() {
- return (HeadsUpEntryPhone) getTopHeadsUpEntry();
+ if (NotificationsHeadsUpRefactor.isEnabled()) {
+ return (HeadsUpEntryPhone) mTopHeadsUpRow.getValue();
+ } else {
+ return (HeadsUpEntryPhone) getTopHeadsUpEntry();
+ }
}
@Override
@@ -427,26 +490,73 @@
return headsUpEntry == null || headsUpEntry != topEntry || super.canRemoveImmediately(key);
}
+ @Override
+ @NonNull
+ public Flow<HeadsUpRowRepository> getTopHeadsUpRow() {
+ return mTopHeadsUpRow;
+ }
+
+ @Override
+ @NonNull
+ public Flow<Set<HeadsUpRowRepository>> getActiveHeadsUpRows() {
+ return mHeadsUpNotificationRows;
+ }
+
+ @Override
+ @NonNull
+ public Flow<Boolean> getHeadsUpAnimatingAway() {
+ return mHeadsUpGoingAway;
+ }
+
///////////////////////////////////////////////////////////////////////////////////////////////
// HeadsUpEntryPhone:
- protected class HeadsUpEntryPhone extends BaseHeadsUpManager.HeadsUpEntry {
+ protected class HeadsUpEntryPhone extends BaseHeadsUpManager.HeadsUpEntry implements
+ HeadsUpRowRepository {
private boolean mGutsShownPinned;
+ private final MutableStateFlow<Boolean> mIsPinned = StateFlowKt.MutableStateFlow(false);
/**
* If the time this entry has been on was extended
*/
private boolean extended;
-
@Override
public boolean isSticky() {
return super.isSticky() || mGutsShownPinned;
}
- public void setEntry(@NonNull final NotificationEntry entry) {
- Runnable removeHeadsUpRunnable = () -> {
+ public HeadsUpEntryPhone() {
+ super();
+ }
+
+ public HeadsUpEntryPhone(NotificationEntry entry) {
+ super(entry);
+ }
+
+ @Override
+ @NonNull
+ public String getKey() {
+ return requireEntry().getKey();
+ }
+
+ @Override
+ @NonNull
+ public StateFlow<Boolean> isPinned() {
+ return mIsPinned;
+ }
+
+ @Override
+ protected void setRowPinned(boolean pinned) {
+ // TODO(b/327624082): replace this super call with a ViewBinder
+ super.setRowPinned(pinned);
+ mIsPinned.setValue(pinned);
+ }
+
+ @Override
+ protected Runnable createRemoveRunnable(NotificationEntry entry) {
+ return () -> {
if (!mVisualStabilityProvider.isReorderingAllowed()
// We don't want to allow reordering while pulsing, but headsup need to
// time out anyway
@@ -460,8 +570,6 @@
removeEntry(entry.getKey());
}
};
-
- setEntry(entry, removeHeadsUpRunnable);
}
@Override
@@ -521,6 +629,17 @@
protected long calculateFinishTime() {
return super.calculateFinishTime() + (extended ? mExtensionTime : 0);
}
+
+ @Override
+ @NonNull
+ public Object getElementKey() {
+ return requireEntry().getRow();
+ }
+
+ private NotificationEntry requireEntry() {
+ /* check if */ NotificationsHeadsUpRefactor.isUnexpectedlyInLegacyMode();
+ return Objects.requireNonNull(mEntry);
+ }
}
private final StateListener mStatusBarStateListener = new StateListener() {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LegacyNotificationIconAreaControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LegacyNotificationIconAreaControllerImpl.java
index 94f62e0..f84efbb 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LegacyNotificationIconAreaControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LegacyNotificationIconAreaControllerImpl.java
@@ -16,7 +16,6 @@
package com.android.systemui.statusbar.phone;
import static com.android.systemui.Flags.newAodTransition;
-import static com.android.systemui.Flags.migrateClocksToBlueprint;
import android.content.Context;
import android.content.res.Resources;
@@ -41,6 +40,7 @@
import com.android.systemui.demomode.DemoMode;
import com.android.systemui.demomode.DemoModeController;
import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.keyguard.MigrateClocksToBlueprint;
import com.android.systemui.plugins.DarkIconDispatcher;
import com.android.systemui.plugins.DarkIconDispatcher.DarkReceiver;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
@@ -545,7 +545,7 @@
return;
}
if (mScreenOffAnimationController.shouldAnimateAodIcons()) {
- if (!migrateClocksToBlueprint()) {
+ if (!MigrateClocksToBlueprint.isEnabled()) {
mAodIcons.setTranslationY(-mAodIconAppearTranslation);
}
mAodIcons.setAlpha(0);
@@ -557,14 +557,14 @@
.start();
} else {
mAodIcons.setAlpha(1.0f);
- if (!migrateClocksToBlueprint()) {
+ if (!MigrateClocksToBlueprint.isEnabled()) {
mAodIcons.setTranslationY(0);
}
}
}
private void animateInAodIconTranslation() {
- if (!migrateClocksToBlueprint()) {
+ if (!MigrateClocksToBlueprint.isEnabled()) {
mAodIcons.animate()
.setInterpolator(Interpolators.DECELERATE_QUINT)
.translationY(0)
@@ -667,7 +667,7 @@
}
} else {
mAodIcons.setAlpha(1.0f);
- if (!migrateClocksToBlueprint()) {
+ if (!MigrateClocksToBlueprint.isEnabled()) {
mAodIcons.setTranslationY(0);
}
mAodIcons.setVisibility(visible ? View.VISIBLE : View.INVISIBLE);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationController.kt
index 67d2299..f3c7090 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationController.kt
@@ -19,9 +19,9 @@
import com.android.internal.jank.InteractionJankMonitor.CUJ_SCREEN_OFF_SHOW_AOD
import com.android.systemui.DejankUtils
import com.android.systemui.Flags.lightRevealMigration
-import com.android.systemui.Flags.migrateClocksToBlueprint
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.keyguard.KeyguardViewMediator
+import com.android.systemui.keyguard.MigrateClocksToBlueprint
import com.android.systemui.keyguard.WakefulnessLifecycle
import com.android.systemui.shade.ShadeViewController
import com.android.systemui.shade.domain.interactor.PanelExpansionInteractor
@@ -45,9 +45,7 @@
*/
private const val ANIMATE_IN_KEYGUARD_DELAY = 600L
-/**
- * Duration for the light reveal portion of the animation.
- */
+/** Duration for the light reveal portion of the animation. */
private const val LIGHT_REVEAL_ANIMATION_DURATION = 500L
/**
@@ -58,7 +56,9 @@
* and then animates in the AOD UI.
*/
@SysUISingleton
-class UnlockedScreenOffAnimationController @Inject constructor(
+class UnlockedScreenOffAnimationController
+@Inject
+constructor(
private val context: Context,
private val wakefulnessLifecycle: WakefulnessLifecycle,
private val statusBarStateControllerImpl: StatusBarStateControllerImpl,
@@ -95,52 +95,61 @@
*/
private var decidedToAnimateGoingToSleep: Boolean? = null
- private val lightRevealAnimator = ValueAnimator.ofFloat(1f, 0f).apply {
- duration = LIGHT_REVEAL_ANIMATION_DURATION
- interpolator = Interpolators.LINEAR
- addUpdateListener {
- if (lightRevealMigration()) return@addUpdateListener
- if (lightRevealScrim.revealEffect !is CircleReveal) {
- lightRevealScrim.revealAmount = it.animatedValue as Float
- }
- if (lightRevealScrim.isScrimAlmostOccludes &&
- interactionJankMonitor.isInstrumenting(CUJ_SCREEN_OFF)) {
- // ends the instrument when the scrim almost occludes the screen.
- // because the following janky frames might not be perceptible.
- interactionJankMonitor.end(CUJ_SCREEN_OFF)
- }
- }
- addListener(object : AnimatorListenerAdapter() {
- override fun onAnimationCancel(animation: Animator) {
- if (lightRevealMigration()) return
+ private val lightRevealAnimator =
+ ValueAnimator.ofFloat(1f, 0f).apply {
+ duration = LIGHT_REVEAL_ANIMATION_DURATION
+ interpolator = Interpolators.LINEAR
+ addUpdateListener {
+ if (lightRevealMigration()) return@addUpdateListener
if (lightRevealScrim.revealEffect !is CircleReveal) {
- lightRevealScrim.revealAmount = 1f
+ lightRevealScrim.revealAmount = it.animatedValue as Float
+ }
+ if (
+ lightRevealScrim.isScrimAlmostOccludes &&
+ interactionJankMonitor.isInstrumenting(CUJ_SCREEN_OFF)
+ ) {
+ // ends the instrument when the scrim almost occludes the screen.
+ // because the following janky frames might not be perceptible.
+ interactionJankMonitor.end(CUJ_SCREEN_OFF)
}
}
+ addListener(
+ object : AnimatorListenerAdapter() {
+ override fun onAnimationCancel(animation: Animator) {
+ if (lightRevealMigration()) return
+ if (lightRevealScrim.revealEffect !is CircleReveal) {
+ lightRevealScrim.revealAmount = 1f
+ }
+ }
- override fun onAnimationEnd(animation: Animator) {
- lightRevealAnimationPlaying = false
- interactionJankMonitor.end(CUJ_SCREEN_OFF)
- }
+ override fun onAnimationEnd(animation: Animator) {
+ lightRevealAnimationPlaying = false
+ interactionJankMonitor.end(CUJ_SCREEN_OFF)
+ }
- override fun onAnimationStart(animation: Animator) {
- interactionJankMonitor.begin(
- notifShadeWindowControllerLazy.get().windowRootView, CUJ_SCREEN_OFF)
- }
- })
- }
+ override fun onAnimationStart(animation: Animator) {
+ interactionJankMonitor.begin(
+ notifShadeWindowControllerLazy.get().windowRootView,
+ CUJ_SCREEN_OFF
+ )
+ }
+ }
+ )
+ }
// FrameCallback used to delay starting the light reveal animation until the next frame
- private val startLightRevealCallback = namedRunnable("startLightReveal") {
- lightRevealAnimationPlaying = true
- lightRevealAnimator.start()
- }
-
- private val animatorDurationScaleObserver = object : ContentObserver(null) {
- override fun onChange(selfChange: Boolean) {
- updateAnimatorDurationScale()
+ private val startLightRevealCallback =
+ namedRunnable("startLightReveal") {
+ lightRevealAnimationPlaying = true
+ lightRevealAnimator.start()
}
- }
+
+ private val animatorDurationScaleObserver =
+ object : ContentObserver(null) {
+ override fun onChange(selfChange: Boolean) {
+ updateAnimatorDurationScale()
+ }
+ }
override fun initialize(
centralSurfaces: CentralSurfaces,
@@ -154,22 +163,21 @@
updateAnimatorDurationScale()
globalSettings.registerContentObserver(
- Settings.Global.getUriFor(Settings.Global.ANIMATOR_DURATION_SCALE),
- /* notify for descendants */ false,
- animatorDurationScaleObserver)
+ Settings.Global.getUriFor(Settings.Global.ANIMATOR_DURATION_SCALE),
+ /* notify for descendants */ false,
+ animatorDurationScaleObserver
+ )
wakefulnessLifecycle.addObserver(this)
}
fun updateAnimatorDurationScale() {
- animatorDurationScale = fixScale(
- globalSettings.getFloat(Settings.Global.ANIMATOR_DURATION_SCALE, 1f))
+ animatorDurationScale =
+ fixScale(globalSettings.getFloat(Settings.Global.ANIMATOR_DURATION_SCALE, 1f))
}
- override fun shouldDelayKeyguardShow(): Boolean =
- shouldPlayAnimation()
+ override fun shouldDelayKeyguardShow(): Boolean = shouldPlayAnimation()
- override fun isKeyguardShowDelayed(): Boolean =
- isAnimationPlaying()
+ override fun isKeyguardShowDelayed(): Boolean = isAnimationPlaying()
/**
* Animates in the provided keyguard view, ending in the same position that it will be in on
@@ -190,15 +198,21 @@
// We animate the Y properly separately using the PropertyAnimator, as the panel
// view also needs to update the end position.
PropertyAnimator.cancelAnimation(keyguardView, AnimatableProperty.Y)
- PropertyAnimator.setProperty(keyguardView, AnimatableProperty.Y, currentY,
- AnimationProperties().setDuration(duration.toLong()),
- true /* animate */)
+ PropertyAnimator.setProperty(
+ keyguardView,
+ AnimatableProperty.Y,
+ currentY,
+ AnimationProperties().setDuration(duration.toLong()),
+ true /* animate */
+ )
// Cancel any existing CUJs before starting the animation
interactionJankMonitor.cancel(CUJ_SCREEN_OFF_SHOW_AOD)
PropertyAnimator.cancelAnimation(keyguardView, AnimatableProperty.ALPHA)
PropertyAnimator.setProperty(
- keyguardView, AnimatableProperty.ALPHA, 1f,
+ keyguardView,
+ AnimatableProperty.ALPHA,
+ 1f,
AnimationProperties()
.setDelay(0)
.setDuration(duration.toLong())
@@ -230,13 +244,14 @@
interactionJankMonitor.cancel(CUJ_SCREEN_OFF_SHOW_AOD)
}
.setCustomInterpolator(View.ALPHA, Interpolators.FAST_OUT_SLOW_IN),
- true /* animate */)
- val builder = InteractionJankMonitor.Configuration.Builder
- .withView(
+ true /* animate */
+ )
+ val builder =
+ InteractionJankMonitor.Configuration.Builder.withView(
InteractionJankMonitor.CUJ_SCREEN_OFF_SHOW_AOD,
checkNotNull(notifShadeWindowControllerLazy.get().windowRootView)
- )
- .setTag(statusBarStateControllerImpl.getClockId())
+ )
+ .setTag(statusBarStateControllerImpl.getClockId())
interactionJankMonitor.begin(builder)
}
@@ -284,25 +299,34 @@
// chance of missing the first frame, so to mitigate this we should start the animation
// on the next frame.
DejankUtils.postAfterTraversal(startLightRevealCallback)
- handler.postDelayed({
- // Only run this callback if the device is sleeping (not interactive). This callback
- // is removed in onStartedWakingUp, but since that event is asynchronously
- // dispatched, a race condition could make it possible for this callback to be run
- // as the device is waking up. That results in the AOD UI being shown while we wake
- // up, with unpredictable consequences.
- if (!powerManager.isInteractive(Display.DEFAULT_DISPLAY) &&
- shouldAnimateInKeyguard) {
- if (!migrateClocksToBlueprint()) {
- // Tracking this state should no longer be relevant, as the isInteractive
- // check covers it
- aodUiAnimationPlaying = true
- }
+ handler.postDelayed(
+ {
+ // Only run this callback if the device is sleeping (not interactive). This
+ // callback
+ // is removed in onStartedWakingUp, but since that event is asynchronously
+ // dispatched, a race condition could make it possible for this callback to be
+ // run
+ // as the device is waking up. That results in the AOD UI being shown while we
+ // wake
+ // up, with unpredictable consequences.
+ if (
+ !powerManager.isInteractive(Display.DEFAULT_DISPLAY) &&
+ shouldAnimateInKeyguard
+ ) {
+ if (!MigrateClocksToBlueprint.isEnabled) {
+ // Tracking this state should no longer be relevant, as the
+ // isInteractive
+ // check covers it
+ aodUiAnimationPlaying = true
+ }
- // Show AOD. That'll cause the KeyguardVisibilityHelper to call
- // #animateInKeyguard.
- shadeViewController.showAodUi()
- }
- }, (ANIMATE_IN_KEYGUARD_DELAY * animatorDurationScale).toLong())
+ // Show AOD. That'll cause the KeyguardVisibilityHelper to call
+ // #animateInKeyguard.
+ shadeViewController.showAodUi()
+ }
+ },
+ (ANIMATE_IN_KEYGUARD_DELAY * animatorDurationScale).toLong()
+ )
return true
} else {
@@ -335,8 +359,12 @@
}
// If animations are disabled system-wide, don't play this one either.
- if (Settings.Global.getString(
- context.contentResolver, Settings.Global.ANIMATOR_DURATION_SCALE) == "0") {
+ if (
+ Settings.Global.getString(
+ context.contentResolver,
+ Settings.Global.ANIMATOR_DURATION_SCALE
+ ) == "0"
+ ) {
return false
}
@@ -360,8 +388,10 @@
// If we're not allowed to rotate the keyguard, it can only be displayed in zero-degree
// portrait. If we're in another orientation, disable the screen off animation so we don't
// animate in the keyguard AOD UI sideways or upside down.
- if (!keyguardStateController.isKeyguardScreenRotationAllowed &&
- context.display?.rotation != Surface.ROTATION_0) {
+ if (
+ !keyguardStateController.isKeyguardScreenRotationAllowed &&
+ context.display?.rotation != Surface.ROTATION_0
+ ) {
return false
}
@@ -380,23 +410,18 @@
return isScreenOffLightRevealAnimationPlaying() || aodUiAnimationPlaying
}
- override fun shouldAnimateInKeyguard(): Boolean =
- shouldAnimateInKeyguard
+ override fun shouldAnimateInKeyguard(): Boolean = shouldAnimateInKeyguard
- override fun shouldHideScrimOnWakeUp(): Boolean =
- isScreenOffLightRevealAnimationPlaying()
+ override fun shouldHideScrimOnWakeUp(): Boolean = isScreenOffLightRevealAnimationPlaying()
override fun overrideNotificationsDozeAmount(): Boolean =
shouldPlayUnlockedScreenOffAnimation() && isAnimationPlaying()
- override fun shouldShowAodIconsWhenShade(): Boolean =
- isAnimationPlaying()
+ override fun shouldShowAodIconsWhenShade(): Boolean = isAnimationPlaying()
- override fun shouldAnimateAodIcons(): Boolean =
- shouldPlayUnlockedScreenOffAnimation()
+ override fun shouldAnimateAodIcons(): Boolean = shouldPlayUnlockedScreenOffAnimation()
- override fun shouldPlayAnimation(): Boolean =
- shouldPlayUnlockedScreenOffAnimation()
+ override fun shouldPlayAnimation(): Boolean = shouldPlayUnlockedScreenOffAnimation()
/**
* Whether the light reveal animation is playing. The second part of the screen off animation,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseHeadsUpManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseHeadsUpManager.java
index 50de3cb..20a82a4 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseHeadsUpManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseHeadsUpManager.java
@@ -39,6 +39,7 @@
import com.android.systemui.res.R;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.InflationFlag;
+import com.android.systemui.statusbar.notification.shared.NotificationsHeadsUpRefactor;
import com.android.systemui.util.ListenerSet;
import com.android.systemui.util.concurrency.DelayableExecutor;
import com.android.systemui.util.settings.GlobalSettings;
@@ -162,11 +163,7 @@
*/
@Override
public void showNotification(@NonNull NotificationEntry entry) {
- HeadsUpEntry headsUpEntry = createHeadsUpEntry();
-
- // Attach NotificationEntry for AvalancheController to log key and
- // record mPostTime for AvalancheController sorting
- headsUpEntry.setEntry(entry);
+ HeadsUpEntry headsUpEntry = createHeadsUpEntry(entry);
Runnable runnable = () -> {
// TODO(b/315362456) log outside runnable too
@@ -175,6 +172,7 @@
// Add new entry and begin managing it
mHeadsUpEntryMap.put(entry.getKey(), headsUpEntry);
onEntryAdded(headsUpEntry);
+ // TODO(b/328390331) move accessibility events to the view layer
entry.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
entry.setIsHeadsUpEntry(true);
@@ -235,7 +233,7 @@
// with the groupmanager
return;
}
-
+ // TODO(b/328390331) move accessibility events to the view layer
headsUpEntry.mEntry.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
if (shouldHeadsUpAgain) {
@@ -335,15 +333,15 @@
if (!isPinned) {
headsUpEntry.mWasUnpinned = true;
}
- if (headsUpEntry.isPinned() != isPinned) {
- headsUpEntry.setPinned(isPinned);
+ if (headsUpEntry.isRowPinned() != isPinned) {
+ headsUpEntry.setRowPinned(isPinned);
updatePinnedMode();
if (isPinned && entry.getSbn() != null) {
mUiEventLogger.logWithInstanceId(
NotificationPeekEvent.NOTIFICATION_PEEK, entry.getSbn().getUid(),
entry.getSbn().getPackageName(), entry.getSbn().getInstanceId());
}
- // TODO(b/325936094) convert these listeners to collecting a flow
+ // TODO(b/325936094) use the isPinned Flow instead
for (OnHeadsUpChangedListener listener : mListeners) {
if (isPinned) {
listener.onHeadsUpPinned(entry);
@@ -362,7 +360,7 @@
* Manager-specific logic that should occur when an entry is added.
* @param headsUpEntry entry added
*/
- void onEntryAdded(HeadsUpEntry headsUpEntry) {
+ protected void onEntryAdded(HeadsUpEntry headsUpEntry) {
NotificationEntry entry = headsUpEntry.mEntry;
entry.setHeadsUp(true);
@@ -375,7 +373,7 @@
}
/**
- * Remove a notification and reset the entry.
+ * Remove a notification from the alerting entries.
* @param key key of notification to remove
*/
protected final void removeEntry(@NonNull String key) {
@@ -394,8 +392,13 @@
entry.demoteStickyHun();
mHeadsUpEntryMap.remove(key);
onEntryRemoved(headsUpEntry);
+ // TODO(b/328390331) move accessibility events to the view layer
entry.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
- headsUpEntry.reset();
+ if (NotificationsHeadsUpRefactor.isEnabled()) {
+ headsUpEntry.cancelAutoRemovalCallbacks("removeEntry");
+ } else {
+ headsUpEntry.reset();
+ }
};
mAvalancheController.delete(headsUpEntry, runnable, "removeEntry");
}
@@ -415,7 +418,16 @@
}
}
- private void updatePinnedMode() {
+ /**
+ * Manager-specific logic, that should occur, when the entry is updated, and its posted time has
+ * changed.
+ *
+ * @param headsUpEntry entry updated
+ */
+ protected void onEntryUpdated(HeadsUpEntry headsUpEntry) {
+ }
+
+ protected void updatePinnedMode() {
boolean hasPinnedNotification = hasPinnedNotificationInternal();
if (hasPinnedNotification == mHasPinnedNotification) {
return;
@@ -470,7 +482,7 @@
@Nullable
protected HeadsUpEntry getHeadsUpEntry(@NonNull String key) {
// TODO(b/315362456) See if callers need to check AvalancheController
- return (HeadsUpEntry) mHeadsUpEntryMap.get(key);
+ return mHeadsUpEntryMap.get(key);
}
/**
@@ -490,7 +502,7 @@
HeadsUpEntry topEntry = null;
for (HeadsUpEntry entry: mHeadsUpEntryMap.values()) {
if (topEntry == null || entry.compareTo(topEntry) < 0) {
- topEntry = (HeadsUpEntry) entry;
+ topEntry = entry;
}
}
return topEntry;
@@ -657,8 +669,8 @@
}
@NonNull
- protected HeadsUpEntry createHeadsUpEntry() {
- return new HeadsUpEntry();
+ protected HeadsUpEntry createHeadsUpEntry(NotificationEntry entry) {
+ return new HeadsUpEntry(entry);
}
/**
@@ -694,11 +706,23 @@
@Nullable private Runnable mCancelRemoveRunnable;
- public void setEntry(@NonNull final NotificationEntry entry) {
- setEntry(entry, () -> removeEntry(entry.getKey()));
+ public HeadsUpEntry() {
+ NotificationsHeadsUpRefactor.assertInLegacyMode();
}
- public void setEntry(@NonNull final NotificationEntry entry,
+ public HeadsUpEntry(NotificationEntry entry) {
+ // Attach NotificationEntry for AvalancheController to log key and
+ // record mPostTime for AvalancheController sorting
+ setEntry(entry, createRemoveRunnable(entry));
+ }
+
+ /** Attach a NotificationEntry. */
+ public void setEntry(@NonNull final NotificationEntry entry) {
+ NotificationsHeadsUpRefactor.assertInLegacyMode();
+ setEntry(entry, createRemoveRunnable(entry));
+ }
+
+ private void setEntry(@NonNull final NotificationEntry entry,
@Nullable Runnable removeRunnable) {
mEntry = entry;
mRemoveRunnable = removeRunnable;
@@ -707,11 +731,11 @@
updateEntry(true /* updatePostTime */, "setEntry");
}
- public boolean isPinned() {
+ protected boolean isRowPinned() {
return mEntry != null && mEntry.isRowPinned();
}
- public void setPinned(boolean pinned) {
+ protected void setRowPinned(boolean pinned) {
if (mEntry != null) mEntry.setRowPinned(pinned);
}
@@ -751,6 +775,9 @@
return timeLeft;
};
scheduleAutoRemovalCallback(finishTimeCalculator, "updateEntry (not sticky)");
+
+ // Notify the manager, that the posted time has changed.
+ onEntryUpdated(this);
}
/**
@@ -847,6 +874,7 @@
}
public void reset() {
+ NotificationsHeadsUpRefactor.assertInLegacyMode();
cancelAutoRemovalCallbacks("reset()");
mEntry = null;
mRemoveRunnable = null;
@@ -919,6 +947,11 @@
}
}
+ /** Creates a runnable to remove this notification from the alerting entries. */
+ protected Runnable createRemoveRunnable(NotificationEntry entry) {
+ return () -> removeEntry(entry.getKey());
+ }
+
/**
* Calculate what the post time of a notification is at some current time.
* @return the post time
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManager.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManager.kt
index 420701f..52a2e9c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManager.kt
@@ -196,6 +196,7 @@
* Called when a heads up notification is 'going away' or no longer 'going away'. See
* [HeadsUpManager.setHeadsUpGoingAway].
*/
+ // TODO(b/325936094) delete this callback, and listen to the flow instead
fun onHeadsUpGoingAwayStateChanged(headsUpGoingAway: Boolean)
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/PolicyModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/PolicyModule.kt
index 087e100..7a57027 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/PolicyModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/PolicyModule.kt
@@ -42,6 +42,10 @@
import com.android.systemui.qs.tiles.impl.uimodenight.domain.interactor.UiModeNightTileDataInteractor
import com.android.systemui.qs.tiles.impl.uimodenight.domain.interactor.UiModeNightTileUserActionInteractor
import com.android.systemui.qs.tiles.impl.uimodenight.domain.model.UiModeNightTileModel
+import com.android.systemui.qs.tiles.impl.work.domain.interactor.WorkModeTileDataInteractor
+import com.android.systemui.qs.tiles.impl.work.domain.interactor.WorkModeTileUserActionInteractor
+import com.android.systemui.qs.tiles.impl.work.domain.model.WorkModeTileModel
+import com.android.systemui.qs.tiles.impl.work.ui.WorkModeTileMapper
import com.android.systemui.qs.tiles.viewmodel.QSTileConfig
import com.android.systemui.qs.tiles.viewmodel.QSTileUIConfig
import com.android.systemui.qs.tiles.viewmodel.QSTileViewModel
@@ -69,6 +73,7 @@
const val LOCATION_TILE_SPEC = "location"
const val ALARM_TILE_SPEC = "alarm"
const val UIMODENIGHT_TILE_SPEC = "dark"
+ const val WORK_MODE_TILE_SPEC = "work"
/** Inject flashlight config */
@Provides
@@ -197,6 +202,38 @@
stateInteractor,
mapper,
)
+
+ /** Inject work mode tile config */
+ @Provides
+ @IntoMap
+ @StringKey(WORK_MODE_TILE_SPEC)
+ fun provideWorkModeTileConfig(uiEventLogger: QsEventLogger): QSTileConfig =
+ QSTileConfig(
+ tileSpec = TileSpec.create(WORK_MODE_TILE_SPEC),
+ uiConfig =
+ QSTileUIConfig.Resource(
+ iconRes = com.android.internal.R.drawable.stat_sys_managed_profile_status,
+ labelRes = R.string.quick_settings_work_mode_label,
+ ),
+ instanceId = uiEventLogger.getNewInstanceId(),
+ )
+
+ /** Inject work mode into tileViewModelMap in QSModule */
+ @Provides
+ @IntoMap
+ @StringKey(WORK_MODE_TILE_SPEC)
+ fun provideWorkModeTileViewModel(
+ factory: QSTileViewModelFactory.Static<WorkModeTileModel>,
+ mapper: WorkModeTileMapper,
+ stateInteractor: WorkModeTileDataInteractor,
+ userActionInteractor: WorkModeTileUserActionInteractor
+ ): QSTileViewModel =
+ factory.create(
+ TileSpec.create(WORK_MODE_TILE_SPEC),
+ userActionInteractor,
+ stateInteractor,
+ mapper,
+ )
}
/** Inject FlashlightTile into tileMap in QSModule */
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerImpl.java
index 18ec68b..1f4c3cd 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerImpl.java
@@ -16,6 +16,7 @@
package com.android.systemui.statusbar.policy;
+import static android.permission.flags.Flags.sensitiveNotificationAppProtection;
import static android.provider.Settings.Global.DISABLE_SCREEN_SHARE_PROTECTIONS_FOR_APPS_AND_NOTIFICATIONS;
import static com.android.server.notification.Flags.screenshareNotificationHiding;
@@ -23,6 +24,7 @@
import android.annotation.MainThread;
import android.app.IActivityManager;
import android.content.Context;
+import android.content.pm.PackageManager;
import android.database.ExecutorContentObserver;
import android.media.projection.MediaProjectionInfo;
import android.media.projection.MediaProjectionManager;
@@ -33,6 +35,9 @@
import android.util.ArraySet;
import android.util.Log;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
import com.android.internal.annotations.VisibleForTesting;
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.dagger.qualifiers.Background;
@@ -52,6 +57,7 @@
implements SensitiveNotificationProtectionController {
private static final String LOG_TAG = "SNPC";
private final SensitiveNotificationProtectionControllerLogger mLogger;
+ private final PackageManager mPackageManager;
private final ArraySet<String> mExemptPackages = new ArraySet<>();
private final ListenerSet<Runnable> mListeners = new ListenerSet<>();
private volatile MediaProjectionInfo mProjection;
@@ -64,17 +70,7 @@
public void onStart(MediaProjectionInfo info) {
Trace.beginSection("SNPC.onProjectionStart");
try {
- if (mDisableScreenShareProtections) {
- Log.w(LOG_TAG,
- "Screen share protections disabled, ignoring projectionstart");
- mLogger.logProjectionStart(false, info.getPackageName());
- return;
- }
-
- // Only enable sensitive content protection if sharing full screen
- // Launch cookie only set (non-null) if sharing single app/task
- updateProjectionStateAndNotifyListeners(
- (info.getLaunchCookie() == null) ? info : null);
+ updateProjectionStateAndNotifyListeners(info);
mLogger.logProjectionStart(isSensitiveStateActive(), info.getPackageName());
} finally {
Trace.endSection();
@@ -99,10 +95,12 @@
GlobalSettings settings,
MediaProjectionManager mediaProjectionManager,
IActivityManager activityManager,
+ PackageManager packageManager,
@Main Handler mainHandler,
@Background Executor bgExecutor,
SensitiveNotificationProtectionControllerLogger logger) {
mLogger = logger;
+ mPackageManager = packageManager;
if (!screenshareNotificationHiding()) {
return;
@@ -168,7 +166,7 @@
mExemptPackages.addAll(exemptPackages);
if (mProjection != null) {
- mListeners.forEach(Runnable::run);
+ updateProjectionStateAndNotifyListeners(mProjection);
}
}
@@ -177,13 +175,13 @@
* listeners
*/
@MainThread
- private void updateProjectionStateAndNotifyListeners(MediaProjectionInfo info) {
+ private void updateProjectionStateAndNotifyListeners(@Nullable MediaProjectionInfo info) {
Assert.isMainThread();
// capture previous state
boolean wasSensitive = isSensitiveStateActive();
// update internal state
- mProjection = info;
+ mProjection = getNonExemptProjectionInfo(info);
// if either previous or new state is sensitive, notify listeners.
if (wasSensitive || isSensitiveStateActive()) {
@@ -191,6 +189,36 @@
}
}
+ private MediaProjectionInfo getNonExemptProjectionInfo(@Nullable MediaProjectionInfo info) {
+ if (mDisableScreenShareProtections) {
+ Log.w(LOG_TAG, "Screen share protections disabled");
+ return null;
+ } else if (info != null && mExemptPackages.contains(info.getPackageName())) {
+ Log.w(LOG_TAG, "Screen share protections exempt for package " + info.getPackageName());
+ return null;
+ } else if (info != null && canRecordSensitiveContent(info.getPackageName())) {
+ Log.w(LOG_TAG, "Screen share protections exempt for package " + info.getPackageName()
+ + " via permission");
+ return null;
+ } else if (info != null && info.getLaunchCookie() != null) {
+ // Only enable sensitive content protection if sharing full screen
+ // Launch cookie only set (non-null) if sharing single app/task
+ Log.w(LOG_TAG, "Screen share protections exempt for single app screenshare");
+ return null;
+ }
+ return info;
+ }
+
+ private boolean canRecordSensitiveContent(@NonNull String packageName) {
+ // RECORD_SENSITIVE_CONTENT is flagged api on sensitiveNotificationAppProtection
+ if (sensitiveNotificationAppProtection()) {
+ return mPackageManager.checkPermission(
+ android.Manifest.permission.RECORD_SENSITIVE_CONTENT, packageName)
+ == PackageManager.PERMISSION_GRANTED;
+ }
+ return false;
+ }
+
@Override
public void registerSensitiveStateListener(Runnable onSensitiveStateChanged) {
mListeners.addIfAbsent(onSensitiveStateChanged);
@@ -201,15 +229,9 @@
mListeners.remove(onSensitiveStateChanged);
}
- // TODO(b/323396693): opportunity for optimization
@Override
public boolean isSensitiveStateActive() {
- MediaProjectionInfo projection = mProjection;
- if (projection == null) {
- return false;
- }
-
- return !mExemptPackages.contains(projection.getPackageName());
+ return mProjection != null;
}
@Override
diff --git a/packages/SystemUI/src/com/android/systemui/util/kotlin/DisposableHandles.kt b/packages/SystemUI/src/com/android/systemui/util/kotlin/DisposableHandles.kt
new file mode 100644
index 0000000..de036ea
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/util/kotlin/DisposableHandles.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.util.kotlin
+
+import kotlinx.coroutines.DisposableHandle
+
+/** A mutable collection of [DisposableHandle] objects that is itself a [DisposableHandle] */
+class DisposableHandles : DisposableHandle {
+ private val handles = mutableListOf<DisposableHandle>()
+
+ /** Add the provided handles to this collection. */
+ fun add(vararg handles: DisposableHandle) {
+ this.handles.addAll(handles)
+ }
+
+ /** Same as [add] */
+ operator fun plusAssign(handle: DisposableHandle) {
+ this.handles.add(handle)
+ }
+
+ /** Same as [add] */
+ operator fun plusAssign(handles: Iterable<DisposableHandle>) {
+ this.handles.addAll(handles)
+ }
+
+ /** [dispose] the current contents, then [add] the provided [handles] */
+ fun replaceAll(vararg handles: DisposableHandle) {
+ dispose()
+ add(*handles)
+ }
+
+ /** Dispose of all added handles and empty this collection. */
+ override fun dispose() {
+ handles.forEach { it.dispose() }
+ handles.clear()
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/util/kotlin/ManagedProfileControllerExt.kt b/packages/SystemUI/src/com/android/systemui/util/kotlin/ManagedProfileControllerExt.kt
new file mode 100644
index 0000000..7a2f9b2
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/util/kotlin/ManagedProfileControllerExt.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.util.kotlin
+
+import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.statusbar.phone.ManagedProfileController
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+
+val ManagedProfileController.hasActiveWorkProfile: Flow<Boolean>
+ get() = conflatedCallbackFlow {
+ val callback =
+ object : ManagedProfileController.Callback {
+ override fun onManagedProfileChanged() {
+ trySend(hasActiveProfile())
+ }
+ override fun onManagedProfileRemoved() {
+ // no-op, because the other callback will also be called.
+ }
+ }
+ addCallback(callback) // calls onManagedProfileChanged
+ awaitClose { removeCallback(callback) }
+ }
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dagger/MediaDevicesModule.kt b/packages/SystemUI/src/com/android/systemui/volume/dagger/MediaDevicesModule.kt
index d134e60..155102c9 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dagger/MediaDevicesModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dagger/MediaDevicesModule.kt
@@ -21,7 +21,6 @@
import com.android.settingslib.volume.data.repository.LocalMediaRepository
import com.android.settingslib.volume.data.repository.MediaControllerRepository
import com.android.settingslib.volume.data.repository.MediaControllerRepositoryImpl
-import com.android.settingslib.volume.domain.interactor.LocalMediaInteractor
import com.android.settingslib.volume.shared.AudioManagerEventsReceiver
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
@@ -52,13 +51,6 @@
@Provides
@SysUISingleton
- fun provideLocalMediaInteractor(
- repository: LocalMediaRepository,
- @Application scope: CoroutineScope,
- ): LocalMediaInteractor = LocalMediaInteractor(repository, scope)
-
- @Provides
- @SysUISingleton
fun provideMediaDeviceSessionRepository(
intentsReceiver: AudioManagerEventsReceiver,
mediaSessionManager: MediaSessionManager,
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/bottombar/ui/viewmodel/BottomBarViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/bottombar/ui/viewmodel/BottomBarViewModel.kt
index 8ff2837..0207d6e 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/bottombar/ui/viewmodel/BottomBarViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/bottombar/ui/viewmodel/BottomBarViewModel.kt
@@ -36,10 +36,10 @@
}
fun onSettingsClicked() {
- volumePanelViewModel.dismissPanel()
activityStarter.startActivity(
- Intent(Settings.ACTION_SOUND_SETTINGS).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK),
+ Intent(Settings.ACTION_SOUND_SETTINGS)
+ .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_REORDER_TO_FRONT),
true,
- )
+ ) { volumePanelViewModel.dismissPanel() }
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/data/repository/LocalMediaRepositoryFactory.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/data/repository/LocalMediaRepositoryFactory.kt
index 11b4690..e052f24 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/data/repository/LocalMediaRepositoryFactory.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/data/repository/LocalMediaRepositoryFactory.kt
@@ -15,15 +15,12 @@
*/
package com.android.systemui.volume.panel.component.mediaoutput.data.repository
-import android.media.MediaRouter2Manager
import com.android.settingslib.volume.data.repository.LocalMediaRepository
import com.android.settingslib.volume.data.repository.LocalMediaRepositoryImpl
import com.android.settingslib.volume.shared.AudioManagerEventsReceiver
import com.android.systemui.dagger.qualifiers.Application
-import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.media.controls.util.LocalMediaManagerFactory
import javax.inject.Inject
-import kotlin.coroutines.CoroutineContext
import kotlinx.coroutines.CoroutineScope
interface LocalMediaRepositoryFactory {
@@ -35,18 +32,14 @@
@Inject
constructor(
private val eventsReceiver: AudioManagerEventsReceiver,
- private val mediaRouter2Manager: MediaRouter2Manager,
private val localMediaManagerFactory: LocalMediaManagerFactory,
@Application private val coroutineScope: CoroutineScope,
- @Background private val backgroundCoroutineContext: CoroutineContext,
) : LocalMediaRepositoryFactory {
override fun create(packageName: String?): LocalMediaRepository =
LocalMediaRepositoryImpl(
eventsReceiver,
localMediaManagerFactory.create(packageName),
- mediaRouter2Manager,
coroutineScope,
- backgroundCoroutineContext,
)
}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaDeviceSessionInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaDeviceSessionInteractor.kt
new file mode 100644
index 0000000..b0c8a4a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaDeviceSessionInteractor.kt
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.volume.panel.component.mediaoutput.domain.interactor
+
+import android.media.session.MediaController
+import android.media.session.PlaybackState
+import android.os.Handler
+import com.android.settingslib.volume.data.repository.MediaControllerChange
+import com.android.settingslib.volume.data.repository.MediaControllerRepository
+import com.android.settingslib.volume.data.repository.stateChanges
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.volume.panel.component.mediaoutput.domain.model.MediaDeviceSession
+import com.android.systemui.volume.panel.dagger.scope.VolumePanelScope
+import javax.inject.Inject
+import kotlin.coroutines.CoroutineContext
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.FlowCollector
+import kotlinx.coroutines.flow.filterIsInstance
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onStart
+import kotlinx.coroutines.withContext
+
+/** Allows to observe and change [MediaDeviceSession] state. */
+@OptIn(ExperimentalCoroutinesApi::class)
+@VolumePanelScope
+class MediaDeviceSessionInteractor
+@Inject
+constructor(
+ @Background private val backgroundCoroutineContext: CoroutineContext,
+ @Background private val backgroundHandler: Handler,
+ private val mediaControllerRepository: MediaControllerRepository,
+) {
+
+ /** [PlaybackState] changes for the [MediaDeviceSession]. */
+ fun playbackState(session: MediaDeviceSession): Flow<PlaybackState?> {
+ return stateChanges(session) {
+ emit(MediaControllerChange.PlaybackStateChanged(it.playbackState))
+ }
+ .filterIsInstance(MediaControllerChange.PlaybackStateChanged::class)
+ .map { it.state }
+ }
+
+ /** [MediaController.PlaybackInfo] changes for the [MediaDeviceSession]. */
+ fun playbackInfo(session: MediaDeviceSession): Flow<MediaController.PlaybackInfo?> {
+ return stateChanges(session) {
+ emit(MediaControllerChange.AudioInfoChanged(it.playbackInfo))
+ }
+ .filterIsInstance(MediaControllerChange.AudioInfoChanged::class)
+ .map { it.info }
+ }
+
+ private fun stateChanges(
+ session: MediaDeviceSession,
+ onStart: suspend FlowCollector<MediaControllerChange>.(controller: MediaController) -> Unit,
+ ): Flow<MediaControllerChange?> =
+ mediaControllerRepository.activeSessions
+ .flatMapLatest { controllers ->
+ val controller: MediaController =
+ findControllerForSession(controllers, session)
+ ?: return@flatMapLatest flowOf(null)
+ controller.stateChanges(backgroundHandler).onStart { onStart(controller) }
+ }
+ .flowOn(backgroundCoroutineContext)
+
+ /** Set [MediaDeviceSession] volume to [volume]. */
+ suspend fun setSessionVolume(mediaDeviceSession: MediaDeviceSession, volume: Int): Boolean {
+ if (!mediaDeviceSession.canAdjustVolume) {
+ return false
+ }
+ return withContext(backgroundCoroutineContext) {
+ val controller =
+ findControllerForSession(
+ mediaControllerRepository.activeSessions.value,
+ mediaDeviceSession,
+ )
+ if (controller == null) {
+ false
+ } else {
+ controller.setVolumeTo(volume, 0)
+ true
+ }
+ }
+ }
+
+ private fun findControllerForSession(
+ controllers: Collection<MediaController>,
+ mediaDeviceSession: MediaDeviceSession,
+ ): MediaController? =
+ controllers.firstOrNull { it.sessionToken == mediaDeviceSession.sessionToken }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputActionsInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputActionsInteractor.kt
index cb16abe..ea4c082 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputActionsInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputActionsInteractor.kt
@@ -33,23 +33,15 @@
private val mediaOutputDialogManager: MediaOutputDialogManager,
) {
- fun onBarClick(session: MediaDeviceSession, expandable: Expandable) {
- when (session) {
- is MediaDeviceSession.Active -> {
- mediaOutputDialogManager.createAndShowWithController(
- session.packageName,
- false,
- expandable.dialogController()
- )
- }
- is MediaDeviceSession.Inactive -> {
- mediaOutputDialogManager.createAndShowForSystemRouting(
- expandable.dialogController()
- )
- }
- else -> {
- /* do nothing */
- }
+ fun onBarClick(session: MediaDeviceSession, isPlaybackActive: Boolean, expandable: Expandable) {
+ if (isPlaybackActive) {
+ mediaOutputDialogManager.createAndShowWithController(
+ session.packageName,
+ false,
+ expandable.dialogController()
+ )
+ } else {
+ mediaOutputDialogManager.createAndShowForSystemRouting(expandable.dialogController())
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputInteractor.kt
index 0f53437..e60139e 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputInteractor.kt
@@ -17,17 +17,16 @@
package com.android.systemui.volume.panel.component.mediaoutput.domain.interactor
import android.content.pm.PackageManager
+import android.media.VolumeProvider
import android.media.session.MediaController
-import android.os.Handler
import android.util.Log
import com.android.settingslib.media.MediaDevice
import com.android.settingslib.volume.data.repository.LocalMediaRepository
-import com.android.settingslib.volume.data.repository.MediaControllerChange
import com.android.settingslib.volume.data.repository.MediaControllerRepository
-import com.android.settingslib.volume.data.repository.stateChanges
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.volume.panel.component.mediaoutput.data.repository.LocalMediaRepositoryFactory
import com.android.systemui.volume.panel.component.mediaoutput.domain.model.MediaDeviceSession
+import com.android.systemui.volume.panel.component.mediaoutput.domain.model.MediaDeviceSessions
import com.android.systemui.volume.panel.dagger.scope.VolumePanelScope
import javax.inject.Inject
import kotlin.coroutines.CoroutineContext
@@ -38,12 +37,9 @@
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.distinctUntilChanged
-import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.flow.flatMapLatest
-import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.withContext
@@ -58,35 +54,40 @@
private val packageManager: PackageManager,
@VolumePanelScope private val coroutineScope: CoroutineScope,
@Background private val backgroundCoroutineContext: CoroutineContext,
- @Background private val backgroundHandler: Handler,
- mediaControllerRepository: MediaControllerRepository
+ mediaControllerRepository: MediaControllerRepository,
) {
- /** Current [MediaDeviceSession]. Emits when the session playback changes. */
- val mediaDeviceSession: StateFlow<MediaDeviceSession> =
- mediaControllerRepository.activeLocalMediaController
- .flatMapLatest { it?.mediaDeviceSession() ?: flowOf(MediaDeviceSession.Inactive) }
- .flowOn(backgroundCoroutineContext)
- .stateIn(coroutineScope, SharingStarted.Eagerly, MediaDeviceSession.Inactive)
+ private val activeMediaControllers: Flow<MediaControllers> =
+ mediaControllerRepository.activeSessions
+ .map { getMediaControllers(it) }
+ .shareIn(coroutineScope, SharingStarted.Eagerly, replay = 1)
- private fun MediaController.mediaDeviceSession(): Flow<MediaDeviceSession> {
- return stateChanges(backgroundHandler)
- .onStart { emit(MediaControllerChange.PlaybackStateChanged(playbackState)) }
- .filterIsInstance<MediaControllerChange.PlaybackStateChanged>()
+ /** [MediaDeviceSessions] that contains currently active sessions. */
+ val activeMediaDeviceSessions: Flow<MediaDeviceSessions> =
+ activeMediaControllers.map {
+ MediaDeviceSessions(
+ local = it.local?.mediaDeviceSession(),
+ remote = it.remote?.mediaDeviceSession()
+ )
+ }
+
+ /** Returns the default [MediaDeviceSession] from [activeMediaDeviceSessions] */
+ val defaultActiveMediaSession: StateFlow<MediaDeviceSession?> =
+ activeMediaControllers
.map {
- MediaDeviceSession.Active(
- appLabel = getApplicationLabel(packageName)
- ?: return@map MediaDeviceSession.Inactive,
- packageName = packageName,
- sessionToken = sessionToken,
- playbackState = playbackState,
- )
+ when {
+ it.local?.playbackState?.isActive == true -> it.local.mediaDeviceSession()
+ it.remote?.playbackState?.isActive == true -> it.remote.mediaDeviceSession()
+ it.local != null -> it.local.mediaDeviceSession()
+ else -> null
+ }
}
- }
+ .flowOn(backgroundCoroutineContext)
+ .stateIn(coroutineScope, SharingStarted.Eagerly, null)
private val localMediaRepository: SharedFlow<LocalMediaRepository> =
- mediaDeviceSession
- .map { (it as? MediaDeviceSession.Active)?.packageName }
+ defaultActiveMediaSession
+ .map { it?.packageName }
.distinctUntilChanged()
.map { localMediaRepositoryFactory.create(it) }
.shareIn(coroutineScope, SharingStarted.Eagerly, replay = 1)
@@ -111,6 +112,54 @@
}
}
+ /** Finds local and remote media controllers. */
+ private fun getMediaControllers(
+ controllers: Collection<MediaController>,
+ ): MediaControllers {
+ var localController: MediaController? = null
+ var remoteController: MediaController? = null
+ val remoteMediaSessions: MutableSet<String> = mutableSetOf()
+ for (controller in controllers) {
+ val playbackInfo: MediaController.PlaybackInfo = controller.playbackInfo ?: continue
+ when (playbackInfo.playbackType) {
+ MediaController.PlaybackInfo.PLAYBACK_TYPE_REMOTE -> {
+ // MediaController can't be local if there is a remote one for the same package
+ if (localController?.packageName.equals(controller.packageName)) {
+ localController = null
+ }
+ if (!remoteMediaSessions.contains(controller.packageName)) {
+ remoteMediaSessions.add(controller.packageName)
+ if (remoteController == null) {
+ remoteController = controller
+ }
+ }
+ }
+ MediaController.PlaybackInfo.PLAYBACK_TYPE_LOCAL -> {
+ if (controller.packageName in remoteMediaSessions) continue
+ if (localController != null) continue
+ localController = controller
+ }
+ }
+ }
+ return MediaControllers(local = localController, remote = remoteController)
+ }
+
+ private suspend fun MediaController.mediaDeviceSession(): MediaDeviceSession? {
+ return MediaDeviceSession(
+ packageName = packageName,
+ sessionToken = sessionToken,
+ canAdjustVolume =
+ playbackInfo != null &&
+ playbackInfo?.volumeControl != VolumeProvider.VOLUME_CONTROL_FIXED,
+ appLabel = getApplicationLabel(packageName) ?: return null
+ )
+ }
+
+ private data class MediaControllers(
+ val local: MediaController?,
+ val remote: MediaController?,
+ )
+
private companion object {
const val TAG = "MediaOutputInteractor"
}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/model/MediaDeviceSession.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/model/MediaDeviceSession.kt
index 1bceee9..2a2ce79 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/model/MediaDeviceSession.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/model/MediaDeviceSession.kt
@@ -17,26 +17,15 @@
package com.android.systemui.volume.panel.component.mediaoutput.domain.model
import android.media.session.MediaSession
-import android.media.session.PlaybackState
/** Represents media playing on the connected device. */
-sealed interface MediaDeviceSession {
+data class MediaDeviceSession(
+ val appLabel: CharSequence,
+ val packageName: String,
+ val sessionToken: MediaSession.Token,
+ val canAdjustVolume: Boolean,
+)
- /** Media is playing. */
- data class Active(
- val appLabel: CharSequence,
- val packageName: String,
- val sessionToken: MediaSession.Token,
- val playbackState: PlaybackState?,
- ) : MediaDeviceSession
-
- /** Media is not playing. */
- data object Inactive : MediaDeviceSession
-
- /** Current media state is unknown yet. */
- data object Unknown : MediaDeviceSession
-}
-
-/** Returns true when the audio is playing for the [MediaDeviceSession]. */
-fun MediaDeviceSession.isPlaying(): Boolean =
- this is MediaDeviceSession.Active && playbackState?.isActive == true
+/** Returns true when [other] controls the same sessions as [this]. */
+fun MediaDeviceSession.isTheSameSession(other: MediaDeviceSession?): Boolean =
+ sessionToken == other?.sessionToken
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/model/MediaDeviceSessions.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/model/MediaDeviceSessions.kt
new file mode 100644
index 0000000..ddc0784
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/model/MediaDeviceSessions.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.volume.panel.component.mediaoutput.domain.model
+
+/** Models a pair of local and remote [MediaDeviceSession]s. */
+data class MediaDeviceSessions(
+ val local: MediaDeviceSession?,
+ val remote: MediaDeviceSession?,
+) {
+
+ companion object {
+ /** Returns [MediaDeviceSessions.local]. */
+ val Local: (MediaDeviceSessions) -> MediaDeviceSession? = { it.local }
+ /** Returns [MediaDeviceSessions.remote]. */
+ val Remote: (MediaDeviceSessions) -> MediaDeviceSession? = { it.remote }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/ui/viewmodel/MediaOutputViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/ui/viewmodel/MediaOutputViewModel.kt
index d49cb1e..2530a3a 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/ui/viewmodel/MediaOutputViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/ui/viewmodel/MediaOutputViewModel.kt
@@ -17,24 +17,30 @@
package com.android.systemui.volume.panel.component.mediaoutput.ui.viewmodel
import android.content.Context
+import android.media.session.PlaybackState
import com.android.systemui.animation.Expandable
import com.android.systemui.common.shared.model.Color
import com.android.systemui.common.shared.model.Icon
import com.android.systemui.res.R
+import com.android.systemui.volume.panel.component.mediaoutput.domain.interactor.MediaDeviceSessionInteractor
import com.android.systemui.volume.panel.component.mediaoutput.domain.interactor.MediaOutputActionsInteractor
import com.android.systemui.volume.panel.component.mediaoutput.domain.interactor.MediaOutputInteractor
import com.android.systemui.volume.panel.component.mediaoutput.domain.model.MediaDeviceSession
-import com.android.systemui.volume.panel.component.mediaoutput.domain.model.isPlaying
import com.android.systemui.volume.panel.dagger.scope.VolumePanelScope
import com.android.systemui.volume.panel.ui.viewmodel.VolumePanelViewModel
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
/** Models the UI of the Media Output Volume Panel component. */
+@OptIn(ExperimentalCoroutinesApi::class)
@VolumePanelScope
class MediaOutputViewModel
@Inject
@@ -43,25 +49,36 @@
@VolumePanelScope private val coroutineScope: CoroutineScope,
private val volumePanelViewModel: VolumePanelViewModel,
private val actionsInteractor: MediaOutputActionsInteractor,
+ private val mediaDeviceSessionInteractor: MediaDeviceSessionInteractor,
interactor: MediaOutputInteractor,
) {
- private val mediaDeviceSession: StateFlow<MediaDeviceSession> =
- interactor.mediaDeviceSession.stateIn(
- coroutineScope,
- SharingStarted.Eagerly,
- MediaDeviceSession.Unknown,
- )
+ private val sessionWithPlayback: StateFlow<SessionWithPlayback?> =
+ interactor.defaultActiveMediaSession
+ .flatMapLatest { session ->
+ if (session == null) {
+ flowOf(null)
+ } else {
+ mediaDeviceSessionInteractor.playbackState(session).map { playback ->
+ playback?.let { SessionWithPlayback(session, it) }
+ }
+ }
+ }
+ .stateIn(
+ coroutineScope,
+ SharingStarted.Eagerly,
+ null,
+ )
val connectedDeviceViewModel: StateFlow<ConnectedDeviceViewModel?> =
- combine(mediaDeviceSession, interactor.currentConnectedDevice) {
+ combine(sessionWithPlayback, interactor.currentConnectedDevice) {
mediaDeviceSession,
currentConnectedDevice ->
ConnectedDeviceViewModel(
- if (mediaDeviceSession.isPlaying()) {
+ if (mediaDeviceSession?.playback?.isActive == true) {
context.getString(
R.string.media_output_label_title,
- (mediaDeviceSession as MediaDeviceSession.Active).appLabel
+ mediaDeviceSession.session.appLabel
)
} else {
context.getString(R.string.media_output_title_without_playing)
@@ -76,10 +93,10 @@
)
val deviceIconViewModel: StateFlow<DeviceIconViewModel?> =
- combine(mediaDeviceSession, interactor.currentConnectedDevice) {
+ combine(sessionWithPlayback, interactor.currentConnectedDevice) {
mediaDeviceSession,
currentConnectedDevice ->
- if (mediaDeviceSession.isPlaying()) {
+ if (mediaDeviceSession?.playback?.isActive == true) {
val icon =
currentConnectedDevice?.icon?.let { Icon.Loaded(it, null) }
?: Icon.Resource(
@@ -112,7 +129,14 @@
)
fun onBarClick(expandable: Expandable) {
- actionsInteractor.onBarClick(mediaDeviceSession.value, expandable)
+ sessionWithPlayback.value?.let {
+ actionsInteractor.onBarClick(it.session, it.playback.isActive, expandable)
+ }
volumePanelViewModel.dismissPanel()
}
+
+ private data class SessionWithPlayback(
+ val session: MediaDeviceSession,
+ val playback: PlaybackState,
+ )
}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/domain/interactor/CastVolumeInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/domain/interactor/CastVolumeInteractor.kt
deleted file mode 100644
index 6b62074..0000000
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/domain/interactor/CastVolumeInteractor.kt
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.volume.panel.component.volume.domain.interactor
-
-import com.android.settingslib.volume.domain.interactor.LocalMediaInteractor
-import com.android.settingslib.volume.domain.model.RoutingSession
-import com.android.systemui.volume.panel.dagger.scope.VolumePanelScope
-import javax.inject.Inject
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.flow.SharingStarted
-import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.stateIn
-
-/** Provides a remote media casting state. */
-@VolumePanelScope
-class CastVolumeInteractor
-@Inject
-constructor(
- @VolumePanelScope private val coroutineScope: CoroutineScope,
- private val localMediaInteractor: LocalMediaInteractor,
-) {
-
- /** Returns a list of [RoutingSession] to show in the UI. */
- val remoteRoutingSessions: StateFlow<List<RoutingSession>> =
- localMediaInteractor.remoteRoutingSessions
- .map { it.filter { routingSession -> routingSession.isVolumeSeekBarEnabled } }
- .stateIn(coroutineScope, SharingStarted.Eagerly, emptyList())
-
- /** Sets [routingSession] volume to [volume]. */
- suspend fun setVolume(routingSession: RoutingSession, volume: Int) {
- localMediaInteractor.adjustSessionVolume(routingSession.routingSessionInfo.id, volume)
- }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModel.kt
index 1b73208..3242c28 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModel.kt
@@ -80,7 +80,7 @@
) { model, isEnabled, ringerMode ->
model.toState(isEnabled, ringerMode)
}
- .stateIn(coroutineScope, SharingStarted.Eagerly, EmptyState)
+ .stateIn(coroutineScope, SharingStarted.Eagerly, SliderState.Empty)
override fun onValueChanged(state: SliderState, newValue: Float) {
val audioViewModel = state as? State
@@ -116,6 +116,7 @@
isEnabled = isEnabled,
a11yStep = volumeRange.step,
audioStreamModel = this,
+ isMutable = audioVolumeInteractor.isMutable(audioStream),
)
}
@@ -160,20 +161,10 @@
override val disabledMessage: String?,
override val isEnabled: Boolean,
override val a11yStep: Int,
+ override val isMutable: Boolean,
val audioStreamModel: AudioStreamModel,
) : SliderState
- private data object EmptyState : SliderState {
- override val value: Float = 0f
- override val valueRange: ClosedFloatingPointRange<Float> = 0f..1f
- override val icon: Icon? = null
- override val valueText: String = ""
- override val label: String = ""
- override val disabledMessage: String? = null
- override val a11yStep: Int = 0
- override val isEnabled: Boolean = true
- }
-
@AssistedFactory
interface Factory {
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/CastVolumeSliderViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/CastVolumeSliderViewModel.kt
index 86b2d73..73c8bbf 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/CastVolumeSliderViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/CastVolumeSliderViewModel.kt
@@ -17,11 +17,11 @@
package com.android.systemui.volume.panel.component.volume.slider.ui.viewmodel
import android.content.Context
-import com.android.settingslib.volume.domain.model.RoutingSession
+import android.media.session.MediaController.PlaybackInfo
import com.android.systemui.common.shared.model.Icon
import com.android.systemui.res.R
-import com.android.systemui.volume.panel.component.mediaoutput.domain.interactor.MediaOutputInteractor
-import com.android.systemui.volume.panel.component.volume.domain.interactor.CastVolumeInteractor
+import com.android.systemui.volume.panel.component.mediaoutput.domain.interactor.MediaDeviceSessionInteractor
+import com.android.systemui.volume.panel.component.mediaoutput.domain.model.MediaDeviceSession
import com.android.systemui.volume.panel.component.volume.domain.interactor.VolumeSliderInteractor
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
@@ -30,30 +30,29 @@
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
class CastVolumeSliderViewModel
@AssistedInject
constructor(
- @Assisted private val routingSession: RoutingSession,
+ @Assisted private val session: MediaDeviceSession,
@Assisted private val coroutineScope: CoroutineScope,
private val context: Context,
- mediaOutputInteractor: MediaOutputInteractor,
+ private val mediaDeviceSessionInteractor: MediaDeviceSessionInteractor,
private val volumeSliderInteractor: VolumeSliderInteractor,
- private val castVolumeInteractor: CastVolumeInteractor,
) : SliderViewModel {
- private val volumeRange = 0..routingSession.routingSessionInfo.volumeMax
-
override val slider: StateFlow<SliderState> =
- combine(mediaOutputInteractor.currentConnectedDevice) { _ -> getCurrentState() }
- .stateIn(coroutineScope, SharingStarted.Eagerly, getCurrentState())
+ mediaDeviceSessionInteractor
+ .playbackInfo(session)
+ .mapNotNull { it?.getCurrentState() }
+ .stateIn(coroutineScope, SharingStarted.Eagerly, SliderState.Empty)
override fun onValueChanged(state: SliderState, newValue: Float) {
coroutineScope.launch {
- castVolumeInteractor.setVolume(routingSession, newValue.roundToInt())
+ mediaDeviceSessionInteractor.setSessionVolume(session, newValue.roundToInt())
}
}
@@ -61,15 +60,16 @@
// do nothing because this action isn't supported for Cast sliders.
}
- private fun getCurrentState(): State =
- State(
- value = routingSession.routingSessionInfo.volume.toFloat(),
+ private fun PlaybackInfo.getCurrentState(): State {
+ val volumeRange = 0..maxVolume
+ return State(
+ value = currentVolume.toFloat(),
valueRange = volumeRange.first.toFloat()..volumeRange.last.toFloat(),
icon = Icon.Resource(R.drawable.ic_cast, null),
valueText =
SliderViewModel.formatValue(
volumeSliderInteractor.processVolumeToValue(
- volume = routingSession.routingSessionInfo.volume,
+ volume = currentVolume,
volumeRange = volumeRange,
)
),
@@ -77,6 +77,7 @@
isEnabled = true,
a11yStep = 1
)
+ }
private data class State(
override val value: Float,
@@ -89,13 +90,15 @@
) : SliderState {
override val disabledMessage: String?
get() = null
+ override val isMutable: Boolean
+ get() = false
}
@AssistedFactory
interface Factory {
fun create(
- routingSession: RoutingSession,
+ session: MediaDeviceSession,
coroutineScope: CoroutineScope,
): CastVolumeSliderViewModel
}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/SliderState.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/SliderState.kt
index b87d0a7..8eb0b89 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/SliderState.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/SliderState.kt
@@ -36,4 +36,17 @@
*/
val a11yStep: Int
val disabledMessage: String?
+ val isMutable: Boolean
+
+ data object Empty : SliderState {
+ override val value: Float = 0f
+ override val valueRange: ClosedFloatingPointRange<Float> = 0f..1f
+ override val icon: Icon? = null
+ override val valueText: String = ""
+ override val label: String = ""
+ override val disabledMessage: String? = null
+ override val a11yStep: Int = 0
+ override val isEnabled: Boolean = true
+ override val isMutable: Boolean = false
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/ui/viewmodel/AudioVolumeComponentViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/ui/viewmodel/AudioVolumeComponentViewModel.kt
index aaee24b..4e9a456 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/ui/viewmodel/AudioVolumeComponentViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/ui/viewmodel/AudioVolumeComponentViewModel.kt
@@ -18,9 +18,10 @@
import android.media.AudioManager
import com.android.settingslib.volume.shared.model.AudioStream
+import com.android.systemui.volume.panel.component.mediaoutput.domain.interactor.MediaDeviceSessionInteractor
import com.android.systemui.volume.panel.component.mediaoutput.domain.interactor.MediaOutputInteractor
-import com.android.systemui.volume.panel.component.mediaoutput.domain.model.isPlaying
-import com.android.systemui.volume.panel.component.volume.domain.interactor.CastVolumeInteractor
+import com.android.systemui.volume.panel.component.mediaoutput.domain.model.MediaDeviceSession
+import com.android.systemui.volume.panel.component.mediaoutput.domain.model.isTheSameSession
import com.android.systemui.volume.panel.component.volume.slider.ui.viewmodel.AudioStreamSliderViewModel
import com.android.systemui.volume.panel.component.volume.slider.ui.viewmodel.CastVolumeSliderViewModel
import com.android.systemui.volume.panel.component.volume.slider.ui.viewmodel.SliderViewModel
@@ -29,17 +30,15 @@
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.coroutineScope
-import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.combineTransform
+import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.merge
-import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.stateIn
-import kotlinx.coroutines.flow.transformLatest
import kotlinx.coroutines.launch
/**
@@ -52,50 +51,34 @@
@Inject
constructor(
@VolumePanelScope private val scope: CoroutineScope,
- castVolumeInteractor: CastVolumeInteractor,
mediaOutputInteractor: MediaOutputInteractor,
+ private val mediaDeviceSessionInteractor: MediaDeviceSessionInteractor,
private val streamSliderViewModelFactory: AudioStreamSliderViewModel.Factory,
private val castVolumeSliderViewModelFactory: CastVolumeSliderViewModel.Factory,
) {
- private val remoteSessionsViewModels: Flow<List<SliderViewModel>> =
- castVolumeInteractor.remoteRoutingSessions.transformLatest { routingSessions ->
- coroutineScope {
- emit(
- routingSessions.map { routingSession ->
- castVolumeSliderViewModelFactory.create(routingSession, this)
- }
- )
- }
- }
- private val streamViewModels: Flow<List<SliderViewModel>> =
- flowOf(
- listOf(
- AudioStream(AudioManager.STREAM_MUSIC),
- AudioStream(AudioManager.STREAM_VOICE_CALL),
- AudioStream(AudioManager.STREAM_RING),
- AudioStream(AudioManager.STREAM_NOTIFICATION),
- AudioStream(AudioManager.STREAM_ALARM),
- )
- )
- .transformLatest { streams ->
- coroutineScope {
- emit(
- streams.map { stream ->
- streamSliderViewModelFactory.create(
- AudioStreamSliderViewModel.FactoryAudioStreamWrapper(stream),
- this,
- )
- }
- )
- }
- }
-
val sliderViewModels: StateFlow<List<SliderViewModel>> =
- combine(remoteSessionsViewModels, streamViewModels) {
- remoteSessionsViewModels,
- streamViewModels ->
- remoteSessionsViewModels + streamViewModels
+ combineTransform(
+ mediaOutputInteractor.activeMediaDeviceSessions,
+ mediaOutputInteractor.defaultActiveMediaSession,
+ ) { activeSessions, defaultSession ->
+ coroutineScope {
+ val viewModels = buildList {
+ if (defaultSession?.isTheSameSession(activeSessions.remote) == true) {
+ addRemoteViewModelIfNeeded(this, activeSessions.remote)
+ addStreamViewModel(this, AudioManager.STREAM_MUSIC)
+ } else {
+ addStreamViewModel(this, AudioManager.STREAM_MUSIC)
+ addRemoteViewModelIfNeeded(this, activeSessions.remote)
+ }
+
+ addStreamViewModel(this, AudioManager.STREAM_VOICE_CALL)
+ addStreamViewModel(this, AudioManager.STREAM_RING)
+ addStreamViewModel(this, AudioManager.STREAM_NOTIFICATION)
+ addStreamViewModel(this, AudioManager.STREAM_ALARM)
+ }
+ emit(viewModels)
+ }
}
.stateIn(scope, SharingStarted.Eagerly, emptyList())
@@ -103,12 +86,41 @@
val isExpanded: StateFlow<Boolean> =
merge(
- mutableIsExpanded.onStart { emit(false) },
- mediaOutputInteractor.mediaDeviceSession.map { !it.isPlaying() },
+ mutableIsExpanded,
+ mediaOutputInteractor.defaultActiveMediaSession.flatMapLatest {
+ if (it == null) flowOf(true)
+ else mediaDeviceSessionInteractor.playbackState(it).map { it?.isActive != true }
+ },
)
.stateIn(scope, SharingStarted.Eagerly, false)
fun onExpandedChanged(isExpanded: Boolean) {
scope.launch { mutableIsExpanded.emit(isExpanded) }
}
+
+ private fun CoroutineScope.addRemoteViewModelIfNeeded(
+ list: MutableList<SliderViewModel>,
+ remoteMediaDeviceSession: MediaDeviceSession?
+ ) {
+ if (remoteMediaDeviceSession?.canAdjustVolume == true) {
+ val viewModel =
+ castVolumeSliderViewModelFactory.create(
+ remoteMediaDeviceSession,
+ this,
+ )
+ list.add(viewModel)
+ }
+ }
+
+ private fun CoroutineScope.addStreamViewModel(
+ list: MutableList<SliderViewModel>,
+ stream: Int,
+ ) {
+ val viewModel =
+ streamSliderViewModelFactory.create(
+ AudioStreamSliderViewModel.FactoryAudioStreamWrapper(AudioStream(stream)),
+ this,
+ )
+ list.add(viewModel)
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/ui/activity/VolumePanelActivity.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/ui/activity/VolumePanelActivity.kt
index d430e65..c728fef 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/ui/activity/VolumePanelActivity.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/ui/activity/VolumePanelActivity.kt
@@ -42,7 +42,6 @@
override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge()
super.onCreate(savedInstanceState)
-
volumePanelFlag.assertNewVolumePanel()
setContent { VolumePanelRoot(viewModel = viewModel, onDismiss = ::finish) }
diff --git a/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java b/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java
index 7931fab..e48b639 100644
--- a/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java
+++ b/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java
@@ -363,8 +363,8 @@
}, mSysUiMainExecutor);
mCommandQueue.addCallback(new CommandQueue.Callbacks() {
@Override
- public void enterDesktop(int displayId) {
- desktopMode.enterDesktop(displayId);
+ public void moveFocusedTaskToDesktop(int displayId) {
+ desktopMode.moveFocusedTaskToDesktop(displayId);
}
@Override
public void moveFocusedTaskToFullscreen(int displayId) {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/animation/DialogTransitionAnimatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/animation/DialogTransitionAnimatorTest.kt
index b73e4e6..9182e4101 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/animation/DialogTransitionAnimatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/animation/DialogTransitionAnimatorTest.kt
@@ -36,6 +36,7 @@
import org.mockito.Mockito.any
import org.mockito.Mockito.verify
import org.mockito.junit.MockitoJUnit
+import org.mockito.junit.MockitoRule
@SmallTest
@RunWith(AndroidTestingRunner::class)
@@ -44,8 +45,8 @@
private lateinit var mDialogTransitionAnimator: DialogTransitionAnimator
private val attachedViews = mutableSetOf<View>()
- val interactionJankMonitor = Kosmos().interactionJankMonitor
- @get:Rule val rule = MockitoJUnit.rule()
+ private val interactionJankMonitor = Kosmos().interactionJankMonitor
+ @get:Rule val rule: MockitoRule = MockitoJUnit.rule()
@Before
fun setUp() {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/complication/ComplicationViewModelTransformerTest.java b/packages/SystemUI/tests/src/com/android/systemui/complication/ComplicationViewModelTransformerTest.java
index 206babf..09675e2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/complication/ComplicationViewModelTransformerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/complication/ComplicationViewModelTransformerTest.java
@@ -23,6 +23,7 @@
import android.testing.AndroidTestingRunner;
+import androidx.lifecycle.ViewModel;
import androidx.test.filters.SmallTest;
import com.android.systemui.SysuiTestCase;
@@ -56,7 +57,8 @@
MockitoAnnotations.initMocks(this);
when(mFactory.create(Mockito.any(), Mockito.any())).thenReturn(mComponent);
when(mComponent.getViewModelProvider()).thenReturn(mViewModelProvider);
- when(mViewModelProvider.get(Mockito.any(), Mockito.any())).thenReturn(mViewModel);
+ when(mViewModelProvider.get(Mockito.any(), Mockito.<Class<ViewModel>>any()))
+ .thenReturn(mViewModel);
}
/**
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinderTest.kt
index 5dd37ae..66aa572 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinderTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinderTest.kt
@@ -131,7 +131,6 @@
whenever(clock.smallClock).thenReturn(smallClock)
whenever(largeClock.layout).thenReturn(largeClockFaceLayout)
whenever(smallClock.layout).thenReturn(smallClockFaceLayout)
- whenever(clockViewModel.clock).thenReturn(clock)
currentClock.value = clock
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilterTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataFilterImplTest.kt
similarity index 98%
rename from packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilterTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataFilterImplTest.kt
index 59eb7bb..e56a253 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilterTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataFilterImplTest.kt
@@ -66,7 +66,7 @@
@SmallTest
@RunWith(AndroidTestingRunner::class)
@TestableLooper.RunWithLooper
-class MediaDataFilterTest : SysuiTestCase() {
+class LegacyMediaDataFilterImplTest : SysuiTestCase() {
@Mock private lateinit var listener: MediaDataManager.Listener
@Mock private lateinit var userTracker: UserTracker
@@ -80,7 +80,7 @@
@Mock private lateinit var mediaFlags: MediaFlags
@Mock private lateinit var cardAction: SmartspaceAction
- private lateinit var mediaDataFilter: MediaDataFilter
+ private lateinit var mediaDataFilter: LegacyMediaDataFilterImpl
private lateinit var dataMain: MediaData
private lateinit var dataGuest: MediaData
private lateinit var dataPrivateProfile: MediaData
@@ -92,7 +92,7 @@
MediaPlayerData.clear()
whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(false)
mediaDataFilter =
- MediaDataFilter(
+ LegacyMediaDataFilterImpl(
context,
userTracker,
broadcastSender,
@@ -370,7 +370,7 @@
mediaDataFilter.onMediaDataLoaded(KEY, null, dataMain)
mediaDataFilter.onSwipeToDismiss()
- verify(mediaDataManager).setTimedOut(eq(KEY), eq(true), eq(true))
+ verify(mediaDataManager).setInactive(eq(KEY), eq(true), eq(true))
}
@Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImplTest.kt
similarity index 98%
rename from packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataManagerTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImplTest.kt
index 61bfdb5..5a2d22d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataManagerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImplTest.kt
@@ -114,7 +114,7 @@
@SmallTest
@RunWithLooper(setAsMainLooper = true)
@RunWith(AndroidTestingRunner::class)
-class MediaDataManagerTest : SysuiTestCase() {
+class LegacyMediaDataManagerImplTest : SysuiTestCase() {
@JvmField @Rule val mockito = MockitoJUnit.rule()
@Mock lateinit var mediaControllerFactory: MediaControllerFactory
@@ -133,7 +133,7 @@
@Mock lateinit var mediaSessionBasedFilter: MediaSessionBasedFilter
@Mock lateinit var mediaDeviceManager: MediaDeviceManager
@Mock lateinit var mediaDataCombineLatest: MediaDataCombineLatest
- @Mock lateinit var mediaDataFilter: MediaDataFilter
+ @Mock lateinit var mediaDataFilter: LegacyMediaDataFilterImpl
@Mock lateinit var listener: MediaDataManager.Listener
@Mock lateinit var pendingIntent: PendingIntent
@Mock lateinit var activityStarter: ActivityStarter
@@ -146,7 +146,7 @@
@Mock private lateinit var mediaSmartspaceBaseAction: SmartspaceAction
@Mock private lateinit var mediaFlags: MediaFlags
@Mock private lateinit var logger: MediaUiEventLogger
- lateinit var mediaDataManager: MediaDataManager
+ lateinit var mediaDataManager: LegacyMediaDataManagerImpl
lateinit var mediaNotification: StatusBarNotification
lateinit var remoteCastNotification: StatusBarNotification
@Captor lateinit var mediaDataCaptor: ArgumentCaptor<MediaData>
@@ -189,7 +189,7 @@
1
)
mediaDataManager =
- MediaDataManager(
+ LegacyMediaDataManagerImpl(
context = context,
backgroundExecutor = backgroundExecutor,
uiExecutor = uiExecutor,
@@ -304,13 +304,13 @@
val data = mediaDataCaptor.value
assertThat(data.active).isTrue()
- mediaDataManager.setTimedOut(KEY, timedOut = true)
+ mediaDataManager.setInactive(KEY, timedOut = true)
assertThat(data.active).isFalse()
verify(logger).logMediaTimeout(anyInt(), eq(PACKAGE_NAME), eq(data.instanceId))
}
@Test
- fun testSetTimedOut_resume_dismissesMedia() {
+ fun testsetInactive_resume_dismissesMedia() {
// WHEN resume controls are present, and time out
val desc =
MediaDescription.Builder().run {
@@ -339,7 +339,7 @@
eq(false)
)
- mediaDataManager.setTimedOut(PACKAGE_NAME, timedOut = true)
+ mediaDataManager.setInactive(PACKAGE_NAME, timedOut = true)
verify(logger)
.logMediaTimeout(anyInt(), eq(PACKAGE_NAME), eq(mediaDataCaptor.value.instanceId))
@@ -1485,7 +1485,7 @@
// WHEN the notification times out
clock.advanceTime(100)
val currentTime = clock.elapsedRealtime()
- mediaDataManager.setTimedOut(KEY, true, true)
+ mediaDataManager.setInactive(KEY, true, true)
// THEN the last active time is changed
verify(listener)
@@ -1602,7 +1602,7 @@
eq(false)
)
assertThat(mediaDataCaptor.value.actionsToShowInCompact.size)
- .isEqualTo(MediaDataManager.MAX_COMPACT_ACTIONS)
+ .isEqualTo(LegacyMediaDataManagerImpl.MAX_COMPACT_ACTIONS)
}
@Test
@@ -1615,7 +1615,7 @@
modifyNotification(context).also {
it.setSmallIcon(android.R.drawable.ic_media_pause)
it.setStyle(MediaStyle().apply { setMediaSession(session.sessionToken) })
- for (i in 0..MediaDataManager.MAX_NOTIFICATION_ACTIONS) {
+ for (i in 0..LegacyMediaDataManagerImpl.MAX_NOTIFICATION_ACTIONS) {
it.addAction(action)
}
}
@@ -1638,7 +1638,7 @@
eq(false)
)
assertThat(mediaDataCaptor.value.actions.size)
- .isEqualTo(MediaDataManager.MAX_NOTIFICATION_ACTIONS)
+ .isEqualTo(LegacyMediaDataManagerImpl.MAX_NOTIFICATION_ACTIONS)
}
@Test
@@ -2040,7 +2040,7 @@
// When a media control based on notification is added, times out, and then removed
addNotificationAndLoad()
- mediaDataManager.setTimedOut(KEY, timedOut = true)
+ mediaDataManager.setInactive(KEY, timedOut = true)
assertThat(mediaDataCaptor.value.active).isFalse()
mediaDataManager.onNotificationRemoved(KEY)
@@ -2070,7 +2070,7 @@
// When a media control based on notification is added and times out
addNotificationAndLoad()
- mediaDataManager.setTimedOut(KEY, timedOut = true)
+ mediaDataManager.setInactive(KEY, timedOut = true)
assertThat(mediaDataCaptor.value.active).isFalse()
// and then the session is destroyed
@@ -2142,7 +2142,7 @@
addNotificationAndLoad()
val data = mediaDataCaptor.value
assertThat(data.active).isTrue()
- mediaDataManager.setTimedOut(KEY, timedOut = true)
+ mediaDataManager.setInactive(KEY, timedOut = true)
mediaDataManager.onNotificationRemoved(KEY)
// It remains as a regular player
@@ -2162,7 +2162,7 @@
addNotificationAndLoad()
val data = mediaDataCaptor.value
assertThat(data.active).isTrue()
- mediaDataManager.setTimedOut(KEY, timedOut = true)
+ mediaDataManager.setInactive(KEY, timedOut = true)
sessionCallbackCaptor.value.invoke(KEY)
// It is converted to a resume player
@@ -2249,7 +2249,7 @@
addNotificationAndLoad()
val data = mediaDataCaptor.value
assertThat(data.active).isTrue()
- mediaDataManager.setTimedOut(KEY, timedOut = true)
+ mediaDataManager.setInactive(KEY, timedOut = true)
sessionCallbackCaptor.value.invoke(KEY)
// It is fully removed.
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilterImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilterImplTest.kt
new file mode 100644
index 0000000..564bdc3
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilterImplTest.kt
@@ -0,0 +1,931 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS 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.media.controls.domain.pipeline
+
+import android.app.smartspace.SmartspaceAction
+import android.os.Bundle
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import androidx.test.filters.SmallTest
+import com.android.internal.logging.InstanceId
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.broadcast.BroadcastSender
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.media.controls.MediaTestUtils
+import com.android.systemui.media.controls.data.repository.MediaFilterRepository
+import com.android.systemui.media.controls.shared.model.EXTRA_KEY_TRIGGER_RESUME
+import com.android.systemui.media.controls.shared.model.MediaData
+import com.android.systemui.media.controls.shared.model.SmartspaceMediaData
+import com.android.systemui.media.controls.ui.controller.MediaPlayerData
+import com.android.systemui.media.controls.util.MediaFlags
+import com.android.systemui.media.controls.util.MediaUiEventLogger
+import com.android.systemui.settings.UserTracker
+import com.android.systemui.statusbar.NotificationLockscreenUserManager
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.eq
+import com.android.systemui.util.mockito.whenever
+import com.android.systemui.util.time.FakeSystemClock
+import com.google.common.truth.Truth.assertThat
+import java.util.concurrent.Executor
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyBoolean
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.ArgumentMatchers.anyLong
+import org.mockito.Mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.reset
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+private const val KEY = "TEST_KEY"
+private const val KEY_ALT = "TEST_KEY_2"
+private const val USER_MAIN = 0
+private const val USER_GUEST = 10
+private const val PRIVATE_PROFILE = 12
+private const val PACKAGE = "PKG"
+private val INSTANCE_ID = InstanceId.fakeInstanceId(123)!!
+private const val APP_UID = 99
+private const val SMARTSPACE_KEY = "SMARTSPACE_KEY"
+private const val SMARTSPACE_PACKAGE = "SMARTSPACE_PKG"
+private val SMARTSPACE_INSTANCE_ID = InstanceId.fakeInstanceId(456)!!
+
+@ExperimentalCoroutinesApi
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@TestableLooper.RunWithLooper
+class MediaDataFilterImplTest : SysuiTestCase() {
+
+ @Mock private lateinit var listener: MediaDataManager.Listener
+ @Mock private lateinit var userTracker: UserTracker
+ @Mock private lateinit var broadcastSender: BroadcastSender
+ @Mock private lateinit var mediaDataManager: MediaDataManager
+ @Mock private lateinit var lockscreenUserManager: NotificationLockscreenUserManager
+ @Mock private lateinit var executor: Executor
+ @Mock private lateinit var smartspaceData: SmartspaceMediaData
+ @Mock private lateinit var smartspaceMediaRecommendationItem: SmartspaceAction
+ @Mock private lateinit var logger: MediaUiEventLogger
+ @Mock private lateinit var mediaFlags: MediaFlags
+ @Mock private lateinit var cardAction: SmartspaceAction
+
+ private lateinit var mediaDataFilter: MediaDataFilterImpl
+ private lateinit var mediaFilterRepository: MediaFilterRepository
+ private lateinit var testScope: TestScope
+ private lateinit var dataMain: MediaData
+ private lateinit var dataGuest: MediaData
+ private lateinit var dataPrivateProfile: MediaData
+ private val clock = FakeSystemClock()
+
+ @Before
+ fun setup() {
+ MockitoAnnotations.initMocks(this)
+ MediaPlayerData.clear()
+ whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(false)
+ testScope = TestScope()
+ mediaFilterRepository = MediaFilterRepository()
+ mediaDataFilter =
+ MediaDataFilterImpl(
+ context,
+ userTracker,
+ broadcastSender,
+ lockscreenUserManager,
+ executor,
+ clock,
+ logger,
+ mediaFlags,
+ mediaFilterRepository,
+ )
+ mediaDataFilter.mediaDataManager = mediaDataManager
+ mediaDataFilter.addListener(listener)
+
+ // Start all tests as main user
+ setUser(USER_MAIN)
+
+ // Set up test media data
+ dataMain =
+ MediaTestUtils.emptyMediaData.copy(
+ userId = USER_MAIN,
+ packageName = PACKAGE,
+ instanceId = INSTANCE_ID,
+ appUid = APP_UID
+ )
+ dataGuest = dataMain.copy(userId = USER_GUEST)
+ dataPrivateProfile = dataMain.copy(userId = PRIVATE_PROFILE)
+
+ whenever(smartspaceData.targetId).thenReturn(SMARTSPACE_KEY)
+ whenever(smartspaceData.isActive).thenReturn(true)
+ whenever(smartspaceData.isValid()).thenReturn(true)
+ whenever(smartspaceData.packageName).thenReturn(SMARTSPACE_PACKAGE)
+ whenever(smartspaceData.recommendations)
+ .thenReturn(listOf(smartspaceMediaRecommendationItem))
+ whenever(smartspaceData.headphoneConnectionTimeMillis)
+ .thenReturn(clock.currentTimeMillis() - 100)
+ whenever(smartspaceData.instanceId).thenReturn(SMARTSPACE_INSTANCE_ID)
+ whenever(smartspaceData.cardAction).thenReturn(cardAction)
+ }
+
+ private fun setUser(id: Int) {
+ whenever(lockscreenUserManager.isCurrentProfile(anyInt())).thenReturn(false)
+ whenever(lockscreenUserManager.isProfileAvailable(anyInt())).thenReturn(false)
+ whenever(lockscreenUserManager.isCurrentProfile(eq(id))).thenReturn(true)
+ whenever(lockscreenUserManager.isProfileAvailable(eq(id))).thenReturn(true)
+ whenever(lockscreenUserManager.isProfileAvailable(eq(PRIVATE_PROFILE))).thenReturn(true)
+ mediaDataFilter.handleUserSwitched()
+ }
+
+ private fun setPrivateProfileUnavailable() {
+ whenever(lockscreenUserManager.isCurrentProfile(anyInt())).thenReturn(false)
+ whenever(lockscreenUserManager.isCurrentProfile(eq(USER_MAIN))).thenReturn(true)
+ whenever(lockscreenUserManager.isCurrentProfile(eq(PRIVATE_PROFILE))).thenReturn(true)
+ whenever(lockscreenUserManager.isProfileAvailable(eq(PRIVATE_PROFILE))).thenReturn(false)
+ mediaDataFilter.handleProfileChanged()
+ }
+
+ @Test
+ fun testOnDataLoadedForCurrentUser_callsListener() {
+ // GIVEN a media for main user
+ mediaDataFilter.onMediaDataLoaded(KEY, null, dataMain)
+
+ // THEN we should tell the listener
+ verify(listener)
+ .onMediaDataLoaded(eq(KEY), eq(null), eq(dataMain), eq(true), eq(0), eq(false))
+ }
+
+ @Test
+ fun testOnDataLoadedForGuest_doesNotCallListener() {
+ // GIVEN a media for guest user
+ mediaDataFilter.onMediaDataLoaded(KEY, null, dataGuest)
+
+ // THEN we should NOT tell the listener
+ verify(listener, never())
+ .onMediaDataLoaded(any(), any(), any(), anyBoolean(), anyInt(), anyBoolean())
+ }
+
+ @Test
+ fun testOnRemovedForCurrent_callsListener() {
+ // GIVEN a media was removed for main user
+ mediaDataFilter.onMediaDataLoaded(KEY, null, dataMain)
+ mediaDataFilter.onMediaDataRemoved(KEY)
+
+ // THEN we should tell the listener
+ verify(listener).onMediaDataRemoved(eq(KEY))
+ }
+
+ @Test
+ fun testOnRemovedForGuest_doesNotCallListener() {
+ // GIVEN a media was removed for guest user
+ mediaDataFilter.onMediaDataLoaded(KEY, null, dataGuest)
+ mediaDataFilter.onMediaDataRemoved(KEY)
+
+ // THEN we should NOT tell the listener
+ verify(listener, never()).onMediaDataRemoved(eq(KEY))
+ }
+
+ @Test
+ fun testOnUserSwitched_removesOldUserControls() {
+ // GIVEN that we have a media loaded for main user
+ mediaDataFilter.onMediaDataLoaded(KEY, null, dataMain)
+
+ // and we switch to guest user
+ setUser(USER_GUEST)
+
+ // THEN we should remove the main user's media
+ verify(listener).onMediaDataRemoved(eq(KEY))
+ }
+
+ @Test
+ fun testOnUserSwitched_addsNewUserControls() {
+ // GIVEN that we had some media for both users
+ mediaDataFilter.onMediaDataLoaded(KEY, null, dataMain)
+ mediaDataFilter.onMediaDataLoaded(KEY_ALT, null, dataGuest)
+ reset(listener)
+
+ // and we switch to guest user
+ setUser(USER_GUEST)
+
+ // THEN we should add back the guest user media
+ verify(listener)
+ .onMediaDataLoaded(eq(KEY_ALT), eq(null), eq(dataGuest), eq(true), eq(0), eq(false))
+
+ // but not the main user's
+ verify(listener, never())
+ .onMediaDataLoaded(eq(KEY), any(), eq(dataMain), anyBoolean(), anyInt(), anyBoolean())
+ }
+
+ @Test
+ fun testOnProfileChanged_profileUnavailable_loadControls() {
+ // GIVEN that we had some media for both profiles
+ mediaDataFilter.onMediaDataLoaded(KEY, null, dataMain)
+ mediaDataFilter.onMediaDataLoaded(KEY_ALT, null, dataPrivateProfile)
+ reset(listener)
+
+ // and we change profile status
+ setPrivateProfileUnavailable()
+
+ // THEN we should add the private profile media
+ verify(listener).onMediaDataRemoved(eq(KEY_ALT))
+ }
+
+ @Test
+ fun hasAnyMedia_mediaSet_returnsTrue() =
+ testScope.runTest {
+ val selectedUserEntries by collectLastValue(mediaFilterRepository.selectedUserEntries)
+ mediaDataFilter.onMediaDataLoaded(KEY, oldKey = null, data = dataMain)
+
+ assertThat(hasAnyMedia(selectedUserEntries)).isTrue()
+ }
+
+ @Test
+ fun hasAnyMedia_recommendationSet_returnsFalse() =
+ testScope.runTest {
+ val selectedUserEntries by collectLastValue(mediaFilterRepository.selectedUserEntries)
+ mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
+
+ assertThat(hasAnyMedia(selectedUserEntries)).isFalse()
+ }
+
+ @Test
+ fun hasAnyMediaOrRecommendation_mediaSet_returnsTrue() =
+ testScope.runTest {
+ val selectedUserEntries by collectLastValue(mediaFilterRepository.selectedUserEntries)
+ val smartspaceMediaData by collectLastValue(mediaFilterRepository.smartspaceMediaData)
+ mediaDataFilter.onMediaDataLoaded(KEY, oldKey = null, data = dataMain)
+
+ assertThat(hasAnyMediaOrRecommendation(selectedUserEntries, smartspaceMediaData))
+ .isTrue()
+ }
+
+ @Test
+ fun hasAnyMediaOrRecommendation_recommendationSet_returnsTrue() =
+ testScope.runTest {
+ val selectedUserEntries by collectLastValue(mediaFilterRepository.selectedUserEntries)
+ val smartspaceMediaData by collectLastValue(mediaFilterRepository.smartspaceMediaData)
+ mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
+
+ assertThat(hasAnyMediaOrRecommendation(selectedUserEntries, smartspaceMediaData))
+ .isTrue()
+ }
+
+ @Test
+ fun hasActiveMedia_inactiveMediaSet_returnsFalse() =
+ testScope.runTest {
+ val selectedUserEntries by collectLastValue(mediaFilterRepository.selectedUserEntries)
+
+ val data = dataMain.copy(active = false)
+ mediaDataFilter.onMediaDataLoaded(KEY, oldKey = null, data = data)
+
+ assertThat(hasActiveMedia(selectedUserEntries)).isFalse()
+ }
+
+ @Test
+ fun hasActiveMedia_activeMediaSet_returnsTrue() =
+ testScope.runTest {
+ val selectedUserEntries by collectLastValue(mediaFilterRepository.selectedUserEntries)
+ val data = dataMain.copy(active = true)
+ mediaDataFilter.onMediaDataLoaded(KEY, oldKey = null, data = data)
+
+ assertThat(hasActiveMedia(selectedUserEntries)).isTrue()
+ }
+
+ @Test
+ fun hasActiveMediaOrRecommendation_inactiveMediaSet_returnsFalse() =
+ testScope.runTest {
+ val selectedUserEntries by collectLastValue(mediaFilterRepository.selectedUserEntries)
+ val smartspaceMediaData by collectLastValue(mediaFilterRepository.smartspaceMediaData)
+ val reactivatedKey by collectLastValue(mediaFilterRepository.reactivatedKey)
+ val data = dataMain.copy(active = false)
+ mediaDataFilter.onMediaDataLoaded(KEY, oldKey = null, data = data)
+
+ assertThat(
+ hasActiveMediaOrRecommendation(
+ selectedUserEntries,
+ smartspaceMediaData,
+ reactivatedKey
+ )
+ )
+ .isFalse()
+ }
+
+ @Test
+ fun hasActiveMediaOrRecommendation_activeMediaSet_returnsTrue() =
+ testScope.runTest {
+ val selectedUserEntries by collectLastValue(mediaFilterRepository.selectedUserEntries)
+ val smartspaceMediaData by collectLastValue(mediaFilterRepository.smartspaceMediaData)
+ val reactivatedKey by collectLastValue(mediaFilterRepository.reactivatedKey)
+ val data = dataMain.copy(active = true)
+ mediaDataFilter.onMediaDataLoaded(KEY, oldKey = null, data = data)
+
+ assertThat(
+ hasActiveMediaOrRecommendation(
+ selectedUserEntries,
+ smartspaceMediaData,
+ reactivatedKey
+ )
+ )
+ .isTrue()
+ }
+
+ @Test
+ fun hasActiveMediaOrRecommendation_inactiveRecommendationSet_returnsFalse() =
+ testScope.runTest {
+ val selectedUserEntries by collectLastValue(mediaFilterRepository.selectedUserEntries)
+ val smartspaceMediaData by collectLastValue(mediaFilterRepository.smartspaceMediaData)
+ val reactivatedKey by collectLastValue(mediaFilterRepository.reactivatedKey)
+ whenever(smartspaceData.isActive).thenReturn(false)
+ mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
+
+ assertThat(
+ hasActiveMediaOrRecommendation(
+ selectedUserEntries,
+ smartspaceMediaData,
+ reactivatedKey
+ )
+ )
+ .isFalse()
+ }
+
+ @Test
+ fun hasActiveMediaOrRecommendation_invalidRecommendationSet_returnsFalse() =
+ testScope.runTest {
+ val selectedUserEntries by collectLastValue(mediaFilterRepository.selectedUserEntries)
+ val smartspaceMediaData by collectLastValue(mediaFilterRepository.smartspaceMediaData)
+ val reactivatedKey by collectLastValue(mediaFilterRepository.reactivatedKey)
+ whenever(smartspaceData.isValid()).thenReturn(false)
+ mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
+
+ assertThat(
+ hasActiveMediaOrRecommendation(
+ selectedUserEntries,
+ smartspaceMediaData,
+ reactivatedKey
+ )
+ )
+ .isFalse()
+ }
+
+ @Test
+ fun hasActiveMediaOrRecommendation_activeAndValidRecommendationSet_returnsTrue() =
+ testScope.runTest {
+ val selectedUserEntries by collectLastValue(mediaFilterRepository.selectedUserEntries)
+ val smartspaceMediaData by collectLastValue(mediaFilterRepository.smartspaceMediaData)
+ val reactivatedKey by collectLastValue(mediaFilterRepository.reactivatedKey)
+ whenever(smartspaceData.isActive).thenReturn(true)
+ whenever(smartspaceData.isValid()).thenReturn(true)
+ mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
+
+ assertThat(
+ hasActiveMediaOrRecommendation(
+ selectedUserEntries,
+ smartspaceMediaData,
+ reactivatedKey
+ )
+ )
+ .isTrue()
+ }
+
+ @Test
+ fun testHasAnyMediaOrRecommendation_onlyCurrentUser() =
+ testScope.runTest {
+ val selectedUserEntries by collectLastValue(mediaFilterRepository.selectedUserEntries)
+ val smartspaceMediaData by collectLastValue(mediaFilterRepository.smartspaceMediaData)
+ assertThat(hasAnyMediaOrRecommendation(selectedUserEntries, smartspaceMediaData))
+ .isFalse()
+
+ mediaDataFilter.onMediaDataLoaded(KEY, oldKey = null, data = dataGuest)
+ assertThat(hasAnyMediaOrRecommendation(selectedUserEntries, smartspaceMediaData))
+ .isFalse()
+ assertThat(hasAnyMedia(selectedUserEntries)).isFalse()
+ }
+
+ @Test
+ fun testHasActiveMediaOrRecommendation_onlyCurrentUser() =
+ testScope.runTest {
+ val selectedUserEntries by collectLastValue(mediaFilterRepository.selectedUserEntries)
+ val smartspaceMediaData by collectLastValue(mediaFilterRepository.smartspaceMediaData)
+ val reactivatedKey by collectLastValue(mediaFilterRepository.reactivatedKey)
+ assertThat(
+ hasActiveMediaOrRecommendation(
+ selectedUserEntries,
+ smartspaceMediaData,
+ reactivatedKey
+ )
+ )
+ .isFalse()
+ val data = dataGuest.copy(active = true)
+
+ mediaDataFilter.onMediaDataLoaded(KEY, oldKey = null, data = data)
+ assertThat(
+ hasActiveMediaOrRecommendation(
+ selectedUserEntries,
+ smartspaceMediaData,
+ reactivatedKey
+ )
+ )
+ .isFalse()
+ assertThat(hasAnyMedia(selectedUserEntries)).isFalse()
+ }
+
+ @Test
+ fun testOnNotificationRemoved_doesNotHaveMedia() =
+ testScope.runTest {
+ val selectedUserEntries by collectLastValue(mediaFilterRepository.selectedUserEntries)
+ val smartspaceMediaData by collectLastValue(mediaFilterRepository.smartspaceMediaData)
+
+ mediaDataFilter.onMediaDataLoaded(KEY, oldKey = null, data = dataMain)
+ mediaDataFilter.onMediaDataRemoved(KEY)
+ assertThat(hasAnyMediaOrRecommendation(selectedUserEntries, smartspaceMediaData))
+ .isFalse()
+ assertThat(hasAnyMedia(selectedUserEntries)).isFalse()
+ }
+
+ @Test
+ fun testOnSwipeToDismiss_setsTimedOut() {
+ mediaDataFilter.onMediaDataLoaded(KEY, null, dataMain)
+ mediaDataFilter.onSwipeToDismiss()
+
+ verify(mediaDataManager).setInactive(eq(KEY), eq(true), eq(true))
+ }
+
+ @Test
+ fun testOnSmartspaceMediaDataLoaded_noMedia_activeValidRec_prioritizesSmartspace() =
+ testScope.runTest {
+ val selectedUserEntries by collectLastValue(mediaFilterRepository.selectedUserEntries)
+ val smartspaceMediaData by collectLastValue(mediaFilterRepository.smartspaceMediaData)
+ val reactivatedKey by collectLastValue(mediaFilterRepository.reactivatedKey)
+
+ mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
+
+ verify(listener)
+ .onSmartspaceMediaDataLoaded(eq(SMARTSPACE_KEY), eq(smartspaceData), eq(true))
+ assertThat(
+ hasActiveMediaOrRecommendation(
+ selectedUserEntries,
+ smartspaceMediaData,
+ reactivatedKey
+ )
+ )
+ .isTrue()
+ assertThat(hasActiveMedia(selectedUserEntries)).isFalse()
+ verify(logger).logRecommendationAdded(SMARTSPACE_PACKAGE, SMARTSPACE_INSTANCE_ID)
+ verify(logger, never()).logRecommendationActivated(any(), any(), any())
+ }
+
+ @Test
+ fun testOnSmartspaceMediaDataLoaded_noMedia_inactiveRec_showsNothing() =
+ testScope.runTest {
+ val selectedUserEntries by collectLastValue(mediaFilterRepository.selectedUserEntries)
+ val smartspaceMediaData by collectLastValue(mediaFilterRepository.smartspaceMediaData)
+ val reactivatedKey by collectLastValue(mediaFilterRepository.reactivatedKey)
+
+ whenever(smartspaceData.isActive).thenReturn(false)
+
+ mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
+
+ verify(listener, never())
+ .onMediaDataLoaded(any(), any(), any(), anyBoolean(), anyInt(), anyBoolean())
+ verify(listener, never()).onSmartspaceMediaDataLoaded(any(), any(), anyBoolean())
+ assertThat(
+ hasActiveMediaOrRecommendation(
+ selectedUserEntries,
+ smartspaceMediaData,
+ reactivatedKey
+ )
+ )
+ .isFalse()
+ assertThat(hasActiveMedia(selectedUserEntries)).isFalse()
+ verify(logger, never()).logRecommendationAdded(any(), any())
+ verify(logger, never()).logRecommendationActivated(any(), any(), any())
+ }
+
+ @Test
+ fun testOnSmartspaceMediaDataLoaded_noRecentMedia_activeValidRec_prioritizesSmartspace() =
+ testScope.runTest {
+ val selectedUserEntries by collectLastValue(mediaFilterRepository.selectedUserEntries)
+ val smartspaceMediaData by collectLastValue(mediaFilterRepository.smartspaceMediaData)
+ val reactivatedKey by collectLastValue(mediaFilterRepository.reactivatedKey)
+ val dataOld = dataMain.copy(active = false, lastActive = clock.elapsedRealtime())
+ mediaDataFilter.onMediaDataLoaded(KEY, null, dataOld)
+ clock.advanceTime(MediaDataFilterImpl.SMARTSPACE_MAX_AGE + 100)
+ mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
+
+ verify(listener)
+ .onSmartspaceMediaDataLoaded(eq(SMARTSPACE_KEY), eq(smartspaceData), eq(true))
+ assertThat(
+ hasActiveMediaOrRecommendation(
+ selectedUserEntries,
+ smartspaceMediaData,
+ reactivatedKey
+ )
+ )
+ .isTrue()
+ assertThat(hasActiveMedia(selectedUserEntries)).isFalse()
+ verify(logger).logRecommendationAdded(SMARTSPACE_PACKAGE, SMARTSPACE_INSTANCE_ID)
+ verify(logger, never()).logRecommendationActivated(any(), any(), any())
+ }
+
+ @Test
+ fun testOnSmartspaceMediaDataLoaded_noRecentMedia_inactiveRec_showsNothing() =
+ testScope.runTest {
+ val selectedUserEntries by collectLastValue(mediaFilterRepository.selectedUserEntries)
+ val smartspaceMediaData by collectLastValue(mediaFilterRepository.smartspaceMediaData)
+ val reactivatedKey by collectLastValue(mediaFilterRepository.reactivatedKey)
+ whenever(smartspaceData.isActive).thenReturn(false)
+
+ val dataOld = dataMain.copy(active = false, lastActive = clock.elapsedRealtime())
+ mediaDataFilter.onMediaDataLoaded(KEY, null, dataOld)
+ clock.advanceTime(MediaDataFilterImpl.SMARTSPACE_MAX_AGE + 100)
+ mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
+
+ verify(listener, never()).onSmartspaceMediaDataLoaded(any(), any(), anyBoolean())
+ assertThat(
+ hasActiveMediaOrRecommendation(
+ selectedUserEntries,
+ smartspaceMediaData,
+ reactivatedKey
+ )
+ )
+ .isFalse()
+ assertThat(hasActiveMedia(selectedUserEntries)).isFalse()
+ verify(logger, never()).logRecommendationAdded(any(), any())
+ verify(logger, never()).logRecommendationActivated(any(), any(), any())
+ }
+
+ @Test
+ fun testOnSmartspaceMediaDataLoaded_hasRecentMedia_inactiveRec_showsNothing() =
+ testScope.runTest {
+ val selectedUserEntries by collectLastValue(mediaFilterRepository.selectedUserEntries)
+ val smartspaceMediaData by collectLastValue(mediaFilterRepository.smartspaceMediaData)
+ val reactivatedKey by collectLastValue(mediaFilterRepository.reactivatedKey)
+
+ whenever(smartspaceData.isActive).thenReturn(false)
+
+ // WHEN we have media that was recently played, but not currently active
+ val dataCurrent = dataMain.copy(active = false, lastActive = clock.elapsedRealtime())
+ mediaDataFilter.onMediaDataLoaded(KEY, null, dataCurrent)
+ verify(listener)
+ .onMediaDataLoaded(eq(KEY), eq(null), eq(dataCurrent), eq(true), eq(0), eq(false))
+
+ // AND we get a smartspace signal
+ mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
+
+ // THEN we should tell listeners to treat the media as not active instead
+ verify(listener, never())
+ .onMediaDataLoaded(eq(KEY), eq(KEY), any(), anyBoolean(), anyInt(), anyBoolean())
+ verify(listener, never()).onSmartspaceMediaDataLoaded(any(), any(), anyBoolean())
+ assertThat(
+ hasActiveMediaOrRecommendation(
+ selectedUserEntries,
+ smartspaceMediaData,
+ reactivatedKey
+ )
+ )
+ .isFalse()
+ assertThat(hasActiveMedia(selectedUserEntries)).isFalse()
+ verify(logger, never()).logRecommendationAdded(any(), any())
+ verify(logger, never()).logRecommendationActivated(any(), any(), any())
+ }
+
+ @Test
+ fun testOnSmartspaceMediaDataLoaded_hasRecentMedia_activeInvalidRec_usesMedia() =
+ testScope.runTest {
+ val selectedUserEntries by collectLastValue(mediaFilterRepository.selectedUserEntries)
+ val smartspaceMediaData by collectLastValue(mediaFilterRepository.smartspaceMediaData)
+ val reactivatedKey by collectLastValue(mediaFilterRepository.reactivatedKey)
+ whenever(smartspaceData.isValid()).thenReturn(false)
+
+ // WHEN we have media that was recently played, but not currently active
+ val dataCurrent = dataMain.copy(active = false, lastActive = clock.elapsedRealtime())
+ mediaDataFilter.onMediaDataLoaded(KEY, null, dataCurrent)
+ verify(listener)
+ .onMediaDataLoaded(eq(KEY), eq(null), eq(dataCurrent), eq(true), eq(0), eq(false))
+
+ // AND we get a smartspace signal
+ runCurrent()
+ mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
+
+ // THEN we should tell listeners to treat the media as active instead
+ val dataCurrentAndActive = dataCurrent.copy(active = true)
+ verify(listener)
+ .onMediaDataLoaded(
+ eq(KEY),
+ eq(KEY),
+ eq(dataCurrentAndActive),
+ eq(true),
+ eq(100),
+ eq(true)
+ )
+ assertThat(
+ hasActiveMediaOrRecommendation(
+ selectedUserEntries,
+ smartspaceMediaData,
+ reactivatedKey
+ )
+ )
+ .isTrue()
+ // Smartspace update shouldn't be propagated for the empty rec list.
+ verify(listener, never()).onSmartspaceMediaDataLoaded(any(), any(), anyBoolean())
+ verify(logger, never()).logRecommendationAdded(any(), any())
+ verify(logger).logRecommendationActivated(eq(APP_UID), eq(PACKAGE), eq(INSTANCE_ID))
+ }
+
+ @Test
+ fun testOnSmartspaceMediaDataLoaded_hasRecentMedia_activeValidRec_usesBoth() =
+ testScope.runTest {
+ val selectedUserEntries by collectLastValue(mediaFilterRepository.selectedUserEntries)
+ val smartspaceMediaData by collectLastValue(mediaFilterRepository.smartspaceMediaData)
+ val reactivatedKey by collectLastValue(mediaFilterRepository.reactivatedKey)
+ // WHEN we have media that was recently played, but not currently active
+ val dataCurrent = dataMain.copy(active = false, lastActive = clock.elapsedRealtime())
+ mediaDataFilter.onMediaDataLoaded(KEY, null, dataCurrent)
+ verify(listener)
+ .onMediaDataLoaded(eq(KEY), eq(null), eq(dataCurrent), eq(true), eq(0), eq(false))
+
+ // AND we get a smartspace signal
+ runCurrent()
+ mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
+
+ // THEN we should tell listeners to treat the media as active instead
+ val dataCurrentAndActive = dataCurrent.copy(active = true)
+ verify(listener)
+ .onMediaDataLoaded(
+ eq(KEY),
+ eq(KEY),
+ eq(dataCurrentAndActive),
+ eq(true),
+ eq(100),
+ eq(true)
+ )
+ assertThat(
+ hasActiveMediaOrRecommendation(
+ selectedUserEntries,
+ smartspaceMediaData,
+ reactivatedKey
+ )
+ )
+ .isTrue()
+ // Smartspace update should also be propagated but not prioritized.
+ verify(listener)
+ .onSmartspaceMediaDataLoaded(eq(SMARTSPACE_KEY), eq(smartspaceData), eq(false))
+ verify(logger).logRecommendationAdded(SMARTSPACE_PACKAGE, SMARTSPACE_INSTANCE_ID)
+ verify(logger).logRecommendationActivated(eq(APP_UID), eq(PACKAGE), eq(INSTANCE_ID))
+ }
+
+ @Test
+ fun testOnSmartspaceMediaDataRemoved_usedSmartspace_clearsSmartspace() =
+ testScope.runTest {
+ val selectedUserEntries by collectLastValue(mediaFilterRepository.selectedUserEntries)
+ val smartspaceMediaData by collectLastValue(mediaFilterRepository.smartspaceMediaData)
+ val reactivatedKey by collectLastValue(mediaFilterRepository.reactivatedKey)
+
+ mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
+ mediaDataFilter.onSmartspaceMediaDataRemoved(SMARTSPACE_KEY)
+
+ verify(listener).onSmartspaceMediaDataRemoved(SMARTSPACE_KEY)
+ assertThat(
+ hasActiveMediaOrRecommendation(
+ selectedUserEntries,
+ smartspaceMediaData,
+ reactivatedKey
+ )
+ )
+ .isFalse()
+ assertThat(hasActiveMedia(selectedUserEntries)).isFalse()
+ }
+
+ @Test
+ fun testOnSmartspaceMediaDataRemoved_usedMediaAndSmartspace_clearsBoth() =
+ testScope.runTest {
+ val selectedUserEntries by collectLastValue(mediaFilterRepository.selectedUserEntries)
+ val smartspaceMediaData by collectLastValue(mediaFilterRepository.smartspaceMediaData)
+ val reactivatedKey by collectLastValue(mediaFilterRepository.reactivatedKey)
+ val dataCurrent = dataMain.copy(active = false, lastActive = clock.elapsedRealtime())
+ mediaDataFilter.onMediaDataLoaded(KEY, null, dataCurrent)
+ verify(listener)
+ .onMediaDataLoaded(eq(KEY), eq(null), eq(dataCurrent), eq(true), eq(0), eq(false))
+
+ runCurrent()
+ mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
+
+ val dataCurrentAndActive = dataCurrent.copy(active = true)
+ verify(listener)
+ .onMediaDataLoaded(
+ eq(KEY),
+ eq(KEY),
+ eq(dataCurrentAndActive),
+ eq(true),
+ eq(100),
+ eq(true)
+ )
+
+ mediaDataFilter.onSmartspaceMediaDataRemoved(SMARTSPACE_KEY)
+
+ verify(listener).onSmartspaceMediaDataRemoved(SMARTSPACE_KEY)
+ assertThat(
+ hasActiveMediaOrRecommendation(
+ selectedUserEntries,
+ smartspaceMediaData,
+ reactivatedKey
+ )
+ )
+ .isFalse()
+ assertThat(hasActiveMedia(selectedUserEntries)).isFalse()
+ }
+
+ @Test
+ fun testOnSmartspaceLoaded_persistentEnabled_isInactive_notifiesListeners() =
+ testScope.runTest {
+ val selectedUserEntries by collectLastValue(mediaFilterRepository.selectedUserEntries)
+ val smartspaceMediaData by collectLastValue(mediaFilterRepository.smartspaceMediaData)
+ val reactivatedKey by collectLastValue(mediaFilterRepository.reactivatedKey)
+ whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(true)
+ whenever(smartspaceData.isActive).thenReturn(false)
+
+ mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
+
+ verify(listener)
+ .onSmartspaceMediaDataLoaded(eq(SMARTSPACE_KEY), eq(smartspaceData), eq(false))
+ assertThat(
+ hasActiveMediaOrRecommendation(
+ selectedUserEntries,
+ smartspaceMediaData,
+ reactivatedKey
+ )
+ )
+ .isFalse()
+ assertThat(hasAnyMediaOrRecommendation(selectedUserEntries, smartspaceMediaData))
+ .isTrue()
+ }
+
+ @Test
+ fun testOnSmartspaceLoaded_persistentEnabled_inactive_hasRecentMedia_staysInactive() =
+ testScope.runTest {
+ val selectedUserEntries by collectLastValue(mediaFilterRepository.selectedUserEntries)
+ val smartspaceMediaData by collectLastValue(mediaFilterRepository.smartspaceMediaData)
+ val reactivatedKey by collectLastValue(mediaFilterRepository.reactivatedKey)
+
+ whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(true)
+ whenever(smartspaceData.isActive).thenReturn(false)
+
+ // If there is media that was recently played but inactive
+ val dataCurrent = dataMain.copy(active = false, lastActive = clock.elapsedRealtime())
+ mediaDataFilter.onMediaDataLoaded(KEY, null, dataCurrent)
+ verify(listener)
+ .onMediaDataLoaded(eq(KEY), eq(null), eq(dataCurrent), eq(true), eq(0), eq(false))
+
+ // And an inactive recommendation is loaded
+ mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
+
+ // Smartspace is loaded but the media stays inactive
+ verify(listener)
+ .onSmartspaceMediaDataLoaded(eq(SMARTSPACE_KEY), eq(smartspaceData), eq(false))
+ verify(listener, never())
+ .onMediaDataLoaded(any(), any(), any(), anyBoolean(), anyInt(), anyBoolean())
+ assertThat(
+ hasActiveMediaOrRecommendation(
+ selectedUserEntries,
+ smartspaceMediaData,
+ reactivatedKey
+ )
+ )
+ .isFalse()
+ assertThat(hasAnyMediaOrRecommendation(selectedUserEntries, smartspaceMediaData))
+ .isTrue()
+ }
+
+ @Test
+ fun testOnSwipeToDismiss_persistentEnabled_recommendationSetInactive() {
+ whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(true)
+
+ val data =
+ EMPTY_SMARTSPACE_MEDIA_DATA.copy(
+ targetId = SMARTSPACE_KEY,
+ isActive = true,
+ packageName = SMARTSPACE_PACKAGE,
+ recommendations = listOf(smartspaceMediaRecommendationItem),
+ )
+ mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, data)
+ mediaDataFilter.onSwipeToDismiss()
+
+ verify(mediaDataManager).setRecommendationInactive(eq(SMARTSPACE_KEY))
+ verify(mediaDataManager, never())
+ .dismissSmartspaceRecommendation(eq(SMARTSPACE_KEY), anyLong())
+ }
+
+ @Test
+ fun testSmartspaceLoaded_shouldTriggerResume_doesTrigger() =
+ testScope.runTest {
+ val selectedUserEntries by collectLastValue(mediaFilterRepository.selectedUserEntries)
+ val smartspaceMediaData by collectLastValue(mediaFilterRepository.smartspaceMediaData)
+ val reactivatedKey by collectLastValue(mediaFilterRepository.reactivatedKey)
+ // WHEN we have media that was recently played, but not currently active
+ val dataCurrent = dataMain.copy(active = false, lastActive = clock.elapsedRealtime())
+ mediaDataFilter.onMediaDataLoaded(KEY, null, dataCurrent)
+ verify(listener)
+ .onMediaDataLoaded(eq(KEY), eq(null), eq(dataCurrent), eq(true), eq(0), eq(false))
+
+ // AND we get a smartspace signal with extra to trigger resume
+ runCurrent()
+ val extras = Bundle().apply { putBoolean(EXTRA_KEY_TRIGGER_RESUME, true) }
+ whenever(cardAction.extras).thenReturn(extras)
+ mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
+
+ // THEN we should tell listeners to treat the media as active instead
+ val dataCurrentAndActive = dataCurrent.copy(active = true)
+ verify(listener)
+ .onMediaDataLoaded(
+ eq(KEY),
+ eq(KEY),
+ eq(dataCurrentAndActive),
+ eq(true),
+ eq(100),
+ eq(true)
+ )
+ assertThat(
+ hasActiveMediaOrRecommendation(
+ selectedUserEntries,
+ smartspaceMediaData,
+ reactivatedKey
+ )
+ )
+ .isTrue()
+ // And send the smartspace data, but not prioritized
+ verify(listener)
+ .onSmartspaceMediaDataLoaded(eq(SMARTSPACE_KEY), eq(smartspaceData), eq(false))
+ }
+
+ @Test
+ fun testSmartspaceLoaded_notShouldTriggerResume_doesNotTrigger() {
+ // WHEN we have media that was recently played, but not currently active
+ val dataCurrent = dataMain.copy(active = false, lastActive = clock.elapsedRealtime())
+ mediaDataFilter.onMediaDataLoaded(KEY, null, dataCurrent)
+ verify(listener)
+ .onMediaDataLoaded(eq(KEY), eq(null), eq(dataCurrent), eq(true), eq(0), eq(false))
+
+ // AND we get a smartspace signal with extra to not trigger resume
+ val extras = Bundle().apply { putBoolean(EXTRA_KEY_TRIGGER_RESUME, false) }
+ whenever(cardAction.extras).thenReturn(extras)
+ mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
+
+ // THEN listeners are not updated to show media
+ verify(listener, never())
+ .onMediaDataLoaded(eq(KEY), eq(KEY), any(), eq(true), eq(100), eq(true))
+ // But the smartspace update is still propagated
+ verify(listener)
+ .onSmartspaceMediaDataLoaded(eq(SMARTSPACE_KEY), eq(smartspaceData), eq(false))
+ }
+
+ private fun hasActiveMediaOrRecommendation(
+ entries: Map<String, MediaData>?,
+ smartspaceMediaData: SmartspaceMediaData?,
+ reactivatedKey: String?
+ ): Boolean {
+ if (entries == null || smartspaceMediaData == null) {
+ return false
+ }
+ return entries.any { it.value.active } ||
+ (smartspaceMediaData.isActive &&
+ (smartspaceMediaData.isValid() || reactivatedKey != null))
+ }
+
+ private fun hasActiveMedia(entries: Map<String, MediaData>?): Boolean {
+ return entries?.any { it.value.active } ?: false
+ }
+
+ private fun hasAnyMediaOrRecommendation(
+ entries: Map<String, MediaData>?,
+ smartspaceMediaData: SmartspaceMediaData?
+ ): Boolean {
+ if (entries == null || smartspaceMediaData == null) {
+ return false
+ }
+ return entries.isNotEmpty() ||
+ (if (mediaFlags.isPersistentSsCardEnabled()) {
+ smartspaceMediaData.isValid()
+ } else {
+ smartspaceMediaData.isActive && smartspaceMediaData.isValid()
+ })
+ }
+
+ private fun hasAnyMedia(entries: Map<String, MediaData>?): Boolean {
+ return entries?.isNotEmpty() ?: false
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorTest.kt
similarity index 91%
copy from packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataManagerTest.kt
copy to packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorTest.kt
index 61bfdb5..5c275b4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataManagerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorTest.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2022 The Android Open Source Project
+ * Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -41,6 +41,7 @@
import android.provider.Settings
import android.service.notification.StatusBarNotification
import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
import android.testing.TestableLooper.RunWithLooper
import androidx.media.utils.MediaConstants
import androidx.test.filters.SmallTest
@@ -51,6 +52,9 @@
import com.android.systemui.SysuiTestCase
import com.android.systemui.broadcast.BroadcastDispatcher
import com.android.systemui.dump.DumpManager
+import com.android.systemui.media.controls.data.repository.MediaDataRepository
+import com.android.systemui.media.controls.data.repository.MediaFilterRepository
+import com.android.systemui.media.controls.domain.pipeline.interactor.MediaCarouselInteractor
import com.android.systemui.media.controls.domain.resume.MediaResumeListener
import com.android.systemui.media.controls.domain.resume.ResumeMediaBrowser
import com.android.systemui.media.controls.shared.model.EXTRA_KEY_TRIGGER_SOURCE
@@ -64,13 +68,19 @@
import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.res.R
import com.android.systemui.statusbar.SbnBuilder
-import com.android.systemui.tuner.TunerService
import com.android.systemui.util.concurrency.FakeExecutor
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.capture
import com.android.systemui.util.mockito.eq
+import com.android.systemui.util.mockito.whenever
+import com.android.systemui.util.settings.FakeSettings
import com.android.systemui.util.time.FakeSystemClock
+import com.android.systemui.utils.os.FakeHandler
import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestDispatcher
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
import org.junit.After
import org.junit.Before
import org.junit.Rule
@@ -87,7 +97,6 @@
import org.mockito.Mockito.reset
import org.mockito.Mockito.verify
import org.mockito.Mockito.verifyNoMoreInteractions
-import org.mockito.Mockito.`when` as whenever
import org.mockito.MockitoSession
import org.mockito.junit.MockitoJUnit
import org.mockito.quality.Strictness
@@ -111,10 +120,11 @@
return Mockito.anyObject<T>()
}
+@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@RunWithLooper(setAsMainLooper = true)
@RunWith(AndroidTestingRunner::class)
-class MediaDataManagerTest : SysuiTestCase() {
+class MediaDataProcessorTest : SysuiTestCase() {
@JvmField @Rule val mockito = MockitoJUnit.rule()
@Mock lateinit var mediaControllerFactory: MediaControllerFactory
@@ -122,9 +132,9 @@
@Mock lateinit var transportControls: MediaController.TransportControls
@Mock lateinit var playbackInfo: MediaController.PlaybackInfo
lateinit var session: MediaSession
- lateinit var metadataBuilder: MediaMetadata.Builder
+ private lateinit var metadataBuilder: MediaMetadata.Builder
lateinit var backgroundExecutor: FakeExecutor
- lateinit var foregroundExecutor: FakeExecutor
+ private lateinit var foregroundExecutor: FakeExecutor
lateinit var uiExecutor: FakeExecutor
@Mock lateinit var dumpManager: DumpManager
@Mock lateinit var broadcastDispatcher: BroadcastDispatcher
@@ -133,32 +143,38 @@
@Mock lateinit var mediaSessionBasedFilter: MediaSessionBasedFilter
@Mock lateinit var mediaDeviceManager: MediaDeviceManager
@Mock lateinit var mediaDataCombineLatest: MediaDataCombineLatest
- @Mock lateinit var mediaDataFilter: MediaDataFilter
+ @Mock lateinit var mediaDataFilter: MediaDataFilterImpl
@Mock lateinit var listener: MediaDataManager.Listener
@Mock lateinit var pendingIntent: PendingIntent
@Mock lateinit var activityStarter: ActivityStarter
@Mock lateinit var smartspaceManager: SmartspaceManager
@Mock lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor
- lateinit var smartspaceMediaDataProvider: SmartspaceMediaDataProvider
+ private lateinit var smartspaceMediaDataProvider: SmartspaceMediaDataProvider
@Mock lateinit var mediaSmartspaceTarget: SmartspaceTarget
@Mock private lateinit var mediaRecommendationItem: SmartspaceAction
- lateinit var validRecommendationList: List<SmartspaceAction>
+ private lateinit var validRecommendationList: List<SmartspaceAction>
@Mock private lateinit var mediaSmartspaceBaseAction: SmartspaceAction
@Mock private lateinit var mediaFlags: MediaFlags
@Mock private lateinit var logger: MediaUiEventLogger
- lateinit var mediaDataManager: MediaDataManager
- lateinit var mediaNotification: StatusBarNotification
- lateinit var remoteCastNotification: StatusBarNotification
+ private lateinit var mediaCarouselInteractor: MediaCarouselInteractor
+ private lateinit var mediaDataProcessor: MediaDataProcessor
+ private lateinit var mediaNotification: StatusBarNotification
+ private lateinit var remoteCastNotification: StatusBarNotification
@Captor lateinit var mediaDataCaptor: ArgumentCaptor<MediaData>
private val clock = FakeSystemClock()
- @Mock private lateinit var tunerService: TunerService
- @Captor lateinit var tunableCaptor: ArgumentCaptor<TunerService.Tunable>
@Captor lateinit var stateCallbackCaptor: ArgumentCaptor<(String, PlaybackState) -> Unit>
@Captor lateinit var sessionCallbackCaptor: ArgumentCaptor<(String) -> Unit>
@Captor lateinit var smartSpaceConfigBuilderCaptor: ArgumentCaptor<SmartspaceConfig>
@Mock private lateinit var ugm: IUriGrantsManager
@Mock private lateinit var imageSource: ImageDecoder.Source
+ private lateinit var mediaDataRepository: MediaDataRepository
+ private lateinit var mediaFilterRepository: MediaFilterRepository
+ private lateinit var testScope: TestScope
+ private lateinit var testDispatcher: TestDispatcher
+ private lateinit var testableLooper: TestableLooper
+ private lateinit var fakeHandler: FakeHandler
+ private val settings = FakeSettings()
private val instanceIdSequence = InstanceIdSequenceFake(1 shl 20)
private val originalSmartspaceSetting =
@@ -172,6 +188,8 @@
@Before
fun setup() {
+ whenever(mediaFlags.isMediaControlsRefactorEnabled()).thenReturn(true)
+
staticMockSession =
ExtendedMockito.mockitoSession()
.mockStatic<UriGrantsManager>(UriGrantsManager::class.java)
@@ -182,43 +200,61 @@
foregroundExecutor = FakeExecutor(clock)
backgroundExecutor = FakeExecutor(clock)
uiExecutor = FakeExecutor(clock)
+ testableLooper = TestableLooper.get(this)
+ fakeHandler = FakeHandler(testableLooper.looper)
smartspaceMediaDataProvider = SmartspaceMediaDataProvider()
Settings.Secure.putInt(
context.contentResolver,
Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION,
1
)
- mediaDataManager =
- MediaDataManager(
+ testDispatcher = UnconfinedTestDispatcher()
+ testScope = TestScope(testDispatcher)
+ mediaFilterRepository = MediaFilterRepository()
+ mediaDataRepository = MediaDataRepository(mediaFlags, dumpManager)
+ mediaDataProcessor =
+ MediaDataProcessor(
context = context,
+ applicationScope = testScope,
+ backgroundDispatcher = testDispatcher,
backgroundExecutor = backgroundExecutor,
uiExecutor = uiExecutor,
foregroundExecutor = foregroundExecutor,
+ handler = fakeHandler,
mediaControllerFactory = mediaControllerFactory,
broadcastDispatcher = broadcastDispatcher,
dumpManager = dumpManager,
+ activityStarter = activityStarter,
+ smartspaceMediaDataProvider = smartspaceMediaDataProvider,
+ useMediaResumption = true,
+ useQsMediaPlayer = true,
+ systemClock = clock,
+ secureSettings = settings,
+ mediaFlags = mediaFlags,
+ logger = logger,
+ smartspaceManager = smartspaceManager,
+ keyguardUpdateMonitor = keyguardUpdateMonitor,
+ mediaDataRepository = mediaDataRepository,
+ )
+ mediaDataProcessor.start()
+ mediaCarouselInteractor =
+ MediaCarouselInteractor(
+ applicationScope = testScope.backgroundScope,
+ mediaDataRepository = mediaDataRepository,
+ mediaDataProcessor = mediaDataProcessor,
mediaTimeoutListener = mediaTimeoutListener,
mediaResumeListener = mediaResumeListener,
mediaSessionBasedFilter = mediaSessionBasedFilter,
mediaDeviceManager = mediaDeviceManager,
mediaDataCombineLatest = mediaDataCombineLatest,
mediaDataFilter = mediaDataFilter,
- activityStarter = activityStarter,
- smartspaceMediaDataProvider = smartspaceMediaDataProvider,
- useMediaResumption = true,
- useQsMediaPlayer = true,
- systemClock = clock,
- tunerService = tunerService,
- mediaFlags = mediaFlags,
- logger = logger,
- smartspaceManager = smartspaceManager,
- keyguardUpdateMonitor = keyguardUpdateMonitor,
+ mediaFilterRepository = mediaFilterRepository,
+ mediaFlags = mediaFlags
)
- verify(tunerService)
- .addTunable(capture(tunableCaptor), eq(Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION))
+ mediaCarouselInteractor.start()
verify(mediaTimeoutListener).stateCallback = capture(stateCallbackCaptor)
verify(mediaTimeoutListener).sessionCallback = capture(sessionCallbackCaptor)
- session = MediaSession(context, "MediaDataManagerTestSession")
+ session = MediaSession(context, "MediaDataProcessorTestSession")
mediaNotification =
SbnBuilder().run {
setPkg(PACKAGE_NAME)
@@ -290,7 +326,7 @@
fun tearDown() {
staticMockSession.finishMocking()
session.release()
- mediaDataManager.destroy()
+ mediaDataProcessor.destroy()
Settings.Secure.putInt(
context.contentResolver,
Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION,
@@ -299,25 +335,25 @@
}
@Test
- fun testSetTimedOut_active_deactivatesMedia() {
+ fun testsetInactive_active_deactivatesMedia() {
addNotificationAndLoad()
val data = mediaDataCaptor.value
assertThat(data.active).isTrue()
- mediaDataManager.setTimedOut(KEY, timedOut = true)
+ mediaDataProcessor.setInactive(KEY, timedOut = true)
assertThat(data.active).isFalse()
verify(logger).logMediaTimeout(anyInt(), eq(PACKAGE_NAME), eq(data.instanceId))
}
@Test
- fun testSetTimedOut_resume_dismissesMedia() {
+ fun testsetInactive_resume_dismissesMedia() {
// WHEN resume controls are present, and time out
val desc =
MediaDescription.Builder().run {
setTitle(SESSION_TITLE)
build()
}
- mediaDataManager.addResumptionControls(
+ mediaDataProcessor.addResumptionControls(
USER_ID,
desc,
Runnable {},
@@ -339,7 +375,7 @@
eq(false)
)
- mediaDataManager.setTimedOut(PACKAGE_NAME, timedOut = true)
+ mediaDataProcessor.setInactive(PACKAGE_NAME, timedOut = true)
verify(logger)
.logMediaTimeout(anyInt(), eq(PACKAGE_NAME), eq(mediaDataCaptor.value.instanceId))
@@ -351,7 +387,7 @@
@Test
fun testLoadsMetadataOnBackground() {
- mediaDataManager.onNotificationAdded(KEY, mediaNotification)
+ mediaDataProcessor.onNotificationAdded(KEY, mediaNotification)
assertThat(backgroundExecutor.numPending()).isEqualTo(1)
}
@@ -367,8 +403,7 @@
.build()
)
- mediaDataManager.addListener(listener)
- mediaDataManager.onNotificationAdded(KEY, mediaNotification)
+ mediaDataProcessor.onNotificationAdded(KEY, mediaNotification)
assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
assertThat(foregroundExecutor.runAllReady()).isEqualTo(1)
@@ -386,8 +421,7 @@
@Test
fun testOnMetaDataLoaded_withoutExplicitIndicator() {
- mediaDataManager.addListener(listener)
- mediaDataManager.onNotificationAdded(KEY, mediaNotification)
+ mediaDataProcessor.onNotificationAdded(KEY, mediaNotification)
assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
assertThat(foregroundExecutor.runAllReady()).isEqualTo(1)
@@ -418,8 +452,7 @@
@Test
fun testOnMetaDataLoaded_conservesActiveFlag() {
whenever(mediaControllerFactory.create(anyObject())).thenReturn(controller)
- mediaDataManager.addListener(listener)
- mediaDataManager.onNotificationAdded(KEY, mediaNotification)
+ mediaDataProcessor.onNotificationAdded(KEY, mediaNotification)
assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
assertThat(foregroundExecutor.runAllReady()).isEqualTo(1)
verify(listener)
@@ -464,7 +497,7 @@
build()
}
- mediaDataManager.onNotificationAdded(KEY, notif)
+ mediaDataProcessor.onNotificationAdded(KEY, notif)
assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
assertThat(foregroundExecutor.runAllReady()).isEqualTo(1)
verify(listener)
@@ -498,7 +531,7 @@
build()
}
- mediaDataManager.loadMediaDataInBg(KEY, rcn, null)
+ mediaDataProcessor.loadMediaDataInBg(KEY, rcn, null)
// no crash even though the data structure is incorrect
}
@@ -523,7 +556,7 @@
build()
}
- mediaDataManager.loadMediaDataInBg(KEY, rcn, null)
+ mediaDataProcessor.loadMediaDataInBg(KEY, rcn, null)
// no crash even though the data structure is incorrect
}
@@ -531,7 +564,7 @@
fun testOnNotificationRemoved_callsListener() {
addNotificationAndLoad()
val data = mediaDataCaptor.value
- mediaDataManager.onNotificationRemoved(KEY)
+ mediaDataProcessor.onNotificationRemoved(KEY)
verify(listener).onMediaDataRemoved(eq(KEY))
verify(logger).logMediaRemoved(anyInt(), eq(PACKAGE_NAME), eq(data.instanceId))
}
@@ -549,7 +582,7 @@
.putString(MediaMetadata.METADATA_KEY_TITLE, SESSION_EMPTY_TITLE)
.build()
)
- mediaDataManager.onNotificationAdded(KEY, mediaNotification)
+ mediaDataProcessor.onNotificationAdded(KEY, mediaNotification)
// Then a media control is created with a placeholder title string
assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
@@ -580,7 +613,7 @@
.putString(MediaMetadata.METADATA_KEY_TITLE, SESSION_BLANK_TITLE)
.build()
)
- mediaDataManager.onNotificationAdded(KEY, mediaNotification)
+ mediaDataProcessor.onNotificationAdded(KEY, mediaNotification)
// Then a media control is created with a placeholder title string
assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
@@ -622,7 +655,7 @@
}
build()
}
- mediaDataManager.onNotificationAdded(KEY, mediaNotification)
+ mediaDataProcessor.onNotificationAdded(KEY, mediaNotification)
// Then the media control is added using the notification's title
assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
@@ -646,7 +679,7 @@
val data = mediaDataCaptor.value
val instanceId = data.instanceId
assertThat(data.resumption).isFalse()
- mediaDataManager.onMediaDataLoaded(
+ mediaDataProcessor.onMediaDataLoaded(
KEY,
null,
data.copy(song = SESSION_EMPTY_TITLE, resumeAction = Runnable {})
@@ -654,7 +687,7 @@
// WHEN the notification is removed
reset(listener)
- mediaDataManager.onNotificationRemoved(KEY)
+ mediaDataProcessor.onNotificationRemoved(KEY)
// THEN active media is not converted to resume.
verify(listener, never())
@@ -679,7 +712,7 @@
val data = mediaDataCaptor.value
val instanceId = data.instanceId
assertThat(data.resumption).isFalse()
- mediaDataManager.onMediaDataLoaded(
+ mediaDataProcessor.onMediaDataLoaded(
KEY,
null,
data.copy(song = SESSION_BLANK_TITLE, resumeAction = Runnable {})
@@ -687,7 +720,7 @@
// WHEN the notification is removed
reset(listener)
- mediaDataManager.onNotificationRemoved(KEY)
+ mediaDataProcessor.onNotificationRemoved(KEY)
// THEN active media is not converted to resume.
verify(listener, never())
@@ -711,9 +744,9 @@
addNotificationAndLoad()
val data = mediaDataCaptor.value
assertThat(data.resumption).isFalse()
- mediaDataManager.onMediaDataLoaded(KEY, null, data.copy(resumeAction = Runnable {}))
+ mediaDataProcessor.onMediaDataLoaded(KEY, null, data.copy(resumeAction = Runnable {}))
// WHEN the notification is removed
- mediaDataManager.onNotificationRemoved(KEY)
+ mediaDataProcessor.onNotificationRemoved(KEY)
// THEN the media data indicates that it is for resumption
verify(listener)
.onMediaDataLoaded(
@@ -732,8 +765,8 @@
@Test
fun testOnNotificationRemoved_twoWithResumption() {
// GIVEN that the manager has two notifications with resume actions
- mediaDataManager.onNotificationAdded(KEY, mediaNotification)
- mediaDataManager.onNotificationAdded(KEY_2, mediaNotification)
+ mediaDataProcessor.onNotificationAdded(KEY, mediaNotification)
+ mediaDataProcessor.onNotificationAdded(KEY_2, mediaNotification)
assertThat(backgroundExecutor.runAllReady()).isEqualTo(2)
assertThat(foregroundExecutor.runAllReady()).isEqualTo(2)
@@ -761,11 +794,11 @@
val data2 = mediaDataCaptor.value
assertThat(data2.resumption).isFalse()
- mediaDataManager.onMediaDataLoaded(KEY, null, data.copy(resumeAction = Runnable {}))
- mediaDataManager.onMediaDataLoaded(KEY_2, null, data2.copy(resumeAction = Runnable {}))
+ mediaDataProcessor.onMediaDataLoaded(KEY, null, data.copy(resumeAction = Runnable {}))
+ mediaDataProcessor.onMediaDataLoaded(KEY_2, null, data2.copy(resumeAction = Runnable {}))
reset(listener)
// WHEN the first is removed
- mediaDataManager.onNotificationRemoved(KEY)
+ mediaDataProcessor.onNotificationRemoved(KEY)
// THEN the data is for resumption and the key is migrated to the package name
verify(listener)
.onMediaDataLoaded(
@@ -779,7 +812,7 @@
assertThat(mediaDataCaptor.value.resumption).isTrue()
verify(listener, never()).onMediaDataRemoved(eq(KEY))
// WHEN the second is removed
- mediaDataManager.onNotificationRemoved(KEY_2)
+ mediaDataProcessor.onNotificationRemoved(KEY_2)
// THEN the data is for resumption and the second key is removed
verify(listener)
.onMediaDataLoaded(
@@ -803,7 +836,7 @@
val data = mediaDataCaptor.value
val dataRemoteWithResume =
data.copy(resumeAction = Runnable {}, playbackLocation = MediaData.PLAYBACK_CAST_LOCAL)
- mediaDataManager.onMediaDataLoaded(KEY, null, dataRemoteWithResume)
+ mediaDataProcessor.onMediaDataLoaded(KEY, null, dataRemoteWithResume)
verify(logger)
.logActiveMediaAdded(
anyInt(),
@@ -813,7 +846,7 @@
)
// WHEN the notification is removed
- mediaDataManager.onNotificationRemoved(KEY)
+ mediaDataProcessor.onNotificationRemoved(KEY)
// THEN the media data is removed
verify(listener).onMediaDataRemoved(eq(KEY))
@@ -832,10 +865,10 @@
val data = mediaDataCaptor.value
val dataRemoteWithResume =
data.copy(resumeAction = Runnable {}, playbackLocation = MediaData.PLAYBACK_CAST_LOCAL)
- mediaDataManager.onMediaDataLoaded(KEY, null, dataRemoteWithResume)
+ mediaDataProcessor.onMediaDataLoaded(KEY, null, dataRemoteWithResume)
// WHEN the notification is removed
- mediaDataManager.onNotificationRemoved(KEY)
+ mediaDataProcessor.onNotificationRemoved(KEY)
// THEN the media data is converted to a resume state
verify(listener)
@@ -860,10 +893,10 @@
val data = mediaDataCaptor.value
assertThat(data.playbackLocation).isEqualTo(MediaData.PLAYBACK_CAST_REMOTE)
val dataRemoteWithResume = data.copy(resumeAction = Runnable {})
- mediaDataManager.onMediaDataLoaded(KEY, null, dataRemoteWithResume)
+ mediaDataProcessor.onMediaDataLoaded(KEY, null, dataRemoteWithResume)
// WHEN the RCN is removed
- mediaDataManager.onNotificationRemoved(KEY)
+ mediaDataProcessor.onNotificationRemoved(KEY)
// THEN the media data is removed
verify(listener).onMediaDataRemoved(eq(KEY))
@@ -886,10 +919,10 @@
addNotificationAndLoad()
val data = mediaDataCaptor.value
assertThat(data.resumption).isFalse()
- mediaDataManager.onMediaDataLoaded(KEY, null, data.copy(resumeAction = Runnable {}))
+ mediaDataProcessor.onMediaDataLoaded(KEY, null, data.copy(resumeAction = Runnable {}))
// When the notification is removed
- mediaDataManager.onNotificationRemoved(KEY)
+ mediaDataProcessor.onNotificationRemoved(KEY)
// Then it is converted to resumption
verify(listener)
@@ -913,7 +946,7 @@
addNotificationAndLoad()
val data = mediaDataCaptor.value
- mediaDataManager.onNotificationRemoved(KEY)
+ mediaDataProcessor.onNotificationRemoved(KEY)
verify(listener, never()).onMediaDataRemoved(eq(KEY))
verify(logger, never())
@@ -1076,7 +1109,7 @@
setTitle(SESSION_EMPTY_TITLE)
build()
}
- mediaDataManager.addResumptionControls(
+ mediaDataProcessor.addResumptionControls(
USER_ID,
desc,
Runnable {},
@@ -1110,7 +1143,7 @@
setTitle(SESSION_BLANK_TITLE)
build()
}
- mediaDataManager.addResumptionControls(
+ mediaDataProcessor.addResumptionControls(
USER_ID,
desc,
Runnable {},
@@ -1145,7 +1178,7 @@
addResumeControlAndLoad(desc)
val data = mediaDataCaptor.value
- mediaDataManager.setMediaResumptionEnabled(false)
+ mediaDataProcessor.setMediaResumptionEnabled(false)
// THEN the resume controls are dismissed
verify(listener).onMediaDataRemoved(eq(PACKAGE_NAME))
@@ -1156,7 +1189,7 @@
fun testDismissMedia_listenerCalled() {
addNotificationAndLoad()
val data = mediaDataCaptor.value
- val removed = mediaDataManager.dismissMediaData(KEY, 0L)
+ val removed = mediaDataProcessor.dismissMediaData(KEY, 0L)
assertThat(removed).isTrue()
foregroundExecutor.advanceClockToLast()
@@ -1168,7 +1201,7 @@
@Test
fun testDismissMedia_keyDoesNotExist_returnsFalse() {
- val removed = mediaDataManager.dismissMediaData(KEY, 0L)
+ val removed = mediaDataProcessor.dismissMediaData(KEY, 0L)
assertThat(removed).isFalse()
}
@@ -1186,7 +1219,7 @@
}
build()
}
- mediaDataManager.onNotificationAdded(KEY, notif)
+ mediaDataProcessor.onNotificationAdded(KEY, notif)
// THEN it still loads
assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
@@ -1239,7 +1272,7 @@
.onSmartspaceMediaDataLoaded(
eq(KEY_MEDIA_SMARTSPACE),
eq(
- EMPTY_SMARTSPACE_MEDIA_DATA.copy(
+ SmartspaceMediaData(
targetId = KEY_MEDIA_SMARTSPACE,
isActive = true,
dismissIntent = DISMISS_INTENT,
@@ -1271,7 +1304,7 @@
.onSmartspaceMediaDataLoaded(
eq(KEY_MEDIA_SMARTSPACE),
eq(
- EMPTY_SMARTSPACE_MEDIA_DATA.copy(
+ SmartspaceMediaData(
targetId = KEY_MEDIA_SMARTSPACE,
isActive = true,
dismissIntent = null,
@@ -1404,7 +1437,7 @@
smartspaceMediaDataProvider.onTargetsAvailable(listOf(mediaSmartspaceTarget))
val instanceId = instanceIdSequence.lastInstanceId
- mediaDataManager.setRecommendationInactive(KEY_MEDIA_SMARTSPACE)
+ mediaDataProcessor.setRecommendationInactive(KEY_MEDIA_SMARTSPACE)
uiExecutor.advanceClockToLast()
uiExecutor.runAllReady()
@@ -1431,12 +1464,7 @@
@Test
fun testOnSmartspaceMediaDataLoaded_settingDisabled_doesNothing() {
// WHEN media recommendation setting is off
- Settings.Secure.putInt(
- context.contentResolver,
- Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION,
- 0
- )
- tunableCaptor.value.onTuningChanged(Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION, "0")
+ settings.putInt(Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION, 0)
smartspaceMediaDataProvider.onTargetsAvailable(listOf(mediaSmartspaceTarget))
@@ -1453,12 +1481,7 @@
.onSmartspaceMediaDataLoaded(eq(KEY_MEDIA_SMARTSPACE), anyObject(), anyBoolean())
// WHEN the media recommendation setting is turned off
- Settings.Secure.putInt(
- context.contentResolver,
- Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION,
- 0
- )
- tunableCaptor.value.onTuningChanged(Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION, "0")
+ settings.putInt(Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION, 0)
// THEN listeners are notified
uiExecutor.advanceClockToLast()
@@ -1478,14 +1501,14 @@
@Test
fun testOnMediaDataTimedOut_updatesLastActiveTime() {
// GIVEN that the manager has a notification
- mediaDataManager.onNotificationAdded(KEY, mediaNotification)
+ mediaDataProcessor.onNotificationAdded(KEY, mediaNotification)
assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
assertThat(foregroundExecutor.runAllReady()).isEqualTo(1)
// WHEN the notification times out
clock.advanceTime(100)
val currentTime = clock.elapsedRealtime()
- mediaDataManager.setTimedOut(KEY, true, true)
+ mediaDataProcessor.setInactive(KEY, timedOut = true, forceUpdate = true)
// THEN the last active time is changed
verify(listener)
@@ -1507,12 +1530,12 @@
val data = mediaDataCaptor.value
val instanceId = data.instanceId
assertThat(data.resumption).isFalse()
- mediaDataManager.onMediaDataLoaded(KEY, null, data.copy(resumeAction = Runnable {}))
+ mediaDataProcessor.onMediaDataLoaded(KEY, null, data.copy(resumeAction = Runnable {}))
// WHEN the notification is removed
clock.advanceTime(100)
val currentTime = clock.elapsedRealtime()
- mediaDataManager.onNotificationRemoved(KEY)
+ mediaDataProcessor.onNotificationRemoved(KEY)
// THEN the last active time is changed
verify(listener)
@@ -1539,7 +1562,7 @@
val data = mediaDataCaptor.value
val instanceId = data.instanceId
assertThat(data.resumption).isFalse()
- mediaDataManager.onMediaDataLoaded(
+ mediaDataProcessor.onMediaDataLoaded(
KEY,
null,
data.copy(resumeAction = Runnable {}, active = false)
@@ -1548,7 +1571,7 @@
// WHEN the notification is removed
clock.advanceTime(100)
val currentTime = clock.elapsedRealtime()
- mediaDataManager.onNotificationRemoved(KEY)
+ mediaDataProcessor.onNotificationRemoved(KEY)
// THEN the last active time is not changed
verify(listener)
@@ -1587,7 +1610,7 @@
}
// WHEN the notification is loaded
- mediaDataManager.onNotificationAdded(KEY, notif)
+ mediaDataProcessor.onNotificationAdded(KEY, notif)
assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
assertThat(foregroundExecutor.runAllReady()).isEqualTo(1)
@@ -1602,7 +1625,7 @@
eq(false)
)
assertThat(mediaDataCaptor.value.actionsToShowInCompact.size)
- .isEqualTo(MediaDataManager.MAX_COMPACT_ACTIONS)
+ .isEqualTo(MediaDataProcessor.MAX_COMPACT_ACTIONS)
}
@Test
@@ -1615,7 +1638,7 @@
modifyNotification(context).also {
it.setSmallIcon(android.R.drawable.ic_media_pause)
it.setStyle(MediaStyle().apply { setMediaSession(session.sessionToken) })
- for (i in 0..MediaDataManager.MAX_NOTIFICATION_ACTIONS) {
+ for (i in 0..MediaDataProcessor.MAX_NOTIFICATION_ACTIONS) {
it.addAction(action)
}
}
@@ -1623,7 +1646,7 @@
}
// WHEN the notification is loaded
- mediaDataManager.onNotificationAdded(KEY, notif)
+ mediaDataProcessor.onNotificationAdded(KEY, notif)
assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
assertThat(foregroundExecutor.runAllReady()).isEqualTo(1)
@@ -1638,7 +1661,7 @@
eq(false)
)
assertThat(mediaDataCaptor.value.actions.size)
- .isEqualTo(MediaDataManager.MAX_NOTIFICATION_ACTIONS)
+ .isEqualTo(MediaDataProcessor.MAX_NOTIFICATION_ACTIONS)
}
@Test
@@ -1657,7 +1680,7 @@
}
build()
}
- mediaDataManager.onNotificationAdded(KEY, notifWithAction)
+ mediaDataProcessor.onNotificationAdded(KEY, notifWithAction)
assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
assertThat(foregroundExecutor.runAllReady()).isEqualTo(1)
@@ -1850,7 +1873,7 @@
)
// update to remote cast
- mediaDataManager.onNotificationAdded(KEY, remoteCastNotification)
+ mediaDataProcessor.onNotificationAdded(KEY, remoteCastNotification)
assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
assertThat(foregroundExecutor.runAllReady()).isEqualTo(1)
verify(logger)
@@ -1901,7 +1924,7 @@
addNotificationAndLoad()
val data = mediaDataCaptor.value
assertThat(data.resumption).isFalse()
- mediaDataManager.onMediaDataLoaded(KEY, null, data.copy(token = null))
+ mediaDataProcessor.onMediaDataLoaded(KEY, null, data.copy(token = null))
// And then get a state update
val state = PlaybackState.Builder().build()
@@ -1949,7 +1972,7 @@
// Add resumption controls in order to have semantic actions.
// To make sure that they are not null after changing state.
- mediaDataManager.addResumptionControls(
+ mediaDataProcessor.addResumptionControls(
USER_ID,
desc,
Runnable {},
@@ -2040,9 +2063,9 @@
// When a media control based on notification is added, times out, and then removed
addNotificationAndLoad()
- mediaDataManager.setTimedOut(KEY, timedOut = true)
+ mediaDataProcessor.setInactive(KEY, timedOut = true)
assertThat(mediaDataCaptor.value.active).isFalse()
- mediaDataManager.onNotificationRemoved(KEY)
+ mediaDataProcessor.onNotificationRemoved(KEY)
// It is converted to a resume player
verify(listener)
@@ -2070,7 +2093,7 @@
// When a media control based on notification is added and times out
addNotificationAndLoad()
- mediaDataManager.setTimedOut(KEY, timedOut = true)
+ mediaDataProcessor.setInactive(KEY, timedOut = true)
assertThat(mediaDataCaptor.value.active).isFalse()
// and then the session is destroyed
@@ -2090,7 +2113,7 @@
addNotificationAndLoad()
val data = mediaDataCaptor.value
assertThat(data.active).isTrue()
- mediaDataManager.onNotificationRemoved(KEY)
+ mediaDataProcessor.onNotificationRemoved(KEY)
// It is fully removed
verify(listener).onMediaDataRemoved(eq(KEY))
@@ -2106,10 +2129,10 @@
// When a media control that supports resumption is added
addNotificationAndLoad()
val dataResumable = mediaDataCaptor.value.copy(resumeAction = Runnable {})
- mediaDataManager.onMediaDataLoaded(KEY, null, dataResumable)
+ mediaDataProcessor.onMediaDataLoaded(KEY, null, dataResumable)
// And then removed while still active
- mediaDataManager.onNotificationRemoved(KEY)
+ mediaDataProcessor.onNotificationRemoved(KEY)
// It is converted to a resume player
verify(listener)
@@ -2142,8 +2165,8 @@
addNotificationAndLoad()
val data = mediaDataCaptor.value
assertThat(data.active).isTrue()
- mediaDataManager.setTimedOut(KEY, timedOut = true)
- mediaDataManager.onNotificationRemoved(KEY)
+ mediaDataProcessor.setInactive(KEY, timedOut = true)
+ mediaDataProcessor.onNotificationRemoved(KEY)
// It remains as a regular player
verify(listener, never()).onMediaDataRemoved(eq(KEY))
@@ -2162,7 +2185,7 @@
addNotificationAndLoad()
val data = mediaDataCaptor.value
assertThat(data.active).isTrue()
- mediaDataManager.setTimedOut(KEY, timedOut = true)
+ mediaDataProcessor.setInactive(KEY, timedOut = true)
sessionCallbackCaptor.value.invoke(KEY)
// It is converted to a resume player
@@ -2214,7 +2237,7 @@
// When a media control using session actions and that does allow resumption is added,
addNotificationAndLoad()
val dataResumable = mediaDataCaptor.value.copy(resumeAction = Runnable {})
- mediaDataManager.onMediaDataLoaded(KEY, null, dataResumable)
+ mediaDataProcessor.onMediaDataLoaded(KEY, null, dataResumable)
// And then the session is destroyed without timing out first
sessionCallbackCaptor.value.invoke(KEY)
@@ -2249,7 +2272,7 @@
addNotificationAndLoad()
val data = mediaDataCaptor.value
assertThat(data.active).isTrue()
- mediaDataManager.setTimedOut(KEY, timedOut = true)
+ mediaDataProcessor.setInactive(KEY, timedOut = true)
sessionCallbackCaptor.value.invoke(KEY)
// It is fully removed.
@@ -2293,7 +2316,7 @@
// When a media control using session actions and that does allow resumption is added,
addNotificationAndLoad()
val dataResumable = mediaDataCaptor.value.copy(resumeAction = Runnable {})
- mediaDataManager.onMediaDataLoaded(KEY, null, dataResumable)
+ mediaDataProcessor.onMediaDataLoaded(KEY, null, dataResumable)
// And then the session is destroyed without timing out first
sessionCallbackCaptor.value.invoke(KEY)
@@ -2324,9 +2347,9 @@
whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(true)
// When a notiifcation is added and then removed before it is fully processed
- mediaDataManager.onNotificationAdded(KEY, mediaNotification)
+ mediaDataProcessor.onNotificationAdded(KEY, mediaNotification)
backgroundExecutor.runAllReady()
- mediaDataManager.onNotificationRemoved(KEY)
+ mediaDataProcessor.onNotificationRemoved(KEY)
// We still make sure to remove it
verify(listener).onMediaDataRemoved(eq(KEY))
@@ -2399,7 +2422,7 @@
/** Helper function to add the given notification and capture the resulting MediaData */
private fun addNotificationAndLoad(sbn: StatusBarNotification) {
- mediaDataManager.onNotificationAdded(KEY, sbn)
+ mediaDataProcessor.onNotificationAdded(KEY, sbn)
assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
assertThat(foregroundExecutor.runAllReady()).isEqualTo(1)
verify(listener)
@@ -2426,7 +2449,7 @@
desc: MediaDescription,
packageName: String = PACKAGE_NAME
) {
- mediaDataManager.addResumptionControls(
+ mediaDataProcessor.addResumptionControls(
USER_ID,
desc,
Runnable {},
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceManagerTest.kt
index 7f3d79f..a447e44 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceManagerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceManagerTest.kt
@@ -41,7 +41,6 @@
import com.android.settingslib.media.MediaDevice
import com.android.settingslib.media.PhoneMediaDevice
import com.android.systemui.SysuiTestCase
-import com.android.systemui.dump.DumpManager
import com.android.systemui.media.controls.MediaTestUtils
import com.android.systemui.media.controls.shared.model.MediaData
import com.android.systemui.media.controls.shared.model.MediaDeviceData
@@ -98,7 +97,6 @@
@Mock private lateinit var muteAwaitManager: MediaMuteAwaitConnectionManager
private lateinit var fakeFgExecutor: FakeExecutor
private lateinit var fakeBgExecutor: FakeExecutor
- @Mock private lateinit var dumpster: DumpManager
@Mock private lateinit var listener: MediaDeviceManager.Listener
@Mock private lateinit var device: MediaDevice
@Mock private lateinit var icon: Drawable
@@ -133,7 +131,6 @@
{ localBluetoothManager },
fakeFgExecutor,
fakeBgExecutor,
- dumpster,
)
manager.addListener(listener)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaCarouselControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaCarouselControllerTest.kt
index f755199..59e2696c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaCarouselControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaCarouselControllerTest.kt
@@ -41,7 +41,6 @@
import com.android.systemui.media.controls.domain.pipeline.EMPTY_SMARTSPACE_MEDIA_DATA
import com.android.systemui.media.controls.domain.pipeline.MediaDataManager
import com.android.systemui.media.controls.shared.model.MediaData
-import com.android.systemui.media.controls.shared.model.SmartspaceMediaData
import com.android.systemui.media.controls.ui.controller.MediaHierarchyManager.Companion.LOCATION_QS
import com.android.systemui.media.controls.ui.view.MediaHostState
import com.android.systemui.media.controls.ui.view.MediaScrollView
@@ -111,7 +110,6 @@
@Mock lateinit var logger: MediaUiEventLogger
@Mock lateinit var debugLogger: MediaCarouselControllerLogger
@Mock lateinit var mediaViewController: MediaViewController
- @Mock lateinit var smartspaceMediaData: SmartspaceMediaData
@Mock lateinit var mediaCarousel: MediaScrollView
@Mock lateinit var pageIndicator: PageIndicator
@Mock lateinit var mediaFlags: MediaFlags
@@ -165,7 +163,6 @@
verify(mediaHostStatesManager).addCallback(capture(hostStateCallback))
whenever(mediaControlPanelFactory.get()).thenReturn(panel)
whenever(panel.mediaViewController).thenReturn(mediaViewController)
- whenever(mediaDataManager.smartspaceMediaData).thenReturn(smartspaceMediaData)
whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(false)
MediaPlayerData.clear()
verify(globalSettings)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/gestural/BackPanelControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/gestural/BackPanelControllerTest.kt
index aa54565..6e0919f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/gestural/BackPanelControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/gestural/BackPanelControllerTest.kt
@@ -28,9 +28,10 @@
import android.view.ViewConfiguration
import android.view.WindowManager
import androidx.test.filters.SmallTest
-import com.android.internal.jank.InteractionJankMonitor
import com.android.internal.util.LatencyTracker
import com.android.systemui.SysuiTestCase
+import com.android.systemui.jank.interactionJankMonitor
+import com.android.systemui.kosmos.Kosmos
import com.android.systemui.plugins.NavigationEdgeBackPlugin
import com.android.systemui.statusbar.VibratorHelper
import com.android.systemui.statusbar.policy.ConfigurationController
@@ -41,10 +42,8 @@
import org.mockito.ArgumentMatchers.any
import org.mockito.ArgumentMatchers.eq
import org.mockito.Mock
-import org.mockito.Mockito.anyInt
import org.mockito.Mockito.clearInvocations
import org.mockito.Mockito.verify
-import org.mockito.Mockito.`when`
import org.mockito.MockitoAnnotations
@SmallTest
@@ -62,16 +61,13 @@
@Mock private lateinit var windowManager: WindowManager
@Mock private lateinit var configurationController: ConfigurationController
@Mock private lateinit var latencyTracker: LatencyTracker
- @Mock private lateinit var interactionJankMonitor: InteractionJankMonitor
+ private val interactionJankMonitor = Kosmos().interactionJankMonitor
@Mock private lateinit var layoutParams: WindowManager.LayoutParams
@Mock private lateinit var backCallback: NavigationEdgeBackPlugin.BackCallback
@Before
fun setup() {
MockitoAnnotations.initMocks(this)
- `when`(interactionJankMonitor.begin(any(), anyInt())).thenReturn(true)
- `when`(interactionJankMonitor.end(anyInt())).thenReturn(true)
- `when`(interactionJankMonitor.cancel(anyInt())).thenReturn(true)
mBackPanelController =
BackPanelController(
context,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/people/widget/PeopleSpaceWidgetManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/people/widget/PeopleSpaceWidgetManagerTest.java
index a63b221..db0c0bc 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/people/widget/PeopleSpaceWidgetManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/people/widget/PeopleSpaceWidgetManagerTest.java
@@ -80,6 +80,8 @@
import android.app.people.PeopleManager;
import android.app.people.PeopleSpaceTile;
import android.appwidget.AppWidgetManager;
+import android.appwidget.AppWidgetProviderInfo;
+import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
@@ -101,11 +103,12 @@
import androidx.preference.PreferenceManager;
import androidx.test.filters.SmallTest;
-import com.android.systemui.res.R;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.people.PeopleBackupFollowUpJob;
import com.android.systemui.people.PeopleSpaceUtils;
import com.android.systemui.people.SharedPreferencesHelper;
+import com.android.systemui.res.R;
+import com.android.systemui.settings.FakeUserTracker;
import com.android.systemui.statusbar.NotificationListener;
import com.android.systemui.statusbar.NotificationListener.NotificationHandler;
import com.android.systemui.statusbar.SbnBuilder;
@@ -265,6 +268,8 @@
private final FakeExecutor mFakeExecutor = new FakeExecutor(mClock);
+ private final FakeUserTracker mUserTracker = new FakeUserTracker();
+
@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
@@ -272,7 +277,7 @@
mManager = new PeopleSpaceWidgetManager(mContext, mAppWidgetManager, mIPeopleManager,
mPeopleManager, mLauncherApps, mNotifCollection, mPackageManager,
Optional.of(mBubbles), mUserManager, mBackupManager, mINotificationManager,
- mNotificationManager, mFakeExecutor);
+ mNotificationManager, mFakeExecutor, mUserTracker);
mManager.attach(mListenerService);
verify(mListenerService).addNotificationHandler(mListenerCaptor.capture());
@@ -309,6 +314,12 @@
.setId(1)
.setShortcutInfo(mShortcutInfo)
.build();
+
+ AppWidgetProviderInfo providerInfo = new AppWidgetProviderInfo();
+ providerInfo.provider = new ComponentName("com.android.systemui.tests",
+ "com.android.systemui.people.widget.PeopleSpaceWidgetProvider");
+ when(mAppWidgetManager.getInstalledProvidersForPackage(anyString(), any()))
+ .thenReturn(List.of(providerInfo));
}
@Test
@@ -1562,6 +1573,43 @@
String.valueOf(WIDGET_ID_WITH_KEY_IN_OPTIONS));
}
+ @Test
+ public void testUpdateGeneratedPreview_flagDisabled() {
+ mSetFlagsRule.disableFlags(android.appwidget.flags.Flags.FLAG_GENERATED_PREVIEWS);
+ mManager.updateGeneratedPreviewForUser(mUserTracker.getUserHandle());
+ verify(mAppWidgetManager, times(0)).setWidgetPreview(any(), anyInt(), any());
+ }
+
+ @Test
+ public void testUpdateGeneratedPreview_userLocked() {
+ mSetFlagsRule.enableFlags(android.appwidget.flags.Flags.FLAG_GENERATED_PREVIEWS);
+ when(mUserManager.isUserUnlocked(mUserTracker.getUserHandle())).thenReturn(false);
+
+ mManager.updateGeneratedPreviewForUser(mUserTracker.getUserHandle());
+ verify(mAppWidgetManager, times(0)).setWidgetPreview(any(), anyInt(), any());
+ }
+
+ @Test
+ public void testUpdateGeneratedPreview_userUnlocked() {
+ mSetFlagsRule.enableFlags(android.appwidget.flags.Flags.FLAG_GENERATED_PREVIEWS);
+ when(mUserManager.isUserUnlocked(mUserTracker.getUserHandle())).thenReturn(true);
+ when(mAppWidgetManager.setWidgetPreview(any(), anyInt(), any())).thenReturn(true);
+
+ mManager.updateGeneratedPreviewForUser(mUserTracker.getUserHandle());
+ verify(mAppWidgetManager, times(1)).setWidgetPreview(any(), anyInt(), any());
+ }
+
+ @Test
+ public void testUpdateGeneratedPreview_doesNotSetTwice() {
+ mSetFlagsRule.enableFlags(android.appwidget.flags.Flags.FLAG_GENERATED_PREVIEWS);
+ when(mUserManager.isUserUnlocked(mUserTracker.getUserHandle())).thenReturn(true);
+ when(mAppWidgetManager.setWidgetPreview(any(), anyInt(), any())).thenReturn(true);
+
+ mManager.updateGeneratedPreviewForUser(mUserTracker.getUserHandle());
+ mManager.updateGeneratedPreviewForUser(mUserTracker.getUserHandle());
+ verify(mAppWidgetManager, times(1)).setWidgetPreview(any(), anyInt(), any());
+ }
+
private void setFinalField(String fieldName, int value) {
try {
Field field = NotificationManager.Policy.class.getDeclaredField(fieldName);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelTest.kt
index cc48640..5c6ed70 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelTest.kt
@@ -21,6 +21,7 @@
import android.testing.ViewUtils
import android.view.ContextThemeWrapper
import android.view.View
+import android.view.ViewGroup
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import android.view.accessibility.AccessibilityNodeInfo
import android.widget.FrameLayout
@@ -71,7 +72,7 @@
qsPanel = QSPanel(themedContext, null)
qsPanel.mUsingMediaPlayer = true
- qsPanel.initialize(qsLogger)
+ qsPanel.initialize(qsLogger, true)
// QSPanel inflates a footer inside of it, mocking it here
footer = LinearLayout(themedContext).apply { id = R.id.qs_footer }
qsPanel.addView(footer, MATCH_PARENT, 100)
@@ -218,6 +219,62 @@
verify(tile).addCallback(record.callback)
}
+ @Test
+ fun initializedWithNoMedia_tileLayoutParentIsAlwaysQsPanel() {
+ lateinit var panel: QSPanel
+ lateinit var tileLayout: View
+ testableLooper.runWithLooper {
+ panel = QSPanel(themedContext, null)
+ panel.mUsingMediaPlayer = true
+
+ panel.initialize(qsLogger, /* usingMediaPlayer= */ false)
+ tileLayout = panel.orCreateTileLayout as View
+ // QSPanel inflates a footer inside of it, mocking it here
+ footer = LinearLayout(themedContext).apply { id = R.id.qs_footer }
+ panel.addView(footer, MATCH_PARENT, 100)
+ panel.onFinishInflate()
+ // Provides a parent with non-zero size for QSPanel
+ ViewUtils.attachView(panel)
+ }
+ val mockMediaHost = mock(ViewGroup::class.java)
+
+ panel.setUsingHorizontalLayout(false, mockMediaHost, true)
+
+ assertThat(tileLayout.parent).isSameInstanceAs(panel)
+
+ panel.setUsingHorizontalLayout(true, mockMediaHost, true)
+ assertThat(tileLayout.parent).isSameInstanceAs(panel)
+
+ ViewUtils.detachView(panel)
+ }
+
+ @Test
+ fun initializeWithNoMedia_mediaNeverAttached() {
+ lateinit var panel: QSPanel
+ testableLooper.runWithLooper {
+ panel = QSPanel(themedContext, null)
+ panel.mUsingMediaPlayer = true
+
+ panel.initialize(qsLogger, /* usingMediaPlayer= */ false)
+ panel.orCreateTileLayout as View
+ // QSPanel inflates a footer inside of it, mocking it here
+ footer = LinearLayout(themedContext).apply { id = R.id.qs_footer }
+ panel.addView(footer, MATCH_PARENT, 100)
+ panel.onFinishInflate()
+ // Provides a parent with non-zero size for QSPanel
+ ViewUtils.attachView(panel)
+ }
+ val mockMediaHost = FrameLayout(themedContext)
+
+ panel.setUsingHorizontalLayout(false, mockMediaHost, true)
+ assertThat(mockMediaHost.parent).isNull()
+
+ panel.setUsingHorizontalLayout(true, mockMediaHost, true)
+ assertThat(mockMediaHost.parent).isNull()
+
+ ViewUtils.detachView(panel)
+ }
+
private infix fun View.isLeftOf(other: View): Boolean {
val rect = Rect()
getBoundsOnScreen(rect)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QuickQSPanelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/QuickQSPanelTest.kt
index 3fba393..e5369fc 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/QuickQSPanelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QuickQSPanelTest.kt
@@ -36,7 +36,7 @@
testableLooper.runWithLooper {
quickQSPanel = QuickQSPanel(mContext, null)
- quickQSPanel.initialize(qsLogger)
+ quickQSPanel.initialize(qsLogger, true)
quickQSPanel.onFinishInflate()
// Provides a parent with non-zero size for QSPanel
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/RecordIssueTileTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/RecordIssueTileTest.kt
index 761c411..37654d5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/RecordIssueTileTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/RecordIssueTileTest.kt
@@ -31,6 +31,7 @@
import com.android.systemui.qs.QsEventLogger
import com.android.systemui.qs.logging.QSLogger
import com.android.systemui.qs.pipeline.domain.interactor.PanelInteractor
+import com.android.systemui.recordissue.IssueRecordingState
import com.android.systemui.recordissue.RecordIssueDialogDelegate
import com.android.systemui.res.R
import com.android.systemui.settings.UserContextProvider
@@ -74,6 +75,7 @@
@Mock private lateinit var dialog: SystemUIDialog
private lateinit var testableLooper: TestableLooper
+ private val issueRecordingState = IssueRecordingState()
private lateinit var tile: RecordIssueTile
@Before
@@ -100,13 +102,14 @@
dialogLauncherAnimator,
panelInteractor,
userContextProvider,
+ issueRecordingState,
delegateFactory,
)
}
@Test
fun qsTileUi_shouldLookCorrect_whenInactive() {
- tile.isRecording = false
+ issueRecordingState.isRecording = false
val testState = tile.newTileState()
tile.handleUpdateState(testState, null)
@@ -118,8 +121,7 @@
@Test
fun qsTileUi_shouldLookCorrect_whenRecording() {
- tile.isRecording = true
-
+ issueRecordingState.isRecording = true
val testState = tile.newTileState()
tile.handleUpdateState(testState, null)
@@ -130,7 +132,7 @@
@Test
fun inActiveQsTile_switchesToActive_whenClicked() {
- tile.isRecording = false
+ issueRecordingState.isRecording = false
val testState = tile.newTileState()
tile.handleUpdateState(testState, null)
@@ -140,7 +142,7 @@
@Test
fun activeQsTile_switchesToInActive_whenClicked() {
- tile.isRecording = true
+ issueRecordingState.isRecording = true
val testState = tile.newTileState()
tile.handleUpdateState(testState, null)
@@ -150,7 +152,8 @@
@Test
fun showPrompt_shouldUseKeyguardDismissUtil_ToShowDialog() {
- tile.isRecording = false
+ issueRecordingState.isRecording = false
+
tile.handleClick(null)
testableLooper.processAllMessages()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothAutoOnInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothAutoOnInteractorTest.kt
index 3710713..036d3c8 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothAutoOnInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothAutoOnInteractorTest.kt
@@ -16,22 +16,25 @@
package com.android.systemui.qs.tiles.dialog.bluetooth
-import android.content.pm.UserInfo
+import android.bluetooth.BluetoothAdapter
import android.testing.AndroidTestingRunner
import androidx.test.filters.SmallTest
+import com.android.settingslib.bluetooth.LocalBluetoothManager
import com.android.systemui.SysuiTestCase
-import com.android.systemui.coroutines.collectLastValue
-import com.android.systemui.user.data.repository.FakeUserRepository
-import com.android.systemui.util.settings.FakeSettings
-import com.google.common.truth.Truth
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
import kotlin.test.Test
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Rule
import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyBoolean
+import org.mockito.Mock
import org.mockito.junit.MockitoJUnit
import org.mockito.junit.MockitoRule
@@ -41,8 +44,17 @@
@get:Rule val mockitoRule: MockitoRule = MockitoJUnit.rule()
private val testDispatcher = StandardTestDispatcher()
private val testScope = TestScope(testDispatcher)
- private var secureSettings: FakeSettings = FakeSettings()
- private val userRepository: FakeUserRepository = FakeUserRepository()
+ private val bluetoothAdapter =
+ mock<BluetoothAdapter> {
+ var autoOn = false
+ whenever(isAutoOnEnabled).thenAnswer { autoOn }
+
+ whenever(setAutoOnEnabled(anyBoolean())).thenAnswer { invocation ->
+ autoOn = invocation.getArgument(0) as Boolean
+ autoOn
+ }
+ }
+ @Mock private lateinit var localBluetoothManager: LocalBluetoothManager
private lateinit var bluetoothAutoOnInteractor: BluetoothAutoOnInteractor
@Before
@@ -50,49 +62,35 @@
bluetoothAutoOnInteractor =
BluetoothAutoOnInteractor(
BluetoothAutoOnRepository(
- secureSettings,
- userRepository,
+ localBluetoothManager,
+ bluetoothAdapter,
testScope.backgroundScope,
- testDispatcher
+ testDispatcher,
)
)
}
@Test
- fun testSet_bluetoothAutoOnUnset_doNothing() {
+ fun testSetEnabled_bluetoothAutoOnUnsupported_doNothing() {
testScope.runTest {
+ whenever(bluetoothAdapter.isAutoOnSupported).thenReturn(false)
+
bluetoothAutoOnInteractor.setEnabled(true)
-
- val actualValue by collectLastValue(bluetoothAutoOnInteractor.isEnabled)
-
runCurrent()
- Truth.assertThat(actualValue).isEqualTo(false)
+ assertFalse(bluetoothAdapter.isAutoOnEnabled)
}
}
@Test
- fun testSet_bluetoothAutoOnSet_setNewValue() {
+ fun testSetEnabled_bluetoothAutoOnSupported_setNewValue() {
testScope.runTest {
- userRepository.setUserInfos(listOf(SYSTEM_USER))
- secureSettings.putIntForUser(
- BluetoothAutoOnRepository.SETTING_NAME,
- BluetoothAutoOnInteractor.DISABLED,
- SYSTEM_USER_ID
- )
+ whenever(bluetoothAdapter.isAutoOnSupported).thenReturn(true)
+
bluetoothAutoOnInteractor.setEnabled(true)
-
- val actualValue by collectLastValue(bluetoothAutoOnInteractor.isEnabled)
-
runCurrent()
- Truth.assertThat(actualValue).isEqualTo(true)
+ assertTrue(bluetoothAdapter.isAutoOnEnabled)
}
}
-
- companion object {
- private const val SYSTEM_USER_ID = 0
- private val SYSTEM_USER =
- UserInfo(/* id= */ SYSTEM_USER_ID, /* name= */ "system user", /* flags= */ 0)
- }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothAutoOnRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothAutoOnRepositoryTest.kt
index cd1452a..3119284 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothAutoOnRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothAutoOnRepositoryTest.kt
@@ -16,18 +16,14 @@
package com.android.systemui.qs.tiles.dialog.bluetooth
-import android.content.pm.UserInfo
-import android.os.UserHandle
+import android.bluetooth.BluetoothAdapter
import android.testing.AndroidTestingRunner
import androidx.test.filters.SmallTest
+import com.android.settingslib.bluetooth.BluetoothEventManager
+import com.android.settingslib.bluetooth.LocalBluetoothManager
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
-import com.android.systemui.qs.tiles.dialog.bluetooth.BluetoothAutoOnInteractor.Companion.DISABLED
-import com.android.systemui.qs.tiles.dialog.bluetooth.BluetoothAutoOnInteractor.Companion.ENABLED
-import com.android.systemui.qs.tiles.dialog.bluetooth.BluetoothAutoOnRepository.Companion.SETTING_NAME
-import com.android.systemui.qs.tiles.dialog.bluetooth.BluetoothAutoOnRepository.Companion.UNSET
-import com.android.systemui.user.data.repository.FakeUserRepository
-import com.android.systemui.util.settings.FakeSettings
+import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestScope
@@ -37,6 +33,7 @@
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
+import org.mockito.Mock
import org.mockito.junit.MockitoJUnit
import org.mockito.junit.MockitoRule
@@ -46,83 +43,57 @@
@get:Rule val mockitoRule: MockitoRule = MockitoJUnit.rule()
private val testDispatcher = StandardTestDispatcher()
private val testScope = TestScope(testDispatcher)
- private var secureSettings: FakeSettings = FakeSettings()
- private val userRepository: FakeUserRepository = FakeUserRepository()
+ @Mock private lateinit var bluetoothAdapter: BluetoothAdapter
+ @Mock private lateinit var localBluetoothManager: LocalBluetoothManager
+ @Mock private lateinit var eventManager: BluetoothEventManager
private lateinit var bluetoothAutoOnRepository: BluetoothAutoOnRepository
@Before
fun setUp() {
+ whenever(localBluetoothManager.eventManager).thenReturn(eventManager)
bluetoothAutoOnRepository =
BluetoothAutoOnRepository(
- secureSettings,
- userRepository,
+ localBluetoothManager,
+ bluetoothAdapter,
testScope.backgroundScope,
- testDispatcher
+ testDispatcher,
)
-
- userRepository.setUserInfos(listOf(SECONDARY_USER, SYSTEM_USER))
}
@Test
- fun testGetValue_valueUnset() {
+ fun testIsAutoOn_returnFalse() {
testScope.runTest {
- userRepository.setSelectedUserInfo(SYSTEM_USER)
+ whenever(bluetoothAdapter.isAutoOnEnabled).thenReturn(false)
val actualValue by collectLastValue(bluetoothAutoOnRepository.isAutoOn)
runCurrent()
- assertThat(actualValue).isEqualTo(UNSET)
- assertThat(bluetoothAutoOnRepository.isValuePresent()).isFalse()
+ assertThat(actualValue).isEqualTo(false)
}
}
@Test
- fun testGetValue_valueFalse() {
+ fun testIsAutoOn_returnTrue() {
testScope.runTest {
- userRepository.setSelectedUserInfo(SYSTEM_USER)
+ whenever(bluetoothAdapter.isAutoOnEnabled).thenReturn(true)
val actualValue by collectLastValue(bluetoothAutoOnRepository.isAutoOn)
- secureSettings.putIntForUser(SETTING_NAME, DISABLED, UserHandle.USER_SYSTEM)
runCurrent()
- assertThat(actualValue).isEqualTo(DISABLED)
+ assertThat(actualValue).isEqualTo(true)
}
}
@Test
- fun testGetValue_valueTrue() {
+ fun testIsAutoOnSupported_returnTrue() {
testScope.runTest {
- userRepository.setSelectedUserInfo(SYSTEM_USER)
- val actualValue by collectLastValue(bluetoothAutoOnRepository.isAutoOn)
+ whenever(bluetoothAdapter.isAutoOnSupported).thenReturn(true)
+ val actualValue = bluetoothAutoOnRepository.isAutoOnSupported()
- secureSettings.putIntForUser(SETTING_NAME, ENABLED, UserHandle.USER_SYSTEM)
runCurrent()
- assertThat(actualValue).isEqualTo(ENABLED)
+ assertThat(actualValue).isEqualTo(true)
}
}
-
- @Test
- fun testGetValue_valueTrue_secondaryUser_returnTrue() {
- testScope.runTest {
- userRepository.setSelectedUserInfo(SECONDARY_USER)
- val actualValue by collectLastValue(bluetoothAutoOnRepository.isAutoOn)
-
- secureSettings.putIntForUser(SETTING_NAME, DISABLED, SYSTEM_USER_ID)
- secureSettings.putIntForUser(SETTING_NAME, ENABLED, SECONDARY_USER_ID)
- runCurrent()
-
- assertThat(actualValue).isEqualTo(ENABLED)
- }
- }
-
- companion object {
- private const val SYSTEM_USER_ID = 0
- private const val SECONDARY_USER_ID = 1
- private val SYSTEM_USER =
- UserInfo(/* id= */ SYSTEM_USER_ID, /* name= */ "system user", /* flags= */ 0)
- private val SECONDARY_USER =
- UserInfo(/* id= */ SECONDARY_USER_ID, /* name= */ "secondary user", /* flags= */ 0)
- }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogDelegateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogDelegateTest.kt
index 8ecb953..17b6127 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogDelegateTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogDelegateTest.kt
@@ -109,7 +109,6 @@
mBluetoothTileDialogDelegate =
BluetoothTileDialogDelegate(
- mContext,
uiProperties,
CONTENT_HEIGHT,
ENABLED,
@@ -119,14 +118,12 @@
fakeSystemClock,
uiEventLogger,
logger,
- sysuiDialogFactory,
- LayoutInflater.from(mContext)
+ sysuiDialogFactory
)
whenever(
sysuiDialogFactory.create(
- any(SystemUIDialog.Delegate::class.java),
- any(Context::class.java)
+ any(SystemUIDialog.Delegate::class.java)
)
)
.thenAnswer {
@@ -216,7 +213,6 @@
LayoutInflater.from(mContext).inflate(R.layout.bluetooth_device_item, null, false)
val viewHolder =
BluetoothTileDialogDelegate(
- mContext,
uiProperties,
CONTENT_HEIGHT,
ENABLED,
@@ -227,7 +223,6 @@
uiEventLogger,
logger,
sysuiDialogFactory,
- LayoutInflater.from(mContext)
)
.Adapter(bluetoothTileDialogCallback)
.DeviceItemViewHolder(view)
@@ -273,7 +268,6 @@
val cachedHeight = Int.MAX_VALUE
val dialog =
BluetoothTileDialogDelegate(
- mContext,
BluetoothTileDialogViewModel.UiProperties.build(ENABLED, ENABLED),
cachedHeight,
ENABLED,
@@ -284,7 +278,6 @@
uiEventLogger,
logger,
sysuiDialogFactory,
- LayoutInflater.from(mContext)
)
.createDialog()
dialog.show()
@@ -298,7 +291,6 @@
testScope.runTest {
val dialog =
BluetoothTileDialogDelegate(
- mContext,
BluetoothTileDialogViewModel.UiProperties.build(ENABLED, ENABLED),
MATCH_PARENT,
ENABLED,
@@ -309,7 +301,6 @@
uiEventLogger,
logger,
sysuiDialogFactory,
- LayoutInflater.from(mContext)
)
.createDialog()
dialog.show()
@@ -323,7 +314,6 @@
testScope.runTest {
val dialog =
BluetoothTileDialogDelegate(
- mContext,
BluetoothTileDialogViewModel.UiProperties.build(ENABLED, ENABLED),
MATCH_PARENT,
ENABLED,
@@ -334,7 +324,6 @@
uiEventLogger,
logger,
sysuiDialogFactory,
- LayoutInflater.from(mContext)
)
.createDialog()
dialog.show()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogViewModelTest.kt
index 39e2413..c8a2aa6 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogViewModelTest.kt
@@ -16,7 +16,7 @@
package com.android.systemui.qs.tiles.dialog.bluetooth
-import android.content.pm.UserInfo
+import android.bluetooth.BluetoothAdapter
import android.testing.AndroidTestingRunner
import android.testing.TestableLooper
import android.view.View
@@ -26,19 +26,18 @@
import androidx.test.filters.SmallTest
import com.android.internal.logging.UiEventLogger
import com.android.settingslib.bluetooth.CachedBluetoothDevice
+import com.android.settingslib.bluetooth.LocalBluetoothManager
import com.android.settingslib.flags.Flags
import com.android.systemui.SysuiTestCase
import com.android.systemui.animation.DialogTransitionAnimator
import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.statusbar.phone.SystemUIDialog
-import com.android.systemui.user.data.repository.FakeUserRepository
import com.android.systemui.util.FakeSharedPreferences
import com.android.systemui.util.concurrency.FakeExecutor
import com.android.systemui.util.kotlin.getMutableStateFlow
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.nullable
import com.android.systemui.util.mockito.whenever
-import com.android.systemui.util.settings.FakeSettings
import com.android.systemui.util.time.FakeSystemClock
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.CoroutineDispatcher
@@ -75,6 +74,8 @@
@Mock private lateinit var bluetoothStateInteractor: BluetoothStateInteractor
+ @Mock private lateinit var bluetoothAutoOnInteractor: BluetoothAutoOnInteractor
+
@Mock private lateinit var deviceItemInteractor: DeviceItemInteractor
@Mock private lateinit var activityStarter: ActivityStarter
@@ -87,6 +88,10 @@
@Mock private lateinit var uiEventLogger: UiEventLogger
+ @Mock private lateinit var bluetoothAdapter: BluetoothAdapter
+
+ @Mock private lateinit var localBluetoothManager: LocalBluetoothManager
+
@Mock
private lateinit var mBluetoothTileDialogDelegateDelegateFactory:
BluetoothTileDialogDelegate.Factory
@@ -100,8 +105,6 @@
private lateinit var scheduler: TestCoroutineScheduler
private lateinit var dispatcher: CoroutineDispatcher
private lateinit var testScope: TestScope
- private lateinit var secureSettings: FakeSettings
- private lateinit var userRepository: FakeUserRepository
@Before
fun setUp() {
@@ -109,14 +112,6 @@
scheduler = TestCoroutineScheduler()
dispatcher = UnconfinedTestDispatcher(scheduler)
testScope = TestScope(dispatcher)
- secureSettings = FakeSettings()
- userRepository = FakeUserRepository()
- userRepository.setUserInfos(listOf(SYSTEM_USER))
- secureSettings.putIntForUser(
- BluetoothAutoOnRepository.SETTING_NAME,
- BluetoothAutoOnInteractor.ENABLED,
- SYSTEM_USER_ID
- )
bluetoothTileDialogViewModel =
BluetoothTileDialogViewModel(
deviceItemInteractor,
@@ -124,8 +119,8 @@
// TODO(b/316822488): Create FakeBluetoothAutoOnInteractor.
BluetoothAutoOnInteractor(
BluetoothAutoOnRepository(
- secureSettings,
- userRepository,
+ localBluetoothManager,
+ bluetoothAdapter,
testScope.backgroundScope,
dispatcher
)
@@ -148,7 +143,6 @@
whenever(
mBluetoothTileDialogDelegateDelegateFactory.create(
any(),
- any(),
anyInt(),
ArgumentMatchers.anyBoolean(),
any(),
@@ -157,6 +151,7 @@
)
.thenReturn(bluetoothTileDialogDelegate)
whenever(bluetoothTileDialogDelegate.createDialog()).thenReturn(sysuiDialog)
+ whenever(sysuiDialog.context).thenReturn(mContext)
whenever(bluetoothTileDialogDelegate.bluetoothStateToggle)
.thenReturn(getMutableStateFlow(false))
whenever(bluetoothTileDialogDelegate.deviceItemClick)
@@ -169,7 +164,7 @@
@Test
fun testShowDialog_noAnimation() {
testScope.runTest {
- bluetoothTileDialogViewModel.showDialog(context, null)
+ bluetoothTileDialogViewModel.showDialog(null)
verify(mDialogTransitionAnimator, never()).showFromView(any(), any(), any(), any())
}
@@ -178,7 +173,7 @@
@Test
fun testShowDialog_animated() {
testScope.runTest {
- bluetoothTileDialogViewModel.showDialog(mContext, LinearLayout(mContext))
+ bluetoothTileDialogViewModel.showDialog(LinearLayout(mContext))
verify(mDialogTransitionAnimator).showFromView(any(), any(), nullable(), anyBoolean())
}
@@ -188,7 +183,7 @@
fun testShowDialog_animated_callInBackgroundThread() {
testScope.runTest {
backgroundExecutor.execute {
- bluetoothTileDialogViewModel.showDialog(mContext, LinearLayout(mContext))
+ bluetoothTileDialogViewModel.showDialog(LinearLayout(mContext))
verify(mDialogTransitionAnimator)
.showFromView(any(), any(), nullable(), anyBoolean())
@@ -199,7 +194,7 @@
@Test
fun testShowDialog_fetchDeviceItem() {
testScope.runTest {
- bluetoothTileDialogViewModel.showDialog(context, null)
+ bluetoothTileDialogViewModel.showDialog(null)
verify(deviceItemInteractor).deviceItemUpdate
}
@@ -208,7 +203,7 @@
@Test
fun testShowDialog_withBluetoothStateValue() {
testScope.runTest {
- bluetoothTileDialogViewModel.showDialog(context, null)
+ bluetoothTileDialogViewModel.showDialog(null)
verify(bluetoothStateInteractor).bluetoothStateUpdate
}
@@ -218,7 +213,7 @@
fun testStartSettingsActivity_activityLaunched_dialogDismissed() {
testScope.runTest {
whenever(deviceItem.cachedBluetoothDevice).thenReturn(cachedBluetoothDevice)
- bluetoothTileDialogViewModel.showDialog(context, null)
+ bluetoothTileDialogViewModel.showDialog(null)
val clickedView = View(context)
bluetoothTileDialogViewModel.onPairNewDeviceClicked(clickedView)
@@ -265,26 +260,22 @@
}
@Test
- fun testIsAutoOnToggleFeatureAvailable_flagOn_settingValueSet_returnTrue() {
+ fun testIsAutoOnToggleFeatureAvailable_returnTrue() {
testScope.runTest {
+ whenever(bluetoothAdapter.isAutoOnSupported).thenReturn(true)
+
val actual = bluetoothTileDialogViewModel.isAutoOnToggleFeatureAvailable()
assertThat(actual).isTrue()
}
}
@Test
- fun testIsAutoOnToggleFeatureAvailable_flagOff_settingValueSet_returnFalse() {
+ fun testIsAutoOnToggleFeatureAvailable_returnFalse() {
testScope.runTest {
- mSetFlagsRule.disableFlags(Flags.FLAG_BLUETOOTH_QS_TILE_DIALOG_AUTO_ON_TOGGLE)
+ whenever(bluetoothAdapter.isAutoOnSupported).thenReturn(false)
val actual = bluetoothTileDialogViewModel.isAutoOnToggleFeatureAvailable()
assertThat(actual).isFalse()
}
}
-
- companion object {
- private const val SYSTEM_USER_ID = 0
- private val SYSTEM_USER =
- UserInfo(/* id= */ SYSTEM_USER_ID, /* name= */ "system user", /* flags= */ 0)
- }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/impl/work/ui/WorkModeTileMapperTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/impl/work/ui/WorkModeTileMapperTest.kt
new file mode 100644
index 0000000..4215b8c
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/impl/work/ui/WorkModeTileMapperTest.kt
@@ -0,0 +1,145 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.tiles.impl.work.ui
+
+import android.app.admin.DevicePolicyResources
+import android.app.admin.DevicePolicyResourcesManager
+import android.app.admin.devicePolicyManager
+import android.graphics.drawable.TestStubDrawable
+import android.service.quicksettings.Tile
+import android.widget.Switch
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.qs.tiles.impl.custom.QSTileStateSubject
+import com.android.systemui.qs.tiles.impl.work.domain.model.WorkModeTileModel
+import com.android.systemui.qs.tiles.impl.work.qsWorkModeTileConfig
+import com.android.systemui.qs.tiles.viewmodel.QSTileState
+import com.android.systemui.res.R
+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 org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class WorkModeTileMapperTest : SysuiTestCase() {
+ private val kosmos = Kosmos()
+ private val qsTileConfig = kosmos.qsWorkModeTileConfig
+ private val devicePolicyManager = kosmos.devicePolicyManager
+ private val testLabel = context.getString(R.string.quick_settings_work_mode_label)
+ private val devicePolicyResourceManager = mock<DevicePolicyResourcesManager>()
+ private lateinit var mapper: WorkModeTileMapper
+
+ @Before
+ fun setup() {
+ whenever(devicePolicyManager.resources).thenReturn(devicePolicyResourceManager)
+ whenever(
+ devicePolicyResourceManager.getString(
+ eq(DevicePolicyResources.Strings.SystemUi.QS_WORK_PROFILE_LABEL),
+ any()
+ )
+ )
+ .thenReturn(testLabel)
+ mapper =
+ WorkModeTileMapper(
+ context.orCreateTestableResources
+ .apply {
+ addOverride(
+ com.android.internal.R.drawable.stat_sys_managed_profile_status,
+ TestStubDrawable()
+ )
+ }
+ .resources,
+ context.theme,
+ devicePolicyManager
+ )
+ }
+
+ @Test
+ fun mapsDisabledDataToInactiveState() {
+ val isEnabled = false
+
+ val actualState: QSTileState =
+ mapper.map(qsTileConfig, WorkModeTileModel.HasActiveProfile(isEnabled))
+
+ val expectedState = createWorkModeTileState(QSTileState.ActivationState.INACTIVE)
+ QSTileStateSubject.assertThat(actualState).isEqualTo(expectedState)
+ }
+
+ @Test
+ fun mapsEnabledDataToActiveState() {
+ val isEnabled = true
+
+ val actualState: QSTileState =
+ mapper.map(qsTileConfig, WorkModeTileModel.HasActiveProfile(isEnabled))
+
+ val expectedState = createWorkModeTileState(QSTileState.ActivationState.ACTIVE)
+ QSTileStateSubject.assertThat(actualState).isEqualTo(expectedState)
+ }
+
+ @Test
+ fun mapsNoActiveProfileDataToUnavailableState() {
+ val actualState: QSTileState = mapper.map(qsTileConfig, WorkModeTileModel.NoActiveProfile)
+
+ val expectedState = createWorkModeTileState(QSTileState.ActivationState.UNAVAILABLE)
+ QSTileStateSubject.assertThat(actualState).isEqualTo(expectedState)
+ }
+
+ private fun createWorkModeTileState(
+ activationState: QSTileState.ActivationState,
+ ): QSTileState {
+ val label = testLabel
+ return QSTileState(
+ icon = {
+ Icon.Loaded(
+ context.getDrawable(
+ com.android.internal.R.drawable.stat_sys_managed_profile_status
+ )!!,
+ null
+ )
+ },
+ label = label,
+ activationState = activationState,
+ secondaryLabel =
+ if (activationState == QSTileState.ActivationState.INACTIVE) {
+ context.getString(R.string.quick_settings_work_mode_paused_state)
+ } else if (activationState == QSTileState.ActivationState.UNAVAILABLE) {
+ context.resources
+ .getStringArray(R.array.tile_states_work)[Tile.STATE_UNAVAILABLE]
+ } else {
+ ""
+ },
+ supportedActions =
+ if (activationState == QSTileState.ActivationState.UNAVAILABLE) {
+ setOf()
+ } else {
+ setOf(QSTileState.UserAction.CLICK, QSTileState.UserAction.LONG_CLICK)
+ },
+ contentDescription = label,
+ stateDescription = null,
+ sideViewIcon = QSTileState.SideViewIcon.None,
+ enabledState = QSTileState.EnabledState.ENABLED,
+ expandedAccessibilityClassName = Switch::class.qualifiedName
+ )
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/recents/OverviewProxyServiceTest.kt b/packages/SystemUI/tests/src/com/android/systemui/recents/OverviewProxyServiceTest.kt
index 10d6ebf..1313227 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/recents/OverviewProxyServiceTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/recents/OverviewProxyServiceTest.kt
@@ -21,7 +21,7 @@
import android.content.pm.PackageManager
import android.content.pm.ResolveInfo
import android.os.PowerManager
-import android.os.Process;
+import android.os.Process
import android.os.UserHandle
import android.testing.AndroidTestingRunner
import android.testing.TestableContext
@@ -34,8 +34,6 @@
import com.android.systemui.broadcast.BroadcastDispatcher
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.dump.DumpManager
-import com.android.systemui.flags.FakeFeatureFlags
-import com.android.systemui.flags.Flags
import com.android.systemui.keyguard.KeyguardUnlockAnimationController
import com.android.systemui.keyguard.WakefulnessLifecycle
import com.android.systemui.keyguard.ui.view.InWindowLauncherUnlockAnimationManager
@@ -96,7 +94,6 @@
private val displayTracker = FakeDisplayTracker(mContext)
private val fakeSystemClock = FakeSystemClock()
private val sysUiState = SysUiState(displayTracker, kosmos.sceneContainerPlugin)
- private val featureFlags = FakeFeatureFlags()
private val wakefulnessLifecycle =
WakefulnessLifecycle(mContext, null, fakeSystemClock, dumpManager)
@@ -121,8 +118,7 @@
@Mock
private lateinit var unfoldTransitionProgressForwarder:
Optional<UnfoldTransitionProgressForwarder>
- @Mock
- private lateinit var broadcastDispatcher: BroadcastDispatcher
+ @Mock private lateinit var broadcastDispatcher: BroadcastDispatcher
@Before
fun setUp() {
@@ -205,16 +201,14 @@
@Test
fun connectToOverviewService_primaryUser_expectBindService() {
- val mockitoSession = ExtendedMockito.mockitoSession()
- .spyStatic(Process::class.java)
- .startMocking()
+ val mockitoSession =
+ ExtendedMockito.mockitoSession().spyStatic(Process::class.java).startMocking()
try {
`when`(Process.myUserHandle()).thenReturn(UserHandle.SYSTEM)
val spyContext = spy(context)
val ops = createOverviewProxyService(spyContext)
ops.startConnectionToCurrentUser()
- verify(spyContext, atLeast(1)).bindServiceAsUser(any(), any(),
- anyInt(), any())
+ verify(spyContext, atLeast(1)).bindServiceAsUser(any(), any(), anyInt(), any())
} finally {
mockitoSession.finishMocking()
}
@@ -222,22 +216,20 @@
@Test
fun connectToOverviewService_nonPrimaryUser_expectNoBindService() {
- val mockitoSession = ExtendedMockito.mockitoSession()
- .spyStatic(Process::class.java)
- .startMocking()
+ val mockitoSession =
+ ExtendedMockito.mockitoSession().spyStatic(Process::class.java).startMocking()
try {
`when`(Process.myUserHandle()).thenReturn(UserHandle.of(12345))
val spyContext = spy(context)
val ops = createOverviewProxyService(spyContext)
ops.startConnectionToCurrentUser()
- verify(spyContext, times(0)).bindServiceAsUser(any(), any(),
- anyInt(), any())
+ verify(spyContext, times(0)).bindServiceAsUser(any(), any(), anyInt(), any())
} finally {
mockitoSession.finishMocking()
}
}
- private fun createOverviewProxyService(ctx: Context) : OverviewProxyService {
+ private fun createOverviewProxyService(ctx: Context): OverviewProxyService {
return OverviewProxyService(
ctx,
executor,
@@ -257,7 +249,6 @@
sysuiUnlockAnimationController,
inWindowLauncherUnlockAnimationManager,
assistUtils,
- featureFlags,
FakeSceneContainerFlags(),
dumpManager,
unfoldTransitionProgressForwarder,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/recordissue/RecordIssueDialogDelegateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/recordissue/RecordIssueDialogDelegateTest.kt
index 2e8160b..1cfca68 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/recordissue/RecordIssueDialogDelegateTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/recordissue/RecordIssueDialogDelegateTest.kt
@@ -222,4 +222,9 @@
)
verify(factory, never()).create(any<ScreenCapturePermissionDialogDelegate>())
}
+
+ @Test
+ fun startButton_isDisabled_beforeIssueTypeIsSelected() {
+ assertThat(dialog.getButton(Dialog.BUTTON_POSITIVE).isEnabled).isFalse()
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
index 43fcdf3..c25b910 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
@@ -62,7 +62,6 @@
import androidx.constraintlayout.widget.ConstraintSet;
-import com.android.internal.jank.InteractionJankMonitor;
import com.android.internal.logging.MetricsLogger;
import com.android.internal.logging.UiEventLogger;
import com.android.internal.logging.testing.UiEventLoggerFake;
@@ -299,7 +298,6 @@
@Mock protected RecordingController mRecordingController;
@Mock protected LockscreenGestureLogger mLockscreenGestureLogger;
@Mock protected DumpManager mDumpManager;
- @Mock protected InteractionJankMonitor mInteractionJankMonitor;
@Mock protected NotificationsQSContainerController mNotificationsQSContainerController;
@Mock protected QsFrameTranslateController mQsFrameTranslateController;
@Mock protected StatusBarWindowStateController mStatusBarWindowStateController;
@@ -441,7 +439,7 @@
SystemClock systemClock = new FakeSystemClock();
mStatusBarStateController = new StatusBarStateControllerImpl(
mUiEventLogger,
- mInteractionJankMonitor,
+ mKosmos.getInteractionJankMonitor(),
mJavaAdapter,
() -> mShadeInteractor,
() -> mKosmos.getDeviceUnlockedInteractor(),
@@ -459,7 +457,7 @@
mDozeParameters,
mScreenOffAnimationController,
mKeyguardLogger,
- mInteractionJankMonitor,
+ mKosmos.getInteractionJankMonitor(),
mKeyguardInteractor,
mDumpManager,
mPowerInteractor));
@@ -611,7 +609,7 @@
mock(HeadsUpManager.class),
new StatusBarStateControllerImpl(
new UiEventLoggerFake(),
- mInteractionJankMonitor,
+ mKosmos.getInteractionJankMonitor(),
mJavaAdapter,
() -> mShadeInteractor,
() -> mKosmos.getDeviceUnlockedInteractor(),
@@ -651,10 +649,6 @@
.thenReturn(mKeyguardBottomArea);
when(mNotificationRemoteInputManager.isRemoteInputActive())
.thenReturn(false);
- when(mInteractionJankMonitor.begin(any(), anyInt()))
- .thenReturn(true);
- when(mInteractionJankMonitor.end(anyInt()))
- .thenReturn(true);
doAnswer(invocation -> {
((Runnable) invocation.getArgument(0)).run();
return null;
@@ -820,7 +814,7 @@
mAccessibilityManager,
mLockscreenGestureLogger,
mMetricsLogger,
- mInteractionJankMonitor,
+ mKosmos.getInteractionJankMonitor(),
mShadeLog,
mDumpManager,
mDeviceEntryFaceAuthInteractor,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorTest.java
index 419b0fd..118d27a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorTest.java
@@ -251,7 +251,7 @@
mCollectionListener.onEntryInit(mEntry);
mBeforeFilterListener.onBeforeFinalizeFilter(List.of(mEntry));
verify(mNotifInflater).inflateViews(eq(mEntry), mParamsCaptor.capture(), any());
- assertFalse(mParamsCaptor.getValue().isLowPriority());
+ assertFalse(mParamsCaptor.getValue().isMinimized());
mNotifInflater.invokeInflateCallbackForEntry(mEntry);
// WHEN notification moves to a min priority section
@@ -260,7 +260,7 @@
// THEN we rebind it
verify(mNotifInflater).rebindViews(eq(mEntry), mParamsCaptor.capture(), any());
- assertTrue(mParamsCaptor.getValue().isLowPriority());
+ assertTrue(mParamsCaptor.getValue().isMinimized());
// THEN we do not filter it because it's not the first inflation.
assertFalse(mUninflatedFilter.shouldFilterOut(mEntry, 0));
@@ -273,7 +273,7 @@
mCollectionListener.onEntryInit(mEntry);
mBeforeFilterListener.onBeforeFinalizeFilter(List.of(mEntry));
verify(mNotifInflater).inflateViews(eq(mEntry), mParamsCaptor.capture(), any());
- assertTrue(mParamsCaptor.getValue().isLowPriority());
+ assertTrue(mParamsCaptor.getValue().isMinimized());
mNotifInflater.invokeInflateCallbackForEntry(mEntry);
// WHEN notification is moved under a parent
@@ -282,7 +282,7 @@
// THEN we rebind it as not-minimized
verify(mNotifInflater).rebindViews(eq(mEntry), mParamsCaptor.capture(), any());
- assertFalse(mParamsCaptor.getValue().isLowPriority());
+ assertFalse(mParamsCaptor.getValue().isMinimized());
// THEN we do not filter it because it's not the first inflation.
assertFalse(mUninflatedFilter.shouldFilterOut(mEntry, 0));
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java
index b114e13..0e89d80 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java
@@ -741,7 +741,7 @@
when(mockViewWrapper.getIcon()).thenReturn(mockIcon);
NotificationViewWrapper mockLowPriorityViewWrapper = mock(NotificationViewWrapper.class);
- when(mockContainer.getLowPriorityViewWrapper()).thenReturn(mockLowPriorityViewWrapper);
+ when(mockContainer.getMinimizedGroupHeaderWrapper()).thenReturn(mockLowPriorityViewWrapper);
CachingIconView mockLowPriorityIcon = mock(CachingIconView.class);
when(mockLowPriorityViewWrapper.getIcon()).thenReturn(mockLowPriorityIcon);
@@ -845,7 +845,6 @@
}
@Test
- @EnableFlags(com.android.systemui.Flags.FLAG_NOTIFICATION_ROW_USER_CONTEXT)
public void imageResolver_differentNotificationUser_createsUserContext() throws Exception {
UserHandle user = new UserHandle(33);
Context userContext = new SysuiTestableContext(mContext);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterTest.java
index a0d1075..8c22511 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterTest.java
@@ -231,6 +231,7 @@
NotificationContentInflater.applyRemoteView(
AsyncTask.SERIAL_EXECUTOR,
false /* inflateSynchronously */,
+ /* isMinimized= */ false,
result,
FLAG_CONTENT_VIEW_EXPANDED,
0,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerTest.java
index 6549193..fe0d9d0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerTest.java
@@ -23,6 +23,7 @@
import static android.app.NotificationManager.IMPORTANCE_HIGH;
import static android.service.notification.NotificationListenerService.Ranking.USER_SENTIMENT_NEGATIVE;
+import static com.android.systemui.concurrency.FakeExecutorKosmosKt.getFakeExecutor;
import static com.android.systemui.statusbar.NotificationEntryHelper.modifyRanking;
import static junit.framework.Assert.assertNotNull;
@@ -124,12 +125,11 @@
private NotificationChannel mTestNotificationChannel = new NotificationChannel(
TEST_CHANNEL_ID, TEST_CHANNEL_ID, IMPORTANCE_DEFAULT);
- private KosmosJavaAdapter mKosmos = new KosmosJavaAdapter(this);
- private TestScope mTestScope = mKosmos.getTestScope();
- private JavaAdapter mJavaAdapter = new JavaAdapter(mTestScope.getBackgroundScope());
- private FakeExecutor mExecutor = new FakeExecutor(new FakeSystemClock());
- private TestableLooper mTestableLooper;
- private Handler mHandler;
+ private final KosmosJavaAdapter mKosmos = new KosmosJavaAdapter(this);
+ private final TestScope mTestScope = mKosmos.getTestScope();
+ private final JavaAdapter mJavaAdapter = new JavaAdapter(mTestScope.getBackgroundScope());
+ private final FakeExecutor mExecutor = mKosmos.getFakeExecutor();
+ private final Handler mHandler = mKosmos.getFakeExecutorHandler();
private NotificationTestHelper mHelper;
private NotificationGutsManager mGutsManager;
@@ -171,10 +171,8 @@
@Before
public void setUp() {
- mTestableLooper = TestableLooper.get(this);
allowTestableLooperAsMainThread();
- mHandler = Handler.createAsync(mTestableLooper.getLooper());
- mHelper = new NotificationTestHelper(mContext, mDependency, TestableLooper.get(this));
+ mHelper = new NotificationTestHelper(mContext, mDependency);
when(mAccessibilityManager.isTouchExplorationEnabled()).thenReturn(false);
mWindowRootViewVisibilityInteractor = new WindowRootViewVisibilityInteractor(
@@ -248,7 +246,7 @@
assertTrue(mGutsManager.openGutsInternal(row, 0, 0, menuItem));
assertEquals(View.INVISIBLE, guts.getVisibility());
- mTestableLooper.processAllMessages();
+ mExecutor.runAllReady();
verify(guts).openControls(
anyInt(),
anyInt(),
@@ -261,7 +259,7 @@
verify(guts).closeControls(anyBoolean(), anyBoolean(), anyInt(), anyInt(), anyBoolean());
verify(row, times(1)).setGutsView(any());
- mTestableLooper.processAllMessages();
+ mExecutor.runAllReady();
verify(mHeadsUpManager).setGutsShown(realRow.getEntry(), false);
}
@@ -352,7 +350,7 @@
when(entry.getGuts()).thenReturn(guts);
assertTrue(mGutsManager.openGutsInternal(row, 0, 0, menuItem));
- mTestableLooper.processAllMessages();
+ mExecutor.runAllReady();
verify(guts).openControls(
anyInt(),
anyInt(),
@@ -365,7 +363,7 @@
row.onDensityOrFontScaleChanged();
mGutsManager.onDensityOrFontScaleChanged(entry);
- mTestableLooper.processAllMessages();
+ mExecutor.runAllReady();
mGutsManager.closeAndSaveGuts(false, false, false, 0, 0, false);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerWithScenesTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerWithScenesTest.kt
index 012ff2e..65a960b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerWithScenesTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerWithScenesTest.kt
@@ -27,12 +27,11 @@
import android.content.pm.launcherApps
import android.graphics.Color
import android.os.Binder
-import android.os.Handler
+import android.os.fakeExecutorHandler
import android.os.userManager
import android.provider.Settings
import android.service.notification.NotificationListenerService.Ranking
import android.testing.AndroidTestingRunner
-import android.testing.TestableLooper
import android.testing.TestableLooper.RunWithLooper
import android.util.ArraySet
import android.view.View
@@ -45,6 +44,7 @@
import com.android.internal.logging.testing.UiEventLoggerFake
import com.android.internal.statusbar.statusBarService
import com.android.systemui.SysuiTestCase
+import com.android.systemui.concurrency.fakeExecutor
import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
import com.android.systemui.kosmos.testScope
import com.android.systemui.people.widget.PeopleSpaceWidgetManager
@@ -71,9 +71,7 @@
import com.android.systemui.statusbar.policy.deviceProvisionedController
import com.android.systemui.statusbar.policy.headsUpManager
import com.android.systemui.testKosmos
-import com.android.systemui.util.concurrency.FakeExecutor
import com.android.systemui.util.kotlin.JavaAdapter
-import com.android.systemui.util.time.FakeSystemClock
import com.android.systemui.wmshell.BubblesManager
import java.util.Optional
import junit.framework.Assert
@@ -106,9 +104,8 @@
private val kosmos = testKosmos()
private val testScope = kosmos.testScope
private val javaAdapter = JavaAdapter(testScope.backgroundScope)
- private val executor = FakeExecutor(FakeSystemClock())
- private lateinit var testableLooper: TestableLooper
- private lateinit var handler: Handler
+ private val executor = kosmos.fakeExecutor
+ private val handler = kosmos.fakeExecutorHandler
private lateinit var helper: NotificationTestHelper
private lateinit var gutsManager: NotificationGutsManager
private lateinit var windowRootViewVisibilityInteractor: WindowRootViewVisibilityInteractor
@@ -148,10 +145,8 @@
MockitoAnnotations.initMocks(this)
val sceneContainerFlags = kosmos.fakeSceneContainerFlags
sceneContainerFlags.enabled = true
- testableLooper = TestableLooper.get(this)
allowTestableLooperAsMainThread()
- handler = Handler.createAsync(testableLooper.getLooper())
- helper = NotificationTestHelper(mContext, mDependency, TestableLooper.get(this))
+ helper = NotificationTestHelper(mContext, mDependency)
Mockito.`when`(accessibilityManager.isTouchExplorationEnabled).thenReturn(false)
windowRootViewVisibilityInteractor =
WindowRootViewVisibilityInteractor(
@@ -227,7 +222,7 @@
Mockito.`when`(row.guts).thenReturn(guts)
Assert.assertTrue(gutsManager.openGutsInternal(row, 0, 0, menuItem))
assertEquals(View.INVISIBLE.toLong(), guts.visibility.toLong())
- testableLooper.processAllMessages()
+ executor.runAllReady()
verify(guts)
.openControls(
ArgumentMatchers.anyInt(),
@@ -247,7 +242,7 @@
ArgumentMatchers.anyBoolean()
)
verify(row, Mockito.times(1)).setGutsView(ArgumentMatchers.any())
- testableLooper.processAllMessages()
+ executor.runAllReady()
verify(headsUpManager).setGutsShown(realRow.entry, false)
}
@@ -343,7 +338,7 @@
Mockito.`when`(entry.row).thenReturn(row)
Mockito.`when`(entry.getGuts()).thenReturn(guts)
Assert.assertTrue(gutsManager.openGutsInternal(row, 0, 0, menuItem))
- testableLooper.processAllMessages()
+ executor.runAllReady()
verify(guts)
.openControls(
ArgumentMatchers.anyInt(),
@@ -356,7 +351,7 @@
verify(row).setGutsView(ArgumentMatchers.any())
row.onDensityOrFontScaleChanged()
gutsManager.onDensityOrFontScaleChanged(entry)
- testableLooper.processAllMessages()
+ executor.runAllReady()
gutsManager.closeAndSaveGuts(false, false, false, 0, 0, false)
verify(guts)
.closeControls(
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java
index 09a3eb48..954335e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java
@@ -615,10 +615,8 @@
LayoutInflater inflater = (LayoutInflater) mContext.getSystemService(
Context.LAYOUT_INFLATER_SERVICE);
- if (com.android.systemui.Flags.notificationRowUserContext()) {
- inflater.setFactory2(new RowInflaterTask.RowAsyncLayoutInflater(entry, mSystemClock,
- mRowInflaterTaskLogger));
- }
+ inflater.setFactory2(new RowInflaterTask.RowAsyncLayoutInflater(entry, mSystemClock,
+ mRowInflaterTaskLogger));
mRow = (ExpandableNotificationRow) inflater.inflate(
R.layout.status_bar_notification_row,
null /* root */,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/RowContentBindStageTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/RowContentBindStageTest.java
index 76470db..1534c84 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/RowContentBindStageTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/RowContentBindStageTest.java
@@ -197,7 +197,7 @@
params.clearDirtyContentViews();
// WHEN low priority is set and stage executed.
- params.setUseLowPriority(true);
+ params.setUseMinimized(true);
mRowContentBindStage.executeStage(mEntry, mRow, (en) -> { });
// THEN binder is called with use low priority and contracted/expanded are called to bind.
@@ -210,7 +210,7 @@
anyBoolean(),
any());
BindParams usedParams = bindParamsCaptor.getValue();
- assertTrue(usedParams.isLowPriority);
+ assertTrue(usedParams.isMinimized);
}
@Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationBigPictureTemplateViewWrapperTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationBigPictureTemplateViewWrapperTest.java
index 8f88501..a15b4cd 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationBigPictureTemplateViewWrapperTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationBigPictureTemplateViewWrapperTest.java
@@ -25,8 +25,6 @@
import android.graphics.drawable.Icon;
import android.os.Bundle;
import android.testing.AndroidTestingRunner;
-import android.testing.TestableLooper;
-import android.testing.TestableLooper.RunWithLooper;
import android.view.LayoutInflater;
import android.view.View;
@@ -44,7 +42,6 @@
@RunWith(AndroidTestingRunner.class)
@SmallTest
-@RunWithLooper
public class NotificationBigPictureTemplateViewWrapperTest extends SysuiTestCase {
private View mView;
@@ -53,11 +50,7 @@
@Before
public void setup() throws Exception {
- allowTestableLooperAsMainThread();
- NotificationTestHelper helper = new NotificationTestHelper(
- mContext,
- mDependency,
- TestableLooper.get(this));
+ NotificationTestHelper helper = new NotificationTestHelper(mContext, mDependency);
mView = LayoutInflater.from(mContext).inflate(
com.android.internal.R.layout.notification_template_material_big_picture, null);
mRow = helper.createRow();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationConversationTemplateViewWrapperTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationConversationTemplateViewWrapperTest.kt
index 3fa68bb..fe2971c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationConversationTemplateViewWrapperTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationConversationTemplateViewWrapperTest.kt
@@ -18,8 +18,6 @@
import android.graphics.drawable.AnimatedImageDrawable
import android.testing.AndroidTestingRunner
-import android.testing.TestableLooper
-import android.testing.TestableLooper.RunWithLooper
import android.view.View
import androidx.test.filters.SmallTest
import com.android.internal.R
@@ -41,7 +39,6 @@
@SmallTest
@RunWith(AndroidTestingRunner::class)
-@RunWithLooper
class NotificationConversationTemplateViewWrapperTest : SysuiTestCase() {
private lateinit var mRow: ExpandableNotificationRow
@@ -49,8 +46,7 @@
@Before
fun setUp() {
- allowTestableLooperAsMainThread()
- helper = NotificationTestHelper(mContext, mDependency, TestableLooper.get(this))
+ helper = NotificationTestHelper(mContext, mDependency)
mRow = helper.createRow()
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationCustomViewWrapperTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationCustomViewWrapperTest.java
index 45f7c5a..2d72c7e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationCustomViewWrapperTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationCustomViewWrapperTest.java
@@ -17,8 +17,6 @@
package com.android.systemui.statusbar.notification.row.wrapper;
import android.testing.AndroidTestingRunner;
-import android.testing.TestableLooper;
-import android.testing.TestableLooper.RunWithLooper;
import android.view.View;
import android.widget.RemoteViews;
@@ -36,18 +34,13 @@
@SmallTest
@RunWith(AndroidTestingRunner.class)
-@RunWithLooper
public class NotificationCustomViewWrapperTest extends SysuiTestCase {
private ExpandableNotificationRow mRow;
@Before
public void setUp() throws Exception {
- allowTestableLooperAsMainThread();
- NotificationTestHelper helper = new NotificationTestHelper(
- mContext,
- mDependency,
- TestableLooper.get(this));
+ NotificationTestHelper helper = new NotificationTestHelper(mContext, mDependency);
mRow = helper.createRow();
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationMessagingTemplateViewWrapperTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationMessagingTemplateViewWrapperTest.kt
index c0444b5..f26c18b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationMessagingTemplateViewWrapperTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationMessagingTemplateViewWrapperTest.kt
@@ -18,8 +18,6 @@
import android.graphics.drawable.AnimatedImageDrawable
import android.testing.AndroidTestingRunner
-import android.testing.TestableLooper
-import android.testing.TestableLooper.RunWithLooper
import android.view.View
import androidx.test.filters.SmallTest
import com.android.internal.widget.MessagingGroup
@@ -39,7 +37,6 @@
@SmallTest
@RunWith(AndroidTestingRunner::class)
-@RunWithLooper
class NotificationMessagingTemplateViewWrapperTest : SysuiTestCase() {
private lateinit var mRow: ExpandableNotificationRow
@@ -47,8 +44,7 @@
@Before
fun setUp() {
- allowTestableLooperAsMainThread()
- helper = NotificationTestHelper(mContext, mDependency, TestableLooper.get(this))
+ helper = NotificationTestHelper(mContext, mDependency)
mRow = helper.createRow()
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationTemplateViewWrapperTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationTemplateViewWrapperTest.kt
index f7632aa..54eed26 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationTemplateViewWrapperTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationTemplateViewWrapperTest.kt
@@ -69,7 +69,7 @@
TestUiOffloadThread(looper.looper)
)
- helper = NotificationTestHelper(mContext, mDependency, looper)
+ helper = NotificationTestHelper(mContext, mDependency)
row = helper.createRow()
// Some code in the view iterates through parents so we need some extra containers around
// it.
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationViewWrapperTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationViewWrapperTest.java
index 93a9e59..e3a77d3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationViewWrapperTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationViewWrapperTest.java
@@ -39,7 +39,6 @@
@RunWith(AndroidTestingRunner.class)
@SmallTest
-@RunWithLooper
public class NotificationViewWrapperTest extends SysuiTestCase {
private View mView;
@@ -48,13 +47,9 @@
@Before
public void setup() throws Exception {
- allowTestableLooperAsMainThread();
mView = mock(View.class);
when(mView.getContext()).thenReturn(mContext);
- NotificationTestHelper helper = new NotificationTestHelper(
- mContext,
- mDependency,
- TestableLooper.get(this));
+ NotificationTestHelper helper = new NotificationTestHelper(mContext, mDependency);
mRow = helper.createRow();
mNotificationViewWrapper = new TestableNotificationViewWrapper(mContext, mView, mRow);
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainerTest.java
index 1f38a73..3b16f14 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainerTest.java
@@ -67,7 +67,7 @@
@Test
public void testGetMaxAllowedVisibleChildren_lowPriority() {
- mChildrenContainer.setIsLowPriority(true);
+ mChildrenContainer.setIsMinimized(true);
Assert.assertEquals(mChildrenContainer.getMaxAllowedVisibleChildren(),
NotificationChildrenContainer.NUMBER_OF_CHILDREN_WHEN_SYSTEM_EXPANDED);
}
@@ -81,7 +81,7 @@
@Test
public void testGetMaxAllowedVisibleChildren_lowPriority_expandedChildren() {
- mChildrenContainer.setIsLowPriority(true);
+ mChildrenContainer.setIsMinimized(true);
mChildrenContainer.setChildrenExpanded(true);
Assert.assertEquals(mChildrenContainer.getMaxAllowedVisibleChildren(),
NotificationChildrenContainer.NUMBER_OF_CHILDREN_WHEN_SYSTEM_EXPANDED);
@@ -89,7 +89,7 @@
@Test
public void testGetMaxAllowedVisibleChildren_lowPriority_userLocked() {
- mChildrenContainer.setIsLowPriority(true);
+ mChildrenContainer.setIsMinimized(true);
mChildrenContainer.setUserLocked(true);
Assert.assertEquals(mChildrenContainer.getMaxAllowedVisibleChildren(),
NotificationChildrenContainer.NUMBER_OF_CHILDREN_WHEN_SYSTEM_EXPANDED);
@@ -118,7 +118,7 @@
@Test
public void testShowingAsLowPriority_lowPriority() {
- mChildrenContainer.setIsLowPriority(true);
+ mChildrenContainer.setIsMinimized(true);
Assert.assertTrue(mChildrenContainer.showingAsLowPriority());
}
@@ -129,7 +129,7 @@
@Test
public void testShowingAsLowPriority_lowPriority_expanded() {
- mChildrenContainer.setIsLowPriority(true);
+ mChildrenContainer.setIsMinimized(true);
mGroup.setExpandable(true);
mGroup.setUserExpanded(true, false);
Assert.assertFalse(mChildrenContainer.showingAsLowPriority());
@@ -140,7 +140,7 @@
mGroup.setUserLocked(true);
mGroup.setExpandable(true);
mGroup.setUserExpanded(true);
- mChildrenContainer.setIsLowPriority(true);
+ mChildrenContainer.setIsMinimized(true);
Assert.assertEquals(mChildrenContainer.getMaxAllowedVisibleChildren(),
NotificationChildrenContainer.NUMBER_OF_CHILDREN_WHEN_CHILDREN_EXPANDED);
}
@@ -148,14 +148,14 @@
@Test
@DisableFlags(AsyncGroupHeaderViewInflation.FLAG_NAME)
public void testLowPriorityHeaderCleared() {
- mGroup.setIsLowPriority(true);
+ mGroup.setIsMinimized(true);
NotificationHeaderView lowPriorityHeaderView =
- mChildrenContainer.getLowPriorityViewWrapper().getNotificationHeader();
+ mChildrenContainer.getMinimizedGroupHeaderWrapper().getNotificationHeader();
Assert.assertEquals(View.VISIBLE, lowPriorityHeaderView.getVisibility());
Assert.assertSame(mChildrenContainer, lowPriorityHeaderView.getParent());
- mGroup.setIsLowPriority(false);
+ mGroup.setIsMinimized(false);
assertNull(lowPriorityHeaderView.getParent());
- assertNull(mChildrenContainer.getLowPriorityViewWrapper());
+ assertNull(mChildrenContainer.getMinimizedGroupHeaderWrapper());
}
@Test
@@ -169,7 +169,7 @@
@Test
@EnableFlags(AsyncGroupHeaderViewInflation.FLAG_NAME)
public void testSetLowPriorityWithAsyncInflation_noHeaderReInflation() {
- mChildrenContainer.setIsLowPriority(true);
+ mChildrenContainer.setIsMinimized(true);
assertNull("We don't inflate header from the main thread with Async "
+ "Inflation enabled", mChildrenContainer.getCurrentHeaderView());
}
@@ -179,21 +179,21 @@
public void setLowPriorityBeforeLowPriorityHeaderSet() {
//Given: the children container does not have a low-priority header, and is not low-priority
- assertNull(mChildrenContainer.getLowPriorityViewWrapper());
- mGroup.setIsLowPriority(false);
+ assertNull(mChildrenContainer.getMinimizedGroupHeaderWrapper());
+ mGroup.setIsMinimized(false);
//When: set the children container to be low-priority and set the low-priority header
- mGroup.setIsLowPriority(true);
- mGroup.setLowPriorityGroupHeader(createHeaderView(/* lowPriorityHeader= */ true));
+ mGroup.setIsMinimized(true);
+ mGroup.setMinimizedGroupHeader(createHeaderView(/* lowPriorityHeader= */ true));
//Then: the low-priority group header should be visible
NotificationHeaderView lowPriorityHeaderView =
- mChildrenContainer.getLowPriorityViewWrapper().getNotificationHeader();
+ mChildrenContainer.getMinimizedGroupHeaderWrapper().getNotificationHeader();
Assert.assertEquals(View.VISIBLE, lowPriorityHeaderView.getVisibility());
Assert.assertSame(mChildrenContainer, lowPriorityHeaderView.getParent());
//When: set the children container to be not low-priority and set the normal header
- mGroup.setIsLowPriority(false);
+ mGroup.setIsMinimized(false);
mGroup.setGroupHeader(createHeaderView(/* lowPriorityHeader= */ false));
//Then: the low-priority group header should not be visible , normal header should be
@@ -211,9 +211,9 @@
public void changeLowPriorityAfterHeaderSet() {
//Given: the children container does not have headers, and is not low-priority
- assertNull(mChildrenContainer.getLowPriorityViewWrapper());
+ assertNull(mChildrenContainer.getMinimizedGroupHeaderWrapper());
assertNull(mChildrenContainer.getNotificationHeaderWrapper());
- mGroup.setIsLowPriority(false);
+ mGroup.setIsMinimized(false);
//When: set the set the normal header
mGroup.setGroupHeader(createHeaderView(/* lowPriorityHeader= */ false));
@@ -225,14 +225,14 @@
Assert.assertSame(mChildrenContainer, headerView.getParent());
//When: set the set the row to be low priority, and set the low-priority header
- mGroup.setIsLowPriority(true);
- mGroup.setLowPriorityGroupHeader(createHeaderView(/* lowPriorityHeader= */ true));
+ mGroup.setIsMinimized(true);
+ mGroup.setMinimizedGroupHeader(createHeaderView(/* lowPriorityHeader= */ true));
//Then: the header view should not be visible, the low-priority group header should be
// visible
Assert.assertEquals(View.INVISIBLE, headerView.getVisibility());
NotificationHeaderView lowPriorityHeaderView =
- mChildrenContainer.getLowPriorityViewWrapper().getNotificationHeader();
+ mChildrenContainer.getMinimizedGroupHeaderWrapper().getNotificationHeader();
Assert.assertEquals(View.VISIBLE, lowPriorityHeaderView.getVisibility());
}
@@ -263,7 +263,7 @@
@Test
@DisableFlags(AsyncGroupHeaderViewInflation.FLAG_NAME)
public void applyRoundnessAndInvalidate_should_be_immediately_applied_on_headerLowPriority() {
- mChildrenContainer.setIsLowPriority(true);
+ mChildrenContainer.setIsMinimized(true);
NotificationHeaderViewWrapper header = mChildrenContainer.getNotificationHeaderWrapper();
Assert.assertEquals(0f, header.getTopRoundness(), 0.001f);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java
index a4f88fb..10d2191 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java
@@ -49,7 +49,6 @@
import androidx.test.filters.SmallTest;
-import com.android.internal.jank.InteractionJankMonitor;
import com.android.internal.logging.MetricsLogger;
import com.android.internal.logging.UiEventLogger;
import com.android.internal.logging.nano.MetricsProto;
@@ -63,6 +62,7 @@
import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository;
import com.android.systemui.keyguard.shared.model.KeyguardState;
import com.android.systemui.keyguard.shared.model.TransitionStep;
+import com.android.systemui.kosmos.KosmosJavaAdapter;
import com.android.systemui.media.controls.ui.controller.KeyguardMediaController;
import com.android.systemui.plugins.ActivityStarter;
import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin;
@@ -130,6 +130,7 @@
@RunWith(AndroidTestingRunner.class)
public class NotificationStackScrollLayoutControllerTest extends SysuiTestCase {
+ protected KosmosJavaAdapter mKosmos = new KosmosJavaAdapter(this);
private final FakeFeatureFlags mFeatureFlags = new FakeFeatureFlags();
@Mock private NotificationGutsManager mNotificationGutsManager;
@Mock private NotificationsController mNotificationsController;
@@ -167,7 +168,6 @@
@Mock private SceneContainerFlags mSceneContainerFlags;
@Mock private Provider<WindowRootView> mWindowRootView;
@Mock private NotificationStackAppearanceInteractor mNotificationStackAppearanceInteractor;
- @Mock private InteractionJankMonitor mJankMonitor;
private final StackStateLogger mStackLogger = new StackStateLogger(logcatLogBuffer(),
logcatLogBuffer());
private final NotificationStackScrollLogger mLogger = new NotificationStackScrollLogger(
@@ -1030,7 +1030,7 @@
mSceneContainerFlags,
mWindowRootView,
mNotificationStackAppearanceInteractor,
- mJankMonitor,
+ mKosmos.getInteractionJankMonitor(),
mStackLogger,
mLogger,
mNotificationStackSizeCalculator,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt
index 0a18eb6..c308a98 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt
@@ -35,9 +35,13 @@
import com.android.systemui.res.R
import com.android.systemui.shade.data.repository.fakeShadeRepository
import com.android.systemui.statusbar.data.repository.fakeRemoteInputRepository
+import com.android.systemui.statusbar.notification.data.repository.FakeHeadsUpRowRepository
import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository
import com.android.systemui.statusbar.notification.data.repository.setActiveNotifs
import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor
+import com.android.systemui.statusbar.notification.shared.NotificationsHeadsUpRefactor
+import com.android.systemui.statusbar.notification.stack.data.repository.headsUpNotificationRepository
+import com.android.systemui.statusbar.notification.stack.data.repository.setNotifications
import com.android.systemui.statusbar.policy.data.repository.fakeUserSetupRepository
import com.android.systemui.statusbar.policy.data.repository.zenModeRepository
import com.android.systemui.statusbar.policy.fakeConfigurationController
@@ -70,6 +74,7 @@
private val fakeRemoteInputRepository = kosmos.fakeRemoteInputRepository
private val fakeShadeRepository = kosmos.fakeShadeRepository
private val fakeUserSetupRepository = kosmos.fakeUserSetupRepository
+ private val headsUpRepository = kosmos.headsUpNotificationRepository
private val zenModeRepository = kosmos.zenModeRepository
val underTest = kosmos.notificationListViewModel
@@ -125,35 +130,35 @@
}
@Test
- fun testShouldShowEmptyShadeView_trueWhenNoNotifs() =
+ fun testShouldIncludeEmptyShadeView_trueWhenNoNotifs() =
testScope.runTest {
- val shouldShow by collectLastValue(underTest.shouldShowEmptyShadeView)
+ val shouldInclude by collectLastValue(underTest.shouldShowEmptyShadeView)
// WHEN has no notifs
activeNotificationListRepository.setActiveNotifs(count = 0)
runCurrent()
// THEN empty shade is visible
- assertThat(shouldShow).isTrue()
+ assertThat(shouldInclude).isTrue()
}
@Test
- fun testShouldShowEmptyShadeView_falseWhenNotifs() =
+ fun testShouldIncludeEmptyShadeView_falseWhenNotifs() =
testScope.runTest {
- val shouldShow by collectLastValue(underTest.shouldShowEmptyShadeView)
+ val shouldInclude by collectLastValue(underTest.shouldShowEmptyShadeView)
// WHEN has notifs
activeNotificationListRepository.setActiveNotifs(count = 2)
runCurrent()
// THEN empty shade is not visible
- assertThat(shouldShow).isFalse()
+ assertThat(shouldInclude).isFalse()
}
@Test
- fun testShouldShowEmptyShadeView_falseWhenQsExpandedDefault() =
+ fun testShouldIncludeEmptyShadeView_falseWhenQsExpandedDefault() =
testScope.runTest {
- val shouldShow by collectLastValue(underTest.shouldShowEmptyShadeView)
+ val shouldInclude by collectLastValue(underTest.shouldShowEmptyShadeView)
// WHEN has no notifs
activeNotificationListRepository.setActiveNotifs(count = 0)
@@ -162,13 +167,13 @@
runCurrent()
// THEN empty shade is not visible
- assertThat(shouldShow).isFalse()
+ assertThat(shouldInclude).isFalse()
}
@Test
- fun testShouldShowEmptyShadeView_trueWhenQsExpandedInSplitShade() =
+ fun testShouldIncludeEmptyShadeView_trueWhenQsExpandedInSplitShade() =
testScope.runTest {
- val shouldShow by collectLastValue(underTest.shouldShowEmptyShadeView)
+ val shouldInclude by collectLastValue(underTest.shouldShowEmptyShadeView)
// WHEN has no notifs
activeNotificationListRepository.setActiveNotifs(count = 0)
@@ -180,13 +185,13 @@
runCurrent()
// THEN empty shade is visible
- assertThat(shouldShow).isTrue()
+ assertThat(shouldInclude).isTrue()
}
@Test
- fun testShouldShowEmptyShadeView_trueWhenLockedShade() =
+ fun testShouldIncludeEmptyShadeView_trueWhenLockedShade() =
testScope.runTest {
- val shouldShow by collectLastValue(underTest.shouldShowEmptyShadeView)
+ val shouldInclude by collectLastValue(underTest.shouldShowEmptyShadeView)
// WHEN has no notifs
activeNotificationListRepository.setActiveNotifs(count = 0)
@@ -195,13 +200,13 @@
runCurrent()
// THEN empty shade is visible
- assertThat(shouldShow).isTrue()
+ assertThat(shouldInclude).isTrue()
}
@Test
- fun testShouldShowEmptyShadeView_falseWhenKeyguard() =
+ fun testShouldIncludeEmptyShadeView_falseWhenKeyguard() =
testScope.runTest {
- val shouldShow by collectLastValue(underTest.shouldShowEmptyShadeView)
+ val shouldInclude by collectLastValue(underTest.shouldShowEmptyShadeView)
// WHEN has no notifs
activeNotificationListRepository.setActiveNotifs(count = 0)
@@ -210,13 +215,13 @@
runCurrent()
// THEN empty shade is not visible
- assertThat(shouldShow).isFalse()
+ assertThat(shouldInclude).isFalse()
}
@Test
- fun testShouldShowEmptyShadeView_falseWhenStartingToSleep() =
+ fun testShouldIncludeEmptyShadeView_falseWhenStartingToSleep() =
testScope.runTest {
- val shouldShow by collectLastValue(underTest.shouldShowEmptyShadeView)
+ val shouldInclude by collectLastValue(underTest.shouldShowEmptyShadeView)
// WHEN has no notifs
activeNotificationListRepository.setActiveNotifs(count = 0)
@@ -227,7 +232,7 @@
runCurrent()
// THEN empty shade is not visible
- assertThat(shouldShow).isFalse()
+ assertThat(shouldInclude).isFalse()
}
@Test
@@ -277,9 +282,9 @@
}
@Test
- fun testShouldShowFooterView_trueWhenShade() =
+ fun testShouldIncludeFooterView_trueWhenShade() =
testScope.runTest {
- val shouldShow by collectLastValue(underTest.shouldShowFooterView)
+ val shouldInclude by collectLastValue(underTest.shouldIncludeFooterView)
// WHEN has notifs
activeNotificationListRepository.setActiveNotifs(count = 2)
@@ -289,13 +294,13 @@
runCurrent()
// THEN footer is visible
- assertThat(shouldShow?.value).isTrue()
+ assertThat(shouldInclude?.value).isTrue()
}
@Test
- fun testShouldShowFooterView_trueWhenLockedShade() =
+ fun testShouldIncludeFooterView_trueWhenLockedShade() =
testScope.runTest {
- val shouldShow by collectLastValue(underTest.shouldShowFooterView)
+ val shouldInclude by collectLastValue(underTest.shouldIncludeFooterView)
// WHEN has notifs
activeNotificationListRepository.setActiveNotifs(count = 2)
@@ -305,13 +310,13 @@
runCurrent()
// THEN footer is visible
- assertThat(shouldShow?.value).isTrue()
+ assertThat(shouldInclude?.value).isTrue()
}
@Test
- fun testShouldShowFooterView_falseWhenKeyguard() =
+ fun testShouldIncludeFooterView_falseWhenKeyguard() =
testScope.runTest {
- val shouldShow by collectLastValue(underTest.shouldShowFooterView)
+ val shouldInclude by collectLastValue(underTest.shouldIncludeFooterView)
// WHEN has notifs
activeNotificationListRepository.setActiveNotifs(count = 2)
@@ -320,13 +325,13 @@
runCurrent()
// THEN footer is not visible
- assertThat(shouldShow?.value).isFalse()
+ assertThat(shouldInclude?.value).isFalse()
}
@Test
- fun testShouldShowFooterView_falseWhenUserNotSetUp() =
+ fun testShouldIncludeFooterView_falseWhenUserNotSetUp() =
testScope.runTest {
- val shouldShow by collectLastValue(underTest.shouldShowFooterView)
+ val shouldInclude by collectLastValue(underTest.shouldIncludeFooterView)
// WHEN has notifs
activeNotificationListRepository.setActiveNotifs(count = 2)
@@ -338,13 +343,13 @@
runCurrent()
// THEN footer is not visible
- assertThat(shouldShow?.value).isFalse()
+ assertThat(shouldInclude?.value).isFalse()
}
@Test
- fun testShouldShowFooterView_falseWhenStartingToSleep() =
+ fun testShouldIncludeFooterView_falseWhenStartingToSleep() =
testScope.runTest {
- val shouldShow by collectLastValue(underTest.shouldShowFooterView)
+ val shouldInclude by collectLastValue(underTest.shouldIncludeFooterView)
// WHEN has notifs
activeNotificationListRepository.setActiveNotifs(count = 2)
@@ -356,13 +361,13 @@
runCurrent()
// THEN footer is not visible
- assertThat(shouldShow?.value).isFalse()
+ assertThat(shouldInclude?.value).isFalse()
}
@Test
- fun testShouldShowFooterView_falseWhenQsExpandedDefault() =
+ fun testShouldIncludeFooterView_falseWhenQsExpandedDefault() =
testScope.runTest {
- val shouldShow by collectLastValue(underTest.shouldShowFooterView)
+ val shouldInclude by collectLastValue(underTest.shouldIncludeFooterView)
// WHEN has notifs
activeNotificationListRepository.setActiveNotifs(count = 2)
@@ -375,13 +380,13 @@
runCurrent()
// THEN footer is not visible
- assertThat(shouldShow?.value).isFalse()
+ assertThat(shouldInclude?.value).isFalse()
}
@Test
- fun testShouldShowFooterView_trueWhenQsExpandedSplitShade() =
+ fun testShouldIncludeFooterView_trueWhenQsExpandedSplitShade() =
testScope.runTest {
- val shouldShow by collectLastValue(underTest.shouldShowFooterView)
+ val shouldInclude by collectLastValue(underTest.shouldIncludeFooterView)
// WHEN has notifs
activeNotificationListRepository.setActiveNotifs(count = 2)
@@ -396,13 +401,13 @@
runCurrent()
// THEN footer is visible
- assertThat(shouldShow?.value).isTrue()
+ assertThat(shouldInclude?.value).isTrue()
}
@Test
- fun testShouldShowFooterView_falseWhenRemoteInputActive() =
+ fun testShouldIncludeFooterView_falseWhenRemoteInputActive() =
testScope.runTest {
- val shouldShow by collectLastValue(underTest.shouldShowFooterView)
+ val shouldInclude by collectLastValue(underTest.shouldIncludeFooterView)
// WHEN has notifs
activeNotificationListRepository.setActiveNotifs(count = 2)
@@ -414,29 +419,13 @@
runCurrent()
// THEN footer is not visible
- assertThat(shouldShow?.value).isFalse()
+ assertThat(shouldInclude?.value).isFalse()
}
@Test
- fun testShouldShowFooterView_falseWhenShadeIsClosed() =
+ fun testShouldIncludeFooterView_animatesWhenShade() =
testScope.runTest {
- val shouldShow by collectLastValue(underTest.shouldShowFooterView)
-
- // WHEN has notifs
- activeNotificationListRepository.setActiveNotifs(count = 2)
- // AND shade is closed
- fakeKeyguardRepository.setStatusBarState(StatusBarState.SHADE)
- fakeShadeRepository.setLegacyShadeExpansion(0f)
- runCurrent()
-
- // THEN footer is not visible
- assertThat(shouldShow?.value).isFalse()
- }
-
- @Test
- fun testShouldShowFooterView_animatesWhenShade() =
- testScope.runTest {
- val shouldShow by collectLastValue(underTest.shouldShowFooterView)
+ val shouldInclude by collectLastValue(underTest.shouldIncludeFooterView)
// WHEN has notifs
activeNotificationListRepository.setActiveNotifs(count = 2)
@@ -446,13 +435,13 @@
runCurrent()
// THEN footer visibility animates
- assertThat(shouldShow?.isAnimating).isTrue()
+ assertThat(shouldInclude?.isAnimating).isTrue()
}
@Test
- fun testShouldShowFooterView_notAnimatingOnKeyguard() =
+ fun testShouldIncludeFooterView_notAnimatingOnKeyguard() =
testScope.runTest {
- val shouldShow by collectLastValue(underTest.shouldShowFooterView)
+ val shouldInclude by collectLastValue(underTest.shouldIncludeFooterView)
// WHEN has notifs
activeNotificationListRepository.setActiveNotifs(count = 2)
@@ -462,6 +451,150 @@
runCurrent()
// THEN footer visibility does not animate
- assertThat(shouldShow?.isAnimating).isFalse()
+ assertThat(shouldInclude?.isAnimating).isFalse()
+ }
+
+ @Test
+ fun testShouldHideFooterView_trueWhenShadeIsClosed() =
+ testScope.runTest {
+ val shouldHide by collectLastValue(underTest.shouldHideFooterView)
+
+ // WHEN shade is closed
+ fakeKeyguardRepository.setStatusBarState(StatusBarState.SHADE)
+ fakeShadeRepository.setLegacyShadeExpansion(0f)
+ runCurrent()
+
+ // THEN footer is hidden
+ assertThat(shouldHide).isTrue()
+ }
+
+ @Test
+ fun testShouldHideFooterView_falseWhenShadeIsOpen() =
+ testScope.runTest {
+ val shouldHide by collectLastValue(underTest.shouldHideFooterView)
+
+ // WHEN shade is open
+ fakeKeyguardRepository.setStatusBarState(StatusBarState.SHADE)
+ fakeShadeRepository.setLegacyShadeExpansion(1f)
+ runCurrent()
+
+ // THEN footer is hidden
+ assertThat(shouldHide).isFalse()
+ }
+
+ @Test
+ @EnableFlags(NotificationsHeadsUpRefactor.FLAG_NAME)
+ fun testPinnedHeadsUpRows_filtersForPinnedItems() =
+ testScope.runTest {
+ val pinnedHeadsUpRows by collectLastValue(underTest.pinnedHeadsUpRows)
+
+ // WHEN there are no pinned rows
+ val rows =
+ arrayListOf(
+ fakeHeadsUpRowRepository(key = "0"),
+ fakeHeadsUpRowRepository(key = "1"),
+ fakeHeadsUpRowRepository(key = "2"),
+ )
+ headsUpRepository.setNotifications(
+ rows,
+ )
+ runCurrent()
+
+ // THEN the list is empty
+ assertThat(pinnedHeadsUpRows).isEmpty()
+
+ // WHEN a row gets pinned
+ rows[0].isPinned.value = true
+ runCurrent()
+
+ // THEN it's added to the list
+ assertThat(pinnedHeadsUpRows).containsExactly(rows[0])
+
+ // WHEN more rows are pinned
+ rows[1].isPinned.value = true
+ runCurrent()
+
+ // THEN they are all in the list
+ assertThat(pinnedHeadsUpRows).containsExactly(rows[0], rows[1])
+
+ // WHEN a row gets unpinned
+ rows[0].isPinned.value = false
+ runCurrent()
+
+ // THEN it's removed from the list
+ assertThat(pinnedHeadsUpRows).containsExactly(rows[1])
+ }
+
+ @Test
+ @EnableFlags(NotificationsHeadsUpRefactor.FLAG_NAME)
+ fun testHasPinnedHeadsUpRows_true() =
+ testScope.runTest {
+ val hasPinnedHeadsUpRow by collectLastValue(underTest.hasPinnedHeadsUpRow)
+
+ headsUpRepository.setNotifications(
+ fakeHeadsUpRowRepository(key = "0", isPinned = true),
+ fakeHeadsUpRowRepository(key = "1")
+ )
+ runCurrent()
+
+ assertThat(hasPinnedHeadsUpRow).isTrue()
+ }
+
+ @Test
+ @EnableFlags(NotificationsHeadsUpRefactor.FLAG_NAME)
+ fun testHasPinnedHeadsUpRows_false() =
+ testScope.runTest {
+ val hasPinnedHeadsUpRow by collectLastValue(underTest.hasPinnedHeadsUpRow)
+
+ headsUpRepository.setNotifications(
+ fakeHeadsUpRowRepository(key = "0"),
+ fakeHeadsUpRowRepository(key = "1"),
+ )
+ runCurrent()
+
+ assertThat(hasPinnedHeadsUpRow).isFalse()
+ }
+
+ @Test
+ @EnableFlags(NotificationsHeadsUpRefactor.FLAG_NAME)
+ fun testTopHeadsUpRow_emptyList_null() =
+ testScope.runTest {
+ val topHeadsUpRow by collectLastValue(underTest.topHeadsUpRow)
+
+ headsUpRepository.setNotifications(emptyList())
+ runCurrent()
+
+ assertThat(topHeadsUpRow).isNull()
+ }
+
+ @Test
+ @EnableFlags(NotificationsHeadsUpRefactor.FLAG_NAME)
+ fun testHeadsUpAnimationsEnabled_true() =
+ testScope.runTest {
+ val animationsEnabled by collectLastValue(underTest.headsUpAnimationsEnabled)
+
+ fakeShadeRepository.setQsExpansion(0.0f)
+ fakeKeyguardRepository.setKeyguardShowing(false)
+ runCurrent()
+
+ assertThat(animationsEnabled).isTrue()
+ }
+
+ @Test
+ @EnableFlags(NotificationsHeadsUpRefactor.FLAG_NAME)
+ fun testHeadsUpAnimationsEnabled_keyguardShowing_false() =
+ testScope.runTest {
+ val animationsEnabled by collectLastValue(underTest.headsUpAnimationsEnabled)
+
+ fakeShadeRepository.setQsExpansion(0.0f)
+ fakeKeyguardRepository.setKeyguardShowing(true)
+ runCurrent()
+
+ assertThat(animationsEnabled).isFalse()
+ }
+
+ private fun fakeHeadsUpRowRepository(key: String, isPinned: Boolean = false) =
+ FakeHeadsUpRowRepository(key = key, elementKey = Any()).apply {
+ this.isPinned.value = isPinned
}
}
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 d5c4053..8e8dd4d 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
@@ -188,7 +188,7 @@
public void vibrateOnNavigationKeyDown_usesPerformHapticFeedback() {
mSbcqCallbacks.vibrateOnNavigationKeyDown();
- verify(mShadeViewController).performHapticFeedback(
+ verify(mShadeController).performHapticFeedback(
HapticFeedbackConstants.GESTURE_START
);
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerFlagDisabledTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerFlagDisabledTest.kt
index 933b5b5..358709f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerFlagDisabledTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerFlagDisabledTest.kt
@@ -17,6 +17,7 @@
package com.android.systemui.statusbar.policy
import android.app.IActivityManager
+import android.content.pm.PackageManager
import android.media.projection.MediaProjectionManager
import android.os.Handler
import android.platform.test.annotations.DisableFlags
@@ -44,6 +45,7 @@
@Mock private lateinit var handler: Handler
@Mock private lateinit var activityManager: IActivityManager
@Mock private lateinit var mediaProjectionManager: MediaProjectionManager
+ @Mock private lateinit var packageManager: PackageManager
private lateinit var controller: SensitiveNotificationProtectionControllerImpl
@Before
@@ -56,6 +58,7 @@
FakeGlobalSettings(),
mediaProjectionManager,
activityManager,
+ packageManager,
handler,
FakeExecutor(FakeSystemClock()),
logger
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerTest.kt
index 4b4e315..7dfe6d0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerTest.kt
@@ -25,9 +25,14 @@
import android.app.NotificationChannel
import android.app.NotificationManager.IMPORTANCE_HIGH
import android.app.NotificationManager.VISIBILITY_NO_OVERRIDE
+import android.content.pm.PackageManager
import android.media.projection.MediaProjectionInfo
import android.media.projection.MediaProjectionManager
+import android.permission.flags.Flags.FLAG_SENSITIVE_NOTIFICATION_APP_PROTECTION
import android.platform.test.annotations.EnableFlags
+import android.platform.test.annotations.RequiresFlagsDisabled
+import android.platform.test.annotations.RequiresFlagsEnabled
+import android.platform.test.flag.junit.DeviceFlagsValueProvider
import android.provider.Settings.Global.DISABLE_SCREEN_SHARE_PROTECTIONS_FOR_APPS_AND_NOTIFICATIONS
import android.testing.AndroidTestingRunner
import android.testing.TestableLooper.RunWithLooper
@@ -48,9 +53,11 @@
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertTrue
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.anyString
import org.mockito.Mock
import org.mockito.Mockito.mock
import org.mockito.Mockito.times
@@ -64,10 +71,13 @@
@RunWithLooper
@EnableFlags(Flags.FLAG_SCREENSHARE_NOTIFICATION_HIDING)
class SensitiveNotificationProtectionControllerTest : SysuiTestCase() {
+ @get:Rule val checkFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule()
+
private val logger = SensitiveNotificationProtectionControllerLogger(logcatLogBuffer())
@Mock private lateinit var activityManager: IActivityManager
@Mock private lateinit var mediaProjectionManager: MediaProjectionManager
+ @Mock private lateinit var packageManager: PackageManager
@Mock private lateinit var mediaProjectionInfo: MediaProjectionInfo
@Mock private lateinit var listener1: Runnable
@Mock private lateinit var listener2: Runnable
@@ -87,6 +97,9 @@
whenever(activityManager.bugreportWhitelistedPackages)
.thenReturn(listOf(BUGREPORT_PACKAGE_NAME))
+ whenever(packageManager.checkPermission(anyString(), anyString()))
+ .thenReturn(PackageManager.PERMISSION_DENIED)
+
executor = FakeExecutor(FakeSystemClock())
globalSettings = FakeGlobalSettings()
controller =
@@ -95,6 +108,7 @@
globalSettings,
mediaProjectionManager,
activityManager,
+ packageManager,
mockExecutorHandler(executor),
executor,
logger
@@ -237,6 +251,36 @@
}
@Test
+ @RequiresFlagsDisabled(FLAG_SENSITIVE_NOTIFICATION_APP_PROTECTION)
+ fun isSensitiveStateActive_projectionActive_permissionExempt_flagDisabled_true() {
+ whenever(
+ packageManager.checkPermission(
+ android.Manifest.permission.RECORD_SENSITIVE_CONTENT,
+ mediaProjectionInfo.packageName
+ )
+ )
+ .thenReturn(PackageManager.PERMISSION_GRANTED)
+ mediaProjectionCallback.onStart(mediaProjectionInfo)
+
+ assertTrue(controller.isSensitiveStateActive)
+ }
+
+ @Test
+ @RequiresFlagsEnabled(FLAG_SENSITIVE_NOTIFICATION_APP_PROTECTION)
+ fun isSensitiveStateActive_projectionActive_permissionExempt_false() {
+ whenever(
+ packageManager.checkPermission(
+ android.Manifest.permission.RECORD_SENSITIVE_CONTENT,
+ mediaProjectionInfo.packageName
+ )
+ )
+ .thenReturn(PackageManager.PERMISSION_GRANTED)
+ mediaProjectionCallback.onStart(mediaProjectionInfo)
+
+ assertFalse(controller.isSensitiveStateActive)
+ }
+
+ @Test
fun isSensitiveStateActive_projectionActive_bugReportHandlerExempt_false() {
whenever(mediaProjectionInfo.packageName).thenReturn(BUGREPORT_PACKAGE_NAME)
mediaProjectionCallback.onStart(mediaProjectionInfo)
@@ -309,6 +353,40 @@
}
@Test
+ @RequiresFlagsDisabled(FLAG_SENSITIVE_NOTIFICATION_APP_PROTECTION)
+ fun shouldProtectNotification_projectionActive_permissionExempt_flagDisabled_true() {
+ whenever(
+ packageManager.checkPermission(
+ android.Manifest.permission.RECORD_SENSITIVE_CONTENT,
+ mediaProjectionInfo.packageName
+ )
+ )
+ .thenReturn(PackageManager.PERMISSION_GRANTED)
+ mediaProjectionCallback.onStart(mediaProjectionInfo)
+
+ val notificationEntry = setupNotificationEntry(TEST_PACKAGE_NAME, false)
+
+ assertTrue(controller.shouldProtectNotification(notificationEntry))
+ }
+
+ @Test
+ @RequiresFlagsEnabled(FLAG_SENSITIVE_NOTIFICATION_APP_PROTECTION)
+ fun shouldProtectNotification_projectionActive_permissionExempt_false() {
+ whenever(
+ packageManager.checkPermission(
+ android.Manifest.permission.RECORD_SENSITIVE_CONTENT,
+ mediaProjectionInfo.packageName
+ )
+ )
+ .thenReturn(PackageManager.PERMISSION_GRANTED)
+ mediaProjectionCallback.onStart(mediaProjectionInfo)
+
+ val notificationEntry = setupNotificationEntry(TEST_PACKAGE_NAME, false)
+
+ assertFalse(controller.shouldProtectNotification(notificationEntry))
+ }
+
+ @Test
fun shouldProtectNotification_projectionActive_bugReportHandlerExempt_false() {
whenever(mediaProjectionInfo.packageName).thenReturn(BUGREPORT_PACKAGE_NAME)
mediaProjectionCallback.onStart(mediaProjectionInfo)
@@ -327,6 +405,7 @@
assertFalse(controller.shouldProtectNotification(notificationEntry))
}
+
@Test
fun shouldProtectNotification_projectionActive_publicNotification_false() {
mediaProjectionCallback.onStart(mediaProjectionInfo)
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FacePropertyRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FacePropertyRepositoryKosmos.kt
index 6ef7419..ba07a84 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FacePropertyRepositoryKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FacePropertyRepositoryKosmos.kt
@@ -19,4 +19,5 @@
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.Kosmos.Fixture
-val Kosmos.facePropertyRepository by Fixture { FakeFacePropertyRepository() }
+val Kosmos.fakeFacePropertyRepository by Fixture { FakeFacePropertyRepository() }
+val Kosmos.facePropertyRepository by Fixture { fakeFacePropertyRepository }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorKosmos.kt
index 27803b2..c065545 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorKosmos.kt
@@ -16,7 +16,6 @@
package com.android.systemui.bouncer.domain.interactor
-import android.content.applicationContext
import com.android.systemui.authentication.domain.interactor.authenticationInteractor
import com.android.systemui.bouncer.data.repository.bouncerRepository
import com.android.systemui.classifier.domain.interactor.falsingInteractor
@@ -29,12 +28,10 @@
val Kosmos.bouncerInteractor by Fixture {
BouncerInteractor(
applicationScope = testScope.backgroundScope,
- applicationContext = applicationContext,
repository = bouncerRepository,
authenticationInteractor = authenticationInteractor,
deviceEntryFaceAuthInteractor = deviceEntryFaceAuthInteractor,
falsingInteractor = falsingInteractor,
powerInteractor = powerInteractor,
- simBouncerInteractor = simBouncerInteractor,
)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/SimBouncerInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/SimBouncerInteractorKosmos.kt
index 8ed9f45..02b79af 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/SimBouncerInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/SimBouncerInteractorKosmos.kt
@@ -38,7 +38,7 @@
telephonyManager = telephonyManager,
resources = mainResources,
keyguardUpdateMonitor = keyguardUpdateMonitor,
- euiccManager = applicationContext.getSystemService(Context.EUICC_SERVICE) as EuiccManager,
+ euiccManager = applicationContext.getSystemService(Context.EUICC_SERVICE) as EuiccManager?,
mobileConnectionsRepository = mobileConnectionsRepository,
)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/ui/viewmodel/BouncerMessageViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/ui/viewmodel/BouncerMessageViewModelKosmos.kt
new file mode 100644
index 0000000..4b64416
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/ui/viewmodel/BouncerMessageViewModelKosmos.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.bouncer.ui.viewmodel
+
+import android.content.applicationContext
+import com.android.systemui.authentication.domain.interactor.authenticationInteractor
+import com.android.systemui.bouncer.domain.interactor.bouncerInteractor
+import com.android.systemui.bouncer.domain.interactor.simBouncerInteractor
+import com.android.systemui.bouncer.shared.flag.composeBouncerFlags
+import com.android.systemui.deviceentry.domain.interactor.biometricMessageInteractor
+import com.android.systemui.deviceentry.domain.interactor.deviceEntryFaceAuthInteractor
+import com.android.systemui.deviceentry.domain.interactor.deviceEntryFingerprintAuthInteractor
+import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.user.ui.viewmodel.userSwitcherViewModel
+import com.android.systemui.util.time.systemClock
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+
+@ExperimentalCoroutinesApi
+val Kosmos.bouncerMessageViewModel by
+ Kosmos.Fixture {
+ BouncerMessageViewModel(
+ applicationContext = applicationContext,
+ applicationScope = testScope.backgroundScope,
+ bouncerInteractor = bouncerInteractor,
+ simBouncerInteractor = simBouncerInteractor,
+ authenticationInteractor = authenticationInteractor,
+ selectedUser = userSwitcherViewModel.selectedUser,
+ clock = systemClock,
+ biometricMessageInteractor = biometricMessageInteractor,
+ faceAuthInteractor = deviceEntryFaceAuthInteractor,
+ deviceEntryInteractor = deviceEntryInteractor,
+ fingerprintInteractor = deviceEntryFingerprintAuthInteractor,
+ flags = composeBouncerFlags,
+ )
+ }
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 6d97238..0f6c7cf 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
@@ -14,6 +14,8 @@
* limitations under the License.
*/
+@file:OptIn(ExperimentalCoroutinesApi::class)
+
package com.android.systemui.bouncer.ui.viewmodel
import android.content.applicationContext
@@ -30,7 +32,7 @@
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
+import kotlinx.coroutines.ExperimentalCoroutinesApi
val Kosmos.bouncerViewModel by Fixture {
BouncerViewModel(
@@ -47,7 +49,7 @@
users = userSwitcherViewModel.users,
userSwitcherMenu = userSwitcherViewModel.menu,
actionButton = bouncerActionButtonInteractor.actionButton,
- clock = systemClock,
devicePolicyManager = mock(),
+ bouncerMessageViewModel = bouncerMessageViewModel,
)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardClockRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardClockRepository.kt
index 5b642ea..eba5a11 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardClockRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardClockRepository.kt
@@ -45,15 +45,9 @@
private val _currentClock: MutableStateFlow<ClockController?> = MutableStateFlow(null)
override val currentClock = _currentClock
- private val _previewClockPair =
- MutableStateFlow(
- Pair(
- Mockito.mock(ClockController::class.java),
- Mockito.mock(ClockController::class.java)
- )
- )
- override val previewClockPair: StateFlow<Pair<ClockController, ClockController>> =
- _previewClockPair
+ private val _previewClock = MutableStateFlow(Mockito.mock(ClockController::class.java))
+ override val previewClock: Flow<ClockController>
+ get() = _previewClock
override val clockEventController: ClockEventController
get() = mock()
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 dcbd577..de6bfb2 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
@@ -18,7 +18,6 @@
package com.android.systemui.keyguard.data.repository
import android.annotation.FloatRange
-import android.util.Log
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.keyguard.shared.model.TransitionInfo
@@ -48,21 +47,8 @@
override val transitions: SharedFlow<TransitionStep> = _transitions
init {
- _transitions.tryEmit(
- TransitionStep(
- transitionState = TransitionState.STARTED,
- from = KeyguardState.OFF,
- to = KeyguardState.LOCKSCREEN,
- )
- )
-
- _transitions.tryEmit(
- TransitionStep(
- transitionState = TransitionState.FINISHED,
- from = KeyguardState.OFF,
- to = KeyguardState.LOCKSCREEN,
- )
- )
+ // Seed the fake repository with the same initial steps the actual repository uses.
+ KeyguardTransitionRepositoryImpl.initialTransitionSteps.forEach { _transitions.tryEmit(it) }
}
/**
@@ -207,16 +193,15 @@
suspend fun sendTransitionSteps(
steps: List<TransitionStep>,
testScope: TestScope,
- validateStep: Boolean = true
+ validateSteps: Boolean = true
) {
steps.forEach {
- sendTransitionStep(step = it, validateStep = validateStep)
+ sendTransitionStep(step = it, validateStep = validateSteps)
testScope.testScheduler.runCurrent()
}
}
override fun startTransition(info: TransitionInfo): UUID? {
- Log.i("TEST", "Start transition: ", Exception())
return if (info.animator == null) UUID.randomUUID() else null
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlowKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlowKosmos.kt
index dad1887..f7de5a4 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlowKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlowKosmos.kt
@@ -23,11 +23,13 @@
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.Kosmos.Fixture
import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.kosmos.testDispatcher
import kotlinx.coroutines.ExperimentalCoroutinesApi
val Kosmos.keyguardTransitionAnimationFlow by Fixture {
KeyguardTransitionAnimationFlow(
scope = applicationCoroutineScope,
+ mainDispatcher = testDispatcher,
transitionInteractor = keyguardTransitionInteractor,
logger = keyguardTransitionAnimationLogger,
)
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModelKosmos.kt
index 73fd999..709f864 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModelKosmos.kt
@@ -25,6 +25,7 @@
import com.android.systemui.keyguard.ui.transitions.DeviceEntryIconTransition
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.Kosmos.Fixture
+import com.android.systemui.kosmos.testScope
import com.android.systemui.scene.shared.flag.sceneContainerFlags
import com.android.systemui.shade.domain.interactor.shadeInteractor
import com.android.systemui.statusbar.phone.statusBarKeyguardViewManager
@@ -50,5 +51,6 @@
keyguardViewController = { statusBarKeyguardViewManager },
deviceEntryInteractor = deviceEntryInteractor,
deviceEntrySourceInteractor = deviceEntrySourceInteractor,
+ scope = testScope.backgroundScope,
)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToGoneTransitionViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToGoneTransitionViewModelKosmos.kt
new file mode 100644
index 0000000..f389142
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToGoneTransitionViewModelKosmos.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.keyguard.ui.viewmodel
+
+import com.android.systemui.keyguard.ui.keyguardTransitionAnimationFlow
+import com.android.systemui.kosmos.Kosmos
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+
+@ExperimentalCoroutinesApi
+val Kosmos.dreamingToGoneTransitionViewModel by
+ Kosmos.Fixture {
+ DreamingToGoneTransitionViewModel(
+ animationFlow = keyguardTransitionAnimationFlow,
+ )
+ }
\ No newline at end of file
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/GoneToLockscreenTransitionViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/GoneToLockscreenTransitionViewModelKosmos.kt
new file mode 100644
index 0000000..1b6fa06
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/GoneToLockscreenTransitionViewModelKosmos.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:OptIn(ExperimentalCoroutinesApi::class)
+
+package com.android.systemui.keyguard.ui.viewmodel
+
+import com.android.systemui.keyguard.ui.keyguardTransitionAnimationFlow
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.Kosmos.Fixture
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+
+var Kosmos.goneToLockscreenTransitionViewModel by Fixture {
+ GoneToLockscreenTransitionViewModel(
+ animationFlow = keyguardTransitionAnimationFlow,
+ )
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt
index a863edf..b91aafe 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt
@@ -46,10 +46,13 @@
dozingToGoneTransitionViewModel = dozingToGoneTransitionViewModel,
dozingToLockscreenTransitionViewModel = dozingToLockscreenTransitionViewModel,
dozingToOccludedTransitionViewModel = dozingToOccludedTransitionViewModel,
+ dreamingToGoneTransitionViewModel = dreamingToGoneTransitionViewModel,
dreamingToLockscreenTransitionViewModel = dreamingToLockscreenTransitionViewModel,
glanceableHubToLockscreenTransitionViewModel = glanceableHubToLockscreenTransitionViewModel,
goneToAodTransitionViewModel = goneToAodTransitionViewModel,
goneToDozingTransitionViewModel = goneToDozingTransitionViewModel,
+ goneToDreamingTransitionViewModel = goneToDreamingTransitionViewModel,
+ goneToLockscreenTransitionViewModel = goneToLockscreenTransitionViewModel,
lockscreenToAodTransitionViewModel = lockscreenToAodTransitionViewModel,
lockscreenToDozingTransitionViewModel = lockscreenToDozingTransitionViewModel,
lockscreenToDreamingTransitionViewModel = lockscreenToDreamingTransitionViewModel,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToLockscreenTransitionViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToLockscreenTransitionViewModelKosmos.kt
index 8566251..370afc3 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToLockscreenTransitionViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToLockscreenTransitionViewModelKosmos.kt
@@ -18,7 +18,6 @@
package com.android.systemui.keyguard.ui.viewmodel
-import com.android.systemui.deviceentry.domain.interactor.deviceEntryUdfpsInteractor
import com.android.systemui.keyguard.ui.keyguardTransitionAnimationFlow
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.Kosmos.Fixture
@@ -26,7 +25,6 @@
val Kosmos.primaryBouncerToLockscreenTransitionViewModel by Fixture {
PrimaryBouncerToLockscreenTransitionViewModel(
- deviceEntryUdfpsInteractor = deviceEntryUdfpsInteractor,
animationFlow = keyguardTransitionAnimationFlow,
)
}
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 e861892..c879588 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
@@ -19,6 +19,7 @@
package com.android.systemui.kosmos
import android.content.applicationContext
+import android.os.fakeExecutorHandler
import com.android.systemui.SysuiTestCase
import com.android.systemui.bouncer.data.repository.bouncerRepository
import com.android.systemui.bouncer.domain.interactor.simBouncerInteractor
@@ -27,6 +28,7 @@
import com.android.systemui.common.ui.domain.interactor.configurationInteractor
import com.android.systemui.communal.data.repository.fakeCommunalRepository
import com.android.systemui.communal.domain.interactor.communalInteractor
+import com.android.systemui.concurrency.fakeExecutor
import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor
import com.android.systemui.deviceentry.domain.interactor.deviceUnlockedInteractor
import com.android.systemui.flags.fakeFeatureFlagsClassic
@@ -65,6 +67,8 @@
val testScope by lazy { kosmos.testScope }
val fakeFeatureFlags by lazy { kosmos.fakeFeatureFlagsClassic }
val fakeSceneContainerFlags by lazy { kosmos.fakeSceneContainerFlags }
+ val fakeExecutor by lazy { kosmos.fakeExecutor }
+ val fakeExecutorHandler by lazy { kosmos.fakeExecutorHandler }
val configurationRepository by lazy { kosmos.fakeConfigurationRepository }
val configurationInteractor by lazy { kosmos.configurationInteractor }
val bouncerRepository by lazy { kosmos.bouncerRepository }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/data/repository/MediaDataRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/data/repository/MediaDataRepositoryKosmos.kt
new file mode 100644
index 0000000..5c17cb9
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/data/repository/MediaDataRepositoryKosmos.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.media.controls.data.repository
+
+import com.android.systemui.dump.dumpManager
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.Kosmos.Fixture
+import com.android.systemui.media.controls.util.mediaFlags
+
+val Kosmos.mediaDataRepository by Fixture {
+ MediaDataRepository(mediaFlags = mediaFlags, dumpManager = dumpManager)
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/data/repository/MediaFilterRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/data/repository/MediaFilterRepositoryKosmos.kt
new file mode 100644
index 0000000..7ce810e
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/data/repository/MediaFilterRepositoryKosmos.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.media.controls.data.repository
+
+import com.android.systemui.kosmos.Kosmos
+
+val Kosmos.mediaFilterRepository by Kosmos.Fixture { MediaFilterRepository() }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaDataCombineLatestKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaDataCombineLatestKosmos.kt
new file mode 100644
index 0000000..12a6325
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaDataCombineLatestKosmos.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.media.controls.domain.pipeline
+
+import com.android.systemui.kosmos.Kosmos
+
+val Kosmos.mediaDataCombineLatest by Kosmos.Fixture { MediaDataCombineLatest() }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilterKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilterKosmos.kt
new file mode 100644
index 0000000..d56222e
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilterKosmos.kt
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.media.controls.domain.pipeline
+
+import android.content.applicationContext
+import com.android.systemui.broadcast.BroadcastSender
+import com.android.systemui.concurrency.fakeExecutor
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.media.controls.data.repository.mediaFilterRepository
+import com.android.systemui.media.controls.util.mediaFlags
+import com.android.systemui.media.controls.util.mediaUiEventLogger
+import com.android.systemui.settings.userTracker
+import com.android.systemui.statusbar.notificationLockscreenUserManager
+import com.android.systemui.util.time.systemClock
+import com.android.systemui.util.wakelock.WakeLockFake
+
+val Kosmos.mediaDataFilter by
+ Kosmos.Fixture {
+ MediaDataFilterImpl(
+ context = applicationContext,
+ userTracker = userTracker,
+ broadcastSender =
+ BroadcastSender(
+ applicationContext,
+ WakeLockFake.Builder(applicationContext),
+ fakeExecutor
+ ),
+ lockscreenUserManager = notificationLockscreenUserManager,
+ executor = fakeExecutor,
+ systemClock = systemClock,
+ logger = mediaUiEventLogger,
+ mediaFlags = mediaFlags,
+ mediaFilterRepository = mediaFilterRepository,
+ )
+ }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorKosmos.kt
new file mode 100644
index 0000000..cc1ad1f
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorKosmos.kt
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.media.controls.domain.pipeline
+
+import android.app.smartspace.SmartspaceManager
+import android.content.applicationContext
+import android.os.fakeExecutorHandler
+import com.android.keyguard.keyguardUpdateMonitor
+import com.android.systemui.broadcast.broadcastDispatcher
+import com.android.systemui.concurrency.fakeExecutor
+import com.android.systemui.dump.dumpManager
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.media.controls.data.repository.mediaDataRepository
+import com.android.systemui.media.controls.shared.model.SmartspaceMediaDataProvider
+import com.android.systemui.media.controls.util.mediaControllerFactory
+import com.android.systemui.media.controls.util.mediaFlags
+import com.android.systemui.media.controls.util.mediaUiEventLogger
+import com.android.systemui.plugins.activityStarter
+import com.android.systemui.util.Utils
+import com.android.systemui.util.settings.fakeSettings
+import com.android.systemui.util.time.systemClock
+
+val Kosmos.mediaDataProcessor by
+ Kosmos.Fixture {
+ MediaDataProcessor(
+ context = applicationContext,
+ applicationScope = applicationCoroutineScope,
+ backgroundDispatcher = testDispatcher,
+ backgroundExecutor = fakeExecutor,
+ uiExecutor = fakeExecutor,
+ foregroundExecutor = fakeExecutor,
+ handler = fakeExecutorHandler,
+ mediaControllerFactory = mediaControllerFactory,
+ broadcastDispatcher = broadcastDispatcher,
+ dumpManager = dumpManager,
+ activityStarter = activityStarter,
+ smartspaceMediaDataProvider = SmartspaceMediaDataProvider(),
+ useMediaResumption = Utils.useMediaResumption(applicationContext),
+ useQsMediaPlayer = Utils.useQsMediaPlayer(applicationContext),
+ systemClock = systemClock,
+ secureSettings = fakeSettings,
+ mediaFlags = mediaFlags,
+ logger = mediaUiEventLogger,
+ smartspaceManager = SmartspaceManager(applicationContext),
+ keyguardUpdateMonitor = keyguardUpdateMonitor,
+ mediaDataRepository = mediaDataRepository,
+ )
+ }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceManagerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceManagerKosmos.kt
new file mode 100644
index 0000000..b98f557
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceManagerKosmos.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.media.controls.domain.pipeline
+
+import android.content.applicationContext
+import android.media.MediaRouter2Manager
+import android.os.fakeExecutorHandler
+import com.android.settingslib.bluetooth.LocalBluetoothManager
+import com.android.systemui.concurrency.fakeExecutor
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.media.controls.util.localMediaManagerFactory
+import com.android.systemui.media.controls.util.mediaControllerFactory
+import com.android.systemui.media.muteawait.mediaMuteAwaitConnectionManagerFactory
+import com.android.systemui.statusbar.policy.configurationController
+
+val Kosmos.mediaDeviceManager by
+ Kosmos.Fixture {
+ MediaDeviceManager(
+ context = applicationContext,
+ controllerFactory = mediaControllerFactory,
+ localMediaManagerFactory = localMediaManagerFactory,
+ mr2manager = { MediaRouter2Manager.getInstance(applicationContext) },
+ muteAwaitConnectionManagerFactory = mediaMuteAwaitConnectionManagerFactory,
+ configurationController = configurationController,
+ localBluetoothManager = {
+ LocalBluetoothManager.create(applicationContext, fakeExecutorHandler)
+ },
+ fgExecutor = fakeExecutor,
+ bgExecutor = fakeExecutor,
+ )
+ }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaResumeListenerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaResumeListenerKosmos.kt
new file mode 100644
index 0000000..2a3e84b
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaResumeListenerKosmos.kt
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.media.controls.domain.pipeline
+
+import android.content.applicationContext
+import com.android.systemui.broadcast.broadcastDispatcher
+import com.android.systemui.concurrency.fakeExecutor
+import com.android.systemui.dump.dumpManager
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.media.controls.domain.resume.MediaResumeListener
+import com.android.systemui.media.controls.domain.resume.resumeMediaBrowserFactory
+import com.android.systemui.media.controls.util.mediaFlags
+import com.android.systemui.settings.userTracker
+import com.android.systemui.tuner.TunerService
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.time.systemClock
+
+val Kosmos.mediaResumeListener by
+ Kosmos.Fixture {
+ MediaResumeListener(
+ context = applicationContext,
+ broadcastDispatcher = broadcastDispatcher,
+ userTracker = userTracker,
+ mainExecutor = fakeExecutor,
+ backgroundExecutor = fakeExecutor,
+ tunerService = mock<TunerService> {},
+ mediaBrowserFactory = resumeMediaBrowserFactory,
+ dumpManager = dumpManager,
+ systemClock = systemClock,
+ mediaFlags = mediaFlags,
+ )
+ }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaSessionBasedFilterKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaSessionBasedFilterKosmos.kt
new file mode 100644
index 0000000..9b02a5b
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaSessionBasedFilterKosmos.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.media.controls.domain.pipeline
+
+import android.content.applicationContext
+import android.media.session.MediaSessionManager
+import com.android.systemui.concurrency.fakeExecutor
+import com.android.systemui.kosmos.Kosmos
+
+val Kosmos.mediaSessionBasedFilter by
+ Kosmos.Fixture {
+ MediaSessionBasedFilter(
+ context = applicationContext,
+ sessionManager = MediaSessionManager(applicationContext),
+ foregroundExecutor = fakeExecutor,
+ backgroundExecutor = fakeExecutor,
+ )
+ }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaTimeoutListenerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaTimeoutListenerKosmos.kt
new file mode 100644
index 0000000..6ec6378
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaTimeoutListenerKosmos.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.media.controls.domain.pipeline
+
+import com.android.systemui.concurrency.fakeExecutor
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.log.logcatLogBuffer
+import com.android.systemui.media.controls.util.mediaControllerFactory
+import com.android.systemui.media.controls.util.mediaFlags
+import com.android.systemui.plugins.statusbar.statusBarStateController
+import com.android.systemui.util.time.systemClock
+
+val Kosmos.mediaTimeoutListener by
+ Kosmos.Fixture {
+ MediaTimeoutListener(
+ mediaControllerFactory = mediaControllerFactory,
+ mainExecutor = fakeExecutor,
+ logger = MediaTimeoutLogger(logcatLogBuffer("MediaTimeoutLogBuffer")),
+ statusBarStateController = statusBarStateController,
+ systemClock = systemClock,
+ mediaFlags = mediaFlags,
+ )
+ }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/interactor/MediaCarouselInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/interactor/MediaCarouselInteractorKosmos.kt
new file mode 100644
index 0000000..e5e2aff
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/interactor/MediaCarouselInteractorKosmos.kt
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.media.controls.domain.pipeline.interactor
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.media.controls.data.repository.mediaDataRepository
+import com.android.systemui.media.controls.data.repository.mediaFilterRepository
+import com.android.systemui.media.controls.domain.pipeline.mediaDataCombineLatest
+import com.android.systemui.media.controls.domain.pipeline.mediaDataFilter
+import com.android.systemui.media.controls.domain.pipeline.mediaDataProcessor
+import com.android.systemui.media.controls.domain.pipeline.mediaDeviceManager
+import com.android.systemui.media.controls.domain.pipeline.mediaResumeListener
+import com.android.systemui.media.controls.domain.pipeline.mediaSessionBasedFilter
+import com.android.systemui.media.controls.domain.pipeline.mediaTimeoutListener
+import com.android.systemui.media.controls.util.mediaFlags
+
+val Kosmos.mediaCarouselInteractor by
+ Kosmos.Fixture {
+ MediaCarouselInteractor(
+ applicationScope = applicationCoroutineScope,
+ mediaDataRepository = mediaDataRepository,
+ mediaDataProcessor = mediaDataProcessor,
+ mediaTimeoutListener = mediaTimeoutListener,
+ mediaResumeListener = mediaResumeListener,
+ mediaSessionBasedFilter = mediaSessionBasedFilter,
+ mediaDeviceManager = mediaDeviceManager,
+ mediaDataCombineLatest = mediaDataCombineLatest,
+ mediaDataFilter = mediaDataFilter,
+ mediaFilterRepository = mediaFilterRepository,
+ mediaFlags = mediaFlags,
+ )
+ }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/resume/MediaBrowserFactoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/resume/MediaBrowserFactoryKosmos.kt
new file mode 100644
index 0000000..2621869
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/resume/MediaBrowserFactoryKosmos.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.media.controls.domain.resume
+
+import android.content.applicationContext
+import com.android.systemui.kosmos.Kosmos
+
+val Kosmos.mediaBrowserFactory by Kosmos.Fixture { MediaBrowserFactory(applicationContext) }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/resume/ResumeMediaBrowserFactoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/resume/ResumeMediaBrowserFactoryKosmos.kt
new file mode 100644
index 0000000..ed720bd
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/resume/ResumeMediaBrowserFactoryKosmos.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.media.controls.domain.resume
+
+import android.content.applicationContext
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.log.logcatLogBuffer
+
+val Kosmos.resumeMediaBrowserFactory by
+ Kosmos.Fixture {
+ ResumeMediaBrowserFactory(
+ applicationContext,
+ mediaBrowserFactory,
+ ResumeMediaBrowserLogger(logcatLogBuffer("ResumeMediaLogBuffer"))
+ )
+ }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/util/LocalMediaManagerFactoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/util/LocalMediaManagerFactoryKosmos.kt
new file mode 100644
index 0000000..2e0c9b8
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/util/LocalMediaManagerFactoryKosmos.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.media.controls.util
+
+import android.content.applicationContext
+import android.os.fakeExecutorHandler
+import com.android.settingslib.bluetooth.LocalBluetoothManager
+import com.android.systemui.kosmos.Kosmos
+
+val Kosmos.localMediaManagerFactory by
+ Kosmos.Fixture {
+ LocalMediaManagerFactory(
+ context = applicationContext,
+ localBluetoothManager =
+ LocalBluetoothManager.create(applicationContext, fakeExecutorHandler),
+ )
+ }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/util/MediaControllerFactoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/util/MediaControllerFactoryKosmos.kt
new file mode 100644
index 0000000..1ce6e82
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/util/MediaControllerFactoryKosmos.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.media.controls.util
+
+import android.content.applicationContext
+import com.android.systemui.kosmos.Kosmos
+
+val Kosmos.mediaControllerFactory by Kosmos.Fixture { MediaControllerFactory(applicationContext) }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/util/MediaFlagsKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/util/MediaFlagsKosmos.kt
new file mode 100644
index 0000000..6f652f2
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/util/MediaFlagsKosmos.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.media.controls.util
+
+import com.android.systemui.flags.featureFlagsClassic
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.scene.shared.flag.sceneContainerFlags
+
+val Kosmos.mediaFlags by
+ Kosmos.Fixture {
+ MediaFlags(featureFlags = featureFlagsClassic, sceneContainerFlags = sceneContainerFlags)
+ }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/util/MediaUiEventLoggerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/util/MediaUiEventLoggerKosmos.kt
new file mode 100644
index 0000000..b01876d
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/util/MediaUiEventLoggerKosmos.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.media.controls.util
+
+import com.android.internal.logging.uiEventLogger
+import com.android.systemui.kosmos.Kosmos
+
+val Kosmos.mediaUiEventLogger by Kosmos.Fixture { MediaUiEventLogger(uiEventLogger) }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/media/muteawait/MediaMuteAwaitConnectionManagerFactoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/media/muteawait/MediaMuteAwaitConnectionManagerFactoryKosmos.kt
new file mode 100644
index 0000000..b78bd58
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/media/muteawait/MediaMuteAwaitConnectionManagerFactoryKosmos.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.media.muteawait
+
+import android.content.applicationContext
+import com.android.systemui.concurrency.fakeExecutor
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.log.logcatLogBuffer
+
+val Kosmos.mediaMuteAwaitConnectionManagerFactory by
+ Kosmos.Fixture {
+ MediaMuteAwaitConnectionManagerFactory(
+ context = applicationContext,
+ logger = MediaMuteAwaitLogger(logcatLogBuffer("MediaMuteAwaitLogBuffer")),
+ mainExecutor = fakeExecutor,
+ )
+ }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/work/WorkModeTileKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/work/WorkModeTileKosmos.kt
new file mode 100644
index 0000000..c04c5ed
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/work/WorkModeTileKosmos.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.tiles.impl.work
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.qs.qsEventLogger
+import com.android.systemui.statusbar.policy.PolicyModule
+
+val Kosmos.qsWorkModeTileConfig by
+ Kosmos.Fixture { PolicyModule.provideWorkModeTileConfig(qsEventLogger) }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeControllerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeControllerKosmos.kt
index f4acf4d..16c5b72 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeControllerKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeControllerKosmos.kt
@@ -31,6 +31,7 @@
import com.android.systemui.shade.domain.interactor.shadeInteractor
import com.android.systemui.statusbar.CommandQueue
import com.android.systemui.statusbar.NotificationShadeWindowController
+import com.android.systemui.statusbar.VibratorHelper
import com.android.systemui.statusbar.notification.row.NotificationGutsManager
import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout
import com.android.systemui.statusbar.notification.stack.ui.viewmodel.windowRootViewVisibilityInteractor
@@ -52,6 +53,7 @@
notificationStackScrollLayout = mock<NotificationStackScrollLayout>(),
deviceEntryInteractor = deviceEntryInteractor,
touchLog = mock<LogBuffer>(),
+ vibratorHelper = mock<VibratorHelper>(),
commandQueue = mock<CommandQueue>(),
statusBarKeyguardViewManager = mock<StatusBarKeyguardViewManager>(),
notificationShadeWindowController = mock<NotificationShadeWindowController>(),
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/repository/FakeHeadsUpRowRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/repository/FakeHeadsUpRowRepository.kt
new file mode 100644
index 0000000..2e983a8
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/repository/FakeHeadsUpRowRepository.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.statusbar.notification.data.repository
+
+import kotlinx.coroutines.flow.MutableStateFlow
+
+class FakeHeadsUpRowRepository(override val key: String, override val elementKey: Any) :
+ HeadsUpRowRepository {
+ override val isPinned: MutableStateFlow<Boolean> = MutableStateFlow(false)
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/data/repository/HeadsUpNotificationRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/data/repository/HeadsUpNotificationRepositoryKosmos.kt
index 25864ae..165c942 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/data/repository/HeadsUpNotificationRepositoryKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/data/repository/HeadsUpNotificationRepositoryKosmos.kt
@@ -18,11 +18,16 @@
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.Kosmos.Fixture
-import com.android.systemui.statusbar.notification.data.repository.HeadsUpNotificationRepository
+import com.android.systemui.statusbar.notification.data.repository.HeadsUpRepository
+import com.android.systemui.statusbar.notification.data.repository.HeadsUpRowRepository
+import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
val Kosmos.headsUpNotificationRepository by Fixture { FakeHeadsUpNotificationRepository() }
-class FakeHeadsUpNotificationRepository : HeadsUpNotificationRepository {
- override val hasPinnedHeadsUp = MutableStateFlow(false)
+class FakeHeadsUpNotificationRepository : HeadsUpRepository {
+ override val headsUpAnimatingAway: MutableStateFlow<Boolean> = MutableStateFlow(false)
+ override val topHeadsUpRow: Flow<HeadsUpRowRepository?> = MutableStateFlow(null)
+ override val activeHeadsUpRows: MutableStateFlow<Set<HeadsUpRowRepository>> =
+ MutableStateFlow(emptySet())
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/data/repository/HeadsUpNotificationsRepositoryExt.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/data/repository/HeadsUpNotificationsRepositoryExt.kt
new file mode 100644
index 0000000..9be7dfe
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/data/repository/HeadsUpNotificationsRepositoryExt.kt
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.stack.data.repository
+
+import com.android.systemui.statusbar.notification.data.repository.HeadsUpRowRepository
+
+fun FakeHeadsUpNotificationRepository.setNotifications(notifications: List<HeadsUpRowRepository>) {
+ setNotifications(*notifications.toTypedArray())
+}
+
+fun FakeHeadsUpNotificationRepository.setNotifications(vararg notifications: HeadsUpRowRepository) {
+ this.activeHeadsUpRows.value = notifications.toSet()
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractorKosmos.kt
index 546a1e0..5605d10 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractorKosmos.kt
@@ -18,10 +18,12 @@
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.Kosmos.Fixture
+import com.android.systemui.shade.domain.interactor.shadeInteractor
import com.android.systemui.statusbar.notification.stack.data.repository.notificationStackAppearanceRepository
val Kosmos.notificationStackAppearanceInteractor by Fixture {
NotificationStackAppearanceInteractor(
repository = notificationStackAppearanceRepository,
+ shadeInteractor = shadeInteractor,
)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinderKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinderKosmos.kt
index 2de26f1..ee3216b 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinderKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinderKosmos.kt
@@ -28,6 +28,7 @@
import com.android.systemui.statusbar.notification.stack.displaySwitchNotificationsHiderTracker
import com.android.systemui.statusbar.notification.stack.ui.view.notificationStatsLogger
import com.android.systemui.statusbar.notification.stack.ui.viewmodel.notificationListViewModel
+import com.android.systemui.statusbar.notification.ui.viewbinder.headsUpNotificationViewBinder
import com.android.systemui.statusbar.phone.notificationIconAreaController
import java.util.Optional
@@ -37,6 +38,7 @@
backgroundDispatcher = testDispatcher,
configuration = configurationState,
falsingManager = falsingManager,
+ hunBinder = headsUpNotificationViewBinder,
iconAreaController = notificationIconAreaController,
loggerOptional = Optional.of(notificationStatsLogger),
metricsLogger = metricsLogger,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelKosmos.kt
index 930a4bb..c65d0a3 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelKosmos.kt
@@ -16,6 +16,7 @@
package com.android.systemui.statusbar.notification.stack.ui.viewmodel
+import com.android.systemui.keyguard.domain.interactor.keyguardInteractor
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.Kosmos.Fixture
import com.android.systemui.kosmos.testDispatcher
@@ -25,6 +26,7 @@
import com.android.systemui.statusbar.notification.domain.interactor.seenNotificationsInteractor
import com.android.systemui.statusbar.notification.footer.ui.viewmodel.footerViewModel
import com.android.systemui.statusbar.notification.shelf.ui.viewmodel.notificationShelfViewModel
+import com.android.systemui.statusbar.notification.stack.domain.interactor.headsUpNotificationInteractor
import com.android.systemui.statusbar.notification.stack.domain.interactor.notificationStackInteractor
import com.android.systemui.statusbar.policy.domain.interactor.userSetupInteractor
import com.android.systemui.statusbar.policy.domain.interactor.zenModeInteractor
@@ -38,6 +40,8 @@
Optional.of(notificationListLoggerViewModel),
activeNotificationsInteractor,
notificationStackInteractor,
+ headsUpNotificationInteractor,
+ keyguardInteractor,
remoteInputInteractor,
seenNotificationsInteractor,
shadeInteractor,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/ui/viewbinder/HeadsUpNotificationViewBinderKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/ui/viewbinder/HeadsUpNotificationViewBinderKosmos.kt
new file mode 100644
index 0000000..6a995c0
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/ui/viewbinder/HeadsUpNotificationViewBinderKosmos.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.statusbar.notification.ui.viewbinder
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.statusbar.notification.stack.ui.viewmodel.notificationListViewModel
+
+val Kosmos.headsUpNotificationViewBinder by
+ Kosmos.Fixture { HeadsUpNotificationViewBinder(viewModel = notificationListViewModel) }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeManagedProfileController.java b/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeManagedProfileController.java
index 18b07cf..59adb11 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeManagedProfileController.java
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeManagedProfileController.java
@@ -19,24 +19,65 @@
import com.android.systemui.statusbar.phone.ManagedProfileController;
import com.android.systemui.statusbar.phone.ManagedProfileController.Callback;
+import java.util.ArrayList;
+import java.util.List;
+
public class FakeManagedProfileController extends BaseLeakChecker<Callback> implements
ManagedProfileController {
+
+ private List<Callback> mCallbackList = new ArrayList<>();
+ private boolean mIsEnabled = false;
+ private boolean mHasActiveProfile = false;
+
public FakeManagedProfileController(LeakCheck test) {
super(test, "profile");
}
@Override
+ public void addCallback(Callback cb) {
+ mCallbackList.add(cb);
+ cb.onManagedProfileChanged();
+ }
+
+ @Override
+ public void removeCallback(Callback cb) {
+ mCallbackList.remove(cb);
+ }
+
+ @Override
public void setWorkModeEnabled(boolean enabled) {
+ if (mIsEnabled != enabled) {
+ mIsEnabled = enabled;
+ for (Callback cb: mCallbackList) {
+ cb.onManagedProfileChanged();
+ }
+ }
}
@Override
public boolean hasActiveProfile() {
- return false;
+ return mHasActiveProfile;
+ }
+
+ /**
+ * Triggers onManagedProfileChanged on callbacks when value flips.
+ */
+ public void setHasActiveProfile(boolean hasActiveProfile) {
+ if (mHasActiveProfile != hasActiveProfile) {
+ mHasActiveProfile = hasActiveProfile;
+ for (Callback cb: mCallbackList) {
+ cb.onManagedProfileChanged();
+ if (!hasActiveProfile) {
+ cb.onManagedProfileRemoved();
+ }
+ }
+ }
+
}
@Override
public boolean isWorkModeEnabled() {
- return false;
+ return mIsEnabled;
}
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/MediaControllerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/MediaControllerKosmos.kt
new file mode 100644
index 0000000..5db1724
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/MediaControllerKosmos.kt
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.volume
+
+import android.content.packageManager
+import android.content.pm.ApplicationInfo
+import android.media.AudioAttributes
+import android.media.session.MediaController
+import android.media.session.MediaSession
+import com.android.systemui.kosmos.Kosmos
+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
+
+private const val LOCAL_PACKAGE = "local.test.pkg"
+var Kosmos.localMediaController: MediaController by
+ Kosmos.Fixture {
+ val appInfo: ApplicationInfo = mock {
+ whenever(loadLabel(any())).thenReturn("local_media_controller_label")
+ }
+ whenever(packageManager.getApplicationInfo(eq(LOCAL_PACKAGE), any<Int>()))
+ .thenReturn(appInfo)
+
+ val localSessionToken: MediaSession.Token = MediaSession.Token(0, mock {})
+ mock {
+ whenever(packageName).thenReturn(LOCAL_PACKAGE)
+ whenever(playbackInfo)
+ .thenReturn(
+ MediaController.PlaybackInfo(
+ MediaController.PlaybackInfo.PLAYBACK_TYPE_LOCAL,
+ 0,
+ 0,
+ 0,
+ AudioAttributes.Builder().build(),
+ "",
+ )
+ )
+ whenever(sessionToken).thenReturn(localSessionToken)
+ }
+ }
+
+private const val REMOTE_PACKAGE = "remote.test.pkg"
+var Kosmos.remoteMediaController: MediaController by
+ Kosmos.Fixture {
+ val appInfo: ApplicationInfo = mock {
+ whenever(loadLabel(any())).thenReturn("remote_media_controller_label")
+ }
+ whenever(packageManager.getApplicationInfo(eq(REMOTE_PACKAGE), any<Int>()))
+ .thenReturn(appInfo)
+
+ val remoteSessionToken: MediaSession.Token = MediaSession.Token(0, mock {})
+ mock {
+ whenever(packageName).thenReturn(REMOTE_PACKAGE)
+ whenever(playbackInfo)
+ .thenReturn(
+ MediaController.PlaybackInfo(
+ MediaController.PlaybackInfo.PLAYBACK_TYPE_REMOTE,
+ 0,
+ 0,
+ 0,
+ AudioAttributes.Builder().build(),
+ "",
+ )
+ )
+ whenever(sessionToken).thenReturn(remoteSessionToken)
+ }
+ }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/MediaOutputKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/MediaOutputKosmos.kt
index 3938f77..fa3a19b 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/MediaOutputKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/MediaOutputKosmos.kt
@@ -18,7 +18,6 @@
import android.content.packageManager
import android.content.pm.ApplicationInfo
-import android.media.session.MediaController
import android.os.Handler
import android.testing.TestableLooper
import com.android.systemui.kosmos.Kosmos
@@ -32,11 +31,10 @@
import com.android.systemui.volume.data.repository.FakeMediaControllerRepository
import com.android.systemui.volume.panel.component.mediaoutput.data.repository.FakeLocalMediaRepositoryFactory
import com.android.systemui.volume.panel.component.mediaoutput.data.repository.LocalMediaRepositoryFactory
+import com.android.systemui.volume.panel.component.mediaoutput.domain.interactor.MediaDeviceSessionInteractor
import com.android.systemui.volume.panel.component.mediaoutput.domain.interactor.MediaOutputActionsInteractor
import com.android.systemui.volume.panel.component.mediaoutput.domain.interactor.MediaOutputInteractor
-var Kosmos.mediaController: MediaController by Kosmos.Fixture { mock {} }
-
val Kosmos.localMediaRepository by Kosmos.Fixture { FakeLocalMediaRepository() }
val Kosmos.localMediaRepositoryFactory: LocalMediaRepositoryFactory by
Kosmos.Fixture { FakeLocalMediaRepositoryFactory { localMediaRepository } }
@@ -56,6 +54,14 @@
},
testScope.backgroundScope,
testScope.testScheduler,
+ mediaControllerRepository,
+ )
+ }
+
+val Kosmos.mediaDeviceSessionInteractor by
+ Kosmos.Fixture {
+ MediaDeviceSessionInteractor(
+ testScope.testScheduler,
Handler(TestableLooper.get(testCase).looper),
mediaControllerRepository,
)
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/FakeLocalMediaRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/FakeLocalMediaRepository.kt
index 284bd55..909be75 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/FakeLocalMediaRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/FakeLocalMediaRepository.kt
@@ -17,7 +17,6 @@
package com.android.systemui.volume.data.repository
import com.android.settingslib.media.MediaDevice
-import com.android.settingslib.volume.data.model.RoutingSession
import com.android.settingslib.volume.data.repository.LocalMediaRepository
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
@@ -25,35 +24,11 @@
class FakeLocalMediaRepository : LocalMediaRepository {
- private val volumeBySession: MutableMap<String?, Int> = mutableMapOf()
-
- private val mutableMediaDevices = MutableStateFlow<List<MediaDevice>>(emptyList())
- override val mediaDevices: StateFlow<List<MediaDevice>>
- get() = mutableMediaDevices.asStateFlow()
-
private val mutableCurrentConnectedDevice = MutableStateFlow<MediaDevice?>(null)
override val currentConnectedDevice: StateFlow<MediaDevice?>
get() = mutableCurrentConnectedDevice.asStateFlow()
- private val mutableRemoteRoutingSessions = MutableStateFlow<List<RoutingSession>>(emptyList())
- override val remoteRoutingSessions: StateFlow<List<RoutingSession>>
- get() = mutableRemoteRoutingSessions.asStateFlow()
-
- fun updateMediaDevices(devices: List<MediaDevice>) {
- mutableMediaDevices.value = devices
- }
-
fun updateCurrentConnectedDevice(device: MediaDevice?) {
mutableCurrentConnectedDevice.value = device
}
-
- fun updateRemoteRoutingSessions(sessions: List<RoutingSession>) {
- mutableRemoteRoutingSessions.value = sessions
- }
-
- fun getSessionVolume(sessionId: String?): Int = volumeBySession.getOrDefault(sessionId, 0)
-
- override suspend fun adjustSessionVolume(sessionId: String?, volume: Int) {
- volumeBySession[sessionId] = volume
- }
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/FakeMediaControllerRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/FakeMediaControllerRepository.kt
index 6d52e52..8ab5bd90 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/FakeMediaControllerRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/FakeMediaControllerRepository.kt
@@ -24,11 +24,11 @@
class FakeMediaControllerRepository : MediaControllerRepository {
- private val mutableActiveLocalMediaController = MutableStateFlow<MediaController?>(null)
- override val activeLocalMediaController: StateFlow<MediaController?> =
- mutableActiveLocalMediaController.asStateFlow()
+ private val mutableActiveSessions = MutableStateFlow<List<MediaController>>(emptyList())
+ override val activeSessions: StateFlow<List<MediaController>>
+ get() = mutableActiveSessions.asStateFlow()
- fun setActiveLocalMediaController(controller: MediaController?) {
- mutableActiveLocalMediaController.value = controller
+ fun setActiveSessions(sessions: List<MediaController>) {
+ mutableActiveSessions.value = sessions
}
}
diff --git a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/Parcel_host.java b/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/Parcel_host.java
index 81ad31e..61ec7b4 100644
--- a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/Parcel_host.java
+++ b/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/Parcel_host.java
@@ -383,9 +383,21 @@
// Assume false for now, because we don't support writing FDs yet.
return false;
}
+
public static boolean nativeHasFileDescriptorsInRange(
long nativePtr, int offset, int length) {
// Assume false for now, because we don't support writing FDs yet.
return false;
}
+
+ public static boolean nativeHasBinders(long nativePtr) {
+ // Assume false for now, because we don't support adding binders.
+ return false;
+ }
+
+ public static boolean nativeHasBindersInRange(
+ long nativePtr, int offset, int length) {
+ // Assume false for now, because we don't support writing FDs yet.
+ return false;
+ }
}
diff --git a/services/Android.bp b/services/Android.bp
index 98a7979..7bbb42e 100644
--- a/services/Android.bp
+++ b/services/Android.bp
@@ -253,6 +253,7 @@
required: [
"libukey2_jni_shared",
+ "protolog.conf.json.gz",
],
lint: {
baseline_filename: "lint-baseline.xml",
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
index 4e14dee..3e7682a 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
@@ -993,6 +993,12 @@
intent.getStringExtra(Intent.EXTRA_SETTING_PREVIOUS_VALUE),
intent.getStringExtra(Intent.EXTRA_SETTING_NEW_VALUE));
}
+ } else if (Settings.Secure.ACCESSIBILITY_QS_TARGETS.equals(which)) {
+ if (!android.view.accessibility.Flags.a11yQsShortcut()) {
+ return;
+ }
+ restoreAccessibilityQsTargets(
+ intent.getStringExtra(Intent.EXTRA_SETTING_NEW_VALUE));
}
}
}
@@ -2131,6 +2137,29 @@
onUserStateChangedLocked(userState);
}
+ /**
+ * User could configure accessibility shortcut during the SUW before restoring user data.
+ * Merges the current value and the new value to make sure we don't lost the setting the user's
+ * preferences of accessibility qs shortcut updated in SUW are not lost.
+ *
+ * Called only during settings restore; currently supports only the owner user
+ * TODO: http://b/22388012
+ */
+ private void restoreAccessibilityQsTargets(String newValue) {
+ synchronized (mLock) {
+ final AccessibilityUserState userState = getUserStateLocked(UserHandle.USER_SYSTEM);
+ final Set<String> mergedTargets = userState.getA11yQsTargets();
+ readColonDelimitedStringToSet(newValue, str -> str, mergedTargets,
+ /* doMerge = */ true);
+
+ userState.updateA11yQsTargetLocked(mergedTargets);
+ persistColonDelimitedSetToSettingLocked(Settings.Secure.ACCESSIBILITY_QS_TARGETS,
+ UserHandle.USER_SYSTEM, mergedTargets, str -> str);
+ scheduleNotifyClientsOfServicesStateChangeLocked(userState);
+ onUserStateChangedLocked(userState);
+ }
+ }
+
private int getClientStateLocked(AccessibilityUserState userState) {
return userState.getClientStateLocked(
mUiAutomationManager.canIntrospect(),
@@ -2674,11 +2703,13 @@
Map<ComponentName, AccessibilityServiceConnection> componentNameToServiceMap =
userState.mComponentNameToServiceMap;
boolean isUnlockingOrUnlocked = mUmi.isUserUnlockingOrUnlocked(userState.mUserId);
+ Set<ComponentName> installedComponentNames = new HashSet<>();
for (int i = 0, count = userState.mInstalledServices.size(); i < count; i++) {
AccessibilityServiceInfo installedService = userState.mInstalledServices.get(i);
ComponentName componentName = ComponentName.unflattenFromString(
installedService.getId());
+ installedComponentNames.add(componentName);
AccessibilityServiceConnection service = componentNameToServiceMap.get(componentName);
@@ -2738,6 +2769,28 @@
audioManager.setAccessibilityServiceUids(mTempIntArray);
}
mActivityTaskManagerService.setAccessibilityServiceUids(mTempIntArray);
+ final Iterator<ComponentName> it = userState.mEnabledServices.iterator();
+ boolean anyServiceRemoved = false;
+ while (it.hasNext()) {
+ final ComponentName comp = it.next();
+ if (!installedComponentNames.contains(comp)) {
+ it.remove();
+ userState.mTouchExplorationGrantedServices.remove(comp);
+ anyServiceRemoved = true;
+ }
+ }
+ if (anyServiceRemoved) {
+ // Update the enabled services setting.
+ persistComponentNamesToSettingLocked(
+ Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES,
+ userState.mEnabledServices,
+ userState.mUserId);
+ // Update the touch exploration granted services setting.
+ persistComponentNamesToSettingLocked(
+ Settings.Secure.TOUCH_EXPLORATION_GRANTED_ACCESSIBILITY_SERVICES,
+ userState.mTouchExplorationGrantedServices,
+ userState.mUserId);
+ }
updateAccessibilityEnabledSettingLocked(userState);
}
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityUserState.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityUserState.java
index 9a1d379..7008e8e 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilityUserState.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityUserState.java
@@ -112,6 +112,10 @@
* TileService's or the a11y framework tile component names (e.g.
* {@link AccessibilityShortcutController#COLOR_INVERSION_TILE_COMPONENT_NAME}) instead of the
* A11y Feature's component names.
+ * <p/>
+ * In addition, {@link #mA11yTilesInQsPanel} stores what's on the QS Panel, whereas
+ * {@link #mAccessibilityQsTargets} stores the targets that configured qs as their shortcut and
+ * also grant full device control permission.
*/
private final ArraySet<ComponentName> mA11yTilesInQsPanel = new ArraySet<>();
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityWindowManager.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityWindowManager.java
index c570d65..d307484 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilityWindowManager.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityWindowManager.java
@@ -79,6 +79,8 @@
private static int sNextWindowId;
+ private final Region mTmpRegion = new Region();
+
private final Object mLock;
private final Handler mHandler;
private final WindowManagerInternal mWindowManagerInternal;
@@ -613,7 +615,7 @@
}
// If the window is completely covered by other windows - ignore.
- if (unaccountedSpace.quickReject(regionInScreen)) {
+ if (!mTmpRegion.op(unaccountedSpace, regionInScreen, Region.Op.INTERSECT)) {
return false;
}
diff --git a/services/autofill/features.aconfig b/services/autofill/features.aconfig
index 532db12..c130cee 100644
--- a/services/autofill/features.aconfig
+++ b/services/autofill/features.aconfig
@@ -16,6 +16,7 @@
flag {
name: "autofill_credman_dev_integration"
+ is_exported: true
namespace: "autofill"
description: "Guards against Autofill-Credman Phase1 developer integration via new APIs"
bug: "320730001"
diff --git a/services/autofill/java/com/android/server/autofill/AutofillManagerService.java b/services/autofill/java/com/android/server/autofill/AutofillManagerService.java
index e4f1d3a..07fcb50 100644
--- a/services/autofill/java/com/android/server/autofill/AutofillManagerService.java
+++ b/services/autofill/java/com/android/server/autofill/AutofillManagerService.java
@@ -718,7 +718,9 @@
+ ", mPccUseFallbackDetection=" + mPccUseFallbackDetection
+ ", mPccProviderHints=" + mPccProviderHints
+ ", mAutofillCredmanIntegrationEnabled="
- + mAutofillCredmanIntegrationEnabled);
+ + mAutofillCredmanIntegrationEnabled
+ + ", mIsFillFieldsFromCurrentSessionOnly="
+ + mIsFillFieldsFromCurrentSessionOnly);
}
}
}
diff --git a/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java b/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java
index e1291e5..14a331c 100644
--- a/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java
+++ b/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java
@@ -1672,9 +1672,10 @@
@Override // from InlineSuggestionRenderCallbacksImpl
public void onServiceDied(@NonNull RemoteInlineSuggestionRenderService service) {
- // Don't do anything; eventually the system will bind to it again...
Slog.w(TAG, "remote service died: " + service);
- mRemoteInlineSuggestionRenderService = null;
+ synchronized (mLock) {
+ resetExtServiceLocked();
+ }
}
}
diff --git a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
index 8244d20..3ec6e47 100644
--- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
+++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
@@ -23,6 +23,7 @@
import static android.companion.virtual.VirtualDeviceParams.DEVICE_POLICY_DEFAULT;
import static android.companion.virtual.VirtualDeviceParams.NAVIGATION_POLICY_DEFAULT_ALLOWED;
import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_ACTIVITY;
+import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_AUDIO;
import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_CAMERA;
import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_CLIPBOARD;
import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_RECENTS;
@@ -82,6 +83,8 @@
import android.hardware.input.VirtualStylusMotionEvent;
import android.hardware.input.VirtualTouchEvent;
import android.hardware.input.VirtualTouchscreenConfig;
+import android.media.AudioManager;
+import android.media.audiopolicy.AudioMix;
import android.os.Binder;
import android.os.IBinder;
import android.os.LocaleList;
@@ -1063,6 +1066,37 @@
}
@Override
+ public boolean hasCustomAudioInputSupport() throws RemoteException {
+ if (!Flags.vdmPublicApis()) {
+ return false;
+ }
+
+ if (!android.media.audiopolicy.Flags.audioMixTestApi()) {
+ return false;
+ }
+ if (!android.media.audiopolicy.Flags.recordAudioDeviceAwarePermission()) {
+ return false;
+ }
+
+ if (getDevicePolicy(POLICY_TYPE_AUDIO) == VirtualDeviceParams.DEVICE_POLICY_DEFAULT) {
+ return false;
+ }
+ final long token = Binder.clearCallingIdentity();
+ try {
+ AudioManager audioManager = mContext.getSystemService(AudioManager.class);
+ for (AudioMix mix : audioManager.getRegisteredPolicyMixes()) {
+ if (mix.matchesVirtualDeviceId(getDeviceId())
+ && mix.getMixType() == AudioMix.MIX_TYPE_RECORDERS) {
+ return true;
+ }
+ }
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ return false;
+ }
+
+ @Override
protected void dump(FileDescriptor fd, PrintWriter fout, String[] args) {
String indent = " ";
fout.println(" VirtualDevice: ");
diff --git a/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureManagerService.java b/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureManagerService.java
index 6b5ba96..2607ed3 100644
--- a/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureManagerService.java
+++ b/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureManagerService.java
@@ -1297,15 +1297,19 @@
@Override
public void onLoginDetected(@NonNull ParceledListSlice<ContentCaptureEvent> events) {
- RemoteContentProtectionService service = createRemoteContentProtectionService();
- if (service == null) {
- return;
- }
- try {
- service.onLoginDetected(events);
- } catch (Exception ex) {
- Slog.e(TAG, "Failed to call remote service", ex);
- }
+ Binder.withCleanCallingIdentity(
+ () -> {
+ RemoteContentProtectionService service =
+ createRemoteContentProtectionService();
+ if (service == null) {
+ return;
+ }
+ try {
+ service.onLoginDetected(events);
+ } catch (Exception ex) {
+ Slog.e(TAG, "Failed to call remote service", ex);
+ }
+ });
}
}
diff --git a/services/core/Android.bp b/services/core/Android.bp
index d1d7ee7..7f5867f 100644
--- a/services/core/Android.bp
+++ b/services/core/Android.bp
@@ -242,6 +242,7 @@
"apache-commons-math",
"backstage_power_flags_lib",
"notification_flags_lib",
+ "power_hint_flags_lib",
"biometrics_flags_lib",
"am_flags_lib",
"com_android_server_accessibility_flags_lib",
diff --git a/services/core/java/com/android/server/SensitiveContentProtectionManagerService.java b/services/core/java/com/android/server/SensitiveContentProtectionManagerService.java
index ecd14ce..589d8b3 100644
--- a/services/core/java/com/android/server/SensitiveContentProtectionManagerService.java
+++ b/services/core/java/com/android/server/SensitiveContentProtectionManagerService.java
@@ -69,6 +69,8 @@
final Object mSensitiveContentProtectionLock = new Object();
+ private final ArraySet<PackageInfo> mPackagesShowingSensitiveContent = new ArraySet<>();
+
@GuardedBy("mSensitiveContentProtectionLock")
private boolean mProjectionActive = false;
@@ -77,23 +79,24 @@
@Override
public void onStart(MediaProjectionInfo info) {
if (DEBUG) Log.d(TAG, "onStart projection: " + info);
- Trace.beginSection(
+ Trace.traceBegin(Trace.TRACE_TAG_SYSTEM_SERVER,
"SensitiveContentProtectionManagerService.onProjectionStart");
try {
onProjectionStart(info.getPackageName());
} finally {
- Trace.endSection();
+ Trace.traceEnd(Trace.TRACE_TAG_SYSTEM_SERVER);
}
}
@Override
public void onStop(MediaProjectionInfo info) {
if (DEBUG) Log.d(TAG, "onStop projection: " + info);
- Trace.beginSection("SensitiveContentProtectionManagerService.onProjectionStop");
+ Trace.traceBegin(Trace.TRACE_TAG_SYSTEM_SERVER,
+ "SensitiveContentProtectionManagerService.onProjectionStop");
try {
onProjectionEnd();
} finally {
- Trace.endSection();
+ Trace.traceEnd(Trace.TRACE_TAG_SYSTEM_SERVER);
}
}
};
@@ -204,6 +207,10 @@
if (sensitiveNotificationAppProtection()) {
updateAppsThatShouldBlockScreenCapture();
}
+
+ if (sensitiveContentAppProtection() && mPackagesShowingSensitiveContent.size() > 0) {
+ mWindowManager.addBlockScreenCaptureForApps(mPackagesShowingSensitiveContent);
+ }
}
}
@@ -285,7 +292,8 @@
@Override
public void onListenerConnected() {
super.onListenerConnected();
- Trace.beginSection("SensitiveContentProtectionManagerService.onListenerConnected");
+ Trace.traceBegin(Trace.TRACE_TAG_SYSTEM_SERVER,
+ "SensitiveContentProtectionManagerService.onListenerConnected");
try {
// Projection started before notification listener was connected
synchronized (mSensitiveContentProtectionLock) {
@@ -294,14 +302,15 @@
}
}
} finally {
- Trace.endSection();
+ Trace.traceEnd(Trace.TRACE_TAG_SYSTEM_SERVER);
}
}
@Override
public void onNotificationPosted(StatusBarNotification sbn, RankingMap rankingMap) {
super.onNotificationPosted(sbn, rankingMap);
- Trace.beginSection("SensitiveContentProtectionManagerService.onNotificationPosted");
+ Trace.traceBegin(Trace.TRACE_TAG_SYSTEM_SERVER,
+ "SensitiveContentProtectionManagerService.onNotificationPosted");
try {
synchronized (mSensitiveContentProtectionLock) {
if (!mProjectionActive) {
@@ -317,14 +326,14 @@
}
}
} finally {
- Trace.endSection();
+ Trace.traceEnd(Trace.TRACE_TAG_SYSTEM_SERVER);
}
}
@Override
public void onNotificationRankingUpdate(RankingMap rankingMap) {
super.onNotificationRankingUpdate(rankingMap);
- Trace.beginSection(
+ Trace.traceBegin(Trace.TRACE_TAG_SYSTEM_SERVER,
"SensitiveContentProtectionManagerService.onNotificationRankingUpdate");
try {
synchronized (mSensitiveContentProtectionLock) {
@@ -333,7 +342,7 @@
}
}
} finally {
- Trace.endSection();
+ Trace.traceEnd(Trace.TRACE_TAG_SYSTEM_SERVER);
}
}
}
@@ -351,17 +360,27 @@
void setSensitiveContentProtection(IBinder windowToken, String packageName, int uid,
boolean isShowingSensitiveContent) {
synchronized (mSensitiveContentProtectionLock) {
+ // The window token distinguish this package from packages added for notifications.
+ PackageInfo packageInfo = new PackageInfo(packageName, uid, windowToken);
+ // track these packages to protect when screen share starts.
+ if (isShowingSensitiveContent) {
+ mPackagesShowingSensitiveContent.add(packageInfo);
+ if (mPackagesShowingSensitiveContent.size() > 100) {
+ Log.w(TAG, "Unexpectedly large number of sensitive windows, count: "
+ + mPackagesShowingSensitiveContent.size());
+ }
+ } else {
+ mPackagesShowingSensitiveContent.remove(packageInfo);
+ }
if (!mProjectionActive) {
return;
}
if (DEBUG) {
- Log.d(TAG, "setSensitiveContentProtection - windowToken=" + windowToken
- + ", package=" + packageName + ", uid=" + uid
- + ", isShowingSensitiveContent=" + isShowingSensitiveContent);
+ Log.d(TAG, "setSensitiveContentProtection - current package=" + packageInfo
+ + ", isShowingSensitiveContent=" + isShowingSensitiveContent
+ + ", sensitive packages=" + mPackagesShowingSensitiveContent);
}
- // The window token distinguish this package from packages added for notifications.
- PackageInfo packageInfo = new PackageInfo(packageName, uid, windowToken);
ArraySet<PackageInfo> packageInfos = new ArraySet<>();
packageInfos.add(packageInfo);
if (isShowingSensitiveContent) {
@@ -382,20 +401,26 @@
public void setSensitiveContentProtection(IBinder windowToken, String packageName,
boolean isShowingSensitiveContent) {
- Trace.beginSection(
+ Trace.traceBegin(Trace.TRACE_TAG_SYSTEM_SERVER,
"SensitiveContentProtectionManagerService.setSensitiveContentProtection");
try {
int callingUid = Binder.getCallingUid();
verifyCallingPackage(callingUid, packageName);
final long identity = Binder.clearCallingIdentity();
try {
+ if (isShowingSensitiveContent
+ && mWindowManager.getWindowName(windowToken) == null) {
+ Log.e(TAG, "window token is not know to WMS, can't apply protection,"
+ + " token: " + windowToken + ", package: " + packageName);
+ return;
+ }
SensitiveContentProtectionManagerService.this.setSensitiveContentProtection(
windowToken, packageName, callingUid, isShowingSensitiveContent);
} finally {
Binder.restoreCallingIdentity(identity);
}
} finally {
- Trace.endSection();
+ Trace.traceEnd(Trace.TRACE_TAG_SYSTEM_SERVER);
}
}
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index 03ab5b3..4364f16 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -9973,7 +9973,7 @@
"getHistoricalProcessStartReasons");
if (uid != INVALID_UID) {
mProcessList.getAppStartInfoTracker().getStartInfo(
- packageName, userId, callingPid, maxNum, results);
+ packageName, uid, callingPid, maxNum, results);
}
} else {
// If no package name is given, use the caller's uid as the filter uid.
diff --git a/services/core/java/com/android/server/am/ActivityManagerShellCommand.java b/services/core/java/com/android/server/am/ActivityManagerShellCommand.java
index 4ebabdc..5a97e87 100644
--- a/services/core/java/com/android/server/am/ActivityManagerShellCommand.java
+++ b/services/core/java/com/android/server/am/ActivityManagerShellCommand.java
@@ -1164,8 +1164,7 @@
synchronized (mInternal) {
synchronized (mInternal.mProcLock) {
app.mOptRecord.setFreezeSticky(isSticky);
- mInternal.mOomAdjuster.mCachedAppOptimizer.freezeAppAsyncInternalLSP(
- app, 0 /* delayMillis */, true /* force */, false /* immediate */);
+ mInternal.mOomAdjuster.mCachedAppOptimizer.forceFreezeAppAsyncLSP(app);
}
}
return 0;
diff --git a/services/core/java/com/android/server/am/BroadcastConstants.java b/services/core/java/com/android/server/am/BroadcastConstants.java
index 91cfb8f..e676b1f 100644
--- a/services/core/java/com/android/server/am/BroadcastConstants.java
+++ b/services/core/java/com/android/server/am/BroadcastConstants.java
@@ -281,7 +281,7 @@
* For {@link BroadcastQueueModernImpl}: Maximum number of outgoing broadcasts from a
* freezable process that will be allowed before killing the process.
*/
- public long MAX_FROZEN_OUTGOING_BROADCASTS = DEFAULT_MAX_FROZEN_OUTGOING_BROADCASTS;
+ public int MAX_FROZEN_OUTGOING_BROADCASTS = DEFAULT_MAX_FROZEN_OUTGOING_BROADCASTS;
private static final String KEY_MAX_FROZEN_OUTGOING_BROADCASTS =
"max_frozen_outgoing_broadcasts";
private static final int DEFAULT_MAX_FROZEN_OUTGOING_BROADCASTS = 32;
diff --git a/services/core/java/com/android/server/am/BroadcastProcessQueue.java b/services/core/java/com/android/server/am/BroadcastProcessQueue.java
index e98e1ba..ed3cd1e 100644
--- a/services/core/java/com/android/server/am/BroadcastProcessQueue.java
+++ b/services/core/java/com/android/server/am/BroadcastProcessQueue.java
@@ -277,6 +277,10 @@
mOutgoingBroadcasts.clear();
}
+ public void clearOutgoingBroadcasts() {
+ mOutgoingBroadcasts.clear();
+ }
+
/**
* Enqueue the given broadcast to be dispatched to this process at some
* future point in time. The target receiver is indicated by the given index
diff --git a/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java b/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java
index a6f6b34..c082889 100644
--- a/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java
+++ b/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java
@@ -166,7 +166,7 @@
/**
* Map from UID to per-process broadcast queues. If a UID hosts more than
* one process, each additional process is stored as a linked list using
- * {@link BroadcastProcessQueue#next}.
+ * {@link BroadcastProcessQueue#processNameNext}.
*
* @see #getProcessQueue
* @see #getOrCreateProcessQueue
@@ -661,6 +661,10 @@
final BroadcastProcessQueue queue = getProcessQueue(app);
if (queue != null) {
setQueueProcess(queue, app);
+ // Outgoing broadcasts should be cleared when the process dies but there have been
+ // issues due to AMS not always informing the BroadcastQueue of process deaths.
+ // So, clear them when a new process starts as well.
+ queue.clearOutgoingBroadcasts();
}
boolean didSomething = false;
@@ -730,6 +734,8 @@
demoteFromRunningLocked(queue);
}
+ queue.clearOutgoingBroadcasts();
+
// Skip any pending registered receivers, since the old process
// would never be around to receive them
boolean didSomething = queue.forEachMatchingBroadcast((r, i) -> {
@@ -781,8 +787,11 @@
final BroadcastProcessQueue queue = getOrCreateProcessQueue(
r.callerApp.processName, r.callerApp.uid);
if (queue.getOutgoingBroadcastCount() >= mConstants.MAX_FROZEN_OUTGOING_BROADCASTS) {
- // TODO: Kill the process if the outgoing broadcasts count is
- // beyond a certain limit.
+ r.callerApp.killLocked("Too many outgoing broadcasts in cached state",
+ ApplicationExitInfo.REASON_OTHER,
+ ApplicationExitInfo.SUBREASON_EXCESSIVE_OUTGOING_BROADCASTS_WHILE_CACHED,
+ true /* noisy */);
+ return;
}
queue.enqueueOutgoingBroadcast(r);
mHistory.onBroadcastFrozenLocked(r);
diff --git a/services/core/java/com/android/server/am/BroadcastSkipPolicy.java b/services/core/java/com/android/server/am/BroadcastSkipPolicy.java
index 66abb42..b8ef03f 100644
--- a/services/core/java/com/android/server/am/BroadcastSkipPolicy.java
+++ b/services/core/java/com/android/server/am/BroadcastSkipPolicy.java
@@ -19,6 +19,7 @@
import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_PERMISSIONS_REVIEW;
import static com.android.server.am.ActivityManagerService.checkComponentPermission;
import static com.android.server.am.BroadcastQueue.TAG;
+import static com.android.server.am.Flags.usePermissionManagerForBroadcastDeliveryCheck;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -27,6 +28,7 @@
import android.app.AppOpsManager;
import android.app.BroadcastOptions;
import android.app.PendingIntent;
+import android.content.AttributionSource;
import android.content.ComponentName;
import android.content.IIntentSender;
import android.content.Intent;
@@ -39,6 +41,7 @@
import android.os.RemoteException;
import android.os.UserHandle;
import android.permission.IPermissionManager;
+import android.permission.PermissionManager;
import android.util.Slog;
import com.android.internal.util.ArrayUtils;
@@ -54,6 +57,9 @@
public class BroadcastSkipPolicy {
private final ActivityManagerService mService;
+ @Nullable
+ private PermissionManager mPermissionManager;
+
public BroadcastSkipPolicy(@NonNull ActivityManagerService service) {
mService = Objects.requireNonNull(service);
}
@@ -283,14 +289,35 @@
if (info.activityInfo.applicationInfo.uid != Process.SYSTEM_UID &&
r.requiredPermissions != null && r.requiredPermissions.length > 0) {
+ final AttributionSource attributionSource;
+ if (usePermissionManagerForBroadcastDeliveryCheck()) {
+ attributionSource =
+ new AttributionSource.Builder(info.activityInfo.applicationInfo.uid)
+ .setPackageName(info.activityInfo.packageName)
+ .build();
+ } else {
+ attributionSource = null;
+ }
for (int i = 0; i < r.requiredPermissions.length; i++) {
String requiredPermission = r.requiredPermissions[i];
try {
- perm = AppGlobals.getPackageManager().
- checkPermission(requiredPermission,
- info.activityInfo.applicationInfo.packageName,
- UserHandle
- .getUserId(info.activityInfo.applicationInfo.uid));
+ if (usePermissionManagerForBroadcastDeliveryCheck()) {
+ final PermissionManager permissionManager = getPermissionManager();
+ if (permissionManager != null) {
+ perm = permissionManager.checkPermissionForDataDelivery(
+ requiredPermission, attributionSource, null /* message */);
+ } else {
+ // Assume permission denial if PermissionManager is not yet available.
+ perm = PackageManager.PERMISSION_DENIED;
+ }
+ } else {
+ perm = AppGlobals.getPackageManager()
+ .checkPermission(
+ requiredPermission,
+ info.activityInfo.applicationInfo.packageName,
+ UserHandle
+ .getUserId(info.activityInfo.applicationInfo.uid));
+ }
} catch (RemoteException e) {
perm = PackageManager.PERMISSION_DENIED;
}
@@ -302,11 +329,13 @@
+ " due to sender " + r.callerPackage
+ " (uid " + r.callingUid + ")";
}
- int appOp = AppOpsManager.permissionToOpCode(requiredPermission);
- if (appOp != AppOpsManager.OP_NONE && appOp != r.appOp) {
- if (!noteOpForManifestReceiver(appOp, r, info, component)) {
- return "Skipping delivery to " + info.activityInfo.packageName
- + " due to required appop " + appOp;
+ if (!usePermissionManagerForBroadcastDeliveryCheck()) {
+ int appOp = AppOpsManager.permissionToOpCode(requiredPermission);
+ if (appOp != AppOpsManager.OP_NONE && appOp != r.appOp) {
+ if (!noteOpForManifestReceiver(appOp, r, info, component)) {
+ return "Skipping delivery to " + info.activityInfo.packageName
+ + " due to required appop " + appOp;
+ }
}
}
}
@@ -694,4 +723,11 @@
return false;
}
+
+ private PermissionManager getPermissionManager() {
+ if (mPermissionManager == null) {
+ mPermissionManager = mService.mContext.getSystemService(PermissionManager.class);
+ }
+ return mPermissionManager;
+ }
}
diff --git a/services/core/java/com/android/server/am/CachedAppOptimizer.java b/services/core/java/com/android/server/am/CachedAppOptimizer.java
index 0cf5575..6e20f6c 100644
--- a/services/core/java/com/android/server/am/CachedAppOptimizer.java
+++ b/services/core/java/com/android/server/am/CachedAppOptimizer.java
@@ -1414,8 +1414,13 @@
}
@GuardedBy({"mAm", "mProcLock"})
+ void forceFreezeAppAsyncLSP(ProcessRecord app) {
+ freezeAppAsyncInternalLSP(app, 0 /* delayMillis */, true /* force */);
+ }
+
+ @GuardedBy({"mAm", "mProcLock"})
private void freezeAppAsyncLSP(ProcessRecord app, @UptimeMillisLong long delayMillis) {
- freezeAppAsyncInternalLSP(app, delayMillis, false, false);
+ freezeAppAsyncInternalLSP(app, delayMillis, false /* force */);
}
@GuardedBy({"mAm", "mProcLock"})
@@ -1427,17 +1432,18 @@
// and remove this method.
@GuardedBy({"mAm", "mProcLock"})
void freezeAppAsyncImmediateLSP(ProcessRecord app) {
- freezeAppAsyncInternalLSP(app, 0, false, true);
+ freezeAppAsyncInternalLSP(app, 0 /* delayMillis */, false /* force */);
}
- // TODO: Update this method to be private and have the existing clients call different methods.
- // This "internal" method should not be directly triggered by clients outside this class.
@GuardedBy({"mAm", "mProcLock"})
- void freezeAppAsyncInternalLSP(ProcessRecord app, @UptimeMillisLong long delayMillis,
- boolean force, boolean immediate) {
+ private void freezeAppAsyncInternalLSP(ProcessRecord app, @UptimeMillisLong long delayMillis,
+ boolean force) {
final ProcessCachedOptimizerRecord opt = app.mOptRecord;
if (opt.isPendingFreeze()) {
- if (immediate) {
+ if (delayMillis == 0) {
+ // Caller is requesting to freeze the process without delay, so remove
+ // any already posted messages which would have been handled with a delay and
+ // post a new message without a delay.
mFreezeHandler.removeMessages(SET_FROZEN_PROCESS_MSG, app);
mFreezeHandler.sendMessage(mFreezeHandler.obtainMessage(
SET_FROZEN_PROCESS_MSG, DO_FREEZE, 0, app));
diff --git a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
index 7df5fdd..48d3c09 100644
--- a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
+++ b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
@@ -127,6 +127,7 @@
"avic",
"bluetooth",
"brownout_mitigation_audio",
+ "brownout_mitigation_modem",
"build",
"biometrics",
"biometrics_framework",
@@ -168,6 +169,7 @@
"pixel_biometrics_face",
"pixel_bluetooth",
"pixel_connectivity_gps",
+ "pixel_sensors",
"pixel_system_sw_video",
"pixel_watch",
"platform_compat",
diff --git a/services/core/java/com/android/server/am/flags.aconfig b/services/core/java/com/android/server/am/flags.aconfig
index 0209944..fd847f1 100644
--- a/services/core/java/com/android/server/am/flags.aconfig
+++ b/services/core/java/com/android/server/am/flags.aconfig
@@ -86,3 +86,11 @@
purpose: PURPOSE_BUGFIX
}
}
+
+flag {
+ namespace: "backstage_power"
+ name: "use_permission_manager_for_broadcast_delivery_check"
+ description: "Use PermissionManager API for broadcast delivery permission checks."
+ bug: "315468967"
+ is_fixed_read_only: true
+}
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index de000bf..0f951746 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -6854,15 +6854,6 @@
ringerMode = RINGER_MODE_SILENT;
}
}
- } else if (mIsSingleVolume && (direction == AudioManager.ADJUST_TOGGLE_MUTE
- || direction == AudioManager.ADJUST_MUTE)) {
- if (mHasVibrator) {
- ringerMode = RINGER_MODE_VIBRATE;
- } else {
- ringerMode = RINGER_MODE_SILENT;
- }
- // Setting the ringer mode will toggle mute
- result &= ~FLAG_ADJUST_VOLUME;
}
break;
case RINGER_MODE_VIBRATE:
@@ -6871,11 +6862,8 @@
"but no vibrator is present");
break;
}
- if ((direction == AudioManager.ADJUST_LOWER)) {
- // This is the case we were muted with the volume turned up
- if (mIsSingleVolume && oldIndex >= 2 * step && isMuted) {
- ringerMode = RINGER_MODE_NORMAL;
- } else if (mPrevVolDirection != AudioManager.ADJUST_LOWER) {
+ if (direction == AudioManager.ADJUST_LOWER) {
+ if (mPrevVolDirection != AudioManager.ADJUST_LOWER) {
if (mVolumePolicy.volumeDownToEnterSilent) {
final long diff = SystemClock.uptimeMillis()
- mLoweredFromNormalToVibrateTime;
@@ -6895,10 +6883,7 @@
result &= ~FLAG_ADJUST_VOLUME;
break;
case RINGER_MODE_SILENT:
- if (mIsSingleVolume && direction == AudioManager.ADJUST_LOWER && oldIndex >= 2 * step && isMuted) {
- // This is the case we were muted with the volume turned up
- ringerMode = RINGER_MODE_NORMAL;
- } else if (direction == AudioManager.ADJUST_RAISE
+ if (direction == AudioManager.ADJUST_RAISE
|| direction == AudioManager.ADJUST_TOGGLE_MUTE
|| direction == AudioManager.ADJUST_UNMUTE) {
if (!mVolumePolicy.volumeUpToExitSilent) {
@@ -12375,7 +12360,8 @@
}
private boolean callerHasPermission(String permission) {
- return mContext.checkCallingPermission(permission) == PackageManager.PERMISSION_GRANTED;
+ return mContext.checkCallingOrSelfPermission(permission)
+ == PackageManager.PERMISSION_GRANTED;
}
/** @return true if projection is a valid MediaProjection that can project audio. */
diff --git a/services/core/java/com/android/server/broadcastradio/aidl/BroadcastRadioServiceImpl.java b/services/core/java/com/android/server/broadcastradio/aidl/BroadcastRadioServiceImpl.java
index d93ff9d..086f3aa 100644
--- a/services/core/java/com/android/server/broadcastradio/aidl/BroadcastRadioServiceImpl.java
+++ b/services/core/java/com/android/server/broadcastradio/aidl/BroadcastRadioServiceImpl.java
@@ -138,7 +138,9 @@
/**
* Constructs BroadcastRadioServiceImpl using AIDL HAL using the list of names of AIDL
- * BroadcastRadio HAL services {@code serviceNameList}
+ * BroadcastRadio HAL services
+ *
+ * @param serviceNameList list of names of AIDL BroadcastRadio HAL services
*/
public BroadcastRadioServiceImpl(ArrayList<String> serviceNameList) {
mNextModuleId = 0;
@@ -169,7 +171,11 @@
}
/**
- * Gets the AIDL RadioModule for the given {@code moduleId}. Null will be returned if not found.
+ * Gets the AIDL RadioModule for the given module Id.
+ *
+ * @param id Id of {@link RadioModule} of AIDL BroadcastRadio HAL service
+ * @return {@code true} if {@link RadioModule} of AIDL BroadcastRadio HAL service is found,
+ * {@code false} otherwise
*/
public boolean hasModule(int id) {
synchronized (mLock) {
diff --git a/services/core/java/com/android/server/connectivity/Vpn.java b/services/core/java/com/android/server/connectivity/Vpn.java
index b7ece2ea..5905b7d 100644
--- a/services/core/java/com/android/server/connectivity/Vpn.java
+++ b/services/core/java/com/android/server/connectivity/Vpn.java
@@ -366,7 +366,6 @@
private PendingIntent mStatusIntent;
private volatile boolean mEnableTeardown = true;
- private final INetworkManagementService mNms;
private final INetd mNetd;
@VisibleForTesting
@GuardedBy("this")
@@ -626,7 +625,6 @@
mSubscriptionManager = mContext.getSystemService(SubscriptionManager.class);
mDeps = deps;
- mNms = netService;
mNetd = netd;
mUserId = userId;
mLooper = looper;
diff --git a/services/core/java/com/android/server/display/AutomaticBrightnessController.java b/services/core/java/com/android/server/display/AutomaticBrightnessController.java
index 4032514..4aab9d2 100644
--- a/services/core/java/com/android/server/display/AutomaticBrightnessController.java
+++ b/services/core/java/com/android/server/display/AutomaticBrightnessController.java
@@ -54,6 +54,7 @@
import com.android.internal.os.BackgroundThread;
import com.android.server.EventLogTags;
import com.android.server.display.brightness.BrightnessEvent;
+import com.android.server.display.brightness.clamper.BrightnessClamperController;
import java.io.PrintWriter;
import java.lang.annotation.Retention;
@@ -252,6 +253,7 @@
// Controls Brightness range (including High Brightness Mode).
private final BrightnessRangeController mBrightnessRangeController;
+ private final BrightnessClamperController mBrightnessClamperController;
// Throttles (caps) maximum allowed brightness
private final BrightnessThrottler mBrightnessThrottler;
@@ -287,7 +289,8 @@
HysteresisLevels screenBrightnessThresholdsIdle, Context context,
BrightnessRangeController brightnessModeController,
BrightnessThrottler brightnessThrottler, int ambientLightHorizonShort,
- int ambientLightHorizonLong, float userLux, float userNits) {
+ int ambientLightHorizonLong, float userLux, float userNits,
+ BrightnessClamperController brightnessClamperController) {
this(new Injector(), callbacks, looper, sensorManager, lightSensor,
brightnessMappingStrategyMap, lightSensorWarmUpTime, brightnessMin, brightnessMax,
dozeScaleFactor, lightSensorRate, initialLightSensorRate,
@@ -297,7 +300,7 @@
screenBrightnessThresholds, ambientBrightnessThresholdsIdle,
screenBrightnessThresholdsIdle, context, brightnessModeController,
brightnessThrottler, ambientLightHorizonShort, ambientLightHorizonLong, userLux,
- userNits
+ userNits, brightnessClamperController
);
}
@@ -313,9 +316,10 @@
HysteresisLevels screenBrightnessThresholds,
HysteresisLevels ambientBrightnessThresholdsIdle,
HysteresisLevels screenBrightnessThresholdsIdle, Context context,
- BrightnessRangeController brightnessModeController,
+ BrightnessRangeController brightnessRangeController,
BrightnessThrottler brightnessThrottler, int ambientLightHorizonShort,
- int ambientLightHorizonLong, float userLux, float userNits) {
+ int ambientLightHorizonLong, float userLux, float userNits,
+ BrightnessClamperController brightnessClamperController) {
mInjector = injector;
mClock = injector.createClock();
mContext = context;
@@ -358,7 +362,8 @@
mPendingForegroundAppPackageName = null;
mForegroundAppCategory = ApplicationInfo.CATEGORY_UNDEFINED;
mPendingForegroundAppCategory = ApplicationInfo.CATEGORY_UNDEFINED;
- mBrightnessRangeController = brightnessModeController;
+ mBrightnessRangeController = brightnessRangeController;
+ mBrightnessClamperController = brightnessClamperController;
mBrightnessThrottler = brightnessThrottler;
mBrightnessMappingStrategyMap = brightnessMappingStrategyMap;
@@ -791,7 +796,7 @@
mAmbientBrightnessThresholds.getDarkeningThreshold(lux);
}
mBrightnessRangeController.onAmbientLuxChange(mAmbientLux);
-
+ mBrightnessClamperController.onAmbientLuxChange(mAmbientLux);
// If the short term model was invalidated and the change is drastic enough, reset it.
mShortTermModel.maybeReset(mAmbientLux);
diff --git a/services/core/java/com/android/server/display/DisplayDeviceConfig.java b/services/core/java/com/android/server/display/DisplayDeviceConfig.java
index 4116669..851d197 100644
--- a/services/core/java/com/android/server/display/DisplayDeviceConfig.java
+++ b/services/core/java/com/android/server/display/DisplayDeviceConfig.java
@@ -61,6 +61,7 @@
import com.android.server.display.config.IdleScreenRefreshRateTimeoutLuxThresholdPoint;
import com.android.server.display.config.IdleScreenRefreshRateTimeoutLuxThresholds;
import com.android.server.display.config.IntegerArray;
+import com.android.server.display.config.LowBrightnessData;
import com.android.server.display.config.LuxThrottling;
import com.android.server.display.config.NitsMap;
import com.android.server.display.config.NonNegativeFloatToFloatPoint;
@@ -555,6 +556,24 @@
* <majorVersion>2</majorVersion>
* <minorVersion>0</minorVersion>
* </usiVersion>
+ * <lowBrightness enabled="true">
+ * <transitionPoint>0.1</transitionPoint>
+ *
+ * <nits>0.2</nits>
+ * <nits>2.0</nits>
+ * <nits>500.0</nits>
+ * <nits>1000.0</nits>
+ *
+ * <backlight>0</backlight>
+ * <backlight>0.0001</backlight>
+ * <backlight>0.5</backlight>
+ * <backlight>1.0</backlight>
+ *
+ * <brightness>0</brightness>
+ * <brightness>0.1</brightness>
+ * <brightness>0.5</brightness>
+ * <brightness>1.0</brightness>
+ * </lowBrightness>
* <screenBrightnessCapForWearBedtimeMode>0.1</screenBrightnessCapForWearBedtimeMode>
* <idleScreenRefreshRateTimeout>
* <luxThresholds>
@@ -568,6 +587,8 @@
* </point>
* </luxThresholds>
* </idleScreenRefreshRateTimeout>
+ *
+ *
* </displayConfiguration>
* }
* </pre>
@@ -732,6 +753,7 @@
private Spline mBacklightToBrightnessSpline;
private Spline mBacklightToNitsSpline;
private Spline mNitsToBacklightSpline;
+
private List<String> mQuirks;
private boolean mIsHighBrightnessModeEnabled = false;
private HighBrightnessModeData mHbmData;
@@ -872,6 +894,10 @@
@Nullable
private HdrBrightnessData mHdrBrightnessData;
+ // Null if low brightness mode is disabled - in config or by flag.
+ @Nullable
+ public LowBrightnessData mLowBrightnessData;
+
/**
* Maximum screen brightness setting when screen brightness capped in Wear Bedtime mode.
*/
@@ -1038,6 +1064,9 @@
* @return The brightness mapping nits array.
*/
public float[] getNits() {
+ if (mLowBrightnessData != null) {
+ return mLowBrightnessData.mNits;
+ }
return mNits;
}
@@ -1046,7 +1075,11 @@
*
* @return The backlight mapping value array.
*/
+ @VisibleForTesting
public float[] getBacklight() {
+ if (mLowBrightnessData != null) {
+ return mLowBrightnessData.mBacklight;
+ }
return mBacklight;
}
@@ -1058,9 +1091,26 @@
* @return backlight value on the HAL scale of 0-1
*/
public float getBacklightFromBrightness(float brightness) {
+ if (mLowBrightnessData != null) {
+ return mLowBrightnessData.mBrightnessToBacklight.interpolate(brightness);
+ }
return mBrightnessToBacklightSpline.interpolate(brightness);
}
+ private float getBrightnessFromBacklight(float brightness) {
+ if (mLowBrightnessData != null) {
+ return mLowBrightnessData.mBacklightToBrightness.interpolate(brightness);
+ }
+ return mBacklightToBrightnessSpline.interpolate(brightness);
+ }
+
+ private Spline getBacklightToBrightnessSpline() {
+ if (mLowBrightnessData != null) {
+ return mLowBrightnessData.mBacklightToBrightness;
+ }
+ return mBacklightToBrightnessSpline;
+ }
+
/**
* Calculates the nits value for the specified backlight value if a mapping exists.
*
@@ -1068,6 +1118,14 @@
* exits.
*/
public float getNitsFromBacklight(float backlight) {
+ if (mLowBrightnessData != null) {
+ if (mLowBrightnessData.mBacklightToNits == null) {
+ return INVALID_NITS;
+ }
+ backlight = Math.max(backlight, mBacklightMinimum);
+ return mLowBrightnessData.mBacklightToNits.interpolate(backlight);
+ }
+
if (mBacklightToNitsSpline == null) {
return INVALID_NITS;
}
@@ -1075,6 +1133,20 @@
return mBacklightToNitsSpline.interpolate(backlight);
}
+ private float getBacklightFromNits(float nits) {
+ if (mLowBrightnessData != null) {
+ return mLowBrightnessData.mNitsToBacklight.interpolate(nits);
+ }
+ return mNitsToBacklightSpline.interpolate(nits);
+ }
+
+ private Spline getNitsToBacklightSpline() {
+ if (mLowBrightnessData != null) {
+ return mLowBrightnessData.mNitsToBacklight;
+ }
+ return mNitsToBacklightSpline;
+ }
+
/**
* @return true if there is sdrHdrRatioMap, false otherwise.
*/
@@ -1101,13 +1173,13 @@
float ratio = Math.min(mSdrToHdrRatioSpline.interpolate(nits), maxDesiredHdrSdrRatio);
float hdrNits = nits * ratio;
- if (mNitsToBacklightSpline == null) {
+ if (getNitsToBacklightSpline() == null) {
return PowerManager.BRIGHTNESS_INVALID;
}
- float hdrBacklight = mNitsToBacklightSpline.interpolate(hdrNits);
+ float hdrBacklight = getBacklightFromNits(hdrNits);
hdrBacklight = Math.max(mBacklightMinimum, Math.min(mBacklightMaximum, hdrBacklight));
- float hdrBrightness = mBacklightToBrightnessSpline.interpolate(hdrBacklight);
+ float hdrBrightness = getBrightnessFromBacklight(hdrBacklight);
if (DEBUG) {
Slog.d(TAG, "getHdrBrightnessFromSdr: sdr brightness " + brightness
@@ -1129,6 +1201,9 @@
* @return brightness array
*/
public float[] getBrightness() {
+ if (mLowBrightnessData != null) {
+ return mLowBrightnessData.mBrightness;
+ }
return mBrightness;
}
@@ -1814,6 +1889,15 @@
}
/**
+ *
+ * @return true if low brightness mode is enabled
+ */
+ @VisibleForTesting
+ public boolean getLbmEnabled() {
+ return mLowBrightnessData != null;
+ }
+
+ /**
* @return Maximum screen brightness setting when screen brightness capped in Wear Bedtime mode.
*/
public float getBrightnessCapForWearBedtimeMode() {
@@ -1952,6 +2036,9 @@
+ "mUsiVersion= " + mHostUsiVersion + "\n"
+ "mHdrBrightnessData= " + mHdrBrightnessData + "\n"
+ "mBrightnessCapForWearBedtimeMode= " + mBrightnessCapForWearBedtimeMode
+ + "\n"
+ + "mLowBrightnessData:" + (mLowBrightnessData != null
+ ? mLowBrightnessData.toString() : "null")
+ "}";
}
@@ -2002,6 +2089,9 @@
loadDensityMapping(config);
loadBrightnessDefaultFromDdcXml(config);
loadBrightnessConstraintsFromConfigXml();
+ if (mFlags.isEvenDimmerEnabled()) {
+ mLowBrightnessData = LowBrightnessData.loadConfig(config);
+ }
loadBrightnessMap(config);
loadThermalThrottlingConfig(config);
loadPowerThrottlingConfigData(config);
@@ -2549,9 +2639,9 @@
// A negative value means that there's no threshold
mLowDisplayBrightnessThresholds[i] = thresholdNits;
} else {
- float thresholdBacklight = mNitsToBacklightSpline.interpolate(thresholdNits);
+ float thresholdBacklight = getBacklightFromNits(thresholdNits);
mLowDisplayBrightnessThresholds[i] =
- mBacklightToBrightnessSpline.interpolate(thresholdBacklight);
+ getBrightnessFromBacklight(thresholdBacklight);
}
mLowAmbientBrightnessThresholds[i] = lowerThresholdDisplayBrightnessPoints
@@ -2600,9 +2690,9 @@
// A negative value means that there's no threshold
mHighDisplayBrightnessThresholds[i] = thresholdNits;
} else {
- float thresholdBacklight = mNitsToBacklightSpline.interpolate(thresholdNits);
+ float thresholdBacklight = getBacklightFromNits(thresholdNits);
mHighDisplayBrightnessThresholds[i] =
- mBacklightToBrightnessSpline.interpolate(thresholdBacklight);
+ getBrightnessFromBacklight(thresholdBacklight);
}
mHighAmbientBrightnessThresholds[i] = higherThresholdDisplayBrightnessPoints
@@ -2619,7 +2709,7 @@
loadAutoBrightnessBrighteningLightDebounceIdle(autoBrightness);
loadAutoBrightnessDarkeningLightDebounceIdle(autoBrightness);
mDisplayBrightnessMapping = new DisplayBrightnessMappingConfig(mContext, mFlags,
- autoBrightness, mBacklightToBrightnessSpline);
+ autoBrightness, getBacklightToBrightnessSpline());
loadEnableAutoBrightness(autoBrightness);
}
@@ -2793,6 +2883,11 @@
// These splines are used to convert from the system brightness value to the HAL backlight
// value
private void createBacklightConversionSplines() {
+
+
+ // Create original brightness splines - not using low brightness mode arrays - this is
+ // so that we can continue to log the original brightness splines.
+
mBrightness = new float[mBacklight.length];
for (int i = 0; i < mBrightness.length; i++) {
mBrightness[i] = MathUtils.map(mBacklight[0],
@@ -2833,7 +2928,7 @@
+ mBacklightMaximum);
}
mHbmData.transitionPoint =
- mBacklightToBrightnessSpline.interpolate(transitionPointBacklightScale);
+ getBrightnessFromBacklight(transitionPointBacklightScale);
final HbmTiming hbmTiming = hbm.getTiming_all();
mHbmData.timeWindowMillis = hbmTiming.getTimeWindowSecs_all().longValue() * 1000;
mHbmData.timeMaxMillis = hbmTiming.getTimeMaxSecs_all().longValue() * 1000;
@@ -2902,7 +2997,7 @@
continue;
}
luxToTransitionPointMap.put(lux,
- mBacklightToBrightnessSpline.interpolate(maxBrightness));
+ getBrightnessFromBacklight(maxBrightness));
}
if (!luxToTransitionPointMap.isEmpty()) {
mLuxThrottlingData.put(mappedType, luxToTransitionPointMap);
@@ -2997,7 +3092,7 @@
private void loadAutoBrightnessConfigsFromConfigXml() {
mDisplayBrightnessMapping = new DisplayBrightnessMappingConfig(mContext, mFlags,
- /* autoBrightnessConfig= */ null, mBacklightToBrightnessSpline);
+ /* autoBrightnessConfig= */ null, getBacklightToBrightnessSpline());
}
private void loadBrightnessChangeThresholdsFromXml() {
diff --git a/services/core/java/com/android/server/display/DisplayPowerController.java b/services/core/java/com/android/server/display/DisplayPowerController.java
index 87d017c..90ad8c0 100644
--- a/services/core/java/com/android/server/display/DisplayPowerController.java
+++ b/services/core/java/com/android/server/display/DisplayPowerController.java
@@ -1165,7 +1165,8 @@
screenBrightnessThresholds, ambientBrightnessThresholdsIdle,
screenBrightnessThresholdsIdle, mContext, mBrightnessRangeController,
mBrightnessThrottler, mDisplayDeviceConfig.getAmbientHorizonShort(),
- mDisplayDeviceConfig.getAmbientHorizonLong(), userLux, userNits);
+ mDisplayDeviceConfig.getAmbientHorizonLong(), userLux, userNits,
+ mBrightnessClamperController);
mDisplayBrightnessController.setAutomaticBrightnessController(
mAutomaticBrightnessController);
@@ -2479,6 +2480,7 @@
public void setBrightnessToFollow(float leadDisplayBrightness, float nits, float ambientLux,
boolean slowChange) {
mBrightnessRangeController.onAmbientLuxChange(ambientLux);
+ mBrightnessClamperController.onAmbientLuxChange(ambientLux);
if (nits == BrightnessMappingStrategy.INVALID_NITS) {
mDisplayBrightnessController.setBrightnessToFollow(leadDisplayBrightness, slowChange);
} else {
@@ -3176,7 +3178,9 @@
HysteresisLevels screenBrightnessThresholdsIdle, Context context,
BrightnessRangeController brightnessModeController,
BrightnessThrottler brightnessThrottler, int ambientLightHorizonShort,
- int ambientLightHorizonLong, float userLux, float userNits) {
+ int ambientLightHorizonLong, float userLux, float userNits,
+ BrightnessClamperController brightnessClamperController) {
+
return new AutomaticBrightnessController(callbacks, looper, sensorManager, lightSensor,
brightnessMappingStrategyMap, lightSensorWarmUpTime, brightnessMin,
brightnessMax, dozeScaleFactor, lightSensorRate, initialLightSensorRate,
@@ -3186,7 +3190,7 @@
screenBrightnessThresholds, ambientBrightnessThresholdsIdle,
screenBrightnessThresholdsIdle, context, brightnessModeController,
brightnessThrottler, ambientLightHorizonShort, ambientLightHorizonLong, userLux,
- userNits);
+ userNits, brightnessClamperController);
}
BrightnessMappingStrategy getDefaultModeBrightnessMapper(Context context,
diff --git a/services/core/java/com/android/server/display/LocalDisplayAdapter.java b/services/core/java/com/android/server/display/LocalDisplayAdapter.java
index b2fd9ed..3b3a03b 100644
--- a/services/core/java/com/android/server/display/LocalDisplayAdapter.java
+++ b/services/core/java/com/android/server/display/LocalDisplayAdapter.java
@@ -37,6 +37,7 @@
import android.os.Trace;
import android.util.DisplayUtils;
import android.util.LongSparseArray;
+import android.util.MathUtils;
import android.util.Slog;
import android.util.SparseArray;
import android.view.Display;
@@ -78,6 +79,13 @@
private static final String UNIQUE_ID_PREFIX = "local:";
private static final String PROPERTY_EMULATOR_CIRCULAR = "ro.boot.emulator.circular";
+ // Min and max strengths for even dimmer feature.
+ private static final float EVEN_DIMMER_MIN_STRENGTH = 0.0f;
+ private static final float EVEN_DIMMER_MAX_STRENGTH = 70.0f; // not too dim yet.
+ private static final float BRIGHTNESS_MIN = 0.0f;
+ // The brightness at which we start using color matrices rather than backlight,
+ // to dim the display
+ private static final float BACKLIGHT_COLOR_TRANSITION_POINT = 0.1f;
private final LongSparseArray<LocalDisplayDevice> mDevices = new LongSparseArray<>();
@@ -91,6 +99,8 @@
private Context mOverlayContext;
+ private int mEvenDimmerStrength = -1;
+
// Called with SyncRoot lock held.
LocalDisplayAdapter(DisplayManagerService.SyncRoot syncRoot, Context context,
Handler handler, Listener listener, DisplayManagerFlags flags,
@@ -928,6 +938,10 @@
final float nits = backlightToNits(backlight);
final float sdrNits = backlightToNits(sdrBacklight);
+ if (getFeatureFlags().isEvenDimmerEnabled()) {
+ applyColorMatrixBasedDimming(brightnessState);
+ }
+
mBacklightAdapter.setBacklight(sdrBacklight, sdrNits, backlight, nits);
Trace.traceCounter(Trace.TRACE_TAG_POWER,
"ScreenBrightness",
@@ -974,6 +988,22 @@
}
}
}
+
+ private void applyColorMatrixBasedDimming(float brightnessState) {
+ int strength = (int) (MathUtils.constrainedMap(
+ EVEN_DIMMER_MAX_STRENGTH, EVEN_DIMMER_MIN_STRENGTH, // to this range
+ BRIGHTNESS_MIN, BACKLIGHT_COLOR_TRANSITION_POINT, // from this range
+ brightnessState) + 0.5); // map this (+ rounded up)
+
+ if (mEvenDimmerStrength < 0 // uninitialised
+ || MathUtils.abs(mEvenDimmerStrength - strength) > 1
+ || strength <= 1) {
+ mEvenDimmerStrength = strength;
+ }
+
+ // TODO: use `enabled` and `mRbcStrength` to set color matrices here
+ // TODO: boolean enabled = mEvenDimmerStrength > 0.0f;
+ }
};
}
return null;
diff --git a/services/core/java/com/android/server/display/brightness/clamper/BrightnessClamperController.java b/services/core/java/com/android/server/display/brightness/clamper/BrightnessClamperController.java
index 18e8fab..d8a4500 100644
--- a/services/core/java/com/android/server/display/brightness/clamper/BrightnessClamperController.java
+++ b/services/core/java/com/android/server/display/brightness/clamper/BrightnessClamperController.java
@@ -189,6 +189,13 @@
mModifiers.forEach(BrightnessStateModifier::stop);
}
+ /**
+ * Notifies modifiers that ambient lux has changed.
+ * @param ambientLux current lux, debounced
+ */
+ public void onAmbientLuxChange(float ambientLux) {
+ mModifiers.forEach(modifier -> modifier.onAmbientLuxChange(ambientLux));
+ }
// Called in DisplayControllerHandler
private void recalculateBrightnessCap() {
diff --git a/services/core/java/com/android/server/display/brightness/clamper/BrightnessLowLuxModifier.java b/services/core/java/com/android/server/display/brightness/clamper/BrightnessLowLuxModifier.java
index 7f1f7a9..a91bb59 100644
--- a/services/core/java/com/android/server/display/brightness/clamper/BrightnessLowLuxModifier.java
+++ b/services/core/java/com/android/server/display/brightness/clamper/BrightnessLowLuxModifier.java
@@ -39,21 +39,21 @@
* Class used to prevent the screen brightness dipping below a certain value, based on current
* lux conditions and user preferred minimum.
*/
-public class BrightnessLowLuxModifier implements
- BrightnessStateModifier {
+public class BrightnessLowLuxModifier extends BrightnessModifier {
// To enable these logs, run:
// 'adb shell setprop persist.log.tag.BrightnessLowLuxModifier DEBUG && adb reboot'
private static final String TAG = "BrightnessLowLuxModifier";
private static final boolean DEBUG = DebugUtils.isDebuggable(TAG);
+ private static final float MIN_NITS = 2.0f;
private final SettingsObserver mSettingsObserver;
private final ContentResolver mContentResolver;
private final Handler mHandler;
private final BrightnessClamperController.ClamperChangeListener mChangeListener;
- protected float mSettingNitsLowerBound = PowerManager.BRIGHTNESS_MIN;
private int mReason;
private float mBrightnessLowerBound;
private boolean mIsActive;
+ private float mAmbientLux;
@VisibleForTesting
BrightnessLowLuxModifier(Handler handler,
@@ -78,17 +78,17 @@
int userId = UserHandle.USER_CURRENT;
float settingNitsLowerBound = Settings.Secure.getFloatForUser(
mContentResolver, Settings.Secure.EVEN_DIMMER_MIN_NITS,
- /* def= */ PowerManager.BRIGHTNESS_MIN, userId);
+ /* def= */ MIN_NITS, userId);
- boolean isActive = Settings.Secure.getIntForUser(mContentResolver,
+ boolean isActive = Settings.Secure.getFloatForUser(mContentResolver,
Settings.Secure.EVEN_DIMMER_ACTIVATED,
- /* def= */ 0, userId) == 1;
+ /* def= */ 0, userId) == 1.0f;
- // TODO: luxBasedNitsLowerBound = mMinNitsToLuxSpline(currentLux);
- float luxBasedNitsLowerBound = 0.0f;
+ // TODO: luxBasedNitsLowerBound = mMinLuxToNitsSpline(currentLux);
+ float luxBasedNitsLowerBound = 2.0f;
- // TODO: final float nitsLowerBound = isActive ? Math.max(settingNitsLowerBound,
- // luxBasedNitsLowerBound) : PowerManager.BRIGHTNESS_MIN;
+ final float nitsLowerBound = isActive ? Math.max(settingNitsLowerBound,
+ luxBasedNitsLowerBound) : MIN_NITS;
final int reason = settingNitsLowerBound > luxBasedNitsLowerBound
? BrightnessReason.MODIFIER_MIN_USER_SET_LOWER_BOUND
@@ -104,8 +104,13 @@
mReason = reason;
if (DEBUG) {
Slog.i(TAG, "isActive: " + isActive
- + ", settingNitsLowerBound: " + settingNitsLowerBound
- + ", lowerBound: " + brightnessLowerBound);
+ + ", brightnessLowerBound: " + brightnessLowerBound
+ + ", mAmbientLux: " + mAmbientLux
+ + ", mReason: " + (
+ mReason == BrightnessReason.MODIFIER_MIN_USER_SET_LOWER_BOUND ? "minSetting"
+ : "lux")
+ + ", nitsLowerBound: " + nitsLowerBound
+ );
}
mBrightnessLowerBound = brightnessLowerBound;
mChangeListener.onChanged();
@@ -132,6 +137,22 @@
}
@Override
+ boolean shouldApply(DisplayManagerInternal.DisplayPowerRequest request) {
+ return mIsActive;
+ }
+
+ @Override
+ float getBrightnessAdjusted(float currentBrightness,
+ DisplayManagerInternal.DisplayPowerRequest request) {
+ return Math.max(mBrightnessLowerBound, currentBrightness);
+ }
+
+ @Override
+ int getModifier() {
+ return mReason;
+ }
+
+ @Override
public void apply(DisplayManagerInternal.DisplayPowerRequest request,
DisplayBrightnessState.Builder stateBuilder) {
stateBuilder.setMinBrightness(mBrightnessLowerBound);
@@ -150,10 +171,16 @@
}
@Override
+ public void onAmbientLuxChange(float ambientLux) {
+ mAmbientLux = ambientLux;
+ recalculateLowerBound();
+ }
+
+ @Override
public void dump(PrintWriter pw) {
pw.println("BrightnessLowLuxModifier:");
- pw.println(" mBrightnessLowerBound=" + mBrightnessLowerBound);
pw.println(" mIsActive=" + mIsActive);
+ pw.println(" mBrightnessLowerBound=" + mBrightnessLowerBound);
pw.println(" mReason=" + mReason);
}
diff --git a/services/core/java/com/android/server/display/brightness/clamper/BrightnessModifier.java b/services/core/java/com/android/server/display/brightness/clamper/BrightnessModifier.java
index be8fa5a..2a3dd87 100644
--- a/services/core/java/com/android/server/display/brightness/clamper/BrightnessModifier.java
+++ b/services/core/java/com/android/server/display/brightness/clamper/BrightnessModifier.java
@@ -68,4 +68,9 @@
public void stop() {
// do nothing
}
+
+ @Override
+ public void onAmbientLuxChange(float ambientLux) {
+ // do nothing
+ }
}
diff --git a/services/core/java/com/android/server/display/brightness/clamper/BrightnessStateModifier.java b/services/core/java/com/android/server/display/brightness/clamper/BrightnessStateModifier.java
index 441ba8f..2234258 100644
--- a/services/core/java/com/android/server/display/brightness/clamper/BrightnessStateModifier.java
+++ b/services/core/java/com/android/server/display/brightness/clamper/BrightnessStateModifier.java
@@ -42,4 +42,10 @@
* Called when stopped. Listeners can be unregistered here.
*/
void stop();
+
+ /**
+ * Allows modifiers to react to ambient lux changes.
+ * @param ambientLux current debounced lux.
+ */
+ void onAmbientLuxChange(float ambientLux);
}
diff --git a/services/core/java/com/android/server/display/config/LowBrightnessData.java b/services/core/java/com/android/server/display/config/LowBrightnessData.java
new file mode 100644
index 0000000..aa82533
--- /dev/null
+++ b/services/core/java/com/android/server/display/config/LowBrightnessData.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.display.config;
+
+import android.annotation.Nullable;
+import android.util.Slog;
+import android.util.Spline;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Brightness config for low brightness mode
+ */
+public class LowBrightnessData {
+ private static final String TAG = "LowBrightnessData";
+
+ /**
+ * Brightness value at which lower brightness methods are used.
+ */
+ public final float mTransitionPoint;
+
+ /**
+ * Nits array, maps to mBacklight
+ */
+ public final float[] mNits;
+
+ /**
+ * Backlight array, maps to mBrightness and mNits
+ */
+ public final float[] mBacklight;
+
+ /**
+ * Brightness array, maps to mBacklight
+ */
+ public final float[] mBrightness;
+ /**
+ * Spline, mapping between backlight and nits
+ */
+ public final Spline mBacklightToNits;
+ /**
+ * Spline, mapping between nits and backlight
+ */
+ public final Spline mNitsToBacklight;
+ /**
+ * Spline, mapping between brightness and backlight
+ */
+ public final Spline mBrightnessToBacklight;
+ /**
+ * Spline, mapping between backlight and brightness
+ */
+ public final Spline mBacklightToBrightness;
+
+ @VisibleForTesting
+ public LowBrightnessData(float transitionPoint, float[] nits,
+ float[] backlight, float[] brightness, Spline backlightToNits,
+ Spline nitsToBacklight, Spline brightnessToBacklight, Spline backlightToBrightness) {
+ mTransitionPoint = transitionPoint;
+ mNits = nits;
+ mBacklight = backlight;
+ mBrightness = brightness;
+ mBacklightToNits = backlightToNits;
+ mNitsToBacklight = nitsToBacklight;
+ mBrightnessToBacklight = brightnessToBacklight;
+ mBacklightToBrightness = backlightToBrightness;
+ }
+
+ @Override
+ public String toString() {
+ return "LowBrightnessData {"
+ + "mTransitionPoint: " + mTransitionPoint
+ + ", mNits: " + Arrays.toString(mNits)
+ + ", mBacklight: " + Arrays.toString(mBacklight)
+ + ", mBrightness: " + Arrays.toString(mBrightness)
+ + ", mBacklightToNits: " + mBacklightToNits
+ + ", mNitsToBacklight: " + mNitsToBacklight
+ + ", mBrightnessToBacklight: " + mBrightnessToBacklight
+ + ", mBacklightToBrightness: " + mBacklightToBrightness
+ + "} ";
+ }
+
+ /**
+ * Loads LowBrightnessData from DisplayConfiguration
+ */
+ @Nullable
+ public static LowBrightnessData loadConfig(DisplayConfiguration config) {
+ final LowBrightnessMode lbm = config.getLowBrightness();
+ if (lbm == null) {
+ return null;
+ }
+
+ boolean lbmIsEnabled = lbm.getEnabled();
+ if (!lbmIsEnabled) {
+ return null;
+ }
+
+ List<Float> nitsList = lbm.getNits();
+ List<Float> backlightList = lbm.getBacklight();
+ List<Float> brightnessList = lbm.getBrightness();
+ float transitionPoints = lbm.getTransitionPoint().floatValue();
+
+ if (nitsList.isEmpty()
+ || backlightList.size() != brightnessList.size()
+ || backlightList.size() != nitsList.size()) {
+ Slog.e(TAG, "Invalid low brightness array lengths");
+ return null;
+ }
+
+ float[] nits = new float[nitsList.size()];
+ float[] backlight = new float[nitsList.size()];
+ float[] brightness = new float[nitsList.size()];
+
+ for (int i = 0; i < nitsList.size(); i++) {
+ nits[i] = nitsList.get(i);
+ backlight[i] = backlightList.get(i);
+ brightness[i] = brightnessList.get(i);
+ }
+
+ return new LowBrightnessData(transitionPoints, nits, backlight, brightness,
+ Spline.createSpline(backlight, nits),
+ Spline.createSpline(nits, backlight),
+ Spline.createSpline(brightness, backlight),
+ Spline.createSpline(backlight, brightness)
+ );
+ }
+}
diff --git a/services/core/java/com/android/server/feature/dropbox_flags.aconfig b/services/core/java/com/android/server/feature/dropbox_flags.aconfig
index fee4bf3..14e964b 100644
--- a/services/core/java/com/android/server/feature/dropbox_flags.aconfig
+++ b/services/core/java/com/android/server/feature/dropbox_flags.aconfig
@@ -2,6 +2,7 @@
flag{
name: "enable_read_dropbox_permission"
+ is_exported: true
namespace: "preload_safety"
description: "Feature flag for permission to Read dropbox data"
bug: "287512663"
diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java
index 05b1cb69..468b902 100644
--- a/services/core/java/com/android/server/input/InputManagerService.java
+++ b/services/core/java/com/android/server/input/InputManagerService.java
@@ -2604,6 +2604,19 @@
mBatteryController.notifyStylusGestureStarted(deviceId, eventTime);
}
+ // Native callback.
+ @SuppressWarnings("unused")
+ private int getPackageUid(String pkg) {
+ if (TextUtils.isEmpty(pkg)) {
+ return Process.INVALID_UID;
+ }
+ try {
+ return mContext.getPackageManager().getPackageUid(pkg, 0 /*flags*/);
+ } catch (PackageManager.NameNotFoundException e) {
+ return Process.INVALID_UID;
+ }
+ }
+
/**
* Flatten a map into a string list, with value positioned directly next to the
* key.
diff --git a/services/core/java/com/android/server/input/KeyboardLayoutManager.java b/services/core/java/com/android/server/input/KeyboardLayoutManager.java
index 283e692..6610081 100644
--- a/services/core/java/com/android/server/input/KeyboardLayoutManager.java
+++ b/services/core/java/com/android/server/input/KeyboardLayoutManager.java
@@ -459,13 +459,16 @@
for (ResolveInfo resolveInfo : pm.queryBroadcastReceiversAsUser(intent,
PackageManager.GET_META_DATA | PackageManager.MATCH_DIRECT_BOOT_AWARE
| PackageManager.MATCH_DIRECT_BOOT_UNAWARE, UserHandle.USER_SYSTEM)) {
+ if (resolveInfo == null || resolveInfo.activityInfo == null) {
+ continue;
+ }
final ActivityInfo activityInfo = resolveInfo.activityInfo;
final int priority = resolveInfo.priority;
visitKeyboardLayoutsInPackage(pm, activityInfo, null, priority, visitor);
}
}
- private void visitKeyboardLayout(String keyboardLayoutDescriptor,
+ private void visitKeyboardLayout(@NonNull String keyboardLayoutDescriptor,
KeyboardLayoutVisitor visitor) {
KeyboardLayoutDescriptor d = KeyboardLayoutDescriptor.parse(keyboardLayoutDescriptor);
if (d != null) {
@@ -482,8 +485,8 @@
}
}
- private void visitKeyboardLayoutsInPackage(PackageManager pm, ActivityInfo receiver,
- String keyboardName, int requestedPriority, KeyboardLayoutVisitor visitor) {
+ private void visitKeyboardLayoutsInPackage(PackageManager pm, @NonNull ActivityInfo receiver,
+ @Nullable String keyboardName, int requestedPriority, KeyboardLayoutVisitor visitor) {
Bundle metaData = receiver.metaData;
if (metaData == null) {
return;
@@ -1415,7 +1418,7 @@
return packageName + "/" + receiverName + "/" + keyboardName;
}
- public static KeyboardLayoutDescriptor parse(String descriptor) {
+ public static KeyboardLayoutDescriptor parse(@NonNull String descriptor) {
int pos = descriptor.indexOf('/');
if (pos < 0 || pos + 1 == descriptor.length()) {
return null;
diff --git a/services/core/java/com/android/server/inputmethod/HandwritingModeController.java b/services/core/java/com/android/server/inputmethod/HandwritingModeController.java
index 23fe5cc..dbdac41 100644
--- a/services/core/java/com/android/server/inputmethod/HandwritingModeController.java
+++ b/services/core/java/com/android/server/inputmethod/HandwritingModeController.java
@@ -16,6 +16,8 @@
package com.android.server.inputmethod;
+import static com.android.text.flags.Flags.handwritingEndOfLineTap;
+
import android.Manifest;
import android.annotation.AnyThread;
import android.annotation.NonNull;
@@ -30,6 +32,7 @@
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
+import android.os.SystemClock;
import android.text.TextUtils;
import android.util.Slog;
import android.view.BatchedInputEventReceiver;
@@ -66,6 +69,7 @@
// Use getHandwritingBufferSize() and not this value directly.
private static final int LONG_EVENT_BUFFER_SIZE = EVENT_BUFFER_SIZE * 20;
private static final long HANDWRITING_DELEGATION_IDLE_TIMEOUT_MS = 3000;
+ private static final long AFTER_STYLUS_UP_ALLOW_PERIOD_MS = 200L;
private final Context mContext;
// This must be the looper for the UiThread.
@@ -78,6 +82,7 @@
private InputEventReceiver mHandwritingEventReceiver;
private Runnable mInkWindowInitRunnable;
private boolean mRecordingGesture;
+ private boolean mRecordingGestureAfterStylusUp;
private int mCurrentDisplayId;
// when set, package names are used for handwriting delegation.
private @Nullable String mDelegatePackageName;
@@ -155,6 +160,15 @@
}
boolean isStylusGestureOngoing() {
+ if (mRecordingGestureAfterStylusUp && !mHandwritingBuffer.isEmpty()) {
+ // If it is less than AFTER_STYLUS_UP_ALLOW_PERIOD_MS after the stylus up event, return
+ // true so that handwriting can start.
+ MotionEvent lastEvent = mHandwritingBuffer.get(mHandwritingBuffer.size() - 1);
+ if (lastEvent.getActionMasked() == MotionEvent.ACTION_UP) {
+ return SystemClock.uptimeMillis() - lastEvent.getEventTime()
+ < AFTER_STYLUS_UP_ALLOW_PERIOD_MS;
+ }
+ }
return mRecordingGesture;
}
@@ -277,7 +291,7 @@
Slog.e(TAG, "Cannot start handwriting session: Invalid request id: " + requestId);
return null;
}
- if (!mRecordingGesture || mHandwritingBuffer.isEmpty()) {
+ if (!isStylusGestureOngoing()) {
Slog.e(TAG, "Cannot start handwriting session: No stylus gesture is being recorded.");
return null;
}
@@ -300,6 +314,7 @@
mHandwritingEventReceiver.dispose();
mHandwritingEventReceiver = null;
mRecordingGesture = false;
+ mRecordingGestureAfterStylusUp = false;
if (mHandwritingSurface.isIntercepting()) {
throw new IllegalStateException(
@@ -362,6 +377,7 @@
clearPendingHandwritingDelegation();
}
mRecordingGesture = false;
+ mRecordingGestureAfterStylusUp = false;
}
private boolean onInputEvent(InputEvent ev) {
@@ -412,15 +428,20 @@
if ((TextUtils.isEmpty(mDelegatePackageName) || mDelegationConnectionlessFlow)
&& (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL)) {
mRecordingGesture = false;
- mHandwritingBuffer.clear();
- return;
+ if (handwritingEndOfLineTap() && action == MotionEvent.ACTION_UP) {
+ mRecordingGestureAfterStylusUp = true;
+ } else {
+ mHandwritingBuffer.clear();
+ return;
+ }
}
if (action == MotionEvent.ACTION_DOWN) {
+ clearBufferIfRecordingAfterStylusUp();
mRecordingGesture = true;
}
- if (!mRecordingGesture) {
+ if (!mRecordingGesture && !mRecordingGestureAfterStylusUp) {
return;
}
@@ -430,12 +451,20 @@
+ " The rest of the gesture will not be recorded.");
}
mRecordingGesture = false;
+ clearBufferIfRecordingAfterStylusUp();
return;
}
mHandwritingBuffer.add(MotionEvent.obtain(event));
}
+ private void clearBufferIfRecordingAfterStylusUp() {
+ if (mRecordingGestureAfterStylusUp) {
+ mHandwritingBuffer.clear();
+ mRecordingGestureAfterStylusUp = false;
+ }
+ }
+
static final class HandwritingSession {
private final int mRequestId;
private final InputChannel mHandwritingChannel;
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
index d0a83a6..cfd64c4 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
@@ -1248,7 +1248,15 @@
mService.publishLocalService();
IInputMethodManager.Stub service;
if (Flags.useZeroJankProxy()) {
- service = new ZeroJankProxy(mService.mHandler::post, mService);
+ service =
+ new ZeroJankProxy(
+ mService.mHandler::post,
+ mService,
+ () -> {
+ synchronized (ImfLock.class) {
+ return mService.isInputShown();
+ }
+ });
} else {
service = mService;
}
diff --git a/services/core/java/com/android/server/inputmethod/ZeroJankProxy.java b/services/core/java/com/android/server/inputmethod/ZeroJankProxy.java
index 396192e..31ce630 100644
--- a/services/core/java/com/android/server/inputmethod/ZeroJankProxy.java
+++ b/services/core/java/com/android/server/inputmethod/ZeroJankProxy.java
@@ -46,7 +46,6 @@
import android.os.RemoteException;
import android.os.ResultReceiver;
import android.os.ShellCallback;
-import android.util.ExceptionUtils;
import android.util.Slog;
import android.view.WindowManager;
import android.view.inputmethod.CursorAnchorInfo;
@@ -77,6 +76,7 @@
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
+import java.util.function.BooleanSupplier;
/**
* A proxy that processes all {@link IInputMethodManager} calls asynchronously.
@@ -86,10 +86,12 @@
private final IInputMethodManager mInner;
private final Executor mExecutor;
+ private final BooleanSupplier mIsInputShown;
- ZeroJankProxy(Executor executor, IInputMethodManager inner) {
+ ZeroJankProxy(Executor executor, IInputMethodManager inner, BooleanSupplier isInputShown) {
mInner = inner;
mExecutor = executor;
+ mIsInputShown = isInputShown;
}
private void offload(ThrowingRunnable r) {
@@ -163,8 +165,19 @@
int lastClickTooType, ResultReceiver resultReceiver,
@SoftInputShowHideReason int reason)
throws RemoteException {
- offload(() -> mInner.showSoftInput(client, windowToken, statsToken, flags, lastClickTooType,
- resultReceiver, reason));
+ offload(
+ () -> {
+ if (!mInner.showSoftInput(
+ client,
+ windowToken,
+ statsToken,
+ flags,
+ lastClickTooType,
+ resultReceiver,
+ reason)) {
+ sendResultReceiverFailure(resultReceiver);
+ }
+ });
return true;
}
@@ -173,11 +186,24 @@
@Nullable ImeTracker.Token statsToken, @InputMethodManager.HideFlags int flags,
ResultReceiver resultReceiver, @SoftInputShowHideReason int reason)
throws RemoteException {
- offload(() -> mInner.hideSoftInput(client, windowToken, statsToken, flags, resultReceiver,
- reason));
+ offload(
+ () -> {
+ if (!mInner.hideSoftInput(
+ client, windowToken, statsToken, flags, resultReceiver, reason)) {
+ sendResultReceiverFailure(resultReceiver);
+ }
+ });
return true;
}
+ private void sendResultReceiverFailure(ResultReceiver resultReceiver) {
+ resultReceiver.send(
+ mIsInputShown.getAsBoolean()
+ ? InputMethodManager.RESULT_UNCHANGED_SHOWN
+ : InputMethodManager.RESULT_UNCHANGED_HIDDEN,
+ null);
+ }
+
@Override
@EnforcePermission(Manifest.permission.TEST_INPUT_METHOD)
public void hideSoftInputFromServerForTest() throws RemoteException {
@@ -415,14 +441,17 @@
private void sendOnStartInputResult(
IInputMethodClient client, InputBindResult res, int startInputSeq) {
- InputMethodManagerService service = (InputMethodManagerService) mInner;
- final ClientState cs = service.getClientState(client);
- if (cs != null && cs.mClient != null) {
- cs.mClient.onStartInputResult(res, startInputSeq);
- } else {
- // client is unbound.
- Slog.i(TAG, "Client that requested startInputOrWindowGainedFocus is no longer"
- + " bound. InputBindResult: " + res + " for startInputSeq: " + startInputSeq);
+ synchronized (ImfLock.class) {
+ InputMethodManagerService service = (InputMethodManagerService) mInner;
+ final ClientState cs = service.getClientState(client);
+ if (cs != null && cs.mClient != null) {
+ cs.mClient.onStartInputResult(res, startInputSeq);
+ } else {
+ // client is unbound.
+ Slog.i(TAG, "Client that requested startInputOrWindowGainedFocus is no longer"
+ + " bound. InputBindResult: " + res + " for startInputSeq: "
+ + startInputSeq);
+ }
}
}
}
diff --git a/services/core/java/com/android/server/media/MediaSessionRecord.java b/services/core/java/com/android/server/media/MediaSessionRecord.java
index a9a8272..5b3934e 100644
--- a/services/core/java/com/android/server/media/MediaSessionRecord.java
+++ b/services/core/java/com/android/server/media/MediaSessionRecord.java
@@ -687,27 +687,20 @@
private static String toVolumeControlTypeString(
@VolumeProvider.ControlType int volumeControlType) {
- switch (volumeControlType) {
- case VOLUME_CONTROL_FIXED:
- return "FIXED";
- case VOLUME_CONTROL_RELATIVE:
- return "RELATIVE";
- case VOLUME_CONTROL_ABSOLUTE:
- return "ABSOLUTE";
- default:
- return TextUtils.formatSimple("unknown(%d)", volumeControlType);
- }
+ return switch (volumeControlType) {
+ case VOLUME_CONTROL_FIXED -> "FIXED";
+ case VOLUME_CONTROL_RELATIVE -> "RELATIVE";
+ case VOLUME_CONTROL_ABSOLUTE -> "ABSOLUTE";
+ default -> TextUtils.formatSimple("unknown(%d)", volumeControlType);
+ };
}
private static String toVolumeTypeString(@PlaybackInfo.PlaybackType int volumeType) {
- switch (volumeType) {
- case PLAYBACK_TYPE_LOCAL:
- return "LOCAL";
- case PLAYBACK_TYPE_REMOTE:
- return "REMOTE";
- default:
- return TextUtils.formatSimple("unknown(%d)", volumeType);
- }
+ return switch (volumeType) {
+ case PLAYBACK_TYPE_LOCAL -> "LOCAL";
+ case PLAYBACK_TYPE_REMOTE -> "REMOTE";
+ default -> TextUtils.formatSimple("unknown(%d)", volumeType);
+ };
}
@Override
diff --git a/services/core/java/com/android/server/net/NetworkPolicyManagerService.java b/services/core/java/com/android/server/net/NetworkPolicyManagerService.java
index 18b495b..25095ed 100644
--- a/services/core/java/com/android/server/net/NetworkPolicyManagerService.java
+++ b/services/core/java/com/android/server/net/NetworkPolicyManagerService.java
@@ -4328,7 +4328,9 @@
@GuardedBy("mUidRulesFirstLock")
private boolean updateUidStateUL(int uid, int procState, long procStateSeq,
@ProcessCapability int capability) {
- Trace.traceBegin(Trace.TRACE_TAG_NETWORK, "updateUidStateUL");
+ Trace.traceBegin(Trace.TRACE_TAG_NETWORK, "updateUidStateUL: " + uid + "/"
+ + ActivityManager.procStateToString(procState) + "/" + procStateSeq + "/"
+ + ActivityManager.getCapabilitiesSummary(capability));
try {
final UidState oldUidState = mUidState.get(uid);
if (oldUidState != null && procStateSeq < oldUidState.procStateSeq) {
diff --git a/services/core/java/com/android/server/notification/PreferencesHelper.java b/services/core/java/com/android/server/notification/PreferencesHelper.java
index 4f3cdbc..50ca984 100644
--- a/services/core/java/com/android/server/notification/PreferencesHelper.java
+++ b/services/core/java/com/android/server/notification/PreferencesHelper.java
@@ -310,6 +310,7 @@
parser.getAttributeInt(null, ATT_VISIBILITY, DEFAULT_VISIBILITY),
parser.getAttributeBoolean(null, ATT_SHOW_BADGE, DEFAULT_SHOW_BADGE),
bubblePref);
+ r.bubblePreference = bubblePref;
r.priority = parser.getAttributeInt(null, ATT_PRIORITY, DEFAULT_PRIORITY);
r.visibility = parser.getAttributeInt(null, ATT_VISIBILITY, DEFAULT_VISIBILITY);
r.showBadge = parser.getAttributeBoolean(null, ATT_SHOW_BADGE, DEFAULT_SHOW_BADGE);
@@ -676,7 +677,7 @@
* @param bubblePreference whether bubbles are allowed.
*/
public void setBubblesAllowed(String pkg, int uid, int bubblePreference) {
- boolean changed = false;
+ boolean changed;
synchronized (mPackagePreferences) {
PackagePreferences p = getOrCreatePackagePreferencesLocked(pkg, uid);
changed = p.bubblePreference != bubblePreference;
diff --git a/services/core/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceManagerService.java b/services/core/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceManagerService.java
index 28682e3..953300a 100644
--- a/services/core/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceManagerService.java
+++ b/services/core/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceManagerService.java
@@ -37,8 +37,8 @@
import android.content.Context;
import android.content.pm.PackageManager;
import android.content.pm.ServiceInfo;
-import android.os.Binder;
import android.content.res.Resources;
+import android.os.Binder;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
@@ -163,7 +163,7 @@
}
@Override
- public void getVersion(RemoteCallback remoteCallback) throws RemoteException {
+ public void getVersion(RemoteCallback remoteCallback) {
Slog.i(TAG, "OnDeviceIntelligenceManagerInternal getVersion");
Objects.requireNonNull(remoteCallback);
mContext.enforceCallingOrSelfPermission(
@@ -244,7 +244,7 @@
@Override
public void requestFeatureDownload(Feature feature,
- ICancellationSignal cancellationSignal,
+ AndroidFuture cancellationSignalFuture,
IDownloadCallback downloadCallback) throws RemoteException {
Slog.i(TAG, "OnDeviceIntelligenceManagerInternal requestFeatureDownload");
Objects.requireNonNull(feature);
@@ -261,16 +261,17 @@
ensureRemoteIntelligenceServiceInitialized();
mRemoteOnDeviceIntelligenceService.run(
service -> service.requestFeatureDownload(Binder.getCallingUid(), feature,
- cancellationSignal,
+ cancellationSignalFuture,
downloadCallback));
}
@Override
public void requestTokenInfo(Feature feature,
- Bundle request, ICancellationSignal cancellationSignal,
+ Bundle request,
+ AndroidFuture cancellationSignalFuture,
ITokenInfoCallback tokenInfoCallback) throws RemoteException {
- Slog.i(TAG, "OnDeviceIntelligenceManagerInternal prepareFeatureProcessing");
+ Slog.i(TAG, "OnDeviceIntelligenceManagerInternal requestTokenInfo");
Objects.requireNonNull(feature);
Objects.requireNonNull(request);
Objects.requireNonNull(tokenInfoCallback);
@@ -285,10 +286,11 @@
PersistableBundle.EMPTY);
}
ensureRemoteInferenceServiceInitialized();
+
mRemoteInferenceService.run(
service -> service.requestTokenInfo(Binder.getCallingUid(), feature,
request,
- cancellationSignal,
+ cancellationSignalFuture,
tokenInfoCallback));
}
@@ -296,8 +298,8 @@
public void processRequest(Feature feature,
Bundle request,
int requestType,
- ICancellationSignal cancellationSignal,
- IProcessingSignal processingSignal,
+ AndroidFuture cancellationSignalFuture,
+ AndroidFuture processingSignalFuture,
IResponseCallback responseCallback)
throws RemoteException {
Slog.i(TAG, "OnDeviceIntelligenceManagerInternal processRequest");
@@ -316,7 +318,7 @@
mRemoteInferenceService.run(
service -> service.processRequest(Binder.getCallingUid(), feature, request,
requestType,
- cancellationSignal, processingSignal,
+ cancellationSignalFuture, processingSignalFuture,
responseCallback));
}
@@ -324,8 +326,8 @@
public void processRequestStreaming(Feature feature,
Bundle request,
int requestType,
- ICancellationSignal cancellationSignal,
- IProcessingSignal processingSignal,
+ AndroidFuture cancellationSignalFuture,
+ AndroidFuture processingSignalFuture,
IStreamingResponseCallback streamingCallback) throws RemoteException {
Slog.i(TAG, "OnDeviceIntelligenceManagerInternal processRequestStreaming");
Objects.requireNonNull(feature);
@@ -343,7 +345,7 @@
mRemoteInferenceService.run(
service -> service.processRequestStreaming(Binder.getCallingUid(), feature,
request, requestType,
- cancellationSignal, processingSignal,
+ cancellationSignalFuture, processingSignalFuture,
streamingCallback));
}
@@ -356,11 +358,11 @@
};
}
- private void ensureRemoteIntelligenceServiceInitialized() throws RemoteException {
+ private void ensureRemoteIntelligenceServiceInitialized() {
synchronized (mLock) {
if (mRemoteOnDeviceIntelligenceService == null) {
String serviceName = getServiceNames()[0];
- validateService(serviceName, false);
+ Binder.withCleanCallingIdentity(() -> validateServiceElevated(serviceName, false));
mRemoteOnDeviceIntelligenceService = new RemoteOnDeviceIntelligenceService(mContext,
ComponentName.unflattenFromString(serviceName),
UserHandle.SYSTEM.getIdentifier());
@@ -388,29 +390,19 @@
public void updateProcessingState(
Bundle processingState,
IProcessingUpdateStatusCallback callback) {
- try {
- ensureRemoteInferenceServiceInitialized();
- mRemoteInferenceService.run(
- service -> service.updateProcessingState(
- processingState, callback));
- } catch (RemoteException unused) {
- try {
- callback.onFailure(
- OnDeviceIntelligenceException.PROCESSING_UPDATE_STATUS_CONNECTION_FAILED,
- "Received failure invoking the remote processing service.");
- } catch (RemoteException ex) {
- Slog.w(TAG, "Failed to send failure status.", ex);
- }
- }
+ ensureRemoteInferenceServiceInitialized();
+ mRemoteInferenceService.run(
+ service -> service.updateProcessingState(
+ processingState, callback));
}
};
}
- private void ensureRemoteInferenceServiceInitialized() throws RemoteException {
+ private void ensureRemoteInferenceServiceInitialized() {
synchronized (mLock) {
if (mRemoteInferenceService == null) {
String serviceName = getServiceNames()[1];
- validateService(serviceName, true);
+ Binder.withCleanCallingIdentity(() -> validateServiceElevated(serviceName, true));
mRemoteInferenceService = new RemoteOnDeviceSandboxedInferenceService(mContext,
ComponentName.unflattenFromString(serviceName),
UserHandle.SYSTEM.getIdentifier());
@@ -457,35 +449,38 @@
};
}
- @GuardedBy("mLock")
- private void validateService(String serviceName, boolean checkIsolated)
- throws RemoteException {
- if (TextUtils.isEmpty(serviceName)) {
- throw new RuntimeException("");
- }
- ComponentName serviceComponent = ComponentName.unflattenFromString(
- serviceName);
- ServiceInfo serviceInfo = AppGlobals.getPackageManager().getServiceInfo(
- serviceComponent,
- PackageManager.MATCH_DIRECT_BOOT_AWARE
- | PackageManager.MATCH_DIRECT_BOOT_UNAWARE, 0);
- if (serviceInfo != null) {
- if (!checkIsolated) {
- checkServiceRequiresPermission(serviceInfo,
- Manifest.permission.BIND_ON_DEVICE_INTELLIGENCE_SERVICE);
- return;
+ private void validateServiceElevated(String serviceName, boolean checkIsolated) {
+ try {
+ if (TextUtils.isEmpty(serviceName)) {
+ throw new IllegalStateException(
+ "Remote service is not configured to complete the request");
}
+ ComponentName serviceComponent = ComponentName.unflattenFromString(
+ serviceName);
+ ServiceInfo serviceInfo = AppGlobals.getPackageManager().getServiceInfo(
+ serviceComponent,
+ PackageManager.MATCH_DIRECT_BOOT_AWARE
+ | PackageManager.MATCH_DIRECT_BOOT_UNAWARE, 0);
+ if (serviceInfo != null) {
+ if (!checkIsolated) {
+ checkServiceRequiresPermission(serviceInfo,
+ Manifest.permission.BIND_ON_DEVICE_INTELLIGENCE_SERVICE);
+ return;
+ }
- checkServiceRequiresPermission(serviceInfo,
- Manifest.permission.BIND_ON_DEVICE_SANDBOXED_INFERENCE_SERVICE);
- if (!isIsolatedService(serviceInfo)) {
- throw new SecurityException(
- "Call required an isolated service, but the configured service: "
- + serviceName + ", is not isolated");
+ checkServiceRequiresPermission(serviceInfo,
+ Manifest.permission.BIND_ON_DEVICE_SANDBOXED_INFERENCE_SERVICE);
+ if (!isIsolatedService(serviceInfo)) {
+ throw new SecurityException(
+ "Call required an isolated service, but the configured service: "
+ + serviceName + ", is not isolated");
+ }
+ } else {
+ throw new IllegalStateException(
+ "Remote service is not configured to complete the request.");
}
- } else {
- throw new RuntimeException(
- "Could not find service info for serviceName: " + serviceName);
+ } catch (RemoteException e) {
+ throw new IllegalStateException("Could not fetch service info for remote services", e);
}
}
@@ -501,8 +496,7 @@
}
}
- @GuardedBy("mLock")
- private boolean isIsolatedService(@NonNull ServiceInfo serviceInfo) {
+ private static boolean isIsolatedService(@NonNull ServiceInfo serviceInfo) {
return (serviceInfo.flags & ServiceInfo.FLAG_ISOLATED_PROCESS) != 0
&& (serviceInfo.flags & ServiceInfo.FLAG_EXTERNAL_SERVICE) == 0;
}
@@ -544,7 +538,8 @@
Manifest.permission.USE_ON_DEVICE_INTELLIGENCE, TAG);
synchronized (mLock) {
mTemporaryServiceNames = componentNames;
-
+ mRemoteOnDeviceIntelligenceService = null;
+ mRemoteInferenceService = null;
if (mTemporaryHandler == null) {
mTemporaryHandler = new Handler(Looper.getMainLooper(), null, true) {
@Override
diff --git a/services/core/java/com/android/server/pm/ComputerEngine.java b/services/core/java/com/android/server/pm/ComputerEngine.java
index 9480c8e..2005b17 100644
--- a/services/core/java/com/android/server/pm/ComputerEngine.java
+++ b/services/core/java/com/android/server/pm/ComputerEngine.java
@@ -137,6 +137,7 @@
import com.android.internal.util.IndentingPrintWriter;
import com.android.internal.util.Preconditions;
import com.android.modules.utils.TypedXmlSerializer;
+import com.android.server.ondeviceintelligence.OnDeviceIntelligenceManagerInternal;
import com.android.server.pm.dex.DexManager;
import com.android.server.pm.dex.PackageDexUsage;
import com.android.server.pm.parsing.PackageInfoUtils;
@@ -4353,9 +4354,8 @@
if (Process.isSdkSandboxUid(uid)) {
uid = getBaseSdkSandboxUid();
}
- if (Process.isIsolatedUid(uid)
- && mPermissionManager.getHotwordDetectionServiceProvider() != null
- && uid == mPermissionManager.getHotwordDetectionServiceProvider().getUid()) {
+ final int callingUserId = UserHandle.getUserId(callingUid);
+ if (isKnownIsolatedComputeApp(uid, callingUserId)) {
try {
uid = getIsolatedOwner(uid);
} catch (IllegalStateException e) {
@@ -4363,7 +4363,6 @@
Slog.wtf(TAG, "Expected isolated uid " + uid + " to have an owner", e);
}
}
- final int callingUserId = UserHandle.getUserId(callingUid);
final int appId = UserHandle.getAppId(uid);
final Object obj = mSettings.getSettingBase(appId);
if (obj instanceof SharedUserSetting) {
@@ -4399,9 +4398,7 @@
if (Process.isSdkSandboxUid(uid)) {
uid = getBaseSdkSandboxUid();
}
- if (Process.isIsolatedUid(uid)
- && mPermissionManager.getHotwordDetectionServiceProvider() != null
- && uid == mPermissionManager.getHotwordDetectionServiceProvider().getUid()) {
+ if (isKnownIsolatedComputeApp(uid, callingUserId)) {
try {
uid = getIsolatedOwner(uid);
} catch (IllegalStateException e) {
@@ -5802,6 +5799,43 @@
return getPackage(mService.getSdkSandboxPackageName()).getUid();
}
+
+ private boolean isKnownIsolatedComputeApp(int uid, int callingUserId) {
+ if (!Process.isIsolatedUid(uid)) {
+ return false;
+ }
+ final boolean isHotword =
+ mPermissionManager.getHotwordDetectionServiceProvider() != null
+ && uid
+ == mPermissionManager.getHotwordDetectionServiceProvider().getUid();
+ if (isHotword) {
+ return true;
+ }
+ OnDeviceIntelligenceManagerInternal onDeviceIntelligenceManagerInternal =
+ mInjector.getLocalService(OnDeviceIntelligenceManagerInternal.class);
+ if (onDeviceIntelligenceManagerInternal == null) {
+ return false;
+ }
+
+ String onDeviceIntelligencePackage =
+ onDeviceIntelligenceManagerInternal.getRemoteServicePackageName();
+ if (onDeviceIntelligencePackage == null) {
+ return false;
+ }
+
+ try {
+ if (getIsolatedOwner(uid) == getPackageUid(onDeviceIntelligencePackage, 0,
+ callingUserId)) {
+ return true;
+ }
+ } catch (IllegalStateException e) {
+ // If the owner uid doesn't exist, just use the current uid
+ Slog.wtf(TAG, "Expected isolated uid " + uid + " to have an owner", e);
+ }
+
+ return false;
+ }
+
@Nullable
@Override
public SharedUserApi getSharedUser(int sharedUserAppId) {
diff --git a/services/core/java/com/android/server/pm/DeletePackageHelper.java b/services/core/java/com/android/server/pm/DeletePackageHelper.java
index 588c629..fd16221 100644
--- a/services/core/java/com/android/server/pm/DeletePackageHelper.java
+++ b/services/core/java/com/android/server/pm/DeletePackageHelper.java
@@ -542,7 +542,8 @@
final Computer snapshot = mPm.snapshotComputer();
for (final int affectedUserId : outInfo.mRemovedUsers) {
if (hadSuspendAppsPermission.get(affectedUserId)) {
- mPm.unsuspendForSuspendingPackage(snapshot, packageName, affectedUserId);
+ mPm.unsuspendForSuspendingPackage(snapshot, packageName,
+ affectedUserId /*suspendingUserId*/, true /*inAllUsers*/);
mPm.removeAllDistractingPackageRestrictions(snapshot, affectedUserId);
}
}
diff --git a/services/core/java/com/android/server/pm/DexOptHelper.java b/services/core/java/com/android/server/pm/DexOptHelper.java
index 51793f6..c60f0af 100644
--- a/services/core/java/com/android/server/pm/DexOptHelper.java
+++ b/services/core/java/com/android/server/pm/DexOptHelper.java
@@ -46,7 +46,6 @@
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.ApexStagedEvent;
-import android.content.pm.Flags;
import android.content.pm.IPackageManagerNative;
import android.content.pm.IStagedApexObserver;
import android.content.pm.PackageManager;
@@ -663,9 +662,7 @@
}
}, new IntentFilter(Intent.ACTION_LOCKED_BOOT_COMPLETED));
- if (Flags.useArtServiceV2()) {
- StagedApexObserver.registerForStagedApexUpdates(artManager);
- }
+ StagedApexObserver.registerForStagedApexUpdates(artManager);
}
/**
@@ -750,9 +747,7 @@
& PackageManager.INSTALL_IGNORE_DEXOPT_PROFILE)
!= 0;
/*@DexoptFlags*/ int extraFlags =
- ignoreDexoptProfile && Flags.useArtServiceV2()
- ? ArtFlags.FLAG_IGNORE_PROFILE
- : 0;
+ ignoreDexoptProfile ? ArtFlags.FLAG_IGNORE_PROFILE : 0;
DexoptParams params = dexoptOptions.convertToDexoptParams(extraFlags);
DexoptResult dexOptResult = getArtManagerLocal().dexoptPackage(
snapshot, packageName, params);
diff --git a/services/core/java/com/android/server/pm/InstallRequest.java b/services/core/java/com/android/server/pm/InstallRequest.java
index 43075a2..c10196f 100644
--- a/services/core/java/com/android/server/pm/InstallRequest.java
+++ b/services/core/java/com/android/server/pm/InstallRequest.java
@@ -35,7 +35,6 @@
import android.app.AppOpsManager;
import android.content.pm.ArchivedPackageParcel;
import android.content.pm.DataLoaderType;
-import android.content.pm.Flags;
import android.content.pm.IPackageInstallObserver2;
import android.content.pm.PackageInstaller;
import android.content.pm.PackageManager;
@@ -951,7 +950,7 @@
// Only report external profile warnings when installing from adb. The goal is to warn app
// developers if they have provided bad external profiles, so it's not beneficial to report
// those warnings in the normal app install workflow.
- if (isInstallFromAdb() && Flags.useArtServiceV2()) {
+ if (isInstallFromAdb()) {
var externalProfileErrors = new LinkedHashSet<String>();
for (PackageDexoptResult packageResult : dexoptResult.getPackageDexoptResults()) {
for (DexContainerFileDexoptResult fileResult :
diff --git a/services/core/java/com/android/server/pm/LauncherAppsService.java b/services/core/java/com/android/server/pm/LauncherAppsService.java
index c6bb99e..20b669b 100644
--- a/services/core/java/com/android/server/pm/LauncherAppsService.java
+++ b/services/core/java/com/android/server/pm/LauncherAppsService.java
@@ -18,12 +18,12 @@
import static android.Manifest.permission.READ_FRAME_BUFFER;
import static android.app.ActivityOptions.KEY_SPLASH_SCREEN_THEME;
+import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED;
+import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED;
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.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED;
-import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED;
import static android.app.PendingIntent.FLAG_IMMUTABLE;
import static android.app.PendingIntent.FLAG_MUTABLE;
import static android.app.PendingIntent.FLAG_UPDATE_CURRENT;
@@ -555,12 +555,6 @@
return false;
}
- if (!mRoleManager
- .getRoleHoldersAsUser(
- RoleManager.ROLE_HOME, UserHandle.getUserHandleForUid(callingUid))
- .contains(callingPackage.getPackageName())) {
- return false;
- }
if (mContext.checkPermission(
Manifest.permission.ACCESS_HIDDEN_PROFILES_FULL,
callingPid,
@@ -569,6 +563,13 @@
return true;
}
+ if (!mRoleManager
+ .getRoleHoldersAsUser(
+ RoleManager.ROLE_HOME, UserHandle.getUserHandleForUid(callingUid))
+ .contains(callingPackage.getPackageName())) {
+ return false;
+ }
+
// TODO(b/321988638): add option to disable with a flag
return mContext.checkPermission(
android.Manifest.permission.ACCESS_HIDDEN_PROFILES,
diff --git a/services/core/java/com/android/server/pm/PackageManagerInternalBase.java b/services/core/java/com/android/server/pm/PackageManagerInternalBase.java
index 8da1683..7a72e70 100644
--- a/services/core/java/com/android/server/pm/PackageManagerInternalBase.java
+++ b/services/core/java/com/android/server/pm/PackageManagerInternalBase.java
@@ -16,6 +16,7 @@
package com.android.server.pm;
+import static android.app.admin.flags.Flags.crossUserSuspensionEnabled;
import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_DEFAULT;
import static android.content.pm.PackageManager.RESTRICTION_NONE;
@@ -45,6 +46,7 @@
import android.os.Bundle;
import android.os.Handler;
import android.os.Process;
+import android.os.UserHandle;
import android.os.storage.StorageManager;
import android.util.ArrayMap;
import android.util.ArraySet;
@@ -687,14 +689,17 @@
@Override
@Deprecated
public final void unsuspendAdminSuspendedPackages(int affectedUser) {
- final int suspendingUserId = affectedUser;
- mService.unsuspendForSuspendingPackage(snapshot(), PLATFORM_PACKAGE_NAME, suspendingUserId);
+ final int suspendingUserId =
+ crossUserSuspensionEnabled() ? UserHandle.USER_SYSTEM : affectedUser;
+ mService.unsuspendForSuspendingPackage(
+ snapshot(), PLATFORM_PACKAGE_NAME, suspendingUserId, /* inAllUsers= */ false);
}
@Override
@Deprecated
public final boolean isAdminSuspendingAnyPackages(int userId) {
- final int suspendingUserId = userId;
+ final int suspendingUserId =
+ crossUserSuspensionEnabled() ? UserHandle.USER_SYSTEM : userId;
return snapshot().isSuspendingAnyPackages(PLATFORM_PACKAGE_NAME, suspendingUserId, userId);
}
diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java
index d215822..9a2b98f 100644
--- a/services/core/java/com/android/server/pm/PackageManagerService.java
+++ b/services/core/java/com/android/server/pm/PackageManagerService.java
@@ -18,6 +18,7 @@
import static android.Manifest.permission.MANAGE_DEVICE_ADMINS;
import static android.Manifest.permission.SET_HARMFUL_APP_WARNINGS;
import static android.app.AppOpsManager.MODE_IGNORED;
+import static android.app.admin.flags.Flags.crossUserSuspensionEnabled;
import static android.content.pm.PackageManager.APP_METADATA_SOURCE_APK;
import static android.content.pm.PackageManager.APP_METADATA_SOURCE_UNKNOWN;
import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_DEFAULT;
@@ -3181,27 +3182,53 @@
callingMethod);
}
- final int packageUid = snapshot.getPackageUid(suspender.packageName, 0, targetUserId);
- final boolean allowedPackageUid = packageUid == callingUid;
- // TODO(b/139383163): remove special casing for shell and enforce INTERACT_ACROSS_USERS_FULL
- final boolean allowedShell = callingUid == SHELL_UID
- && UserHandle.isSameApp(packageUid, callingUid);
+ if (crossUserSuspensionEnabled()) {
+ final int suspendingPackageUid =
+ snapshot.getPackageUid(suspender.packageName, 0, suspender.userId);
+ if (suspendingPackageUid != callingUid) {
+ throw new SecurityException("Suspender package %s doesn't match calling uid %d"
+ .formatted(suspender.packageName, callingUid));
+ }
+ if (targetUserId != suspender.userId) {
+ mContext.enforceCallingOrSelfPermission(
+ Manifest.permission.INTERACT_ACROSS_USERS_FULL, callingMethod);
+ }
+ } else {
+ // Here only SHELL can suspend across users
+ final int packageUid =
+ snapshot.getPackageUid(suspender.packageName, 0, targetUserId);
+ final boolean allowedPackageUid = packageUid == callingUid;
+ final boolean allowedShell = callingUid == SHELL_UID
+ && UserHandle.isSameApp(packageUid, callingUid);
- if (!allowedShell && !allowedPackageUid) {
- throw new SecurityException("Suspending package " + suspender.packageName
- + " in user " + targetUserId + " does not belong to calling uid " + callingUid);
+ if (!allowedShell && !allowedPackageUid) {
+ throw new SecurityException("Suspending package " + suspender.packageName
+ + " in user " + targetUserId + " does not belong to calling uid "
+ + callingUid);
+ }
}
}
+ /**
+ * @param inAllUsers Whether to unsuspend packages suspended by the given package in other
+ * users. This flag is only used when cross-user suspension is enabled.
+ */
void unsuspendForSuspendingPackage(@NonNull Computer computer, String suspendingPackage,
- @UserIdInt int suspendingUserId) {
+ @UserIdInt int suspendingUserId, boolean inAllUsers) {
// TODO: This can be replaced by a special parameter to iterate all packages, rather than
// this weird pre-collect of all packages.
final String[] allPackages = computer.getPackageStates().keySet().toArray(new String[0]);
final Predicate<UserPackage> suspenderPredicate =
UserPackage.of(suspendingUserId, suspendingPackage)::equals;
- mSuspendPackageHelper.removeSuspensionsBySuspendingPackage(computer,
- allPackages, suspenderPredicate, suspendingUserId);
+ if (!crossUserSuspensionEnabled() || !inAllUsers) {
+ mSuspendPackageHelper.removeSuspensionsBySuspendingPackage(computer,
+ allPackages, suspenderPredicate, suspendingUserId);
+ } else {
+ for (int targetUserId: mUserManager.getUserIds()) {
+ mSuspendPackageHelper.removeSuspensionsBySuspendingPackage(
+ computer, allPackages, suspenderPredicate, targetUserId);
+ }
+ }
}
void removeAllDistractingPackageRestrictions(@NonNull Computer snapshot, int userId) {
@@ -4053,7 +4080,7 @@
// This app should not generally be allowed to get disabled by the UI, but
// if it ever does, we don't want to end up with some of the user's apps
// permanently suspended.
- unsuspendForSuspendingPackage(computer, packageName, userId);
+ unsuspendForSuspendingPackage(computer, packageName, userId, true /* inAllUsers */);
removeAllDistractingPackageRestrictions(computer, userId);
}
success = true;
@@ -4339,6 +4366,19 @@
}
mInstantAppRegistry.onUserRemoved(userId);
mPackageMonitorCallbackHelper.onUserRemoved(userId);
+ if (crossUserSuspensionEnabled()) {
+ cleanUpCrossUserSuspension(userId);
+ }
+ }
+
+ private void cleanUpCrossUserSuspension(int removedUser) {
+ final Computer computer = snapshotComputer();
+ var allPackages = computer.getAllAvailablePackageNames();
+ for (int targetUserId : mUserManager.getUserIds()) {
+ if (targetUserId == removedUser) continue;
+ mSuspendPackageHelper.removeSuspensionsBySuspendingPackage(computer, allPackages,
+ userPackage -> userPackage.userId == removedUser, targetUserId);
+ }
}
/**
@@ -4745,7 +4785,8 @@
if (checkPermission(Manifest.permission.SUSPEND_APPS, packageName, userId)
== PERMISSION_GRANTED) {
final Computer snapshot = snapshotComputer();
- unsuspendForSuspendingPackage(snapshot, packageName, userId);
+ unsuspendForSuspendingPackage(
+ snapshot, packageName, userId, true /* inAllUsers */);
removeAllDistractingPackageRestrictions(snapshot, userId);
synchronized (mLock) {
flushPackageRestrictionsAsUserInternalLocked(userId);
@@ -6239,7 +6280,9 @@
final boolean quarantined = ((flags & PackageManager.FLAG_SUSPEND_QUARANTINED) != 0)
&& Flags.quarantinedEnabled();
final Computer snapshot = snapshotComputer();
- final UserPackage suspender = UserPackage.of(targetUserId, suspendingPackage);
+ final UserPackage suspender = crossUserSuspensionEnabled()
+ ? UserPackage.of(suspendingUserId, suspendingPackage)
+ : UserPackage.of(targetUserId, suspendingPackage);
enforceCanSetPackagesSuspendedAsUser(snapshot, quarantined, suspender, callingUid,
targetUserId, "setPackagesSuspendedAsUser");
return mSuspendPackageHelper.setPackagesSuspended(snapshot, packageNames, suspended,
@@ -6707,7 +6750,10 @@
@Override
public String[] setPackagesSuspendedByAdmin(
@UserIdInt int userId, @NonNull String[] packageNames, boolean suspended) {
- final int suspendingUserId = userId;
+ // Suspension by admin isn't attributed to admin package but to the platform,
+ // Using USER_SYSTEM for consistency with other internal suspenders, like shell or root.
+ final int suspendingUserId =
+ crossUserSuspensionEnabled() ? UserHandle.USER_SYSTEM : userId;
final UserPackage suspender = UserPackage.of(
suspendingUserId, PackageManagerService.PLATFORM_PACKAGE_NAME);
return mSuspendPackageHelper.setPackagesSuspended(snapshotComputer(), packageNames,
diff --git a/services/core/java/com/android/server/pm/PackageSetting.java b/services/core/java/com/android/server/pm/PackageSetting.java
index 12eb88e..b44042c 100644
--- a/services/core/java/com/android/server/pm/PackageSetting.java
+++ b/services/core/java/com/android/server/pm/PackageSetting.java
@@ -16,6 +16,7 @@
package com.android.server.pm;
+import static android.app.admin.flags.Flags.crossUserSuspensionEnabled;
import static android.content.pm.ApplicationInfo.PRIVATE_FLAG_DEFAULT_TO_DEVICE_PROTECTED_STORAGE;
import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_DEFAULT;
import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_DISABLED;
@@ -1240,6 +1241,10 @@
for (int j = 0; j < state.getSuspendParams().size(); j++) {
proto.write(PackageProto.UserInfoProto.SUSPENDING_PACKAGE,
state.getSuspendParams().keyAt(j).packageName);
+ if (crossUserSuspensionEnabled()) {
+ proto.write(PackageProto.UserInfoProto.SUSPENDING_USER,
+ state.getSuspendParams().keyAt(j).userId);
+ }
}
}
proto.write(PackageProto.UserInfoProto.IS_STOPPED, state.isStopped());
diff --git a/services/core/java/com/android/server/pm/RemovePackageHelper.java b/services/core/java/com/android/server/pm/RemovePackageHelper.java
index 6b12781..3a0f7fb 100644
--- a/services/core/java/com/android/server/pm/RemovePackageHelper.java
+++ b/services/core/java/com/android/server/pm/RemovePackageHelper.java
@@ -261,11 +261,6 @@
// Step 1: always destroy app profiles.
mAppDataHelper.destroyAppProfilesLIF(packageName);
- // Everything else is preserved if the DELETE_KEEP_DATA flag is on
- if ((flags & PackageManager.DELETE_KEEP_DATA) != 0) {
- return;
- }
-
final AndroidPackage pkg;
final SharedUserSetting sus;
synchronized (mPm.mLock) {
@@ -282,9 +277,20 @@
resolvedPkg = PackageImpl.buildFakeForDeletion(packageName, ps.getVolumeUuid());
}
+ int appDataDeletionFlags = FLAG_STORAGE_DE | FLAG_STORAGE_CE | FLAG_STORAGE_EXTERNAL;
+ // Personal data is preserved if the DELETE_KEEP_DATA flag is on
+ if ((flags & PackageManager.DELETE_KEEP_DATA) != 0) {
+ if ((flags & PackageManager.DELETE_ARCHIVE) != 0) {
+ mAppDataHelper.clearAppDataLIF(resolvedPkg, userId,
+ appDataDeletionFlags | Installer.FLAG_CLEAR_CACHE_ONLY);
+ mAppDataHelper.clearAppDataLIF(resolvedPkg, userId,
+ appDataDeletionFlags | Installer.FLAG_CLEAR_CODE_CACHE_ONLY);
+ }
+ return;
+ }
+
// Step 2: destroy app data.
- mAppDataHelper.destroyAppDataLIF(resolvedPkg, userId,
- FLAG_STORAGE_DE | FLAG_STORAGE_CE | FLAG_STORAGE_EXTERNAL);
+ mAppDataHelper.destroyAppDataLIF(resolvedPkg, userId, appDataDeletionFlags);
if (userId != UserHandle.USER_ALL) {
ps.setCeDataInode(-1, userId);
ps.setDeDataInode(-1, userId);
diff --git a/services/core/java/com/android/server/pm/Settings.java b/services/core/java/com/android/server/pm/Settings.java
index e35a169..f5ed8d4 100644
--- a/services/core/java/com/android/server/pm/Settings.java
+++ b/services/core/java/com/android/server/pm/Settings.java
@@ -16,6 +16,7 @@
package com.android.server.pm;
+import static android.app.admin.flags.Flags.crossUserSuspensionEnabled;
import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_DEFAULT;
import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_DISABLED;
import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_ENABLED;
@@ -342,6 +343,7 @@
private static final String ATTR_DISTRACTION_FLAGS = "distraction_flags";
private static final String ATTR_SUSPENDED = "suspended";
private static final String ATTR_SUSPENDING_PACKAGE = "suspending-package";
+ private static final String ATTR_SUSPENDING_USER = "suspending-user";
private static final String ATTR_OPTIONAL = "optional";
/**
@@ -2051,7 +2053,20 @@
Slog.wtf(TAG, "No suspendingPackage found inside tag " + TAG_SUSPEND_PARAMS);
return null;
}
- final int suspendingUserId = userId;
+ int suspendingUserId;
+ if (crossUserSuspensionEnabled()) {
+ suspendingUserId = parser.getAttributeInt(
+ null, ATTR_SUSPENDING_USER, UserHandle.USER_NULL);
+ if (suspendingUserId == UserHandle.USER_NULL) {
+ suspendingUserId = switch (suspendingPackage) {
+ case "root", "com.android.shell", PLATFORM_PACKAGE_NAME
+ -> UserHandle.USER_SYSTEM;
+ default -> userId;
+ };
+ }
+ } else {
+ suspendingUserId = userId;
+ }
return Map.entry(
UserPackage.of(suspendingUserId, suspendingPackage),
SuspendParams.restoreFromXml(parser));
@@ -2418,6 +2433,10 @@
serializer.startTag(null, TAG_SUSPEND_PARAMS);
serializer.attribute(null, ATTR_SUSPENDING_PACKAGE,
suspendingPackage.packageName);
+ if (crossUserSuspensionEnabled()) {
+ serializer.attributeInt(null, ATTR_SUSPENDING_USER,
+ suspendingPackage.userId);
+ }
final SuspendParams params =
ustate.getSuspendParams().valueAt(i);
if (params != null) {
diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java
index 9e31748..76bf8fd 100644
--- a/services/core/java/com/android/server/policy/PhoneWindowManager.java
+++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java
@@ -530,6 +530,14 @@
// TODO(b/178103325): Track sleep/requested sleep for every display.
volatile boolean mRequestedOrSleepingDefaultDisplay;
+ /**
+ * This is used to check whether to invoke {@link #updateScreenOffSleepToken} when screen is
+ * turned off. E.g. if it is false when screen is turned off and the display is swapping, it
+ * is expected that the screen will be on in a short time. Then it is unnecessary to acquire
+ * screen-off-sleep-token, so it can avoid intermediate visibility or lifecycle changes.
+ */
+ volatile boolean mIsGoingToSleepDefaultDisplay;
+
volatile boolean mRecentsVisible;
volatile boolean mNavBarVirtualKeyHapticFeedbackEnabled = true;
volatile boolean mPictureInPictureVisible;
@@ -1905,6 +1913,7 @@
accessibilityManager.performSystemAction(
AccessibilityService.GLOBAL_ACTION_ACCESSIBILITY_ALL_APPS);
}
+ dismissKeyboardShortcutsMenu();
}
private void toggleNotificationPanel() {
@@ -3478,13 +3487,6 @@
return true;
}
break;
- case KeyEvent.KEYCODE_T:
- if (firstDown && event.isMetaPressed()) {
- toggleTaskbar();
- logKeyboardSystemsEvent(event, KeyboardLogEvent.TOGGLE_TASKBAR);
- return true;
- }
- break;
case KeyEvent.KEYCODE_DEL:
case KeyEvent.KEYCODE_ESCAPE:
if (firstDown && event.isMetaPressed()) {
@@ -3506,7 +3508,7 @@
if (firstDown && event.isMetaPressed() && event.isCtrlPressed()) {
StatusBarManagerInternal statusbar = getStatusBarManagerInternal();
if (statusbar != null) {
- statusbar.enterDesktop(getTargetDisplayIdForKeyEvent(event));
+ statusbar.moveFocusedTaskToDesktop(getTargetDisplayIdForKeyEvent(event));
logKeyboardSystemsEvent(event, KeyboardLogEvent.DESKTOP_MODE);
return true;
}
@@ -4735,7 +4737,7 @@
if (down) {
// There may have other embedded activities on the same Task. Try to move the
// focus before processing the back event.
- mWindowManagerInternal.moveFocusToTopEmbeddedWindowIfNeeded();
+ mWindowManagerInternal.moveFocusToAdjacentEmbeddedActivityIfNeeded();
mBackKeyHandled = false;
} else {
if (!hasLongPressOnBackBehavior()) {
@@ -5476,6 +5478,15 @@
}
mRequestedOrSleepingDefaultDisplay = true;
+ mIsGoingToSleepDefaultDisplay = true;
+
+ // In case startedGoingToSleep is called after screenTurnedOff (the source caller is in
+ // order but the methods run on different threads) and updateScreenOffSleepToken was
+ // skipped. Then acquire sleep token if screen was off.
+ if (!mDefaultDisplayPolicy.isScreenOnFully() && !mDefaultDisplayPolicy.isScreenOnEarly()
+ && com.android.window.flags.Flags.skipSleepingWhenSwitchingDisplay()) {
+ updateScreenOffSleepToken(true /* acquire */, false /* isSwappingDisplay */);
+ }
if (mKeyguardDelegate != null) {
mKeyguardDelegate.onStartedGoingToSleep(pmSleepReason);
@@ -5499,6 +5510,7 @@
MetricsLogger.histogram(mContext, "screen_timeout", mLockScreenTimeout / 1000);
mRequestedOrSleepingDefaultDisplay = false;
+ mIsGoingToSleepDefaultDisplay = false;
mDefaultDisplayPolicy.setAwake(false);
// We must get this work done here because the power manager will drop
@@ -5534,7 +5546,7 @@
}
EventLogTags.writeScreenToggled(1);
-
+ mIsGoingToSleepDefaultDisplay = false;
mDefaultDisplayPolicy.setAwake(true);
// Since goToSleep performs these functions synchronously, we must
@@ -5636,7 +5648,10 @@
if (DEBUG_WAKEUP) Slog.i(TAG, "Display" + displayId + " turned off...");
if (displayId == DEFAULT_DISPLAY) {
- updateScreenOffSleepToken(true, isSwappingDisplay);
+ if (!isSwappingDisplay || mIsGoingToSleepDefaultDisplay
+ || !com.android.window.flags.Flags.skipSleepingWhenSwitchingDisplay()) {
+ updateScreenOffSleepToken(true /* acquire */, isSwappingDisplay);
+ }
mRequestedOrSleepingDefaultDisplay = false;
mDefaultDisplayPolicy.screenTurnedOff();
synchronized (mLock) {
diff --git a/services/core/java/com/android/server/power/hint/Android.bp b/services/core/java/com/android/server/power/hint/Android.bp
new file mode 100644
index 0000000..8a98de6
--- /dev/null
+++ b/services/core/java/com/android/server/power/hint/Android.bp
@@ -0,0 +1,12 @@
+aconfig_declarations {
+ name: "power_hint_flags",
+ package: "com.android.server.power.hint",
+ srcs: [
+ "flags.aconfig",
+ ],
+}
+
+java_aconfig_library {
+ name: "power_hint_flags_lib",
+ aconfig_declarations: "power_hint_flags",
+}
diff --git a/services/core/java/com/android/server/power/hint/HintManagerService.java b/services/core/java/com/android/server/power/hint/HintManagerService.java
index aa1a41e..3f1b1c1 100644
--- a/services/core/java/com/android/server/power/hint/HintManagerService.java
+++ b/services/core/java/com/android/server/power/hint/HintManagerService.java
@@ -17,6 +17,7 @@
package com.android.server.power.hint;
import static com.android.internal.util.ConcurrentUtils.DIRECT_EXECUTOR;
+import static com.android.server.power.hint.Flags.powerhintThreadCleanup;
import android.annotation.NonNull;
import android.app.ActivityManager;
@@ -26,9 +27,12 @@
import android.content.Context;
import android.hardware.power.WorkDuration;
import android.os.Binder;
+import android.os.Handler;
import android.os.IBinder;
import android.os.IHintManager;
import android.os.IHintSession;
+import android.os.Looper;
+import android.os.Message;
import android.os.PerformanceHintManager;
import android.os.Process;
import android.os.RemoteException;
@@ -36,6 +40,8 @@
import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.ArraySet;
+import android.util.IntArray;
+import android.util.Slog;
import android.util.SparseIntArray;
import android.util.StatsEvent;
@@ -46,20 +52,31 @@
import com.android.internal.util.Preconditions;
import com.android.server.FgThread;
import com.android.server.LocalServices;
+import com.android.server.ServiceThread;
import com.android.server.SystemService;
import com.android.server.utils.Slogf;
import java.io.FileDescriptor;
import java.io.PrintWriter;
+import java.util.ArrayList;
import java.util.Arrays;
+import java.util.HashMap;
import java.util.List;
+import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
/** An hint service implementation that runs in System Server process. */
public final class HintManagerService extends SystemService {
private static final String TAG = "HintManagerService";
private static final boolean DEBUG = false;
+
+ private static final int EVENT_CLEAN_UP_UID = 3;
+ @VisibleForTesting static final int CLEAN_UP_UID_DELAY_MILLIS = 1000;
+
+
@VisibleForTesting final long mHintSessionPreferredRate;
// Multi-level map storing all active AppHintSessions.
@@ -73,9 +90,15 @@
/** Lock to protect HAL handles and listen list. */
private final Object mLock = new Object();
+ @GuardedBy("mNonIsolatedTidsLock")
+ private final Map<Integer, Set<Long>> mNonIsolatedTids;
+
+ private final Object mNonIsolatedTidsLock = new Object();
+
@VisibleForTesting final MyUidObserver mUidObserver;
private final NativeWrapper mNativeWrapper;
+ private final CleanUpHandler mCleanUpHandler;
private final ActivityManagerInternal mAmInternal;
@@ -94,6 +117,13 @@
HintManagerService(Context context, Injector injector) {
super(context);
mContext = context;
+ if (powerhintThreadCleanup()) {
+ mCleanUpHandler = new CleanUpHandler(createCleanUpThread().getLooper());
+ mNonIsolatedTids = new HashMap<>();
+ } else {
+ mCleanUpHandler = null;
+ mNonIsolatedTids = null;
+ }
mActiveSessions = new ArrayMap<>();
mNativeWrapper = injector.createNativeWrapper();
mNativeWrapper.halInit();
@@ -103,6 +133,13 @@
LocalServices.getService(ActivityManagerInternal.class));
}
+ private ServiceThread createCleanUpThread() {
+ final ServiceThread handlerThread = new ServiceThread(TAG,
+ Process.THREAD_PRIORITY_LOWEST, true /*allowIo*/);
+ handlerThread.start();
+ return handlerThread;
+ }
+
@VisibleForTesting
static class Injector {
NativeWrapper createNativeWrapper() {
@@ -306,7 +343,18 @@
public void onUidStateChanged(int uid, int procState, long procStateSeq, int capability) {
FgThread.getHandler().post(() -> {
synchronized (mCacheLock) {
- mProcStatesCache.put(uid, procState);
+ if (powerhintThreadCleanup()) {
+ final boolean before = isUidForeground(uid);
+ mProcStatesCache.put(uid, procState);
+ final boolean after = isUidForeground(uid);
+ if (before != after) {
+ final Message msg = mCleanUpHandler.obtainMessage(EVENT_CLEAN_UP_UID,
+ uid);
+ mCleanUpHandler.sendMessageDelayed(msg, CLEAN_UP_UID_DELAY_MILLIS);
+ }
+ } else {
+ mProcStatesCache.put(uid, procState);
+ }
}
boolean shouldAllowUpdate = isUidForeground(uid);
synchronized (mLock) {
@@ -314,9 +362,10 @@
if (tokenMap == null) {
return;
}
- for (ArraySet<AppHintSession> sessionSet : tokenMap.values()) {
- for (AppHintSession s : sessionSet) {
- s.onProcStateChanged(shouldAllowUpdate);
+ for (int i = tokenMap.size() - 1; i >= 0; i--) {
+ final ArraySet<AppHintSession> sessionSet = tokenMap.valueAt(i);
+ for (int j = sessionSet.size() - 1; j >= 0; j--) {
+ sessionSet.valueAt(j).onProcStateChanged(shouldAllowUpdate);
}
}
}
@@ -324,52 +373,237 @@
}
}
+ final class CleanUpHandler extends Handler {
+ // status of processed tid used for caching
+ private static final int TID_NOT_CHECKED = 0;
+ private static final int TID_PASSED_CHECK = 1;
+ private static final int TID_EXITED = 2;
+
+ CleanUpHandler(Looper looper) {
+ super(looper);
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ if (msg.what == EVENT_CLEAN_UP_UID) {
+ if (hasEqualMessages(msg.what, msg.obj)) {
+ removeEqualMessages(msg.what, msg.obj);
+ final Message newMsg = obtainMessage(msg.what, msg.obj);
+ sendMessageDelayed(newMsg, CLEAN_UP_UID_DELAY_MILLIS);
+ return;
+ }
+ final int uid = (int) msg.obj;
+ boolean isForeground = mUidObserver.isUidForeground(uid);
+ // store all sessions in a list and release the global lock
+ // we don't need to worry about stale data or racing as the session is synchronized
+ // itself and will perform its own closed status check in setThreads call
+ final List<AppHintSession> sessions;
+ synchronized (mLock) {
+ final ArrayMap<IBinder, ArraySet<AppHintSession>> tokenMap =
+ mActiveSessions.get(uid);
+ if (tokenMap == null || tokenMap.isEmpty()) {
+ return;
+ }
+ sessions = new ArrayList<>(tokenMap.size());
+ for (int i = tokenMap.size() - 1; i >= 0; i--) {
+ final ArraySet<AppHintSession> set = tokenMap.valueAt(i);
+ for (int j = set.size() - 1; j >= 0; j--) {
+ sessions.add(set.valueAt(j));
+ }
+ }
+ }
+ final long[] durationList = new long[sessions.size()];
+ final int[] invalidTidCntList = new int[sessions.size()];
+ final SparseIntArray checkedTids = new SparseIntArray();
+ int[] totalTidCnt = new int[1];
+ for (int i = sessions.size() - 1; i >= 0; i--) {
+ final AppHintSession session = sessions.get(i);
+ final long start = System.nanoTime();
+ try {
+ final int invalidCnt = cleanUpSession(session, checkedTids, totalTidCnt);
+ final long elapsed = System.nanoTime() - start;
+ invalidTidCntList[i] = invalidCnt;
+ durationList[i] = elapsed;
+ } catch (Exception e) {
+ Slog.e(TAG, "Failed to clean up session " + session.mHalSessionPtr
+ + " for UID " + session.mUid);
+ }
+ }
+ logCleanUpMetrics(uid, invalidTidCntList, durationList, sessions.size(),
+ totalTidCnt[0], isForeground);
+ }
+ }
+
+ private void logCleanUpMetrics(int uid, int[] count, long[] durationNsList, int sessionCnt,
+ int totalTidCnt, boolean isForeground) {
+ int maxInvalidTidCnt = Integer.MIN_VALUE;
+ int totalInvalidTidCnt = 0;
+ for (int i = 0; i < count.length; i++) {
+ totalInvalidTidCnt += count[i];
+ maxInvalidTidCnt = Math.max(maxInvalidTidCnt, count[i]);
+ }
+ if (DEBUG || totalInvalidTidCnt > 0) {
+ Arrays.sort(durationNsList);
+ long totalDurationNs = 0;
+ for (int i = 0; i < durationNsList.length; i++) {
+ totalDurationNs += durationNsList[i];
+ }
+ int totalDurationUs = (int) TimeUnit.NANOSECONDS.toMicros(totalDurationNs);
+ int maxDurationUs = (int) TimeUnit.NANOSECONDS.toMicros(
+ durationNsList[durationNsList.length - 1]);
+ int minDurationUs = (int) TimeUnit.NANOSECONDS.toMicros(durationNsList[0]);
+ int avgDurationUs = (int) TimeUnit.NANOSECONDS.toMicros(
+ totalDurationNs / durationNsList.length);
+ int th90DurationUs = (int) TimeUnit.NANOSECONDS.toMicros(
+ durationNsList[(int) (durationNsList.length * 0.9)]);
+ Slog.d(TAG,
+ "Invalid tid found for UID" + uid + " in " + totalDurationUs + "us:\n\t"
+ + "count("
+ + " session: " + sessionCnt
+ + " totalTid: " + totalTidCnt
+ + " maxInvalidTid: " + maxInvalidTidCnt
+ + " totalInvalidTid: " + totalInvalidTidCnt + ")\n\t"
+ + "time per session("
+ + " min: " + minDurationUs + "us"
+ + " max: " + maxDurationUs + "us"
+ + " avg: " + avgDurationUs + "us"
+ + " 90%: " + th90DurationUs + "us" + ")\n\t"
+ + "isForeground: " + isForeground);
+ }
+ }
+
+ // This will check if each TID currently linked to the session still exists. If it's
+ // previously registered as not an isolated process, then it will run tkill(pid, tid, 0) to
+ // verify that it's still running under the same pid. Otherwise, it will run
+ // kill(tid, 0) to only check if it exists. The result will be cached in checkedTids
+ // map with tid as the key and checked status as value.
+ public int cleanUpSession(AppHintSession session, SparseIntArray checkedTids, int[] total) {
+ if (session.isClosed()) {
+ return 0;
+ }
+ final int pid = session.mPid;
+ final int[] tids = session.getTidsInternal();
+ if (total != null && total.length == 1) {
+ total[0] += tids.length;
+ }
+ final IntArray filtered = new IntArray(tids.length);
+ for (int i = 0; i < tids.length; i++) {
+ int tid = tids[i];
+ if (checkedTids.get(tid, 0) != TID_NOT_CHECKED) {
+ if (checkedTids.get(tid) == TID_PASSED_CHECK) {
+ filtered.add(tid);
+ }
+ continue;
+ }
+ // if it was registered as a non-isolated then we perform more restricted check
+ final boolean isNotIsolated;
+ synchronized (mNonIsolatedTidsLock) {
+ isNotIsolated = mNonIsolatedTids.containsKey(tid);
+ }
+ try {
+ if (isNotIsolated) {
+ Process.checkTid(pid, tid);
+ } else {
+ Process.checkPid(tid);
+ }
+ checkedTids.put(tid, TID_PASSED_CHECK);
+ filtered.add(tid);
+ } catch (NoSuchElementException e) {
+ checkedTids.put(tid, TID_EXITED);
+ } catch (Exception e) {
+ Slog.w(TAG, "Unexpected exception when checking TID " + tid + " under PID "
+ + pid + "(isolated: " + !isNotIsolated + ")", e);
+ // if anything unexpected happens then we keep it, but don't store it as checked
+ filtered.add(tid);
+ }
+ }
+ final int diff = tids.length - filtered.size();
+ if (diff > 0) {
+ synchronized (session) {
+ // in case thread list is updated during the cleanup then we skip updating
+ // the session but just return the number for reporting purpose
+ final int[] newTids = session.getTidsInternal();
+ if (newTids.length != tids.length) {
+ Slog.d(TAG, "Skipped cleaning up the session as new tids are added");
+ return diff;
+ }
+ Arrays.sort(newTids);
+ Arrays.sort(tids);
+ if (!Arrays.equals(newTids, tids)) {
+ Slog.d(TAG, "Skipped cleaning up the session as new tids are updated");
+ return diff;
+ }
+ Slog.d(TAG, "Cleaned up " + diff + " invalid tids for session "
+ + session.mHalSessionPtr + " with UID " + session.mUid + "\n\t"
+ + "before: " + Arrays.toString(tids) + "\n\t"
+ + "after: " + filtered);
+ final int[] filteredTids = filtered.toArray();
+ if (filteredTids.length == 0) {
+ session.mShouldForcePause = true;
+ if (session.mUpdateAllowed) {
+ session.pause();
+ }
+ } else {
+ session.setThreadsInternal(filteredTids, false);
+ }
+ }
+ }
+ return diff;
+ }
+ }
+
@VisibleForTesting
IHintManager.Stub getBinderServiceInstance() {
return mService;
}
// returns the first invalid tid or null if not found
- private Integer checkTidValid(int uid, int tgid, int [] tids) {
+ private Integer checkTidValid(int uid, int tgid, int [] tids, IntArray nonIsolated) {
// Make sure all tids belongs to the same UID (including isolated UID),
// tids can belong to different application processes.
List<Integer> isolatedPids = null;
- for (int threadId : tids) {
+ for (int i = 0; i < tids.length; i++) {
+ int tid = tids[i];
final String[] procStatusKeys = new String[] {
"Uid:",
"Tgid:"
};
long[] output = new long[procStatusKeys.length];
- Process.readProcLines("/proc/" + threadId + "/status", procStatusKeys, output);
+ Process.readProcLines("/proc/" + tid + "/status", procStatusKeys, output);
int uidOfThreadId = (int) output[0];
int pidOfThreadId = (int) output[1];
- // use PID check for isolated processes, use UID check for non-isolated processes.
- if (pidOfThreadId == tgid || uidOfThreadId == uid) {
+ // use PID check for non-isolated processes
+ if (nonIsolated != null && pidOfThreadId == tgid) {
+ nonIsolated.add(tid);
+ continue;
+ }
+ // use UID check for isolated processes.
+ if (uidOfThreadId == uid) {
continue;
}
// Only call into AM if the tid is either isolated or invalid
if (isolatedPids == null) {
// To avoid deadlock, do not call into AMS if the call is from system.
if (uid == Process.SYSTEM_UID) {
- return threadId;
+ return tid;
}
isolatedPids = mAmInternal.getIsolatedProcesses(uid);
if (isolatedPids == null) {
- return threadId;
+ return tid;
}
}
if (isolatedPids.contains(pidOfThreadId)) {
continue;
}
- return threadId;
+ return tid;
}
return null;
}
private String formatTidCheckErrMsg(int callingUid, int[] tids, Integer invalidTid) {
return "Tid" + invalidTid + " from list " + Arrays.toString(tids)
- + " doesn't belong to the calling application" + callingUid;
+ + " doesn't belong to the calling application " + callingUid;
}
@VisibleForTesting
@@ -387,7 +621,10 @@
final int callingTgid = Process.getThreadGroupLeader(Binder.getCallingPid());
final long identity = Binder.clearCallingIdentity();
try {
- final Integer invalidTid = checkTidValid(callingUid, callingTgid, tids);
+ final IntArray nonIsolated = powerhintThreadCleanup() ? new IntArray(tids.length)
+ : null;
+ final Integer invalidTid = checkTidValid(callingUid, callingTgid, tids,
+ nonIsolated);
if (invalidTid != null) {
final String errMsg = formatTidCheckErrMsg(callingUid, tids, invalidTid);
Slogf.w(TAG, errMsg);
@@ -396,6 +633,14 @@
long halSessionPtr = mNativeWrapper.halCreateHintSession(callingTgid, callingUid,
tids, durationNanos);
+ if (powerhintThreadCleanup()) {
+ synchronized (mNonIsolatedTidsLock) {
+ for (int i = nonIsolated.size() - 1; i >= 0; i--) {
+ mNonIsolatedTids.putIfAbsent(nonIsolated.get(i), new ArraySet<>());
+ mNonIsolatedTids.get(nonIsolated.get(i)).add(halSessionPtr);
+ }
+ }
+ }
if (halSessionPtr == 0) {
return null;
}
@@ -482,6 +727,7 @@
protected boolean mUpdateAllowed;
protected int[] mNewThreadIds;
protected boolean mPowerEfficient;
+ protected boolean mShouldForcePause;
private enum SessionModes {
POWER_EFFICIENCY,
@@ -498,6 +744,7 @@
mTargetDurationNanos = durationNanos;
mUpdateAllowed = true;
mPowerEfficient = false;
+ mShouldForcePause = false;
final boolean allowed = mUidObserver.isUidForeground(mUid);
updateHintAllowed(allowed);
try {
@@ -511,7 +758,7 @@
@VisibleForTesting
boolean updateHintAllowed(boolean allowed) {
synchronized (this) {
- if (allowed && !mUpdateAllowed) resume();
+ if (allowed && !mUpdateAllowed && !mShouldForcePause) resume();
if (!allowed && mUpdateAllowed) pause();
mUpdateAllowed = allowed;
return mUpdateAllowed;
@@ -521,7 +768,7 @@
@Override
public void updateTargetWorkDuration(long targetDurationNanos) {
synchronized (this) {
- if (mHalSessionPtr == 0 || !mUpdateAllowed) {
+ if (mHalSessionPtr == 0 || !mUpdateAllowed || mShouldForcePause) {
return;
}
Preconditions.checkArgument(targetDurationNanos > 0, "Expected"
@@ -534,7 +781,7 @@
@Override
public void reportActualWorkDuration(long[] actualDurationNanos, long[] timeStampNanos) {
synchronized (this) {
- if (mHalSessionPtr == 0 || !mUpdateAllowed) {
+ if (mHalSessionPtr == 0 || !mUpdateAllowed || mShouldForcePause) {
return;
}
Preconditions.checkArgument(actualDurationNanos.length != 0, "the count"
@@ -581,12 +828,25 @@
if (sessionSet.isEmpty()) tokenMap.remove(mToken);
if (tokenMap.isEmpty()) mActiveSessions.remove(mUid);
}
+ if (powerhintThreadCleanup()) {
+ synchronized (mNonIsolatedTidsLock) {
+ final int[] tids = getTidsInternal();
+ for (int tid : tids) {
+ if (mNonIsolatedTids.containsKey(tid)) {
+ mNonIsolatedTids.get(tid).remove(mHalSessionPtr);
+ if (mNonIsolatedTids.get(tid).isEmpty()) {
+ mNonIsolatedTids.remove(tid);
+ }
+ }
+ }
+ }
+ }
}
@Override
public void sendHint(@PerformanceHintManager.Session.Hint int hint) {
synchronized (this) {
- if (mHalSessionPtr == 0 || !mUpdateAllowed) {
+ if (mHalSessionPtr == 0 || !mUpdateAllowed || mShouldForcePause) {
return;
}
Preconditions.checkArgument(hint >= 0, "the hint ID value should be"
@@ -596,33 +856,60 @@
}
public void setThreads(@NonNull int[] tids) {
+ setThreadsInternal(tids, true);
+ }
+
+ private void setThreadsInternal(int[] tids, boolean checkTid) {
+ if (tids.length == 0) {
+ throw new IllegalArgumentException("Thread id list can't be empty.");
+ }
+
synchronized (this) {
if (mHalSessionPtr == 0) {
return;
}
- if (tids.length == 0) {
- throw new IllegalArgumentException("Thread id list can't be empty.");
- }
- final int callingUid = Binder.getCallingUid();
- final int callingTgid = Process.getThreadGroupLeader(Binder.getCallingPid());
- final long identity = Binder.clearCallingIdentity();
- try {
- final Integer invalidTid = checkTidValid(callingUid, callingTgid, tids);
- if (invalidTid != null) {
- final String errMsg = formatTidCheckErrMsg(callingUid, tids, invalidTid);
- Slogf.w(TAG, errMsg);
- throw new SecurityException(errMsg);
- }
- } finally {
- Binder.restoreCallingIdentity(identity);
- }
if (!mUpdateAllowed) {
Slogf.v(TAG, "update hint not allowed, storing tids.");
mNewThreadIds = tids;
+ mShouldForcePause = false;
return;
}
+ if (checkTid) {
+ final int callingUid = Binder.getCallingUid();
+ final int callingTgid = Process.getThreadGroupLeader(Binder.getCallingPid());
+ final IntArray nonIsolated = powerhintThreadCleanup() ? new IntArray() : null;
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ final Integer invalidTid = checkTidValid(callingUid, callingTgid, tids,
+ nonIsolated);
+ if (invalidTid != null) {
+ final String errMsg = formatTidCheckErrMsg(callingUid, tids,
+ invalidTid);
+ Slogf.w(TAG, errMsg);
+ throw new SecurityException(errMsg);
+ }
+ if (powerhintThreadCleanup()) {
+ synchronized (mNonIsolatedTidsLock) {
+ for (int i = nonIsolated.size() - 1; i >= 0; i--) {
+ mNonIsolatedTids.putIfAbsent(nonIsolated.get(i),
+ new ArraySet<>());
+ mNonIsolatedTids.get(nonIsolated.get(i)).add(mHalSessionPtr);
+ }
+ }
+ }
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
mNativeWrapper.halSetThreads(mHalSessionPtr, tids);
mThreadIds = tids;
+ mNewThreadIds = null;
+ // if the update is allowed but the session is force paused by tid clean up, then
+ // it's waiting for this tid update to resume
+ if (mShouldForcePause) {
+ resume();
+ mShouldForcePause = false;
+ }
}
}
@@ -632,10 +919,24 @@
}
}
+ @VisibleForTesting
+ int[] getTidsInternal() {
+ synchronized (this) {
+ return mNewThreadIds != null ? Arrays.copyOf(mNewThreadIds, mNewThreadIds.length)
+ : Arrays.copyOf(mThreadIds, mThreadIds.length);
+ }
+ }
+
+ boolean isClosed() {
+ synchronized (this) {
+ return mHalSessionPtr == 0;
+ }
+ }
+
@Override
public void setMode(int mode, boolean enabled) {
synchronized (this) {
- if (mHalSessionPtr == 0 || !mUpdateAllowed) {
+ if (mHalSessionPtr == 0 || !mUpdateAllowed || mShouldForcePause) {
return;
}
Preconditions.checkArgument(mode >= 0, "the mode Id value should be"
@@ -650,13 +951,13 @@
@Override
public void reportActualWorkDuration2(WorkDuration[] workDurations) {
synchronized (this) {
- if (mHalSessionPtr == 0 || !mUpdateAllowed) {
+ if (mHalSessionPtr == 0 || !mUpdateAllowed || mShouldForcePause) {
return;
}
Preconditions.checkArgument(workDurations.length != 0, "the count"
+ " of work durations shouldn't be 0.");
- for (WorkDuration workDuration : workDurations) {
- validateWorkDuration(workDuration);
+ for (int i = 0; i < workDurations.length; i++) {
+ validateWorkDuration(workDurations[i]);
}
mNativeWrapper.halReportActualWorkDuration(mHalSessionPtr, workDurations);
}
@@ -743,6 +1044,7 @@
pw.println(prefix + "SessionTIDs: " + Arrays.toString(mThreadIds));
pw.println(prefix + "SessionTargetDurationNanos: " + mTargetDurationNanos);
pw.println(prefix + "SessionAllowed: " + mUpdateAllowed);
+ pw.println(prefix + "SessionForcePaused: " + mShouldForcePause);
pw.println(prefix + "PowerEfficient: " + (mPowerEfficient ? "true" : "false"));
}
}
diff --git a/services/core/java/com/android/server/power/hint/flags.aconfig b/services/core/java/com/android/server/power/hint/flags.aconfig
new file mode 100644
index 0000000..f4afcb1
--- /dev/null
+++ b/services/core/java/com/android/server/power/hint/flags.aconfig
@@ -0,0 +1,8 @@
+package: "com.android.server.power.hint"
+
+flag {
+ name: "powerhint_thread_cleanup"
+ namespace: "game"
+ description: "Feature flag for auto PowerHintSession dead thread cleanup"
+ bug: "296160319"
+}
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 b2e01c5..c42ccea 100644
--- a/services/core/java/com/android/server/power/stats/flags.aconfig
+++ b/services/core/java/com/android/server/power/stats/flags.aconfig
@@ -2,6 +2,7 @@
flag {
name: "power_monitor_api"
+ is_exported: true
namespace: "backstage_power"
description: "Feature flag for ODPM API"
bug: "295027807"
diff --git a/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java b/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java
index f7c236a..2ff3861 100644
--- a/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java
+++ b/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java
@@ -267,7 +267,7 @@
void removeQsTile(ComponentName tile);
/**
- * Called when requested to enter desktop from an app.
+ * Called when requested to enter desktop from a focused app.
*/
- void enterDesktop(int displayId);
+ void moveFocusedTaskToDesktop(int displayId);
}
diff --git a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
index 7b3e237..cca5beb 100644
--- a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
+++ b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
@@ -838,15 +838,17 @@
} catch (RemoteException ex) { }
}
}
+
@Override
- public void enterDesktop(int displayId) {
+ public void moveFocusedTaskToDesktop(int displayId) {
IStatusBar bar = mBar;
if (bar != null) {
try {
- bar.enterDesktop(displayId);
+ bar.moveFocusedTaskToDesktop(displayId);
} catch (RemoteException ex) { }
}
}
+
@Override
public void showMediaOutputSwitcher(String packageName) {
IStatusBar bar = mBar;
diff --git a/services/core/java/com/android/server/webkit/flags.aconfig b/services/core/java/com/android/server/webkit/flags.aconfig
index 1411acc..2afbcd6 100644
--- a/services/core/java/com/android/server/webkit/flags.aconfig
+++ b/services/core/java/com/android/server/webkit/flags.aconfig
@@ -2,6 +2,7 @@
flag {
name: "update_service_v2"
+ is_exported: true
namespace: "webview"
description: "Using a new version of the WebView update service"
bug: "308907090"
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index 23f9743..17e6996 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -11012,6 +11012,20 @@
}
/**
+ * Returns the {@link #createTime} if the top window is the `base` window. Note that do not
+ * use the window creation time because the window could be re-created when the activity
+ * relaunched if configuration changed.
+ * <p>
+ * Otherwise, return the creation time of the top window.
+ */
+ long getLastWindowCreateTime() {
+ final WindowState window = getWindow(win -> true);
+ return window != null && window.mAttrs.type != TYPE_BASE_APPLICATION
+ ? window.getCreateTime()
+ : createTime;
+ }
+
+ /**
* Adjust the source rect hint in {@link #pictureInPictureArgs} by window bounds since
* it is relative to its root view (see also b/235599028).
* It is caller's responsibility to make sure this is called exactly once when we update
diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
index 060f1c8..6af496f 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
@@ -5682,29 +5682,6 @@
throw e;
}
- /**
- * Sets the corresponding {@link DisplayArea} information for the process global
- * configuration. To be called when we need to show IME on a different {@link DisplayArea}
- * or display.
- *
- * @param pid The process id associated with the IME window.
- * @param imeContainer The DisplayArea that contains the IME window.
- */
- void onImeWindowSetOnDisplayArea(final int pid, @NonNull final DisplayArea imeContainer) {
- if (pid == MY_PID || pid < 0) {
- ProtoLog.w(WM_DEBUG_CONFIGURATION,
- "Trying to update display configuration for system/invalid process.");
- return;
- }
- final WindowProcessController process = mProcessMap.getProcess(pid);
- if (process == null) {
- ProtoLog.w(WM_DEBUG_CONFIGURATION, "Trying to update display "
- + "configuration for invalid process, pid=%d", pid);
- return;
- }
- process.registerDisplayAreaConfigurationListener(imeContainer);
- }
-
@Override
public void setRunningRemoteTransitionDelegate(IApplicationThread delegate) {
final TransitionController controller = getTransitionController();
diff --git a/services/core/java/com/android/server/wm/BackNavigationController.java b/services/core/java/com/android/server/wm/BackNavigationController.java
index e3ac35c..48d78f5 100644
--- a/services/core/java/com/android/server/wm/BackNavigationController.java
+++ b/services/core/java/com/android/server/wm/BackNavigationController.java
@@ -165,7 +165,7 @@
}
// Move focus to the top embedded window if possible
- if (mWindowManagerService.moveFocusToTopEmbeddedWindow(window)) {
+ if (mWindowManagerService.moveFocusToAdjacentEmbeddedWindow(window)) {
window = wmService.getFocusedWindowLocked();
if (window == null) {
Slog.e(TAG, "New focused window is null, returning null.");
diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java
index eb1f052..46d4ce4 100644
--- a/services/core/java/com/android/server/wm/DisplayContent.java
+++ b/services/core/java/com/android/server/wm/DisplayContent.java
@@ -4171,11 +4171,6 @@
*/
void setInputMethodWindowLocked(WindowState win) {
mInputMethodWindow = win;
- // Update display configuration for IME process.
- if (mInputMethodWindow != null) {
- final int imePid = mInputMethodWindow.mSession.mPid;
- mAtmService.onImeWindowSetOnDisplayArea(imePid, mImeWindowsContainer);
- }
mInsetsStateController.getImeSourceProvider().setWindowContainer(win,
mDisplayPolicy.getImeSourceFrameProvider(), null);
computeImeTarget(true /* updateImeTarget */);
diff --git a/services/core/java/com/android/server/wm/Session.java b/services/core/java/com/android/server/wm/Session.java
index 30134d8..e157318 100644
--- a/services/core/java/com/android/server/wm/Session.java
+++ b/services/core/java/com/android/server/wm/Session.java
@@ -283,14 +283,14 @@
int lastSyncSeqId, ClientWindowFrames outFrames,
MergedConfiguration mergedConfiguration, SurfaceControl outSurfaceControl,
InsetsState outInsetsState, InsetsSourceControl.Array outActiveControls,
- Bundle outSyncSeqIdBundle) {
+ Bundle outBundle) {
if (false) Slog.d(TAG_WM, ">>>>>> ENTERED relayout from "
+ Binder.getCallingPid());
Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, mRelayoutTag);
int res = mService.relayoutWindow(this, window, attrs,
requestedWidth, requestedHeight, viewFlags, flags, seq,
lastSyncSeqId, outFrames, mergedConfiguration, outSurfaceControl, outInsetsState,
- outActiveControls, outSyncSeqIdBundle);
+ outActiveControls, outBundle);
Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
if (false) Slog.d(TAG_WM, "<<<<<< EXITING relayout to "
+ Binder.getCallingPid());
diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java
index 55dc30c..18d2718 100644
--- a/services/core/java/com/android/server/wm/Task.java
+++ b/services/core/java/com/android/server/wm/Task.java
@@ -1274,7 +1274,8 @@
if (!isLeafTaskFragment()) {
final ActivityRecord top = topRunningActivity();
final ActivityRecord resumedActivity = getResumedActivity();
- if (resumedActivity != null && top.getTaskFragment() != this) {
+ if (resumedActivity != null
+ && (top.getTaskFragment() != this || !canBeResumed(resuming))) {
// Pausing the resumed activity because it is occluded by other task fragment.
if (startPausing(false /* uiSleeping*/, resuming, reason)) {
someActivityPaused[0]++;
@@ -3753,11 +3754,9 @@
// Boost the adjacent TaskFragment for dimmer if needed.
final TaskFragment taskFragment = wc.asTaskFragment();
if (taskFragment != null && taskFragment.isEmbedded()) {
- taskFragment.mDimmerSurfaceBoosted = false;
final TaskFragment adjacentTf = taskFragment.getAdjacentTaskFragment();
if (adjacentTf != null && adjacentTf.shouldBoostDimmer()) {
adjacentTf.assignLayer(t, layer++);
- adjacentTf.mDimmerSurfaceBoosted = true;
}
}
@@ -6823,8 +6822,8 @@
* A decor surface is requested by a {@link TaskFragmentOrganizer} and is placed below children
* windows in the Task except for own Activities and TaskFragments in fully trusted mode. The
* decor surface is created and shared with the client app with
- * {@link android.window.TaskFragmentOperation#OP_TYPE_CREATE_TASK_FRAGMENT_DECOR_SURFACE} and
- * be removed with
+ * {@link android.window.TaskFragmentOperation#OP_TYPE_CREATE_OR_MOVE_TASK_FRAGMENT_DECOR_SURFACE}
+ * and be removed with
* {@link android.window.TaskFragmentOperation#OP_TYPE_REMOVE_TASK_FRAGMENT_DECOR_SURFACE}.
*
* When boosted with
diff --git a/services/core/java/com/android/server/wm/TaskFragment.java b/services/core/java/com/android/server/wm/TaskFragment.java
index 3cf561c..dc0e034 100644
--- a/services/core/java/com/android/server/wm/TaskFragment.java
+++ b/services/core/java/com/android/server/wm/TaskFragment.java
@@ -216,9 +216,6 @@
Dimmer mDimmer = Dimmer.DIMMER_REFACTOR
? new SmoothDimmer(this) : new LegacyDimmer(this);
- /** {@code true} if the dimmer surface is boosted. {@code false} otherwise. */
- boolean mDimmerSurfaceBoosted;
-
/** Apply the dim layer on the embedded TaskFragment. */
static final int EMBEDDED_DIM_AREA_TASK_FRAGMENT = 0;
diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java
index 66c2e53..319e2b0 100644
--- a/services/core/java/com/android/server/wm/Transition.java
+++ b/services/core/java/com/android/server/wm/Transition.java
@@ -2468,7 +2468,15 @@
for (WindowContainer<?> p = getAnimatableParent(wc); p != null;
p = getAnimatableParent(p)) {
final ChangeInfo parentChange = changes.get(p);
- if (parentChange == null || !parentChange.hasChanged()) break;
+ if (parentChange == null) {
+ break;
+ }
+ if (!parentChange.hasChanged()) {
+ // In case the target is collected after the parent has been changed, it could
+ // be too late to snapshot the parent change. Skip to see if there is any
+ // parent window further up to be considered as change parent.
+ continue;
+ }
if (p.mRemoteToken == null) {
// Intermediate parents must be those that has window to be managed by Shell.
continue;
diff --git a/services/core/java/com/android/server/wm/WindowManagerInternal.java b/services/core/java/com/android/server/wm/WindowManagerInternal.java
index acc6330..daf8129 100644
--- a/services/core/java/com/android/server/wm/WindowManagerInternal.java
+++ b/services/core/java/com/android/server/wm/WindowManagerInternal.java
@@ -1068,9 +1068,9 @@
public abstract void clearBlockedApps();
/**
- * Moves the current focus to the top activity window if the top activity is embedded.
+ * Moves the current focus to the adjacent activity if it has the latest created window.
*/
- public abstract boolean moveFocusToTopEmbeddedWindowIfNeeded();
+ public abstract boolean moveFocusToAdjacentEmbeddedActivityIfNeeded();
/**
* Returns an instance of {@link ScreenCapture.ScreenshotHardwareBuffer} containing the current
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index 207b1bb..0effa6c 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -304,6 +304,7 @@
import android.view.displayhash.DisplayHash;
import android.view.displayhash.VerifiedDisplayHash;
import android.view.inputmethod.ImeTracker;
+import android.window.ActivityWindowInfo;
import android.window.AddToSurfaceSyncGroupResult;
import android.window.ClientWindowFrames;
import android.window.IGlobalDragListener;
@@ -794,6 +795,8 @@
Settings.Global.getUriFor(Settings.Global.ANIMATOR_DURATION_SCALE);
private final Uri mImmersiveModeConfirmationsUri =
Settings.Secure.getUriFor(Settings.Secure.IMMERSIVE_MODE_CONFIRMATIONS);
+ private final Uri mDisableSecureWindowsUri =
+ Settings.Secure.getUriFor(Settings.Secure.DISABLE_SECURE_WINDOWS);
private final Uri mPolicyControlUri =
Settings.Global.getUriFor(Settings.Global.POLICY_CONTROL);
private final Uri mForceDesktopModeOnExternalDisplaysUri = Settings.Global.getUriFor(
@@ -822,6 +825,8 @@
UserHandle.USER_ALL);
resolver.registerContentObserver(mImmersiveModeConfirmationsUri, false, this,
UserHandle.USER_ALL);
+ resolver.registerContentObserver(mDisableSecureWindowsUri, false, this,
+ UserHandle.USER_ALL);
resolver.registerContentObserver(mPolicyControlUri, false, this, UserHandle.USER_ALL);
resolver.registerContentObserver(mForceDesktopModeOnExternalDisplaysUri, false, this,
UserHandle.USER_ALL);
@@ -876,6 +881,11 @@
return;
}
+ if (mDisableSecureWindowsUri.equals(uri)) {
+ updateDisableSecureWindows();
+ return;
+ }
+
@UpdateAnimationScaleMode
final int mode;
if (mWindowAnimationScaleUri.equals(uri)) {
@@ -895,6 +905,7 @@
void loadSettings() {
updateSystemUiSettings(false /* handleChange */);
updateMaximumObscuringOpacityForTouch();
+ updateDisableSecureWindows();
}
void updateMaximumObscuringOpacityForTouch() {
@@ -977,6 +988,28 @@
});
}
}
+
+ void updateDisableSecureWindows() {
+ if (!SystemProperties.getBoolean(SYSTEM_DEBUGGABLE, false)) {
+ return;
+ }
+
+ final boolean disableSecureWindows;
+ try {
+ disableSecureWindows = Settings.Secure.getIntForUser(mContext.getContentResolver(),
+ Settings.Secure.DISABLE_SECURE_WINDOWS, 0) != 0;
+ } catch (Settings.SettingNotFoundException e) {
+ return;
+ }
+ if (mDisableSecureWindows == disableSecureWindows) {
+ return;
+ }
+
+ synchronized (mGlobalLock) {
+ mDisableSecureWindows = disableSecureWindows;
+ mRoot.refreshSecureSurfaceState();
+ }
+ }
}
PowerManager mPowerManager;
@@ -1115,6 +1148,8 @@
private final ScreenRecordingCallbackController mScreenRecordingCallbackController;
+ private volatile boolean mDisableSecureWindows = false;
+
public static WindowManagerService main(final Context context, final InputManagerService im,
final boolean showBootMsgs, WindowManagerPolicy policy,
ActivityTaskManagerService atm) {
@@ -2213,7 +2248,7 @@
int lastSyncSeqId, ClientWindowFrames outFrames,
MergedConfiguration outMergedConfiguration, SurfaceControl outSurfaceControl,
InsetsState outInsetsState, InsetsSourceControl.Array outActiveControls,
- Bundle outSyncIdBundle) {
+ Bundle outBundle) {
if (outActiveControls != null) {
outActiveControls.set(null);
}
@@ -2544,6 +2579,13 @@
if (outFrames != null && outMergedConfiguration != null) {
win.fillClientWindowFramesAndConfiguration(outFrames, outMergedConfiguration,
false /* useLatestConfig */, shouldRelayout);
+ if (Flags.activityWindowInfoFlag() && outBundle != null
+ && win.mActivityRecord != null) {
+ final ActivityWindowInfo activityWindowInfo = win.mActivityRecord
+ .getActivityWindowInfo();
+ outBundle.putParcelable(IWindowSession.KEY_RELAYOUT_BUNDLE_ACTIVITY_WINDOW_INFO,
+ activityWindowInfo);
+ }
// Set resize-handled here because the values are sent back to the client.
win.onResizeHandled();
@@ -2573,7 +2615,7 @@
win.isVisible() /* visible */, false /* removed */);
}
- if (outSyncIdBundle != null) {
+ if (outBundle != null) {
final int maybeSyncSeqId;
if (win.syncNextBuffer() && viewVisibility == View.VISIBLE
&& win.mSyncSeqId > lastSyncSeqId) {
@@ -2582,7 +2624,7 @@
} else {
maybeSyncSeqId = -1;
}
- outSyncIdBundle.putInt("seqid", maybeSyncSeqId);
+ outBundle.putInt(IWindowSession.KEY_RELAYOUT_BUNDLE_SEQID, maybeSyncSeqId);
}
if (configChanged) {
@@ -6897,6 +6939,7 @@
pw.print(mLastFinishedFreezeSource);
}
pw.println();
+ pw.print(" mDisableSecureWindows="); pw.println(mDisableSecureWindows);
mInputManagerCallback.dump(pw, " ");
mSnapshotController.dump(pw, " ");
@@ -8700,14 +8743,14 @@
}
@Override
- public boolean moveFocusToTopEmbeddedWindowIfNeeded() {
+ public boolean moveFocusToAdjacentEmbeddedActivityIfNeeded() {
synchronized (mGlobalLock) {
final WindowState focusedWindow = getFocusedWindow();
if (focusedWindow == null) {
return false;
}
- if (moveFocusToTopEmbeddedWindow(focusedWindow)) {
+ if (moveFocusToAdjacentEmbeddedWindow(focusedWindow)) {
// Sync the input transactions to ensure the input focus updates as well.
syncInputTransactions(false);
return true;
@@ -9219,9 +9262,10 @@
}
/**
- * Move focus to the top embedded window if possible.
+ * Move focus to the adjacent embedded activity if the adjacent activity is more recently
+ * created or has a window more recently added.
*/
- boolean moveFocusToTopEmbeddedWindow(@NonNull WindowState focusedWindow) {
+ boolean moveFocusToAdjacentEmbeddedWindow(@NonNull WindowState focusedWindow) {
final TaskFragment taskFragment = focusedWindow.getTaskFragment();
if (taskFragment == null) {
// Skip if not an Activity window.
@@ -9233,31 +9277,25 @@
return false;
}
- if (taskFragment.mDimmerSurfaceBoosted) {
- // Skip if the TaskFragment currently has dimmer surface boosted.
+ if (!focusedWindow.mActivityRecord.isEmbedded()) {
+ // Skip if the focused activity is not embedded
return false;
}
- final ActivityRecord topActivity =
- taskFragment.getTask().topRunningActivity(true /* focusableOnly */);
- if (topActivity == null || topActivity == focusedWindow.mActivityRecord) {
- // Skip if the focused activity is already the top-most activity on the Task.
+ final TaskFragment adjacentTaskFragment = taskFragment.getAdjacentTaskFragment();
+ final ActivityRecord adjacentTopActivity =
+ adjacentTaskFragment != null ? adjacentTaskFragment.topRunningActivity() : null;
+ if (adjacentTopActivity == null) {
return false;
}
- if (!topActivity.isEmbedded()) {
- // Skip if the top activity is not embedded
+ if (adjacentTopActivity.getLastWindowCreateTime()
+ < focusedWindow.mActivityRecord.getLastWindowCreateTime()) {
+ // Skip if the current focus activity has more recently active window.
return false;
}
- final TaskFragment topTaskFragment = topActivity.getTaskFragment();
- if (topTaskFragment.isIsolatedNav()
- && taskFragment.getAdjacentTaskFragment() == topTaskFragment) {
- // Skip if the top TaskFragment is adjacent to current focus and is set to isolated nav.
- return false;
- }
-
- moveFocusToActivity(topActivity);
+ moveFocusToActivity(adjacentTopActivity);
return !focusedWindow.isFocused();
}
@@ -10073,4 +10111,8 @@
mDragDropController.setGlobalDragListener(listener);
}
}
+
+ boolean getDisableSecureWindows() {
+ return mDisableSecureWindows;
+ }
}
diff --git a/services/core/java/com/android/server/wm/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java
index d967cde..14ec41f 100644
--- a/services/core/java/com/android/server/wm/WindowOrganizerController.java
+++ b/services/core/java/com/android/server/wm/WindowOrganizerController.java
@@ -23,7 +23,7 @@
import static android.view.Display.DEFAULT_DISPLAY;
import static android.window.TaskFragmentOperation.OP_TYPE_CLEAR_ADJACENT_TASK_FRAGMENTS;
import static android.window.TaskFragmentOperation.OP_TYPE_CREATE_TASK_FRAGMENT;
-import static android.window.TaskFragmentOperation.OP_TYPE_CREATE_TASK_FRAGMENT_DECOR_SURFACE;
+import static android.window.TaskFragmentOperation.OP_TYPE_CREATE_OR_MOVE_TASK_FRAGMENT_DECOR_SURFACE;
import static android.window.TaskFragmentOperation.OP_TYPE_DELETE_TASK_FRAGMENT;
import static android.window.TaskFragmentOperation.OP_TYPE_REMOVE_TASK_FRAGMENT_DECOR_SURFACE;
import static android.window.TaskFragmentOperation.OP_TYPE_REORDER_TO_BOTTOM_OF_TASK;
@@ -1558,7 +1558,7 @@
}
break;
}
- case OP_TYPE_CREATE_TASK_FRAGMENT_DECOR_SURFACE: {
+ case OP_TYPE_CREATE_OR_MOVE_TASK_FRAGMENT_DECOR_SURFACE: {
taskFragment.getTask().moveOrCreateDecorSurfaceFor(taskFragment);
break;
}
diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java
index 2b337ae..37b2d0e 100644
--- a/services/core/java/com/android/server/wm/WindowState.java
+++ b/services/core/java/com/android/server/wm/WindowState.java
@@ -240,6 +240,7 @@
import android.view.animation.AnimationUtils;
import android.view.animation.Interpolator;
import android.view.inputmethod.ImeTracker;
+import android.window.ActivityWindowInfo;
import android.window.ClientWindowFrames;
import android.window.OnBackInvokedCallbackInfo;
@@ -364,6 +365,7 @@
private boolean mDragResizing;
private boolean mDragResizingChangeReported = true;
private boolean mRedrawForSyncReported = true;
+ private long mCreateTime = System.currentTimeMillis();
/**
* Used to assosciate a given set of state changes sent from MSG_RESIZED
@@ -1714,6 +1716,10 @@
: DEFAULT_DISPATCHING_TIMEOUT_MILLIS;
}
+ long getCreateTime() {
+ return mCreateTime;
+ }
+
/**
* Returns true if, at any point, the application token associated with this window has actually
* displayed any windows. This is most useful with the "starting up" window to determine if any
@@ -1893,6 +1899,10 @@
}
boolean isSecureLocked() {
+ if (mWmService.getDisableSecureWindows()) {
+ return false;
+ }
+
if ((mAttrs.flags & WindowManager.LayoutParams.FLAG_SECURE) != 0) {
return true;
}
@@ -3687,19 +3697,32 @@
markRedrawForSyncReported();
+ // App window resize may trigger Activity#onConfigurationChanged, so we need to update
+ // ActivityWindowInfo as well.
+ final IBinder activityToken;
+ final ActivityWindowInfo activityWindowInfo;
+ if (Flags.activityWindowInfoFlag() && mActivityRecord != null) {
+ activityToken = mActivityRecord.token;
+ activityWindowInfo = mActivityRecord.getActivityWindowInfo();
+ } else {
+ activityToken = null;
+ activityWindowInfo = null;
+ }
+
if (Flags.bundleClientTransactionFlag()) {
getProcess().scheduleClientTransactionItem(
WindowStateResizeItem.obtain(mClient, mClientWindowFrames, reportDraw,
mLastReportedConfiguration, getCompatInsetsState(), forceRelayout,
alwaysConsumeSystemBars, displayId,
- syncWithBuffers ? mSyncSeqId : -1, isDragResizing));
+ syncWithBuffers ? mSyncSeqId : -1, isDragResizing,
+ activityToken, activityWindowInfo));
onResizePostDispatched(drawPending, prevRotation, displayId);
} else {
// TODO(b/301870955): cleanup after launch
try {
mClient.resized(mClientWindowFrames, reportDraw, mLastReportedConfiguration,
getCompatInsetsState(), forceRelayout, alwaysConsumeSystemBars, displayId,
- syncWithBuffers ? mSyncSeqId : -1, isDragResizing);
+ syncWithBuffers ? mSyncSeqId : -1, isDragResizing, activityWindowInfo);
onResizePostDispatched(drawPending, prevRotation, displayId);
} catch (RemoteException e) {
// Cancel orientation change of this window to avoid blocking unfreeze display.
diff --git a/services/core/jni/com_android_server_input_InputManagerService.cpp b/services/core/jni/com_android_server_input_InputManagerService.cpp
index 610fcb5..70224db 100644
--- a/services/core/jni/com_android_server_input_InputManagerService.cpp
+++ b/services/core/jni/com_android_server_input_InputManagerService.cpp
@@ -143,6 +143,7 @@
jmethodID getTouchCalibrationForInputDevice;
jmethodID notifyDropWindow;
jmethodID getParentSurfaceForPointers;
+ jmethodID getPackageUid;
} gServiceClassInfo;
static struct {
@@ -362,6 +363,7 @@
void notifyDropWindow(const sp<IBinder>& token, float x, float y) override;
void notifyDeviceInteraction(int32_t deviceId, nsecs_t timestamp,
const std::set<gui::Uid>& uids) override;
+ gui::Uid getPackageUid(std::string package) override;
/* --- PointerControllerPolicyInterface implementation --- */
@@ -1116,6 +1118,21 @@
mInputManager->getMetricsCollector().notifyDeviceInteraction(deviceId, timestamp, uids);
}
+gui::Uid NativeInputManager::getPackageUid(std::string package) {
+ ATRACE_CALL();
+ JNIEnv* env = jniEnv();
+ ScopedLocalFrame localFrame(env);
+
+ ScopedLocalRef<jstring> javaPackage(env, env->NewStringUTF(package.c_str()));
+ const jint uid =
+ env->CallIntMethod(mServiceObj, gServiceClassInfo.getPackageUid, javaPackage.get());
+ if (checkAndClearExceptionFromCallback(env, "getPackageUid")) {
+ LOG(FATAL) << __func__ << ": Failed to get UID for package: " << package;
+ }
+
+ return gui::Uid{static_cast<uint32_t>(uid)};
+}
+
void NativeInputManager::notifySensorEvent(int32_t deviceId, InputDeviceSensorType sensorType,
InputDeviceSensorAccuracy accuracy, nsecs_t timestamp,
const std::vector<float>& values) {
@@ -3101,6 +3118,8 @@
GET_METHOD_ID(gServiceClassInfo.getParentSurfaceForPointers, clazz,
"getParentSurfaceForPointers", "(I)J");
+ GET_METHOD_ID(gServiceClassInfo.getPackageUid, clazz, "getPackageUid", "(Ljava/lang/String;)I");
+
// InputDevice
FIND_CLASS(gInputDeviceClassInfo.clazz, "android/view/InputDevice");
diff --git a/services/core/xsd/display-device-config/display-device-config.xsd b/services/core/xsd/display-device-config/display-device-config.xsd
index d0df2b2..1f54518 100644
--- a/services/core/xsd/display-device-config/display-device-config.xsd
+++ b/services/core/xsd/display-device-config/display-device-config.xsd
@@ -162,6 +162,10 @@
<xs:element type="usiVersion" name="usiVersion">
<xs:annotation name="final"/>
</xs:element>
+ <xs:element type="lowBrightnessMode" name="lowBrightness">
+ <xs:attribute name="enabled" type="xs:boolean" use="optional"/>
+ <xs:annotation name="final"/>
+ </xs:element>
<!-- Maximum screen brightness setting when screen brightness capped in
Wear Bedtime mode. This must be a non-negative decimal within the range defined by
the first and the last brightness value in screenBrightnessMap. -->
@@ -172,6 +176,7 @@
<xs:element type="idleScreenRefreshRateTimeout" name="idleScreenRefreshRateTimeout" minOccurs="0">
<xs:annotation name="final"/>
</xs:element>
+
</xs:sequence>
</xs:complexType>
</xs:element>
@@ -216,6 +221,21 @@
</xs:restriction>
</xs:simpleType>
+ <xs:complexType name="lowBrightnessMode">
+ <xs:sequence>
+ <xs:element name="transitionPoint" type="nonNegativeDecimal" minOccurs="1"
+ maxOccurs="1">
+ </xs:element>
+ <xs:element name="nits" type="xs:float" maxOccurs="unbounded">
+ </xs:element>
+ <xs:element name="backlight" type="xs:float" maxOccurs="unbounded">
+ </xs:element>
+ <xs:element name="brightness" type="xs:float" maxOccurs="unbounded">
+ </xs:element>
+ </xs:sequence>
+ <xs:attribute name="enabled" type="xs:boolean" use="optional"/>
+ </xs:complexType>
+
<xs:complexType name="highBrightnessMode">
<xs:all>
<xs:element name="transitionPoint" type="nonNegativeDecimal" minOccurs="1"
diff --git a/services/core/xsd/display-device-config/schema/current.txt b/services/core/xsd/display-device-config/schema/current.txt
index 00dc908..c39c3d7 100644
--- a/services/core/xsd/display-device-config/schema/current.txt
+++ b/services/core/xsd/display-device-config/schema/current.txt
@@ -113,6 +113,7 @@
method public com.android.server.display.config.HighBrightnessMode getHighBrightnessMode();
method public final com.android.server.display.config.IdleScreenRefreshRateTimeout getIdleScreenRefreshRateTimeout();
method public final com.android.server.display.config.SensorDetails getLightSensor();
+ method public final com.android.server.display.config.LowBrightnessMode getLowBrightness();
method public com.android.server.display.config.LuxThrottling getLuxThrottling();
method @Nullable public final String getName();
method public com.android.server.display.config.PowerThrottlingConfig getPowerThrottlingConfig();
@@ -149,6 +150,7 @@
method public void setHighBrightnessMode(com.android.server.display.config.HighBrightnessMode);
method public final void setIdleScreenRefreshRateTimeout(com.android.server.display.config.IdleScreenRefreshRateTimeout);
method public final void setLightSensor(com.android.server.display.config.SensorDetails);
+ method public final void setLowBrightness(com.android.server.display.config.LowBrightnessMode);
method public void setLuxThrottling(com.android.server.display.config.LuxThrottling);
method public final void setName(@Nullable String);
method public void setPowerThrottlingConfig(com.android.server.display.config.PowerThrottlingConfig);
@@ -248,6 +250,17 @@
method public java.util.List<java.math.BigInteger> getItem();
}
+ public class LowBrightnessMode {
+ ctor public LowBrightnessMode();
+ method public java.util.List<java.lang.Float> getBacklight();
+ method public java.util.List<java.lang.Float> getBrightness();
+ method public boolean getEnabled();
+ method public java.util.List<java.lang.Float> getNits();
+ method public java.math.BigDecimal getTransitionPoint();
+ method public void setEnabled(boolean);
+ method public void setTransitionPoint(java.math.BigDecimal);
+ }
+
public class LuxThrottling {
ctor public LuxThrottling();
method @NonNull public final java.util.List<com.android.server.display.config.BrightnessLimitMap> getBrightnessLimitMap();
diff --git a/services/credentials/java/com/android/server/credentials/CreateRequestSession.java b/services/credentials/java/com/android/server/credentials/CreateRequestSession.java
index 173cb36..cac42b1 100644
--- a/services/credentials/java/com/android/server/credentials/CreateRequestSession.java
+++ b/services/credentials/java/com/android/server/credentials/CreateRequestSession.java
@@ -112,7 +112,8 @@
Manifest.permission.CREDENTIAL_MANAGER_SET_ALLOWED_PROVIDERS),
/*defaultProviderId=*/flattenedPrimaryProviders,
/*isShowAllOptionsRequested=*/ false),
- providerDataList);
+ providerDataList,
+ mRequestSessionMetric);
mClientCallback.onPendingIntent(mPendingIntent);
} catch (RemoteException e) {
mRequestSessionMetric.collectUiReturnedFinalPhase(/*uiReturned=*/ false);
diff --git a/services/credentials/java/com/android/server/credentials/CredentialManagerUi.java b/services/credentials/java/com/android/server/credentials/CredentialManagerUi.java
index f5e1e41..24f6697 100644
--- a/services/credentials/java/com/android/server/credentials/CredentialManagerUi.java
+++ b/services/credentials/java/com/android/server/credentials/CredentialManagerUi.java
@@ -25,6 +25,7 @@
import android.credentials.CredentialManager;
import android.credentials.CredentialProviderInfo;
import android.credentials.selection.DisabledProviderData;
+import android.credentials.selection.IntentCreationResult;
import android.credentials.selection.IntentFactory;
import android.credentials.selection.ProviderData;
import android.credentials.selection.RequestInfo;
@@ -37,6 +38,8 @@
import android.os.UserHandle;
import android.service.credentials.CredentialProviderInfoFactory;
+import com.android.server.credentials.metrics.RequestSessionMetric;
+
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
@@ -159,7 +162,8 @@
* @param providerDataList the list of provider data from remote providers
*/
public PendingIntent createPendingIntent(
- RequestInfo requestInfo, ArrayList<ProviderData> providerDataList) {
+ RequestInfo requestInfo, ArrayList<ProviderData> providerDataList,
+ RequestSessionMetric requestSessionMetric) {
List<CredentialProviderInfo> allProviders =
CredentialProviderInfoFactory.getCredentialProviderServices(
mContext,
@@ -174,10 +178,12 @@
.map(disabledProvider -> new DisabledProviderData(
disabledProvider.getComponentName().flattenToString())).toList();
- Intent intent;
- intent = IntentFactory.createCredentialSelectorIntent(
- mContext, requestInfo, providerDataList,
- new ArrayList<>(disabledProviderDataList), mResultReceiver);
+ IntentCreationResult intentCreationResult = IntentFactory
+ .createCredentialSelectorIntentForCredMan(mContext, requestInfo, providerDataList,
+ new ArrayList<>(disabledProviderDataList), mResultReceiver);
+ requestSessionMetric.collectUiConfigurationResults(
+ mContext, intentCreationResult, mUserId);
+ Intent intent = intentCreationResult.getIntent();
intent.setAction(UUID.randomUUID().toString());
//TODO: Create unique pending intent using request code and cancel any pre-existing pending
// intents
@@ -197,10 +203,15 @@
* of the pinned entry.
*
* @param requestInfo the information about the request
+ * @param requestSessionMetric the metric object for logging
*/
- public Intent createIntentForAutofill(RequestInfo requestInfo) {
- return IntentFactory.createCredentialSelectorIntentForAutofill(
- mContext, requestInfo, new ArrayList<>(),
- mResultReceiver);
+ public Intent createIntentForAutofill(RequestInfo requestInfo,
+ RequestSessionMetric requestSessionMetric) {
+ IntentCreationResult intentCreationResult = IntentFactory
+ .createCredentialSelectorIntentForAutofill(mContext, requestInfo, new ArrayList<>(),
+ mResultReceiver);
+ requestSessionMetric.collectUiConfigurationResults(
+ mContext, intentCreationResult, mUserId);
+ return intentCreationResult.getIntent();
}
}
diff --git a/services/credentials/java/com/android/server/credentials/GetCandidateRequestSession.java b/services/credentials/java/com/android/server/credentials/GetCandidateRequestSession.java
index eff53de..fd2a9a2 100644
--- a/services/credentials/java/com/android/server/credentials/GetCandidateRequestSession.java
+++ b/services/credentials/java/com/android/server/credentials/GetCandidateRequestSession.java
@@ -122,7 +122,8 @@
mRequestId, mClientRequest, mClientAppInfo.getPackageName(),
PermissionUtils.hasPermission(mContext, mClientAppInfo.getPackageName(),
Manifest.permission.CREDENTIAL_MANAGER_SET_ALLOWED_PROVIDERS),
- /*isShowAllOptionsRequested=*/ true));
+ /*isShowAllOptionsRequested=*/ true),
+ mRequestSessionMetric);
List<GetCredentialProviderData> candidateProviderDataList = new ArrayList<>();
for (ProviderData providerData : providerDataList) {
diff --git a/services/credentials/java/com/android/server/credentials/GetRequestSession.java b/services/credentials/java/com/android/server/credentials/GetRequestSession.java
index 6513ae1a..d55d8ef 100644
--- a/services/credentials/java/com/android/server/credentials/GetRequestSession.java
+++ b/services/credentials/java/com/android/server/credentials/GetRequestSession.java
@@ -111,7 +111,8 @@
Manifest.permission
.CREDENTIAL_MANAGER_SET_ALLOWED_PROVIDERS),
/*isShowAllOptionsRequested=*/ false),
- providerDataList);
+ providerDataList,
+ mRequestSessionMetric);
mClientCallback.onPendingIntent(mPendingIntent);
} catch (RemoteException e) {
mRequestSessionMetric.collectUiReturnedFinalPhase(/*uiReturned=*/ false);
diff --git a/services/credentials/java/com/android/server/credentials/MetricUtilities.java b/services/credentials/java/com/android/server/credentials/MetricUtilities.java
index bdea4f9..16bf1778 100644
--- a/services/credentials/java/com/android/server/credentials/MetricUtilities.java
+++ b/services/credentials/java/com/android/server/credentials/MetricUtilities.java
@@ -16,6 +16,7 @@
package com.android.server.credentials;
+import android.annotation.UserIdInt;
import android.content.ComponentName;
import android.content.Context;
import android.content.pm.PackageManager;
@@ -68,17 +69,27 @@
*
* @return the uid of a given package
*/
- protected static int getPackageUid(Context context, ComponentName componentName) {
- int sessUid = -1;
- try {
- // Only for T and above, which is fine for our use case
- sessUid = context.getPackageManager().getApplicationInfo(
- componentName.getPackageName(),
- PackageManager.ApplicationInfoFlags.of(0)).uid;
- } catch (Throwable t) {
- Slog.i(TAG, "Couldn't find required uid");
+ protected static int getPackageUid(Context context, ComponentName componentName,
+ @UserIdInt int userId) {
+ if (componentName == null) {
+ return -1;
}
- return sessUid;
+ return getPackageUid(context, componentName.getPackageName(), userId);
+ }
+
+ /** Returns the package uid, or -1 if not found. */
+ public static int getPackageUid(Context context, String packageName,
+ @UserIdInt int userId) {
+ if (packageName == null) {
+ return -1;
+ }
+ try {
+ return context.getPackageManager().getPackageUidAsUser(packageName,
+ PackageManager.PackageInfoFlags.of(0), userId);
+ } catch (Throwable t) {
+ Slog.i(TAG, "Couldn't find uid for " + packageName + ": " + t);
+ return -1;
+ }
}
/**
diff --git a/services/credentials/java/com/android/server/credentials/PrepareGetRequestSession.java b/services/credentials/java/com/android/server/credentials/PrepareGetRequestSession.java
index 6e8f7c8..e4b5c77 100644
--- a/services/credentials/java/com/android/server/credentials/PrepareGetRequestSession.java
+++ b/services/credentials/java/com/android/server/credentials/PrepareGetRequestSession.java
@@ -193,7 +193,8 @@
PermissionUtils.hasPermission(mContext, mClientAppInfo.getPackageName(),
Manifest.permission.CREDENTIAL_MANAGER_SET_ALLOWED_PROVIDERS),
/*isShowAllOptionsRequested=*/ false),
- providerDataList);
+ providerDataList,
+ mRequestSessionMetric);
} else {
return null;
}
diff --git a/services/credentials/java/com/android/server/credentials/ProviderSession.java b/services/credentials/java/com/android/server/credentials/ProviderSession.java
index c16e232..dfc08f0 100644
--- a/services/credentials/java/com/android/server/credentials/ProviderSession.java
+++ b/services/credentials/java/com/android/server/credentials/ProviderSession.java
@@ -153,7 +153,7 @@
mUserId = userId;
mComponentName = componentName;
mRemoteCredentialService = remoteCredentialService;
- mProviderSessionUid = MetricUtilities.getPackageUid(mContext, mComponentName);
+ mProviderSessionUid = MetricUtilities.getPackageUid(mContext, mComponentName, userId);
mProviderSessionMetric = new ProviderSessionMetric(
((RequestSession) mCallbacks).mRequestSessionMetric.getSessionIdTrackTwo());
}
diff --git a/services/credentials/java/com/android/server/credentials/metrics/OemUiUsageStatus.java b/services/credentials/java/com/android/server/credentials/metrics/OemUiUsageStatus.java
index 2fd3a86..80ce354 100644
--- a/services/credentials/java/com/android/server/credentials/metrics/OemUiUsageStatus.java
+++ b/services/credentials/java/com/android/server/credentials/metrics/OemUiUsageStatus.java
@@ -22,7 +22,12 @@
import static com.android.internal.util.FrameworkStatsLog.CREDENTIAL_MANAGER_FINAL_NO_UID_REPORTED__OEM_UI_USAGE_STATUS__OEM_UI_USAGE_STATUS_SPECIFIED_BUT_NOT_FOUND;
import static com.android.internal.util.FrameworkStatsLog.CREDENTIAL_MANAGER_FINAL_NO_UID_REPORTED__OEM_UI_USAGE_STATUS__OEM_UI_USAGE_STATUS_SPECIFIED_BUT_NOT_ENABLED;
+import android.credentials.selection.IntentCreationResult;
+/**
+ * Result of attempting to use the config_oemCredentialManagerDialogComponent as the Credential
+ * Manager UI.
+ */
public enum OemUiUsageStatus {
UNKNOWN(CREDENTIAL_MANAGER_FINAL_NO_UID_REPORTED__OEM_UI_USAGE_STATUS__OEM_UI_USAGE_STATUS_UNKNOWN),
SUCCESS(CREDENTIAL_MANAGER_FINAL_NO_UID_REPORTED__OEM_UI_USAGE_STATUS__OEM_UI_USAGE_STATUS_SUCCESS),
@@ -39,4 +44,21 @@
public int getLoggingInt() {
return mLoggingInt;
}
+
+ /** Factory method. */
+ public static OemUiUsageStatus createFrom(IntentCreationResult.OemUiUsageStatus from) {
+ switch (from) {
+ case UNKNOWN:
+ return OemUiUsageStatus.UNKNOWN;
+ case SUCCESS:
+ return OemUiUsageStatus.SUCCESS;
+ case OEM_UI_CONFIG_NOT_SPECIFIED:
+ return OemUiUsageStatus.FAILURE_NOT_SPECIFIED;
+ case OEM_UI_CONFIG_SPECIFIED_BUT_NOT_FOUND:
+ return OemUiUsageStatus.FAILURE_SPECIFIED_BUT_NOT_FOUND;
+ case OEM_UI_CONFIG_SPECIFIED_FOUND_BUT_NOT_ENABLED:
+ return OemUiUsageStatus.FAILURE_SPECIFIED_BUT_NOT_ENABLED;
+ }
+ return OemUiUsageStatus.UNKNOWN;
+ }
}
diff --git a/services/credentials/java/com/android/server/credentials/metrics/RequestSessionMetric.java b/services/credentials/java/com/android/server/credentials/metrics/RequestSessionMetric.java
index a77bd3e..619a568 100644
--- a/services/credentials/java/com/android/server/credentials/metrics/RequestSessionMetric.java
+++ b/services/credentials/java/com/android/server/credentials/metrics/RequestSessionMetric.java
@@ -30,9 +30,12 @@
import static com.android.server.credentials.metrics.ApiName.GET_CREDENTIAL_VIA_REGISTRY;
import android.annotation.NonNull;
+import android.annotation.UserIdInt;
import android.content.ComponentName;
+import android.content.Context;
import android.credentials.CreateCredentialRequest;
import android.credentials.GetCredentialRequest;
+import android.credentials.selection.IntentCreationResult;
import android.credentials.selection.UserSelectionDialogResult;
import android.util.Slog;
@@ -270,6 +273,21 @@
}
}
+ /** Log results of the device Credential Manager UI configuration. */
+ public void collectUiConfigurationResults(Context context, IntentCreationResult result,
+ @UserIdInt int userId) {
+ try {
+ mChosenProviderFinalPhaseMetric.setOemUiUid(MetricUtilities.getPackageUid(
+ context, result.getOemUiPackageName(), userId));
+ mChosenProviderFinalPhaseMetric.setFallbackUiUid(MetricUtilities.getPackageUid(
+ context, result.getFallbackUiPackageName(), userId));
+ mChosenProviderFinalPhaseMetric.setOemUiUsageStatus(
+ OemUiUsageStatus.createFrom(result.getOemUiUsageStatus()));
+ } catch (Exception e) {
+ Slog.w(TAG, "Unexpected error during ui configuration result collection: " + e);
+ }
+ }
+
/**
* Allows encapsulating the overall final phase metric status from the chosen and final
* provider.
diff --git a/services/devicepolicy/Android.bp b/services/devicepolicy/Android.bp
index 8dfa685..da965bb 100644
--- a/services/devicepolicy/Android.bp
+++ b/services/devicepolicy/Android.bp
@@ -24,5 +24,6 @@
"app-compat-annotations",
"service-permission.stubs.system_server",
"device_policy_aconfig_flags_lib",
+ "androidx.annotation_annotation",
],
}
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/OverlayPackagesProvider.java b/services/devicepolicy/java/com/android/server/devicepolicy/OverlayPackagesProvider.java
index f3b164c..94c1374 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/OverlayPackagesProvider.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/OverlayPackagesProvider.java
@@ -25,15 +25,16 @@
import static android.content.pm.PackageManager.GET_META_DATA;
import static com.android.internal.util.Preconditions.checkArgument;
-import static com.android.internal.util.Preconditions.checkNotNull;
-import static com.android.server.devicepolicy.DevicePolicyManagerService.dumpResources;
+import static com.android.server.devicepolicy.DevicePolicyManagerService.dumpApps;
import static java.util.Objects.requireNonNull;
+import android.annotation.ArrayRes;
import android.annotation.NonNull;
import android.annotation.UserIdInt;
import android.app.admin.DeviceAdminReceiver;
import android.app.admin.DevicePolicyManager;
+import android.app.admin.flags.Flags;
import android.app.role.RoleManager;
import android.content.ComponentName;
import android.content.Context;
@@ -67,13 +68,16 @@
protected static final String TAG = "OverlayPackagesProvider";
private static final Map<String, String> sActionToMetadataKeyMap = new HashMap<>();
- {
+
+ static {
sActionToMetadataKeyMap.put(ACTION_PROVISION_MANAGED_USER, REQUIRED_APP_MANAGED_USER);
sActionToMetadataKeyMap.put(ACTION_PROVISION_MANAGED_PROFILE, REQUIRED_APP_MANAGED_PROFILE);
sActionToMetadataKeyMap.put(ACTION_PROVISION_MANAGED_DEVICE, REQUIRED_APP_MANAGED_DEVICE);
}
+
private static final Set<String> sAllowedActions = new HashSet<>();
- {
+
+ static {
sAllowedActions.add(ACTION_PROVISION_MANAGED_USER);
sAllowedActions.add(ACTION_PROVISION_MANAGED_PROFILE);
sAllowedActions.add(ACTION_PROVISION_MANAGED_DEVICE);
@@ -83,8 +87,13 @@
private final Context mContext;
private final Injector mInjector;
+ private final RecursiveStringArrayResourceResolver mRecursiveStringArrayResourceResolver;
+
public OverlayPackagesProvider(Context context) {
- this(context, new DefaultInjector());
+ this(
+ context,
+ new DefaultInjector(),
+ new RecursiveStringArrayResourceResolver(context.getResources()));
}
@VisibleForTesting
@@ -113,8 +122,8 @@
public String getDevicePolicyManagementRoleHolderPackageName(Context context) {
return Binder.withCleanCallingIdentity(() -> {
RoleManager roleManager = context.getSystemService(RoleManager.class);
- List<String> roleHolders =
- roleManager.getRoleHolders(RoleManager.ROLE_DEVICE_POLICY_MANAGEMENT);
+ List<String> roleHolders = roleManager.getRoleHolders(
+ RoleManager.ROLE_DEVICE_POLICY_MANAGEMENT);
if (roleHolders.isEmpty()) {
return null;
}
@@ -124,17 +133,20 @@
}
@VisibleForTesting
- OverlayPackagesProvider(Context context, Injector injector) {
+ OverlayPackagesProvider(Context context, Injector injector,
+ RecursiveStringArrayResourceResolver recursiveStringArrayResourceResolver) {
mContext = context;
- mPm = checkNotNull(context.getPackageManager());
- mInjector = checkNotNull(injector);
+ mPm = requireNonNull(context.getPackageManager());
+ mInjector = requireNonNull(injector);
+ mRecursiveStringArrayResourceResolver = requireNonNull(
+ recursiveStringArrayResourceResolver);
}
/**
* Computes non-required apps. All the system apps with a launcher that are not in
* the required set of packages, and all mainline modules that are not declared as required
* via metadata in their manifests, will be considered as non-required apps.
- *
+ * <p>
* Note: If an app is mistakenly listed as both required and disallowed, it will be treated as
* disallowed.
*
@@ -176,12 +188,12 @@
/**
* Returns a subset of {@code packageNames} whose packages are mainline modules declared as
* required apps via their app metadata.
+ *
* @see DevicePolicyManager#REQUIRED_APP_MANAGED_USER
* @see DevicePolicyManager#REQUIRED_APP_MANAGED_DEVICE
* @see DevicePolicyManager#REQUIRED_APP_MANAGED_PROFILE
*/
- private Set<String> getRequiredAppsMainlineModules(
- Set<String> packageNames,
+ private Set<String> getRequiredAppsMainlineModules(Set<String> packageNames,
String provisioningAction) {
final Set<String> result = new HashSet<>();
for (String packageName : packageNames) {
@@ -225,8 +237,8 @@
}
private boolean isApkInApexMainlineModule(String packageName) {
- final String apexPackageName =
- mInjector.getActiveApexPackageNameContainingPackage(packageName);
+ final String apexPackageName = mInjector.getActiveApexPackageNameContainingPackage(
+ packageName);
return apexPackageName != null;
}
@@ -274,112 +286,94 @@
}
private Set<String> getRequiredAppsSet(String provisioningAction) {
- final int resId;
- switch (provisioningAction) {
- case ACTION_PROVISION_MANAGED_USER:
- resId = R.array.required_apps_managed_user;
- break;
- case ACTION_PROVISION_MANAGED_PROFILE:
- resId = R.array.required_apps_managed_profile;
- break;
- case ACTION_PROVISION_MANAGED_DEVICE:
- resId = R.array.required_apps_managed_device;
- break;
- default:
- throw new IllegalArgumentException("Provisioning type "
- + provisioningAction + " not supported.");
- }
- return new ArraySet<>(Arrays.asList(mContext.getResources().getStringArray(resId)));
+ final int resId = switch (provisioningAction) {
+ case ACTION_PROVISION_MANAGED_USER -> R.array.required_apps_managed_user;
+ case ACTION_PROVISION_MANAGED_PROFILE -> R.array.required_apps_managed_profile;
+ case ACTION_PROVISION_MANAGED_DEVICE -> R.array.required_apps_managed_device;
+ default -> throw new IllegalArgumentException(
+ "Provisioning type " + provisioningAction + " not supported.");
+ };
+ return resolveStringArray(resId);
}
private Set<String> getDisallowedAppsSet(String provisioningAction) {
- final int resId;
- switch (provisioningAction) {
- case ACTION_PROVISION_MANAGED_USER:
- resId = R.array.disallowed_apps_managed_user;
- break;
- case ACTION_PROVISION_MANAGED_PROFILE:
- resId = R.array.disallowed_apps_managed_profile;
- break;
- case ACTION_PROVISION_MANAGED_DEVICE:
- resId = R.array.disallowed_apps_managed_device;
- break;
- default:
- throw new IllegalArgumentException("Provisioning type "
- + provisioningAction + " not supported.");
- }
- return new ArraySet<>(Arrays.asList(mContext.getResources().getStringArray(resId)));
+ final int resId = switch (provisioningAction) {
+ case ACTION_PROVISION_MANAGED_USER -> R.array.disallowed_apps_managed_user;
+ case ACTION_PROVISION_MANAGED_PROFILE -> R.array.disallowed_apps_managed_profile;
+ case ACTION_PROVISION_MANAGED_DEVICE -> R.array.disallowed_apps_managed_device;
+ default -> throw new IllegalArgumentException(
+ "Provisioning type " + provisioningAction + " not supported.");
+ };
+ return resolveStringArray(resId);
}
private Set<String> getVendorRequiredAppsSet(String provisioningAction) {
- final int resId;
- switch (provisioningAction) {
- case ACTION_PROVISION_MANAGED_USER:
- resId = R.array.vendor_required_apps_managed_user;
- break;
- case ACTION_PROVISION_MANAGED_PROFILE:
- resId = R.array.vendor_required_apps_managed_profile;
- break;
- case ACTION_PROVISION_MANAGED_DEVICE:
- resId = R.array.vendor_required_apps_managed_device;
- break;
- default:
- throw new IllegalArgumentException("Provisioning type "
- + provisioningAction + " not supported.");
- }
- return new ArraySet<>(Arrays.asList(mContext.getResources().getStringArray(resId)));
+ final int resId = switch (provisioningAction) {
+ case ACTION_PROVISION_MANAGED_USER -> R.array.vendor_required_apps_managed_user;
+ case ACTION_PROVISION_MANAGED_PROFILE -> R.array.vendor_required_apps_managed_profile;
+ case ACTION_PROVISION_MANAGED_DEVICE -> R.array.vendor_required_apps_managed_device;
+ default -> throw new IllegalArgumentException(
+ "Provisioning type " + provisioningAction + " not supported.");
+ };
+ return resolveStringArray(resId);
}
private Set<String> getVendorDisallowedAppsSet(String provisioningAction) {
- final int resId;
- switch (provisioningAction) {
- case ACTION_PROVISION_MANAGED_USER:
- resId = R.array.vendor_disallowed_apps_managed_user;
- break;
- case ACTION_PROVISION_MANAGED_PROFILE:
- resId = R.array.vendor_disallowed_apps_managed_profile;
- break;
- case ACTION_PROVISION_MANAGED_DEVICE:
- resId = R.array.vendor_disallowed_apps_managed_device;
- break;
- default:
- throw new IllegalArgumentException("Provisioning type "
- + provisioningAction + " not supported.");
+ final int resId = switch (provisioningAction) {
+ case ACTION_PROVISION_MANAGED_USER -> R.array.vendor_disallowed_apps_managed_user;
+ case ACTION_PROVISION_MANAGED_PROFILE -> R.array.vendor_disallowed_apps_managed_profile;
+ case ACTION_PROVISION_MANAGED_DEVICE -> R.array.vendor_disallowed_apps_managed_device;
+ default -> throw new IllegalArgumentException(
+ "Provisioning type " + provisioningAction + " not supported.");
+ };
+ return resolveStringArray(resId);
+ }
+
+ private Set<String> resolveStringArray(@ArrayRes int resId) {
+ if (Flags.isRecursiveRequiredAppMergingEnabled()) {
+ return mRecursiveStringArrayResourceResolver.resolve(mContext.getPackageName(), resId);
+ } else {
+ return new ArraySet<>(Arrays.asList(mContext.getResources().getStringArray(resId)));
}
- return new ArraySet<>(Arrays.asList(mContext.getResources().getStringArray(resId)));
}
void dump(IndentingPrintWriter pw) {
pw.println("OverlayPackagesProvider");
pw.increaseIndent();
- dumpResources(pw, mContext, "required_apps_managed_device",
- R.array.required_apps_managed_device);
- dumpResources(pw, mContext, "required_apps_managed_user",
- R.array.required_apps_managed_user);
- dumpResources(pw, mContext, "required_apps_managed_profile",
- R.array.required_apps_managed_profile);
+ dumpApps(pw, "required_apps_managed_device",
+ resolveStringArray(R.array.required_apps_managed_device).toArray(String[]::new));
+ dumpApps(pw, "required_apps_managed_user",
+ resolveStringArray(R.array.required_apps_managed_user).toArray(String[]::new));
+ dumpApps(pw, "required_apps_managed_profile",
+ resolveStringArray(R.array.required_apps_managed_profile).toArray(String[]::new));
- dumpResources(pw, mContext, "disallowed_apps_managed_device",
- R.array.disallowed_apps_managed_device);
- dumpResources(pw, mContext, "disallowed_apps_managed_user",
- R.array.disallowed_apps_managed_user);
- dumpResources(pw, mContext, "disallowed_apps_managed_device",
- R.array.disallowed_apps_managed_device);
+ dumpApps(pw, "disallowed_apps_managed_device",
+ resolveStringArray(R.array.disallowed_apps_managed_device).toArray(String[]::new));
+ dumpApps(pw, "disallowed_apps_managed_user",
+ resolveStringArray(R.array.disallowed_apps_managed_user).toArray(String[]::new));
+ dumpApps(pw, "disallowed_apps_managed_device",
+ resolveStringArray(R.array.disallowed_apps_managed_device).toArray(String[]::new));
- dumpResources(pw, mContext, "vendor_required_apps_managed_device",
- R.array.vendor_required_apps_managed_device);
- dumpResources(pw, mContext, "vendor_required_apps_managed_user",
- R.array.vendor_required_apps_managed_user);
- dumpResources(pw, mContext, "vendor_required_apps_managed_profile",
- R.array.vendor_required_apps_managed_profile);
+ dumpApps(pw, "vendor_required_apps_managed_device",
+ resolveStringArray(R.array.vendor_required_apps_managed_device).toArray(
+ String[]::new));
+ dumpApps(pw, "vendor_required_apps_managed_user",
+ resolveStringArray(R.array.vendor_required_apps_managed_user).toArray(
+ String[]::new));
+ dumpApps(pw, "vendor_required_apps_managed_profile",
+ resolveStringArray(R.array.vendor_required_apps_managed_profile).toArray(
+ String[]::new));
- dumpResources(pw, mContext, "vendor_disallowed_apps_managed_user",
- R.array.vendor_disallowed_apps_managed_user);
- dumpResources(pw, mContext, "vendor_disallowed_apps_managed_device",
- R.array.vendor_disallowed_apps_managed_device);
- dumpResources(pw, mContext, "vendor_disallowed_apps_managed_profile",
- R.array.vendor_disallowed_apps_managed_profile);
+ dumpApps(pw, "vendor_disallowed_apps_managed_user",
+ resolveStringArray(R.array.vendor_disallowed_apps_managed_user).toArray(
+ String[]::new));
+ dumpApps(pw, "vendor_disallowed_apps_managed_device",
+ resolveStringArray(R.array.vendor_disallowed_apps_managed_device).toArray(
+ String[]::new));
+ dumpApps(pw, "vendor_disallowed_apps_managed_profile",
+ resolveStringArray(R.array.vendor_disallowed_apps_managed_profile).toArray(
+ String[]::new));
pw.decreaseIndent();
}
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/RecursiveStringArrayResourceResolver.java b/services/devicepolicy/java/com/android/server/devicepolicy/RecursiveStringArrayResourceResolver.java
new file mode 100644
index 0000000..935e051
--- /dev/null
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/RecursiveStringArrayResourceResolver.java
@@ -0,0 +1,147 @@
+/*
+ * 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.server.devicepolicy;
+
+import android.annotation.SuppressLint;
+import android.content.res.Resources;
+
+import androidx.annotation.ArrayRes;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * A class encapsulating all the logic for recursive string-array resource resolution.
+ */
+public class RecursiveStringArrayResourceResolver {
+ private static final String IMPORT_PREFIX = "#import:";
+ private static final String SEPARATOR = "/";
+ private static final String PWP = ".";
+
+ private final Resources mResources;
+
+ /**
+ * @param resources Android resource access object to use when resolving resources
+ */
+ public RecursiveStringArrayResourceResolver(Resources resources) {
+ this.mResources = resources;
+ }
+
+ /**
+ * Resolves a given {@code <string-array/>} resource specified via
+ * {@param rootId} in {@param pkg}. During resolution all values prefixed with
+ * {@link #IMPORT_PREFIX} are expanded and injected
+ * into the final list at the position of the import statement,
+ * pushing all the following values (and their expansions) down.
+ * Circular imports are tracked and skipped to avoid infinite resolution loops without losing
+ * data.
+ *
+ * <p>
+ * The import statements are expected in a form of
+ * "{@link #IMPORT_PREFIX}{package}{@link #SEPARATOR}{resourceName}"
+ * If the resource being imported is from the same package, its package can be specified as a
+ * {@link #PWP} shorthand `.`
+ * > e.g.:
+ * > {@code "#import:com.android.internal/disallowed_apps_managed_user"}
+ * > {@code "#import:./disallowed_apps_managed_user"}
+ *
+ * <p>
+ * Any incorrect or unresolvable import statement
+ * will cause the entire resolution to fail with an error.
+ *
+ * @param pkg the package owning the resource
+ * @param rootId the id of the {@code <string-array>} resource within {@param pkg} to start the
+ * resolution from
+ * @return a flattened list of all the resolved string array values from the root resource
+ * as well as all the imported arrays
+ */
+ public Set<String> resolve(String pkg, @ArrayRes int rootId) {
+ return resolve(List.of(), pkg, rootId);
+ }
+
+ /**
+ * A version of resolve that tracks already imported resources
+ * to avoid circular imports and wasted work.
+ *
+ * @param cache a list of already resolved packages to be skipped for further resolution
+ */
+ private Set<String> resolve(Collection<String> cache, String pkg, @ArrayRes int rootId) {
+ final var strings = mResources.getStringArray(rootId);
+ final var runningCache = new ArrayList<>(cache);
+
+ final var result = new HashSet<String>();
+ for (var string : strings) {
+ final String ref;
+ if (string.startsWith(IMPORT_PREFIX)) {
+ ref = string.substring(IMPORT_PREFIX.length());
+ } else {
+ ref = null;
+ }
+
+ if (ref == null) {
+ result.add(string);
+ } else if (!runningCache.contains(ref)) {
+ final var next = resolveImport(runningCache, pkg, ref);
+ runningCache.addAll(next);
+ result.addAll(next);
+ }
+ }
+ return result;
+ }
+
+ /**
+ * Resolves an import of the {@code <string-array>} resource
+ * in the context of {@param importingPackage} by the provided {@param ref}.
+ *
+ * @param cache a list of already resolved packages to be passed along into chained
+ * {@link #resolve} calls
+ * @param importingPackage the package that owns the resource which defined the import being
+ * processed.
+ * It is also used to expand all {@link #PWP} shorthands in
+ * {@param ref}
+ * @param ref reference to the resource to be imported in a form of
+ * "{package}{@link #SEPARATOR}{resourceName}".
+ * e.g.: {@code com.android.internal/disallowed_apps_managed_user}
+ */
+ private Set<String> resolveImport(
+ Collection<String> cache,
+ String importingPackage,
+ String ref) {
+ final var chunks = ref.split(SEPARATOR, 2);
+ final var pkg = chunks[0];
+ final var name = chunks[1];
+ final String resolvedPkg;
+ if (Objects.equals(pkg, PWP)) {
+ resolvedPkg = importingPackage;
+ } else {
+ resolvedPkg = pkg;
+ }
+ @SuppressLint("DiscouragedApi") final var importId = mResources.getIdentifier(
+ /* name = */ name,
+ /* defType = */ "array",
+ /* defPackage = */ resolvedPkg);
+ if (importId == 0) {
+ throw new Resources.NotFoundException(
+ /* name= */ String.format("%s:array/%s", resolvedPkg, name));
+ }
+ return resolve(cache, resolvedPkg, importId);
+ }
+}
diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java
index 3b2a3dd..e202bbf 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -1230,10 +1230,6 @@
mSystemServiceManager.startService(ThermalManagerService.class);
t.traceEnd();
- t.traceBegin("StartHintManager");
- mSystemServiceManager.startService(HintManagerService.class);
- t.traceEnd();
-
// Now that the power manager has been started, let the activity manager
// initialize power management features.
t.traceBegin("InitPowerManagement");
@@ -1614,6 +1610,10 @@
t.traceEnd();
}
+ t.traceBegin("StartHintManager");
+ mSystemServiceManager.startService(HintManagerService.class);
+ t.traceEnd();
+
// Grants default permissions and defines roles
t.traceBegin("StartRoleManagerService");
LocalManagerRegistry.addManager(RoleServicePlatformHelper.class,
diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceTestBase.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceTestBase.java
index b9c5b36..b4cf799 100644
--- a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceTestBase.java
+++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceTestBase.java
@@ -203,6 +203,7 @@
.thenReturn(new int[] {0});
when(mMockUserManagerInternal.getUserIds()).thenReturn(new int[] {0});
when(mMockActivityManagerInternal.isSystemReady()).thenReturn(true);
+ when(mMockActivityManagerInternal.getCurrentUserId()).thenReturn(mCallingUserId);
when(mMockPackageManagerInternal.getPackageUid(anyString(), anyLong(), anyInt()))
.thenReturn(Binder.getCallingUid());
when(mMockPackageManagerInternal.isSameApp(anyString(), anyLong(), anyInt(), anyInt()))
diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceWindowGainedFocusTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceWindowGainedFocusTest.java
index cea65b5..9f46d0ba 100644
--- a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceWindowGainedFocusTest.java
+++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceWindowGainedFocusTest.java
@@ -198,7 +198,9 @@
@Test
public void startInputOrWindowGainedFocus_userNotRunning() throws RemoteException {
- when(mMockUserManagerInternal.isUserRunning(anyInt())).thenReturn(false);
+ // Run blockingly on ServiceThread to avoid that interfering with our stubbing.
+ mServiceThread.getThreadHandler().runWithScissors(
+ () -> when(mMockUserManagerInternal.isUserRunning(anyInt())).thenReturn(false), 0);
assertThat(
startInputOrWindowGainedFocus(
diff --git a/services/tests/displayservicetests/src/com/android/server/display/AutomaticBrightnessControllerTest.java b/services/tests/displayservicetests/src/com/android/server/display/AutomaticBrightnessControllerTest.java
index b0f7bfa..54de64e 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/AutomaticBrightnessControllerTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/AutomaticBrightnessControllerTest.java
@@ -52,6 +52,7 @@
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
+import com.android.server.display.brightness.clamper.BrightnessClamperController;
import com.android.server.testutils.OffsettableClock;
import org.junit.After;
@@ -96,6 +97,8 @@
@Mock HysteresisLevels mScreenBrightnessThresholdsIdle;
@Mock Handler mNoOpHandler;
@Mock BrightnessRangeController mBrightnessRangeController;
+ @Mock
+ BrightnessClamperController mBrightnessClamperController;
@Mock BrightnessThrottler mBrightnessThrottler;
@Before
@@ -161,7 +164,8 @@
mAmbientBrightnessThresholdsIdle, mScreenBrightnessThresholdsIdle,
mContext, mBrightnessRangeController, mBrightnessThrottler,
useHorizon ? AMBIENT_LIGHT_HORIZON_SHORT : 1,
- useHorizon ? AMBIENT_LIGHT_HORIZON_LONG : 10000, userLux, userNits
+ useHorizon ? AMBIENT_LIGHT_HORIZON_LONG : 10000, userLux, userNits,
+ mBrightnessClamperController
);
when(mBrightnessRangeController.getCurrentBrightnessMax()).thenReturn(
diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayDeviceConfigTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayDeviceConfigTest.java
index 35b69f8..73a2f65 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/DisplayDeviceConfigTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayDeviceConfigTest.java
@@ -44,6 +44,7 @@
import android.hardware.display.DisplayManagerInternal;
import android.os.PowerManager;
import android.os.Temperature;
+import android.platform.test.annotations.RequiresFlagsEnabled;
import android.provider.Settings;
import android.util.SparseArray;
import android.util.Spline;
@@ -57,6 +58,7 @@
import com.android.server.display.config.IdleScreenRefreshRateTimeoutLuxThresholdPoint;
import com.android.server.display.config.ThermalStatus;
import com.android.server.display.feature.DisplayManagerFlags;
+import com.android.server.display.feature.flags.Flags;
import org.junit.Before;
import org.junit.Test;
@@ -380,7 +382,7 @@
public void testInvalidLuxThrottling() throws Exception {
setupDisplayDeviceConfigFromDisplayConfigFile(
getContent(getInvalidLuxThrottling(), getValidProxSensor(),
- /* includeIdleMode= */ true));
+ /* includeIdleMode= */ true, /* enableEvenDimmer */ false));
Map<DisplayDeviceConfig.BrightnessLimitMapType, Map<Float, Float>> luxThrottlingData =
mDisplayDeviceConfig.getLuxThrottlingData();
@@ -588,7 +590,7 @@
public void testProximitySensorWithEmptyValuesFromDisplayConfig() throws IOException {
setupDisplayDeviceConfigFromDisplayConfigFile(
getContent(getValidLuxThrottling(), getProxSensorWithEmptyValues(),
- /* includeIdleMode= */ true));
+ /* includeIdleMode= */ true, /* enableEvenDimmer */ false));
assertNull(mDisplayDeviceConfig.getProximitySensor());
}
@@ -596,7 +598,7 @@
public void testProximitySensorWithRefreshRatesFromDisplayConfig() throws IOException {
setupDisplayDeviceConfigFromDisplayConfigFile(
getContent(getValidLuxThrottling(), getValidProxSensorWithRefreshRateAndVsyncRate(),
- /* includeIdleMode= */ true));
+ /* includeIdleMode= */ true, /* enableEvenDimmer */ false));
assertEquals("test_proximity_sensor",
mDisplayDeviceConfig.getProximitySensor().type);
assertEquals("Test Proximity Sensor",
@@ -784,7 +786,7 @@
@Test
public void testBrightnessRamps_IdleFallsBackToConfigInteractive() throws IOException {
setupDisplayDeviceConfigFromDisplayConfigFile(getContent(getValidLuxThrottling(),
- getValidProxSensor(), /* includeIdleMode= */ false));
+ getValidProxSensor(), /* includeIdleMode= */ false, /* enableEvenDimmer */ false));
assertEquals(mDisplayDeviceConfig.getBrightnessRampDecreaseMaxMillis(), 3000);
assertEquals(mDisplayDeviceConfig.getBrightnessRampIncreaseMaxMillis(), 2000);
@@ -801,14 +803,14 @@
@Test
public void testBrightnessCapForWearBedtimeMode() throws IOException {
setupDisplayDeviceConfigFromDisplayConfigFile(getContent(getValidLuxThrottling(),
- getValidProxSensor(), /* includeIdleMode= */ false));
+ getValidProxSensor(), /* includeIdleMode= */ false, /* enableEvenDimmer */ false));
assertEquals(0.1f, mDisplayDeviceConfig.getBrightnessCapForWearBedtimeMode(), ZERO_DELTA);
}
@Test
public void testAutoBrightnessBrighteningLevels() throws IOException {
setupDisplayDeviceConfigFromDisplayConfigFile(getContent(getValidLuxThrottling(),
- getValidProxSensor(), /* includeIdleMode= */ false));
+ getValidProxSensor(), /* includeIdleMode= */ false, /* enableEvenDimmer */ false));
assertArrayEquals(new float[]{0.0f, 80},
mDisplayDeviceConfig.getAutoBrightnessBrighteningLevelsLux(
@@ -871,7 +873,7 @@
when(mFlags.areAutoBrightnessModesEnabled()).thenReturn(false);
setupDisplayDeviceConfigFromConfigResourceFile();
setupDisplayDeviceConfigFromDisplayConfigFile(getContent(getValidLuxThrottling(),
- getValidProxSensor(), /* includeIdleMode= */ false));
+ getValidProxSensor(), /* includeIdleMode= */ false, /* enableEvenDimmer */ false));
assertArrayEquals(new float[]{brightnessIntToFloat(50), brightnessIntToFloat(100),
brightnessIntToFloat(150)},
@@ -904,6 +906,18 @@
assertFalse(mDisplayDeviceConfig.isAutoBrightnessAvailable());
}
+ @RequiresFlagsEnabled(Flags.FLAG_EVEN_DIMMER)
+ @Test
+ public void testEvenDimmer() throws IOException {
+ when(mFlags.isEvenDimmerEnabled()).thenReturn(true);
+ setupDisplayDeviceConfigFromDisplayConfigFile(getContent(getValidLuxThrottling(),
+ getValidProxSensor(), /* includeIdleMode= */ false, /* enableEvenDimmer */ true));
+
+ assertTrue(mDisplayDeviceConfig.getLbmEnabled());
+ assertEquals(0.0001f, mDisplayDeviceConfig.getBacklightFromBrightness(0.1f), ZERO_DELTA);
+ assertEquals(0.2f, mDisplayDeviceConfig.getNitsFromBacklight(0.0f), ZERO_DELTA);
+ }
+
private String getValidLuxThrottling() {
return "<luxThrottling>\n"
+ " <brightnessLimitMap>\n"
@@ -1229,11 +1243,11 @@
private String getContent() {
return getContent(getValidLuxThrottling(), getValidProxSensor(),
- /* includeIdleMode= */ true);
+ /* includeIdleMode= */ true, false);
}
private String getContent(String brightnessCapConfig, String proxSensor,
- boolean includeIdleMode) {
+ boolean includeIdleMode, boolean enableEvenDimmer) {
return "<?xml version='1.0' encoding='utf-8' standalone='yes' ?>\n"
+ "<displayConfiguration>\n"
+ "<name>Example Display</name>\n"
@@ -1603,6 +1617,7 @@
+ "<majorVersion>2</majorVersion>\n"
+ "<minorVersion>0</minorVersion>\n"
+ "</usiVersion>\n"
+ + evenDimmerConfig(enableEvenDimmer)
+ "<screenBrightnessCapForWearBedtimeMode>"
+ "0.1"
+ "</screenBrightnessCapForWearBedtimeMode>"
@@ -1621,6 +1636,24 @@
+ "</displayConfiguration>\n";
}
+ private String evenDimmerConfig(boolean enabled) {
+ return (enabled ? "<lowBrightness enabled=\"true\">" : "<lowBrightness enabled=\"false\">")
+ + " <transitionPoint>0.1</transitionPoint>\n"
+ + " <nits>0.2</nits>\n"
+ + " <nits>2.0</nits>\n"
+ + " <nits>500.0</nits>\n"
+ + " <nits>1000.0</nits>\n"
+ + " <backlight>0</backlight>\n"
+ + " <backlight>0.0001</backlight>\n"
+ + " <backlight>0.5</backlight>\n"
+ + " <backlight>1.0</backlight>\n"
+ + " <brightness>0</brightness>\n"
+ + " <brightness>0.1</brightness>\n"
+ + " <brightness>0.5</brightness>\n"
+ + " <brightness>1.0</brightness>\n"
+ + "</lowBrightness>";
+ }
+
private void mockDeviceConfigs() {
when(mResources.getFloat(com.android.internal.R.dimen
.config_screenBrightnessSettingDefaultFloat)).thenReturn(0.5f);
diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java
index 01598ae..740ffc9 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java
@@ -1184,7 +1184,8 @@
/* ambientLightHorizonShort= */ anyInt(),
/* ambientLightHorizonLong= */ anyInt(),
eq(lux),
- eq(nits)
+ eq(nits),
+ any(BrightnessClamperController.class)
);
}
@@ -2121,7 +2122,8 @@
HysteresisLevels screenBrightnessThresholdsIdle, Context context,
BrightnessRangeController brightnessRangeController,
BrightnessThrottler brightnessThrottler, int ambientLightHorizonShort,
- int ambientLightHorizonLong, float userLux, float userNits) {
+ int ambientLightHorizonLong, float userLux, float userNits,
+ BrightnessClamperController brightnessClamperController) {
return mAutomaticBrightnessController;
}
diff --git a/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/BrightnessLowLuxModifierTest.kt b/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/BrightnessLowLuxModifierTest.kt
index ac7d1f5..e4a7d98 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/BrightnessLowLuxModifierTest.kt
+++ b/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/BrightnessLowLuxModifierTest.kt
@@ -65,7 +65,7 @@
Settings.Secure.putIntForUser(context.contentResolver,
Settings.Secure.EVEN_DIMMER_ACTIVATED, 1, userId)
Settings.Secure.putFloatForUser(context.contentResolver,
- Settings.Secure.EVEN_DIMMER_MIN_NITS, 0.7f, userId)
+ Settings.Secure.EVEN_DIMMER_MIN_NITS, 30.0f, userId)
modifier.recalculateLowerBound()
testHandler.flush()
assertThat(modifier.isActive).isTrue()
@@ -81,11 +81,22 @@
Settings.Secure.EVEN_DIMMER_ACTIVATED, 1, userId)
Settings.Secure.putFloatForUser(context.contentResolver,
Settings.Secure.EVEN_DIMMER_MIN_NITS, 0.0f, userId)
- modifier.recalculateLowerBound()
+ modifier.onAmbientLuxChange(3000.0f)
testHandler.flush()
assertThat(modifier.isActive).isTrue()
// Test restriction from lux setting
assertThat(modifier.brightnessReason).isEqualTo(BrightnessReason.MODIFIER_MIN_LUX)
}
+
+ @Test
+ fun testSettingOffDisablesModifier() {
+ Settings.Secure.putIntForUser(context.contentResolver,
+ Settings.Secure.EVEN_DIMMER_ACTIVATED, 0, userId)
+ assertThat(modifier.brightnessLowerBound).isEqualTo(PowerManager.BRIGHTNESS_MIN)
+ modifier.onAmbientLuxChange(3000.0f)
+ testHandler.flush()
+ assertThat(modifier.isActive).isFalse()
+ assertThat(modifier.brightnessLowerBound).isEqualTo(PowerManager.BRIGHTNESS_MIN)
+ }
}
diff --git a/services/tests/mockingservicestests/Android.bp b/services/tests/mockingservicestests/Android.bp
index 6d3b8ac..4149e44 100644
--- a/services/tests/mockingservicestests/Android.bp
+++ b/services/tests/mockingservicestests/Android.bp
@@ -75,6 +75,7 @@
"compatibility-device-util-axt",
"flag-junit",
"am_flags_lib",
+ "device_policy_aconfig_flags_lib",
],
libs: [
diff --git a/services/tests/mockingservicestests/src/com/android/server/RescuePartyTest.java b/services/tests/mockingservicestests/src/com/android/server/RescuePartyTest.java
index c30ac2d..682569f 100644
--- a/services/tests/mockingservicestests/src/com/android/server/RescuePartyTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/RescuePartyTest.java
@@ -26,6 +26,7 @@
import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.when;
import static com.android.server.RescueParty.LEVEL_FACTORY_RESET;
+import static com.android.server.RescueParty.RESCUE_LEVEL_FACTORY_RESET;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
@@ -41,9 +42,11 @@
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.VersionedPackage;
+import android.crashrecovery.flags.Flags;
import android.os.RecoverySystem;
import android.os.SystemProperties;
import android.os.UserHandle;
+import android.platform.test.flag.junit.SetFlagsRule;
import android.provider.DeviceConfig;
import android.provider.Settings;
import android.util.ArraySet;
@@ -55,6 +58,7 @@
import org.junit.After;
import org.junit.Before;
+import org.junit.Rule;
import org.junit.Test;
import org.mockito.Answers;
import org.mockito.ArgumentCaptor;
@@ -69,6 +73,7 @@
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
+import java.util.Map;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
@@ -100,6 +105,9 @@
private static final int THROTTLING_DURATION_MIN = 10;
+ @Rule
+ public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
private MockitoSession mSession;
private HashMap<String, String> mSystemSettingsMap;
private HashMap<String, String> mCrashRecoveryPropertiesMap;
@@ -267,6 +275,42 @@
}
@Test
+ public void testBootLoopDetectionWithExecutionForAllRescueLevelsRecoverabilityDetection() {
+ mSetFlagsRule.enableFlags(Flags.FLAG_RECOVERABILITY_DETECTION);
+ RescueParty.onSettingsProviderPublished(mMockContext);
+ verify(() -> DeviceConfig.setMonitorCallback(eq(mMockContentResolver),
+ any(Executor.class),
+ mMonitorCallbackCaptor.capture()));
+ HashMap<String, Integer> verifiedTimesMap = new HashMap<String, Integer>();
+
+ // Record DeviceConfig accesses
+ DeviceConfig.MonitorCallback monitorCallback = mMonitorCallbackCaptor.getValue();
+ monitorCallback.onDeviceConfigAccess(CALLING_PACKAGE1, NAMESPACE1);
+ monitorCallback.onDeviceConfigAccess(CALLING_PACKAGE1, NAMESPACE2);
+
+ final String[] expectedAllResetNamespaces = new String[]{NAMESPACE1, NAMESPACE2};
+
+ noteBoot(1);
+ verifyDeviceConfigReset(expectedAllResetNamespaces, verifiedTimesMap);
+
+ noteBoot(2);
+ assertTrue(RescueParty.isRebootPropertySet());
+
+ noteBoot(3);
+ verifyOnlySettingsReset(Settings.RESET_MODE_UNTRUSTED_DEFAULTS);
+
+ noteBoot(4);
+ verifyOnlySettingsReset(Settings.RESET_MODE_UNTRUSTED_CHANGES);
+
+ noteBoot(5);
+ verifyOnlySettingsReset(Settings.RESET_MODE_TRUSTED_DEFAULTS);
+
+ setCrashRecoveryPropAttemptingReboot(false);
+ noteBoot(6);
+ assertTrue(RescueParty.isFactoryResetPropertySet());
+ }
+
+ @Test
public void testPersistentAppCrashDetectionWithExecutionForAllRescueLevels() {
noteAppCrash(1, true);
@@ -292,6 +336,47 @@
}
@Test
+ public void testPersistentAppCrashDetectionWithExecutionForAllRescueLevelsRecoverability() {
+ mSetFlagsRule.enableFlags(Flags.FLAG_RECOVERABILITY_DETECTION);
+ RescueParty.onSettingsProviderPublished(mMockContext);
+ verify(() -> DeviceConfig.setMonitorCallback(eq(mMockContentResolver),
+ any(Executor.class),
+ mMonitorCallbackCaptor.capture()));
+ HashMap<String, Integer> verifiedTimesMap = new HashMap<String, Integer>();
+
+ // Record DeviceConfig accesses
+ DeviceConfig.MonitorCallback monitorCallback = mMonitorCallbackCaptor.getValue();
+ monitorCallback.onDeviceConfigAccess(PERSISTENT_PACKAGE, NAMESPACE1);
+ monitorCallback.onDeviceConfigAccess(CALLING_PACKAGE1, NAMESPACE1);
+ monitorCallback.onDeviceConfigAccess(CALLING_PACKAGE1, NAMESPACE2);
+
+ final String[] expectedResetNamespaces = new String[]{NAMESPACE1};
+ final String[] expectedAllResetNamespaces = new String[]{NAMESPACE1, NAMESPACE2};
+
+ noteAppCrash(1, true);
+ verifyDeviceConfigReset(expectedResetNamespaces, verifiedTimesMap);
+
+ noteAppCrash(2, true);
+ verifyDeviceConfigReset(expectedAllResetNamespaces, verifiedTimesMap);
+
+ noteAppCrash(3, true);
+ assertTrue(RescueParty.isRebootPropertySet());
+
+ noteAppCrash(4, true);
+ verifyOnlySettingsReset(Settings.RESET_MODE_UNTRUSTED_DEFAULTS);
+
+ noteAppCrash(5, true);
+ verifyOnlySettingsReset(Settings.RESET_MODE_UNTRUSTED_CHANGES);
+
+ noteAppCrash(6, true);
+ verifyOnlySettingsReset(Settings.RESET_MODE_TRUSTED_DEFAULTS);
+
+ setCrashRecoveryPropAttemptingReboot(false);
+ noteAppCrash(7, true);
+ assertTrue(RescueParty.isFactoryResetPropertySet());
+ }
+
+ @Test
public void testNonPersistentAppOnlyPerformsFlagResets() {
noteAppCrash(1, false);
@@ -316,6 +401,45 @@
}
@Test
+ public void testNonPersistentAppOnlyPerformsFlagResetsRecoverabilityDetection() {
+ mSetFlagsRule.enableFlags(Flags.FLAG_RECOVERABILITY_DETECTION);
+ RescueParty.onSettingsProviderPublished(mMockContext);
+ verify(() -> DeviceConfig.setMonitorCallback(eq(mMockContentResolver),
+ any(Executor.class),
+ mMonitorCallbackCaptor.capture()));
+ HashMap<String, Integer> verifiedTimesMap = new HashMap<String, Integer>();
+
+ // Record DeviceConfig accesses
+ DeviceConfig.MonitorCallback monitorCallback = mMonitorCallbackCaptor.getValue();
+ monitorCallback.onDeviceConfigAccess(NON_PERSISTENT_PACKAGE, NAMESPACE1);
+ monitorCallback.onDeviceConfigAccess(CALLING_PACKAGE1, NAMESPACE1);
+ monitorCallback.onDeviceConfigAccess(CALLING_PACKAGE1, NAMESPACE2);
+
+ final String[] expectedResetNamespaces = new String[]{NAMESPACE1};
+ final String[] expectedAllResetNamespaces = new String[]{NAMESPACE1, NAMESPACE2};
+
+ noteAppCrash(1, false);
+ verifyDeviceConfigReset(expectedResetNamespaces, verifiedTimesMap);
+
+ noteAppCrash(2, false);
+ verifyDeviceConfigReset(expectedAllResetNamespaces, verifiedTimesMap);
+
+ noteAppCrash(3, false);
+ assertFalse(RescueParty.isRebootPropertySet());
+
+ noteAppCrash(4, false);
+ verifyNoSettingsReset(Settings.RESET_MODE_UNTRUSTED_DEFAULTS);
+ noteAppCrash(5, false);
+ verifyNoSettingsReset(Settings.RESET_MODE_UNTRUSTED_CHANGES);
+ noteAppCrash(6, false);
+ verifyNoSettingsReset(Settings.RESET_MODE_TRUSTED_DEFAULTS);
+
+ setCrashRecoveryPropAttemptingReboot(false);
+ noteAppCrash(7, false);
+ assertFalse(RescueParty.isFactoryResetPropertySet());
+ }
+
+ @Test
public void testNonPersistentAppCrashDetectionWithScopedResets() {
RescueParty.onSettingsProviderPublished(mMockContext);
verify(() -> DeviceConfig.setMonitorCallback(eq(mMockContentResolver),
@@ -451,6 +575,19 @@
}
@Test
+ public void testIsRecoveryTriggeredRebootRecoverabilityDetection() {
+ mSetFlagsRule.enableFlags(Flags.FLAG_RECOVERABILITY_DETECTION);
+ for (int i = 0; i < RESCUE_LEVEL_FACTORY_RESET; i++) {
+ noteBoot(i + 1);
+ }
+ assertFalse(RescueParty.isFactoryResetPropertySet());
+ setCrashRecoveryPropAttemptingReboot(false);
+ noteBoot(RESCUE_LEVEL_FACTORY_RESET + 1);
+ assertTrue(RescueParty.isRecoveryTriggeredReboot());
+ assertTrue(RescueParty.isFactoryResetPropertySet());
+ }
+
+ @Test
public void testIsRecoveryTriggeredRebootOnlyAfterRebootCompleted() {
for (int i = 0; i < LEVEL_FACTORY_RESET; i++) {
noteBoot(i + 1);
@@ -469,6 +606,25 @@
}
@Test
+ public void testIsRecoveryTriggeredRebootOnlyAfterRebootCompletedRecoverabilityDetection() {
+ mSetFlagsRule.enableFlags(Flags.FLAG_RECOVERABILITY_DETECTION);
+ for (int i = 0; i < RESCUE_LEVEL_FACTORY_RESET; i++) {
+ noteBoot(i + 1);
+ }
+ int mitigationCount = RESCUE_LEVEL_FACTORY_RESET + 1;
+ assertFalse(RescueParty.isFactoryResetPropertySet());
+ noteBoot(mitigationCount++);
+ assertFalse(RescueParty.isFactoryResetPropertySet());
+ noteBoot(mitigationCount++);
+ assertFalse(RescueParty.isFactoryResetPropertySet());
+ noteBoot(mitigationCount++);
+ setCrashRecoveryPropAttemptingReboot(false);
+ noteBoot(mitigationCount + 1);
+ assertTrue(RescueParty.isRecoveryTriggeredReboot());
+ assertTrue(RescueParty.isFactoryResetPropertySet());
+ }
+
+ @Test
public void testThrottlingOnBootFailures() {
setCrashRecoveryPropAttemptingReboot(false);
long now = System.currentTimeMillis();
@@ -481,6 +637,19 @@
}
@Test
+ public void testThrottlingOnBootFailuresRecoverabilityDetection() {
+ mSetFlagsRule.enableFlags(Flags.FLAG_RECOVERABILITY_DETECTION);
+ setCrashRecoveryPropAttemptingReboot(false);
+ long now = System.currentTimeMillis();
+ long beforeTimeout = now - TimeUnit.MINUTES.toMillis(THROTTLING_DURATION_MIN - 1);
+ setCrashRecoveryPropLastFactoryReset(beforeTimeout);
+ for (int i = 1; i <= RESCUE_LEVEL_FACTORY_RESET; i++) {
+ noteBoot(i);
+ }
+ assertFalse(RescueParty.isRecoveryTriggeredReboot());
+ }
+
+ @Test
public void testThrottlingOnAppCrash() {
setCrashRecoveryPropAttemptingReboot(false);
long now = System.currentTimeMillis();
@@ -493,6 +662,19 @@
}
@Test
+ public void testThrottlingOnAppCrashRecoverabilityDetection() {
+ mSetFlagsRule.enableFlags(Flags.FLAG_RECOVERABILITY_DETECTION);
+ setCrashRecoveryPropAttemptingReboot(false);
+ long now = System.currentTimeMillis();
+ long beforeTimeout = now - TimeUnit.MINUTES.toMillis(THROTTLING_DURATION_MIN - 1);
+ setCrashRecoveryPropLastFactoryReset(beforeTimeout);
+ for (int i = 0; i <= RESCUE_LEVEL_FACTORY_RESET; i++) {
+ noteAppCrash(i + 1, true);
+ }
+ assertFalse(RescueParty.isRecoveryTriggeredReboot());
+ }
+
+ @Test
public void testNotThrottlingAfterTimeoutOnBootFailures() {
setCrashRecoveryPropAttemptingReboot(false);
long now = System.currentTimeMillis();
@@ -503,6 +685,20 @@
}
assertTrue(RescueParty.isRecoveryTriggeredReboot());
}
+
+ @Test
+ public void testNotThrottlingAfterTimeoutOnBootFailuresRecoverabilityDetection() {
+ mSetFlagsRule.enableFlags(Flags.FLAG_RECOVERABILITY_DETECTION);
+ setCrashRecoveryPropAttemptingReboot(false);
+ long now = System.currentTimeMillis();
+ long afterTimeout = now - TimeUnit.MINUTES.toMillis(THROTTLING_DURATION_MIN + 1);
+ setCrashRecoveryPropLastFactoryReset(afterTimeout);
+ for (int i = 1; i <= RESCUE_LEVEL_FACTORY_RESET; i++) {
+ noteBoot(i);
+ }
+ assertTrue(RescueParty.isRecoveryTriggeredReboot());
+ }
+
@Test
public void testNotThrottlingAfterTimeoutOnAppCrash() {
setCrashRecoveryPropAttemptingReboot(false);
@@ -516,6 +712,19 @@
}
@Test
+ public void testNotThrottlingAfterTimeoutOnAppCrashRecoverabilityDetection() {
+ mSetFlagsRule.enableFlags(Flags.FLAG_RECOVERABILITY_DETECTION);
+ setCrashRecoveryPropAttemptingReboot(false);
+ long now = System.currentTimeMillis();
+ long afterTimeout = now - TimeUnit.MINUTES.toMillis(THROTTLING_DURATION_MIN + 1);
+ setCrashRecoveryPropLastFactoryReset(afterTimeout);
+ for (int i = 0; i <= RESCUE_LEVEL_FACTORY_RESET; i++) {
+ noteAppCrash(i + 1, true);
+ }
+ assertTrue(RescueParty.isRecoveryTriggeredReboot());
+ }
+
+ @Test
public void testNativeRescuePartyResets() {
doReturn(true).when(() -> SettingsToPropertiesMapper.isNativeFlagsResetPerformed());
doReturn(FAKE_RESET_NATIVE_NAMESPACES).when(
@@ -531,6 +740,7 @@
@Test
public void testExplicitlyEnablingAndDisablingRescue() {
+ mSetFlagsRule.enableFlags(Flags.FLAG_RECOVERABILITY_DETECTION);
SystemProperties.set(RescueParty.PROP_ENABLE_RESCUE, Boolean.toString(false));
SystemProperties.set(PROP_DISABLE_RESCUE, Boolean.toString(true));
assertEquals(RescuePartyObserver.getInstance(mMockContext).execute(sFailingPackage,
@@ -543,6 +753,7 @@
@Test
public void testDisablingRescueByDeviceConfigFlag() {
+ mSetFlagsRule.enableFlags(Flags.FLAG_RECOVERABILITY_DETECTION);
SystemProperties.set(RescueParty.PROP_ENABLE_RESCUE, Boolean.toString(false));
SystemProperties.set(PROP_DEVICE_CONFIG_DISABLE_FLAG, Boolean.toString(true));
@@ -568,6 +779,20 @@
}
@Test
+ public void testDisablingFactoryResetByDeviceConfigFlagRecoverabilityDetection() {
+ mSetFlagsRule.enableFlags(Flags.FLAG_RECOVERABILITY_DETECTION);
+ SystemProperties.set(PROP_DISABLE_FACTORY_RESET_FLAG, Boolean.toString(true));
+
+ for (int i = 0; i < RESCUE_LEVEL_FACTORY_RESET; i++) {
+ noteBoot(i + 1);
+ }
+ assertFalse(RescueParty.isFactoryResetPropertySet());
+
+ // Restore the property value initialized in SetUp()
+ SystemProperties.set(PROP_DISABLE_FACTORY_RESET_FLAG, "");
+ }
+
+ @Test
public void testHealthCheckLevels() {
RescuePartyObserver observer = RescuePartyObserver.getInstance(mMockContext);
@@ -594,6 +819,46 @@
}
@Test
+ public void testHealthCheckLevelsRecoverabilityDetection() {
+ mSetFlagsRule.enableFlags(Flags.FLAG_RECOVERABILITY_DETECTION);
+ RescuePartyObserver observer = RescuePartyObserver.getInstance(mMockContext);
+
+ // Ensure that no action is taken for cases where the failure reason is unknown
+ assertEquals(observer.onHealthCheckFailed(sFailingPackage,
+ PackageWatchdog.FAILURE_REASON_UNKNOWN, 1),
+ PackageHealthObserverImpact.USER_IMPACT_LEVEL_0);
+
+ // Ensure the correct user impact is returned for each mitigation count.
+ assertEquals(observer.onHealthCheckFailed(sFailingPackage,
+ PackageWatchdog.FAILURE_REASON_APP_NOT_RESPONDING, 1),
+ PackageHealthObserverImpact.USER_IMPACT_LEVEL_10);
+
+ assertEquals(observer.onHealthCheckFailed(sFailingPackage,
+ PackageWatchdog.FAILURE_REASON_APP_NOT_RESPONDING, 2),
+ PackageHealthObserverImpact.USER_IMPACT_LEVEL_20);
+
+ assertEquals(observer.onHealthCheckFailed(sFailingPackage,
+ PackageWatchdog.FAILURE_REASON_APP_NOT_RESPONDING, 3),
+ PackageHealthObserverImpact.USER_IMPACT_LEVEL_20);
+
+ assertEquals(observer.onHealthCheckFailed(sFailingPackage,
+ PackageWatchdog.FAILURE_REASON_APP_NOT_RESPONDING, 4),
+ PackageHealthObserverImpact.USER_IMPACT_LEVEL_20);
+
+ assertEquals(observer.onHealthCheckFailed(sFailingPackage,
+ PackageWatchdog.FAILURE_REASON_APP_NOT_RESPONDING, 5),
+ PackageHealthObserverImpact.USER_IMPACT_LEVEL_20);
+
+ assertEquals(observer.onHealthCheckFailed(sFailingPackage,
+ PackageWatchdog.FAILURE_REASON_APP_NOT_RESPONDING, 6),
+ PackageHealthObserverImpact.USER_IMPACT_LEVEL_20);
+
+ assertEquals(observer.onHealthCheckFailed(sFailingPackage,
+ PackageWatchdog.FAILURE_REASON_APP_NOT_RESPONDING, 7),
+ PackageHealthObserverImpact.USER_IMPACT_LEVEL_20);
+ }
+
+ @Test
public void testBootLoopLevels() {
RescuePartyObserver observer = RescuePartyObserver.getInstance(mMockContext);
@@ -606,6 +871,19 @@
}
@Test
+ public void testBootLoopLevelsRecoverabilityDetection() {
+ mSetFlagsRule.enableFlags(Flags.FLAG_RECOVERABILITY_DETECTION);
+ RescuePartyObserver observer = RescuePartyObserver.getInstance(mMockContext);
+
+ assertEquals(observer.onBootLoop(1), PackageHealthObserverImpact.USER_IMPACT_LEVEL_20);
+ assertEquals(observer.onBootLoop(2), PackageHealthObserverImpact.USER_IMPACT_LEVEL_50);
+ assertEquals(observer.onBootLoop(3), PackageHealthObserverImpact.USER_IMPACT_LEVEL_71);
+ assertEquals(observer.onBootLoop(4), PackageHealthObserverImpact.USER_IMPACT_LEVEL_75);
+ assertEquals(observer.onBootLoop(5), PackageHealthObserverImpact.USER_IMPACT_LEVEL_80);
+ assertEquals(observer.onBootLoop(6), PackageHealthObserverImpact.USER_IMPACT_LEVEL_100);
+ }
+
+ @Test
public void testResetDeviceConfigForPackagesOnlyRuntimeMap() {
RescueParty.onSettingsProviderPublished(mMockContext);
verify(() -> DeviceConfig.setMonitorCallback(eq(mMockContentResolver),
@@ -727,11 +1005,26 @@
private void verifySettingsResets(int resetMode, String[] resetNamespaces,
HashMap<String, Integer> configResetVerifiedTimesMap) {
+ verifyOnlySettingsReset(resetMode);
+ verifyDeviceConfigReset(resetNamespaces, configResetVerifiedTimesMap);
+ }
+
+ private void verifyOnlySettingsReset(int resetMode) {
verify(() -> Settings.Global.resetToDefaultsAsUser(mMockContentResolver, null,
resetMode, UserHandle.USER_SYSTEM));
verify(() -> Settings.Secure.resetToDefaultsAsUser(eq(mMockContentResolver), isNull(),
eq(resetMode), anyInt()));
- // Verify DeviceConfig resets
+ }
+
+ private void verifyNoSettingsReset(int resetMode) {
+ verify(() -> Settings.Global.resetToDefaultsAsUser(mMockContentResolver, null,
+ resetMode, UserHandle.USER_SYSTEM), never());
+ verify(() -> Settings.Secure.resetToDefaultsAsUser(eq(mMockContentResolver), isNull(),
+ eq(resetMode), anyInt()), never());
+ }
+
+ private void verifyDeviceConfigReset(String[] resetNamespaces,
+ Map<String, Integer> configResetVerifiedTimesMap) {
if (resetNamespaces == null) {
verify(() -> DeviceConfig.resetToDefaults(anyInt(), anyString()), never());
} else {
@@ -818,9 +1111,16 @@
// mock properties in BootThreshold
try {
- mSpyBootThreshold = spy(watchdog.new BootThreshold(
- PackageWatchdog.DEFAULT_BOOT_LOOP_TRIGGER_COUNT,
- PackageWatchdog.DEFAULT_BOOT_LOOP_TRIGGER_WINDOW_MS));
+ if (Flags.recoverabilityDetection()) {
+ mSpyBootThreshold = spy(watchdog.new BootThreshold(
+ PackageWatchdog.DEFAULT_BOOT_LOOP_TRIGGER_COUNT,
+ PackageWatchdog.DEFAULT_BOOT_LOOP_TRIGGER_WINDOW_MS,
+ PackageWatchdog.DEFAULT_BOOT_LOOP_MITIGATION_INCREMENT));
+ } else {
+ mSpyBootThreshold = spy(watchdog.new BootThreshold(
+ PackageWatchdog.DEFAULT_BOOT_LOOP_TRIGGER_COUNT,
+ PackageWatchdog.DEFAULT_BOOT_LOOP_TRIGGER_WINDOW_MS));
+ }
mCrashRecoveryPropertiesMap = new HashMap<>();
doAnswer((Answer<Integer>) invocationOnMock -> {
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java
index 420af86..1b2c0e4 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java
@@ -41,6 +41,7 @@
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.same;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doNothing;
@@ -57,6 +58,7 @@
import android.app.Activity;
import android.app.ActivityManager;
import android.app.AppOpsManager;
+import android.app.ApplicationExitInfo;
import android.app.BackgroundStartPrivileges;
import android.app.BroadcastOptions;
import android.app.IApplicationThread;
@@ -239,6 +241,7 @@
mConstants.TIMEOUT = 200;
mConstants.ALLOW_BG_ACTIVITY_START_TIMEOUT = 0;
mConstants.PENDING_COLD_START_CHECK_INTERVAL_MILLIS = 500;
+ mConstants.MAX_FROZEN_OUTGOING_BROADCASTS = 10;
}
@After
@@ -2368,6 +2371,34 @@
verifyScheduleReceiver(times(1), receiverYellowApp, timeTick);
}
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_DEFER_OUTGOING_BROADCASTS)
+ public void testKillProcess_excessiveOutgoingBroadcastsWhileCached() throws Exception {
+ final ProcessRecord callerApp = makeActiveProcessRecord(PACKAGE_RED);
+ setProcessFreezable(callerApp, true /* pendingFreeze */, false /* frozen */);
+ waitForIdle();
+
+ final int count = mConstants.MAX_FROZEN_OUTGOING_BROADCASTS + 1;
+ for (int i = 0; i < count; ++i) {
+ final Intent timeTick = new Intent(Intent.ACTION_TIME_TICK + "_" + i);
+ enqueueBroadcast(makeBroadcastRecord(timeTick, callerApp, List.of(
+ makeManifestReceiver(PACKAGE_BLUE, CLASS_BLUE))));
+ }
+ // Verify that we invoke the call to freeze the caller app.
+ verify(mAms.mOomAdjuster.mCachedAppOptimizer, atLeastOnce())
+ .freezeAppAsyncImmediateLSP(callerApp);
+
+ // Verify that the caller process is killed
+ assertTrue(callerApp.isKilled());
+ verify(mProcessList).noteAppKill(same(callerApp),
+ eq(ApplicationExitInfo.REASON_OTHER),
+ eq(ApplicationExitInfo.SUBREASON_EXCESSIVE_OUTGOING_BROADCASTS_WHILE_CACHED),
+ any(String.class));
+
+ waitForIdle();
+ assertNull(mAms.getProcessRecordLocked(PACKAGE_BLUE, getUidForPackage(PACKAGE_BLUE)));
+ }
+
private long getReceiverScheduledTime(@NonNull BroadcastRecord r, @NonNull Object receiver) {
for (int i = 0; i < r.receivers.size(); ++i) {
if (isReceiverEquals(receiver, r.receivers.get(i))) {
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/ServiceBindingOomAdjPolicyTest.java b/services/tests/mockingservicestests/src/com/android/server/am/ServiceBindingOomAdjPolicyTest.java
index 97b7af8..680ab16 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/ServiceBindingOomAdjPolicyTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/ServiceBindingOomAdjPolicyTest.java
@@ -36,7 +36,6 @@
import static org.junit.Assert.assertNotEquals;
import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.anyString;
@@ -185,8 +184,8 @@
doReturn(false).when(mAms.mAtmInternal).hasSystemAlertWindowPermission(anyInt(), anyInt(),
any());
doReturn(true).when(mAms.mOomAdjuster.mCachedAppOptimizer).useFreezer();
- doNothing().when(mAms.mOomAdjuster.mCachedAppOptimizer).freezeAppAsyncInternalLSP(
- any(), anyLong(), anyBoolean(), anyBoolean());
+ doNothing().when(mAms.mOomAdjuster.mCachedAppOptimizer).freezeAppAsyncAtEarliestLSP(
+ any());
doReturn(false).when(mAms.mAppProfiler).updateLowMemStateLSP(anyInt(), anyInt(),
anyInt(), anyLong());
@@ -503,7 +502,7 @@
if (clientApp.isFreezable()) {
verify(mAms.mOomAdjuster.mCachedAppOptimizer,
times(Flags.serviceBindingOomAdjPolicy() ? 1 : 0))
- .freezeAppAsyncInternalLSP(eq(clientApp), eq(0L), anyBoolean(), anyBoolean());
+ .freezeAppAsyncAtEarliestLSP(eq(clientApp));
clearInvocations(mAms.mOomAdjuster.mCachedAppOptimizer);
}
diff --git a/services/tests/servicestests/Android.bp b/services/tests/servicestests/Android.bp
index 37967fa..65986ea 100644
--- a/services/tests/servicestests/Android.bp
+++ b/services/tests/servicestests/Android.bp
@@ -62,6 +62,7 @@
"cts-wm-util",
"platform-compat-test-rules",
"mockito-target-minus-junit4",
+ "mockito-kotlin2",
"platform-test-annotations",
"ShortcutManagerTestUtils",
"truth",
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java
index b2ecea1..9d32ed8 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java
@@ -21,7 +21,6 @@
import static android.provider.Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN;
import static android.provider.Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_NONE;
import static android.provider.Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW;
-import static android.view.accessibility.Flags.FLAG_CLEANUP_ACCESSIBILITY_WARNING_DIALOG;
import static android.view.accessibility.Flags.FLAG_SKIP_ACCESSIBILITY_WARNING_DIALOG_FOR_TRUSTED_SERVICES;
import static com.android.internal.accessibility.AccessibilityShortcutController.ACCESSIBILITY_HEARING_AIDS_COMPONENT_NAME;
@@ -50,9 +49,11 @@
import android.accessibilityservice.IAccessibilityServiceClient;
import android.app.PendingIntent;
import android.app.RemoteAction;
+import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
+import android.content.IntentFilter;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
@@ -67,6 +68,7 @@
import android.os.IBinder;
import android.os.LocaleList;
import android.os.UserHandle;
+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;
@@ -74,6 +76,7 @@
import android.testing.AndroidTestingRunner;
import android.testing.TestableContext;
import android.testing.TestableLooper;
+import android.util.ArrayMap;
import android.util.ArraySet;
import android.view.Display;
import android.view.DisplayAdjustments;
@@ -123,6 +126,7 @@
import java.util.ArrayList;
import java.util.HashSet;
+import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -880,7 +884,6 @@
}
@Test
- @RequiresFlagsEnabled(FLAG_CLEANUP_ACCESSIBILITY_WARNING_DIALOG)
public void testIsAccessibilityServiceWarningRequired_requiredByDefault() {
mockManageAccessibilityGranted(mTestableContext);
final AccessibilityServiceInfo info = mockAccessibilityServiceInfo(COMPONENT_NAME);
@@ -889,7 +892,6 @@
}
@Test
- @RequiresFlagsEnabled(FLAG_CLEANUP_ACCESSIBILITY_WARNING_DIALOG)
public void testIsAccessibilityServiceWarningRequired_notRequiredIfAlreadyEnabled() {
mockManageAccessibilityGranted(mTestableContext);
final AccessibilityServiceInfo info_a = mockAccessibilityServiceInfo(COMPONENT_NAME);
@@ -904,7 +906,6 @@
}
@Test
- @RequiresFlagsEnabled(FLAG_CLEANUP_ACCESSIBILITY_WARNING_DIALOG)
public void testIsAccessibilityServiceWarningRequired_notRequiredIfExistingShortcut() {
mockManageAccessibilityGranted(mTestableContext);
final AccessibilityServiceInfo info_a = mockAccessibilityServiceInfo(
@@ -925,9 +926,7 @@
}
@Test
- @RequiresFlagsEnabled({
- FLAG_CLEANUP_ACCESSIBILITY_WARNING_DIALOG,
- FLAG_SKIP_ACCESSIBILITY_WARNING_DIALOG_FOR_TRUSTED_SERVICES})
+ @RequiresFlagsEnabled(FLAG_SKIP_ACCESSIBILITY_WARNING_DIALOG_FOR_TRUSTED_SERVICES)
public void testIsAccessibilityServiceWarningRequired_notRequiredIfAllowlisted() {
mockManageAccessibilityGranted(mTestableContext);
final AccessibilityServiceInfo info_a = mockAccessibilityServiceInfo(
@@ -1464,6 +1463,52 @@
AccessibilityShortcutController.DALTONIZER_COMPONENT_NAME.flattenToString());
}
+ @Test
+ @RequiresFlagsEnabled(android.view.accessibility.Flags.FLAG_A11Y_QS_SHORTCUT)
+ public void restoreAccessibilityQsTargets_a11yQsTargetsRestored() {
+ String daltonizerTile =
+ AccessibilityShortcutController.DALTONIZER_COMPONENT_NAME.flattenToString();
+ String colorInversionTile =
+ AccessibilityShortcutController.COLOR_INVERSION_COMPONENT_NAME.flattenToString();
+ final AccessibilityUserState userState = new AccessibilityUserState(
+ UserHandle.USER_SYSTEM, mTestableContext, mA11yms);
+ userState.updateA11yQsTargetLocked(Set.of(daltonizerTile));
+ mA11yms.mUserStates.put(UserHandle.USER_SYSTEM, userState);
+
+ Intent intent = new Intent(Intent.ACTION_SETTING_RESTORED)
+ .addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY)
+ .putExtra(Intent.EXTRA_SETTING_NAME, Settings.Secure.ACCESSIBILITY_QS_TARGETS)
+ .putExtra(Intent.EXTRA_SETTING_NEW_VALUE, colorInversionTile);
+ sendBroadcastToAccessibilityManagerService(intent);
+ mTestableLooper.processAllMessages();
+
+ assertThat(mA11yms.mUserStates.get(UserHandle.USER_SYSTEM).getA11yQsTargets())
+ .containsExactlyElementsIn(Set.of(daltonizerTile, colorInversionTile));
+ }
+
+ @Test
+ @RequiresFlagsDisabled(android.view.accessibility.Flags.FLAG_A11Y_QS_SHORTCUT)
+ public void restoreAccessibilityQsTargets_a11yQsTargetsNotRestored() {
+ String daltonizerTile =
+ AccessibilityShortcutController.DALTONIZER_COMPONENT_NAME.flattenToString();
+ String colorInversionTile =
+ AccessibilityShortcutController.COLOR_INVERSION_COMPONENT_NAME.flattenToString();
+ final AccessibilityUserState userState = new AccessibilityUserState(
+ UserHandle.USER_SYSTEM, mTestableContext, mA11yms);
+ userState.updateA11yQsTargetLocked(Set.of(daltonizerTile));
+ mA11yms.mUserStates.put(UserHandle.USER_SYSTEM, userState);
+
+ Intent intent = new Intent(Intent.ACTION_SETTING_RESTORED)
+ .addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY)
+ .putExtra(Intent.EXTRA_SETTING_NAME, Settings.Secure.ACCESSIBILITY_QS_TARGETS)
+ .putExtra(Intent.EXTRA_SETTING_NEW_VALUE, colorInversionTile);
+ sendBroadcastToAccessibilityManagerService(intent);
+ mTestableLooper.processAllMessages();
+
+ assertThat(userState.getA11yQsTargets())
+ .containsExactlyElementsIn(Set.of(daltonizerTile));
+ }
+
private static AccessibilityServiceInfo mockAccessibilityServiceInfo(
ComponentName componentName) {
return mockAccessibilityServiceInfo(
@@ -1542,6 +1587,14 @@
mA11yms.getCurrentUserState().updateTileServiceMapForAccessibilityServiceLocked();
}
+ private void sendBroadcastToAccessibilityManagerService(Intent intent) {
+ if (!mTestableContext.getBroadcastReceivers().containsKey(intent.getAction())) {
+ return;
+ }
+ mTestableContext.getBroadcastReceivers().get(intent.getAction()).forEach(
+ broadcastReceiver -> broadcastReceiver.onReceive(mTestableContext, intent));
+ }
+
public static class FakeInputFilter extends AccessibilityInputFilter {
FakeInputFilter(Context context,
AccessibilityManagerService service) {
@@ -1552,6 +1605,7 @@
private static class A11yTestableContext extends TestableContext {
private final Context mMockContext;
+ private final Map<String, List<BroadcastReceiver>> mBroadcastReceivers = new ArrayMap<>();
A11yTestableContext(Context base) {
super(base);
@@ -1563,8 +1617,29 @@
mMockContext.startActivityAsUser(intent, options, user);
}
+ @Override
+ public Intent registerReceiverAsUser(BroadcastReceiver receiver, UserHandle user,
+ IntentFilter filter, String broadcastPermission, Handler scheduler) {
+ Iterator<String> actions = filter.actionsIterator();
+ if (actions != null) {
+ while (actions.hasNext()) {
+ String action = actions.next();
+ List<BroadcastReceiver> actionReceivers =
+ mBroadcastReceivers.getOrDefault(action, new ArrayList<>());
+ actionReceivers.add(receiver);
+ mBroadcastReceivers.put(action, actionReceivers);
+ }
+ }
+ return super.registerReceiverAsUser(
+ receiver, user, filter, broadcastPermission, scheduler);
+ }
+
Context getMockContext() {
return mMockContext;
}
+
+ Map<String, List<BroadcastReceiver>> getBroadcastReceivers() {
+ return mBroadcastReceivers;
+ }
}
}
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityWindowManagerWithAccessibilityWindowTest.java b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityWindowManagerWithAccessibilityWindowTest.java
index 6e8d6dc..f44879f 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityWindowManagerWithAccessibilityWindowTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityWindowManagerWithAccessibilityWindowTest.java
@@ -470,6 +470,27 @@
}
@Test
+ public void onWindowsChanged_shouldNotReportfullyOccludedWindow() {
+ final AccessibilityWindow frontWindow = mWindows.get(Display.DEFAULT_DISPLAY).get(0);
+ setRegionForMockAccessibilityWindow(frontWindow, new Region(100, 100, 300, 300));
+ final int frontWindowId = mA11yWindowManager.findWindowIdLocked(
+ USER_SYSTEM_ID, frontWindow.getWindowInfo().token);
+
+ // index 1 is focused. Let's use the next one for this test.
+ final AccessibilityWindow occludedWindow = mWindows.get(Display.DEFAULT_DISPLAY).get(2);
+ setRegionForMockAccessibilityWindow(occludedWindow, new Region(150, 150, 250, 250));
+ final int occludedWindowId = mA11yWindowManager.findWindowIdLocked(
+ USER_SYSTEM_ID, occludedWindow.getWindowInfo().token);
+
+ onAccessibilityWindowsChanged(Display.DEFAULT_DISPLAY, SEND_ON_WINDOW_CHANGES);
+
+ final List<AccessibilityWindowInfo> a11yWindows =
+ mA11yWindowManager.getWindowListLocked(Display.DEFAULT_DISPLAY);
+ assertThat(a11yWindows, hasItem(windowId(frontWindowId)));
+ assertThat(a11yWindows, not(hasItem(windowId(occludedWindowId))));
+ }
+
+ @Test
public void onWindowsChangedAndForceSend_shouldUpdateWindows() {
assertNotEquals("new title",
toString(mA11yWindowManager.getWindowListLocked(Display.DEFAULT_DISPLAY)
diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceTest.java
index a4628ee..4d1d17f 100644
--- a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceTest.java
+++ b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceTest.java
@@ -141,6 +141,7 @@
@Test
public void virtualDevice_hasCustomAudioInputSupport() throws Exception {
mSetFlagsRule.enableFlags(Flags.FLAG_VDM_PUBLIC_APIS);
+ mSetFlagsRule.enableFlags(android.media.audiopolicy.Flags.FLAG_AUDIO_MIX_TEST_API);
VirtualDevice virtualDevice =
new VirtualDevice(
@@ -150,6 +151,10 @@
assertThat(virtualDevice.hasCustomAudioInputSupport()).isFalse();
when(mVirtualDevice.getDevicePolicy(POLICY_TYPE_AUDIO)).thenReturn(DEVICE_POLICY_CUSTOM);
+ when(mVirtualDevice.hasCustomAudioInputSupport()).thenReturn(false);
+ assertThat(virtualDevice.hasCustomAudioInputSupport()).isFalse();
+
+ when(mVirtualDevice.hasCustomAudioInputSupport()).thenReturn(true);
assertThat(virtualDevice.hasCustomAudioInputSupport()).isTrue();
}
diff --git a/services/tests/servicestests/src/com/android/server/devicepolicy/OverlayPackagesProviderTest.java b/services/tests/servicestests/src/com/android/server/devicepolicy/OverlayPackagesProviderTest.java
index 4f6fc3d..0a696ef 100644
--- a/services/tests/servicestests/src/com/android/server/devicepolicy/OverlayPackagesProviderTest.java
+++ b/services/tests/servicestests/src/com/android/server/devicepolicy/OverlayPackagesProviderTest.java
@@ -47,7 +47,7 @@
import androidx.annotation.NonNull;
import androidx.test.InstrumentationRegistry;
-import androidx.test.runner.AndroidJUnit4;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.android.internal.R;
@@ -67,9 +67,7 @@
/**
* Run this test with:
- *
* {@code atest FrameworksServicesTests:com.android.server.devicepolicy.OwnersTest}
- *
*/
@RunWith(AndroidJUnit4.class)
public class OverlayPackagesProviderTest {
@@ -87,8 +85,8 @@
private FakePackageManager mPackageManager;
private String[] mSystemAppsWithLauncher;
- private Set<String> mRegularMainlineModules = new HashSet<>();
- private Map<String, String> mMainlineModuleToDeclaredMetadataMap = new HashMap<>();
+ private final Set<String> mRegularMainlineModules = new HashSet<>();
+ private final Map<String, String> mMainlineModuleToDeclaredMetadataMap = new HashMap<>();
private OverlayPackagesProvider mHelper;
@Before
@@ -115,7 +113,8 @@
setVendorDisallowedAppsManagedUser();
mRealResources = InstrumentationRegistry.getTargetContext().getResources();
- mHelper = new OverlayPackagesProvider(mTestContext, mInjector);
+ mHelper = new OverlayPackagesProvider(mTestContext, mInjector,
+ new RecursiveStringArrayResourceResolver(mResources));
}
@Test
@@ -213,7 +212,7 @@
}
/**
- * @see {@link #testAllowedAndDisallowedAtTheSameTimeManagedDevice}
+ * @see #testAllowedAndDisallowedAtTheSameTimeManagedDevice
*/
@Test
public void testAllowedAndDisallowedAtTheSameTimeManagedUser() {
@@ -224,7 +223,7 @@
}
/**
- * @see {@link #testAllowedAndDisallowedAtTheSameTimeManagedDevice}
+ * @see #testAllowedAndDisallowedAtTheSameTimeManagedDevice
*/
@Test
public void testAllowedAndDisallowedAtTheSameTimeManagedProfile() {
@@ -447,7 +446,7 @@
}
private void setSystemInputMethods(String... packageNames) {
- List<InputMethodInfo> inputMethods = new ArrayList<InputMethodInfo>();
+ List<InputMethodInfo> inputMethods = new ArrayList<>();
for (String packageName : packageNames) {
ApplicationInfo aInfo = new ApplicationInfo();
aInfo.flags = ApplicationInfo.FLAG_SYSTEM;
@@ -467,6 +466,7 @@
mSystemAppsWithLauncher = apps;
}
+ @SafeVarargs
private <T> Set<T> setFromArray(T... array) {
if (array == null) {
return null;
@@ -475,6 +475,7 @@
}
class FakePackageManager extends MockPackageManager {
+ @NonNull
@Override
public List<ResolveInfo> queryIntentActivitiesAsUser(Intent intent, int flags, int userId) {
assertWithMessage("Expected an intent with action ACTION_MAIN")
diff --git a/services/tests/servicestests/src/com/android/server/devicepolicy/RecursiveStringArrayResourceResolverTest.kt b/services/tests/servicestests/src/com/android/server/devicepolicy/RecursiveStringArrayResourceResolverTest.kt
new file mode 100644
index 0000000..647f6c7
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/devicepolicy/RecursiveStringArrayResourceResolverTest.kt
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.devicepolicy
+
+import android.annotation.ArrayRes
+import android.content.res.Resources
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertWithMessage
+import com.google.errorprone.annotations.CanIgnoreReturnValue
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.whenever
+
+
+/**
+ * Run this test with:
+ * `atest FrameworksServicesTests:com.android.server.devicepolicy.RecursiveStringArrayResourceResolverTest`
+ */
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class RecursiveStringArrayResourceResolverTest {
+ private companion object {
+ const val PACKAGE = "com.android.test"
+ const val ROOT_RESOURCE = "my_root_resource"
+ const val SUB_RESOURCE = "my_sub_resource"
+ const val EXTERNAL_PACKAGE = "com.external.test"
+ const val EXTERNAL_RESOURCE = "my_external_resource"
+ }
+
+ private val mResources = mock<Resources>()
+ private val mTarget = RecursiveStringArrayResourceResolver(mResources)
+
+ /**
+ * Mocks [Resources.getIdentifier] and [Resources.getStringArray] to return [values] and reference under a generated ID.
+ * @receiver mocked [Resources] container to configure
+ * @param pkg package name to "contain" mocked resource
+ * @param name mocked resource name
+ * @param values string-array resource values to return when mock is queried
+ * @return generated resource ID
+ */
+ @ArrayRes
+ @CanIgnoreReturnValue
+ private fun Resources.mockStringArrayResource(pkg: String, name: String, vararg values: String): Int {
+ val anId = (pkg + name).hashCode()
+ println("Mocking Resources::getIdentifier(name=\"$name\", defType=\"array\", defPackage=\"$pkg\") -> $anId")
+ whenever(getIdentifier(eq(name), eq("array"), eq(pkg))).thenReturn(anId)
+ println("Mocking Resources::getStringArray(id=$anId) -> ${values.asList()}")
+ whenever(getStringArray(eq(anId))).thenReturn(values)
+ return anId
+ }
+
+ @Test
+ fun testCanResolveTheArrayWithoutImports() {
+ val values = arrayOf("app.a", "app.b")
+ val mockId = mResources.mockStringArrayResource(pkg = PACKAGE, name = ROOT_RESOURCE, values = values)
+
+ val actual = mTarget.resolve(/* pkg= */ PACKAGE, /* rootId = */ mockId)
+
+ assertWithMessage("Values are resolved correctly")
+ .that(actual).containsExactlyElementsIn(values)
+ }
+
+ @Test
+ fun testCanResolveTheArrayWithImports() {
+ val externalValues = arrayOf("ext.a", "ext.b", "#import:$PACKAGE/$SUB_RESOURCE")
+ mResources.mockStringArrayResource(pkg = EXTERNAL_PACKAGE, name = EXTERNAL_RESOURCE, values = externalValues)
+ val subValues = arrayOf("sub.a", "sub.b")
+ mResources.mockStringArrayResource(pkg = PACKAGE, name = SUB_RESOURCE, values = subValues)
+ val values = arrayOf("app.a", "#import:./$SUB_RESOURCE", "app.b", "#import:$EXTERNAL_PACKAGE/$EXTERNAL_RESOURCE", "app.c")
+ val mockId = mResources.mockStringArrayResource(pkg = PACKAGE, name = ROOT_RESOURCE, values = values)
+
+ val actual = mTarget.resolve(/* pkg= */ PACKAGE, /* rootId= */ mockId)
+
+ assertWithMessage("Values are resolved correctly")
+ .that(actual).containsExactlyElementsIn((externalValues + subValues + values)
+ .filterNot { it.startsWith("#import:") }
+ .toSet())
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/power/hint/HintManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/power/hint/HintManagerServiceTest.java
index 66599e9..510e7c4 100644
--- a/services/tests/servicestests/src/com/android/server/power/hint/HintManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/power/hint/HintManagerServiceTest.java
@@ -17,6 +17,8 @@
package com.android.server.power.hint;
+import static com.android.server.power.hint.HintManagerService.CLEAN_UP_UID_DELAY_MILLIS;
+
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertArrayEquals;
@@ -45,6 +47,9 @@
import android.os.IHintSession;
import android.os.PerformanceHintManager;
import android.os.Process;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
import android.util.Log;
import com.android.server.FgThread;
@@ -54,11 +59,13 @@
import com.android.server.power.hint.HintManagerService.NativeWrapper;
import org.junit.Before;
+import org.junit.Rule;
import org.junit.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CountDownLatch;
@@ -71,7 +78,7 @@
* Tests for {@link com.android.server.power.hint.HintManagerService}.
*
* Build/Install/Run:
- * atest FrameworksServicesTests:HintManagerServiceTest
+ * atest FrameworksServicesTests:HintManagerServiceTest
*/
public class HintManagerServiceTest {
private static final String TAG = "HintManagerServiceTest";
@@ -110,9 +117,15 @@
makeWorkDuration(2L, 13L, 2L, 8L, 0L),
};
- @Mock private Context mContext;
- @Mock private HintManagerService.NativeWrapper mNativeWrapperMock;
- @Mock private ActivityManagerInternal mAmInternalMock;
+ @Mock
+ private Context mContext;
+ @Mock
+ private HintManagerService.NativeWrapper mNativeWrapperMock;
+ @Mock
+ private ActivityManagerInternal mAmInternalMock;
+ @Rule
+ public final CheckFlagsRule mCheckFlagsRule =
+ DeviceFlagsValueProvider.createCheckFlagsRule();
private HintManagerService mService;
@@ -122,12 +135,11 @@
when(mNativeWrapperMock.halGetHintSessionPreferredRate())
.thenReturn(DEFAULT_HINT_PREFERRED_RATE);
when(mNativeWrapperMock.halCreateHintSession(eq(TGID), eq(UID), eq(SESSION_TIDS_A),
- eq(DEFAULT_TARGET_DURATION))).thenReturn(1L);
+ eq(DEFAULT_TARGET_DURATION))).thenReturn(1L);
when(mNativeWrapperMock.halCreateHintSession(eq(TGID), eq(UID), eq(SESSION_TIDS_B),
- eq(DEFAULT_TARGET_DURATION))).thenReturn(2L);
+ eq(DEFAULT_TARGET_DURATION))).thenReturn(2L);
when(mNativeWrapperMock.halCreateHintSession(eq(TGID), eq(UID), eq(SESSION_TIDS_C),
- eq(0L))).thenReturn(1L);
- when(mAmInternalMock.getIsolatedProcesses(anyInt())).thenReturn(null);
+ eq(0L))).thenReturn(1L);
LocalServices.removeServiceForTest(ActivityManagerInternal.class);
LocalServices.addService(ActivityManagerInternal.class, mAmInternalMock);
}
@@ -434,6 +446,163 @@
}
@Test
+ @RequiresFlagsEnabled(Flags.FLAG_POWERHINT_THREAD_CLEANUP)
+ public void testCleanupDeadThreads() throws Exception {
+ HintManagerService service = createService();
+ IBinder token = new Binder();
+ CountDownLatch stopLatch1 = new CountDownLatch(1);
+ int threadCount = 3;
+ int[] tids1 = createThreads(threadCount, stopLatch1);
+ long sessionPtr1 = 111;
+ when(mNativeWrapperMock.halCreateHintSession(eq(TGID), eq(UID), eq(tids1),
+ eq(DEFAULT_TARGET_DURATION))).thenReturn(sessionPtr1);
+ AppHintSession session1 = (AppHintSession) service.getBinderServiceInstance()
+ .createHintSession(token, tids1, DEFAULT_TARGET_DURATION);
+ assertNotNull(session1);
+
+ // for test only to avoid conflicting with any real thread that exists on device
+ int isoProc1 = -100;
+ int isoProc2 = 9999;
+ when(mAmInternalMock.getIsolatedProcesses(eq(UID))).thenReturn(List.of(0));
+
+ CountDownLatch stopLatch2 = new CountDownLatch(1);
+ int[] tids2 = createThreads(threadCount, stopLatch2);
+ int[] tids2WithIsolated = Arrays.copyOf(tids2, tids2.length + 2);
+ int[] expectedTids2 = Arrays.copyOf(tids2, tids2.length + 1);
+ expectedTids2[tids2.length] = isoProc1;
+ tids2WithIsolated[threadCount] = isoProc1;
+ tids2WithIsolated[threadCount + 1] = isoProc2;
+ long sessionPtr2 = 222;
+ when(mNativeWrapperMock.halCreateHintSession(eq(TGID), eq(UID), eq(tids2WithIsolated),
+ eq(DEFAULT_TARGET_DURATION))).thenReturn(sessionPtr2);
+ AppHintSession session2 = (AppHintSession) service.getBinderServiceInstance()
+ .createHintSession(token, tids2WithIsolated, DEFAULT_TARGET_DURATION);
+ assertNotNull(session2);
+
+ // trigger clean up through UID state change by making the process background
+ service.mUidObserver.onUidStateChanged(UID,
+ ActivityManager.PROCESS_STATE_IMPORTANT_BACKGROUND, 0, 0);
+ LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(500) + TimeUnit.MILLISECONDS.toNanos(
+ CLEAN_UP_UID_DELAY_MILLIS));
+ verify(mNativeWrapperMock, never()).halSetThreads(eq(sessionPtr1), any());
+ verify(mNativeWrapperMock, never()).halSetThreads(eq(sessionPtr2), any());
+ // the new TIDs pending list should be updated
+ assertArrayEquals(session2.getTidsInternal(), expectedTids2);
+ reset(mNativeWrapperMock);
+
+ // this should resume and update the threads so those never-existed invalid isolated
+ // processes should be cleaned up
+ service.mUidObserver.onUidStateChanged(UID,
+ ActivityManager.PROCESS_STATE_IMPORTANT_FOREGROUND, 0, 0);
+ // wait for the async uid state change to trigger resume and setThreads
+ LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(500));
+ verify(mNativeWrapperMock, times(1)).halSetThreads(eq(sessionPtr2), eq(expectedTids2));
+ reset(mNativeWrapperMock);
+
+ // let all session 1 threads to exit and the cleanup should force pause the session
+ stopLatch1.countDown();
+ LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(100));
+ service.mUidObserver.onUidStateChanged(UID,
+ ActivityManager.PROCESS_STATE_IMPORTANT_FOREGROUND, 0, 0);
+ LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(500) + TimeUnit.MILLISECONDS.toNanos(
+ CLEAN_UP_UID_DELAY_MILLIS));
+ verify(mNativeWrapperMock, times(1)).halPauseHintSession(eq(sessionPtr1));
+ verify(mNativeWrapperMock, never()).halSetThreads(eq(sessionPtr1), any());
+ verify(mNativeWrapperMock, never()).halSetThreads(eq(sessionPtr2), any());
+ // all hints will have no effect as the session is force paused while proc in foreground
+ verifyAllHintsEnabled(session1, false);
+ verifyAllHintsEnabled(session2, true);
+ reset(mNativeWrapperMock);
+
+ // in foreground, set new tids for session 1 then it should be resumed and all hints allowed
+ stopLatch1 = new CountDownLatch(1);
+ tids1 = createThreads(threadCount, stopLatch1);
+ session1.setThreads(tids1);
+ verify(mNativeWrapperMock, times(1)).halSetThreads(eq(sessionPtr1), eq(tids1));
+ verify(mNativeWrapperMock, times(1)).halResumeHintSession(eq(sessionPtr1));
+ verifyAllHintsEnabled(session1, true);
+ reset(mNativeWrapperMock);
+
+ // let all session 1 and 2 non isolated threads to exit
+ stopLatch1.countDown();
+ stopLatch2.countDown();
+ expectedTids2 = new int[]{isoProc1};
+ LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(100));
+ service.mUidObserver.onUidStateChanged(UID,
+ ActivityManager.PROCESS_STATE_IMPORTANT_BACKGROUND, 0, 0);
+ LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(500) + TimeUnit.MILLISECONDS.toNanos(
+ CLEAN_UP_UID_DELAY_MILLIS));
+ verify(mNativeWrapperMock, times(1)).halPauseHintSession(eq(sessionPtr1));
+ verify(mNativeWrapperMock, never()).halSetThreads(eq(sessionPtr1), any());
+ verify(mNativeWrapperMock, never()).halSetThreads(eq(sessionPtr2), any());
+ // in background, set threads for session 1 then it should not be force paused next time
+ session1.setThreads(SESSION_TIDS_A);
+ // the new TIDs pending list should be updated
+ assertArrayEquals(session1.getTidsInternal(), SESSION_TIDS_A);
+ assertArrayEquals(session2.getTidsInternal(), expectedTids2);
+ verifyAllHintsEnabled(session1, false);
+ verifyAllHintsEnabled(session2, false);
+ reset(mNativeWrapperMock);
+
+ service.mUidObserver.onUidStateChanged(UID,
+ ActivityManager.PROCESS_STATE_IMPORTANT_FOREGROUND, 0, 0);
+ LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(500) + TimeUnit.MILLISECONDS.toNanos(
+ CLEAN_UP_UID_DELAY_MILLIS));
+ verify(mNativeWrapperMock, times(1)).halSetThreads(eq(sessionPtr1),
+ eq(SESSION_TIDS_A));
+ verify(mNativeWrapperMock, times(1)).halSetThreads(eq(sessionPtr2),
+ eq(expectedTids2));
+ verifyAllHintsEnabled(session1, true);
+ verifyAllHintsEnabled(session2, true);
+ }
+
+ private void verifyAllHintsEnabled(AppHintSession session, boolean verifyEnabled) {
+ session.reportActualWorkDuration2(new WorkDuration[]{makeWorkDuration(1, 3, 2, 1, 1000)});
+ session.reportActualWorkDuration(new long[]{1}, new long[]{2});
+ session.updateTargetWorkDuration(3);
+ session.setMode(0, true);
+ session.sendHint(1);
+ if (verifyEnabled) {
+ verify(mNativeWrapperMock, times(1)).halReportActualWorkDuration(
+ eq(session.mHalSessionPtr), any());
+ verify(mNativeWrapperMock, times(1)).halSetMode(eq(session.mHalSessionPtr), anyInt(),
+ anyBoolean());
+ verify(mNativeWrapperMock, times(1)).halUpdateTargetWorkDuration(
+ eq(session.mHalSessionPtr), anyLong());
+ verify(mNativeWrapperMock, times(1)).halSendHint(eq(session.mHalSessionPtr), anyInt());
+ } else {
+ verify(mNativeWrapperMock, never()).halReportActualWorkDuration(
+ eq(session.mHalSessionPtr), any());
+ verify(mNativeWrapperMock, never()).halSetMode(eq(session.mHalSessionPtr), anyInt(),
+ anyBoolean());
+ verify(mNativeWrapperMock, never()).halUpdateTargetWorkDuration(
+ eq(session.mHalSessionPtr), anyLong());
+ verify(mNativeWrapperMock, never()).halSendHint(eq(session.mHalSessionPtr), anyInt());
+ }
+ }
+
+ private int[] createThreads(int threadCount, CountDownLatch stopLatch)
+ throws InterruptedException {
+ int[] tids = new int[threadCount];
+ AtomicInteger k = new AtomicInteger(0);
+ CountDownLatch latch = new CountDownLatch(threadCount);
+ for (int j = 0; j < threadCount; j++) {
+ Thread thread = new Thread(() -> {
+ try {
+ tids[k.getAndIncrement()] = android.os.Process.myTid();
+ latch.countDown();
+ stopLatch.await();
+ } catch (InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ });
+ thread.start();
+ }
+ latch.await();
+ return tids;
+ }
+
+ @Test
public void testSetMode() throws Exception {
HintManagerService service = createService();
IBinder token = new Binder();
@@ -457,7 +626,8 @@
// Set session to background, then the duration would not be updated.
service.mUidObserver.onUidStateChanged(
a.mUid, ActivityManager.PROCESS_STATE_TRANSIENT_BACKGROUND, 0, 0);
- FgThread.getHandler().runWithScissors(() -> { }, 500);
+ FgThread.getHandler().runWithScissors(() -> {
+ }, 500);
assertFalse(service.mUidObserver.isUidForeground(a.mUid));
a.setMode(0, true);
verify(mNativeWrapperMock, never()).halSetMode(anyLong(), anyInt(), anyBoolean());
@@ -519,7 +689,10 @@
LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(500));
service.mUidObserver.onUidStateChanged(UID,
ActivityManager.PROCESS_STATE_TRANSIENT_BACKGROUND, 0, 0);
- LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(500));
+ // let the cleanup work proceed
+ LockSupport.parkNanos(
+ TimeUnit.MILLISECONDS.toNanos(500) + TimeUnit.MILLISECONDS.toNanos(
+ CLEAN_UP_UID_DELAY_MILLIS));
}
Log.d(TAG, "notifier thread min " + min + " max " + max + " avg " + sum / count);
service.mUidObserver.onUidGone(UID, true);
diff --git a/services/tests/servicestests/src/com/android/server/power/hint/TEST_MAPPING b/services/tests/servicestests/src/com/android/server/power/hint/TEST_MAPPING
new file mode 100644
index 0000000..2d5df07
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/power/hint/TEST_MAPPING
@@ -0,0 +1,15 @@
+{
+ "postsubmit": [
+ {
+ "name": "FrameworksServicesTests",
+ "options": [
+ {
+ "include-filter": "com.android.server.power.hint"
+ },
+ {
+ "exclude-annotation": "androidx.test.filters.FlakyTest"
+ }
+ ]
+ }
+ ]
+}
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java
index bfc47fd..cee6cdb 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java
@@ -3962,6 +3962,20 @@
}
@Test
+ public void testReadXml_existingPackage_bubblePrefsRestored() throws Exception {
+ mHelper.setBubblesAllowed(PKG_O, UID_O, BUBBLE_PREFERENCE_ALL);
+ assertEquals(BUBBLE_PREFERENCE_ALL, mHelper.getBubblePreference(PKG_O, UID_O));
+
+ mXmlHelper.setBubblesAllowed(PKG_O, UID_O, BUBBLE_PREFERENCE_NONE);
+ assertEquals(BUBBLE_PREFERENCE_NONE, mXmlHelper.getBubblePreference(PKG_O, UID_O));
+
+ ByteArrayOutputStream stream = writeXmlAndPurge(PKG_O, UID_O, false, UserHandle.USER_ALL);
+ loadStreamXml(stream, true, UserHandle.USER_ALL);
+
+ assertEquals(BUBBLE_PREFERENCE_ALL, mXmlHelper.getBubblePreference(PKG_O, UID_O));
+ }
+
+ @Test
public void testUpdateNotificationChannel_fixedPermission() {
List<UserInfo> users = ImmutableList.of(new UserInfo(UserHandle.USER_SYSTEM, "user0", 0));
when(mPermissionHelper.isPermissionFixed(PKG_O, 0)).thenReturn(true);
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 8cbcc22..5861d88 100644
--- a/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java
+++ b/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java
@@ -500,7 +500,8 @@
InOrder batteryVerifier = inOrder(mBatteryStatsMock);
batteryVerifier.verify(mBatteryStatsMock)
.noteVibratorOn(UID, oneShotDuration + mVibrationConfig.getRampDownDurationMs());
- batteryVerifier.verify(mBatteryStatsMock).noteVibratorOff(UID);
+ batteryVerifier
+ .verify(mBatteryStatsMock, timeout(TEST_TIMEOUT_MILLIS)).noteVibratorOff(UID);
}
@Test
diff --git a/services/tests/wmtests/src/com/android/server/policy/PhoneWindowManagerTests.java b/services/tests/wmtests/src/com/android/server/policy/PhoneWindowManagerTests.java
index 29467f2..a80e2f8 100644
--- a/services/tests/wmtests/src/com/android/server/policy/PhoneWindowManagerTests.java
+++ b/services/tests/wmtests/src/com/android/server/policy/PhoneWindowManagerTests.java
@@ -16,10 +16,14 @@
package com.android.server.policy;
+import static android.view.Display.DEFAULT_DISPLAY;
import static android.view.WindowManager.LayoutParams.TYPE_ACCESSIBILITY_OVERLAY;
import static android.view.WindowManager.LayoutParams.TYPE_WALLPAPER;
import static android.view.WindowManagerGlobal.ADD_OKAY;
+import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
+
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock;
@@ -33,18 +37,27 @@
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.clearInvocations;
import android.app.ActivityManager;
import android.app.AppOpsManager;
+import android.content.Context;
+import android.os.PowerManager;
import android.platform.test.flag.junit.SetFlagsRule;
import androidx.test.filters.SmallTest;
+import com.android.server.LocalServices;
import com.android.server.pm.UserManagerInternal;
import com.android.server.wm.ActivityTaskManagerInternal;
+import com.android.server.wm.DisplayPolicy;
+import com.android.server.wm.DisplayRotation;
+import com.android.server.wm.WindowManagerInternal;
import org.junit.After;
import org.junit.Before;
@@ -64,16 +77,27 @@
public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
PhoneWindowManager mPhoneWindowManager;
+ private ActivityTaskManagerInternal mAtmInternal;
+ private Context mContext;
@Before
public void setUp() {
mPhoneWindowManager = spy(new PhoneWindowManager());
spyOn(ActivityManager.getService());
+ mContext = getInstrumentation().getTargetContext();
+ spyOn(mContext);
+ mAtmInternal = mock(ActivityTaskManagerInternal.class);
+ LocalServices.addService(ActivityTaskManagerInternal.class, mAtmInternal);
+ mPhoneWindowManager.mActivityTaskManagerInternal = mAtmInternal;
+ LocalServices.addService(WindowManagerInternal.class, mock(WindowManagerInternal.class));
}
@After
public void tearDown() {
reset(ActivityManager.getService());
+ reset(mContext);
+ LocalServices.removeServiceForTest(ActivityTaskManagerInternal.class);
+ LocalServices.removeServiceForTest(WindowManagerInternal.class);
}
@Test
@@ -99,6 +123,60 @@
}
@Test
+ public void testScreenTurnedOff() {
+ mSetFlagsRule.enableFlags(com.android.window.flags.Flags
+ .FLAG_SKIP_SLEEPING_WHEN_SWITCHING_DISPLAY);
+ doNothing().when(mPhoneWindowManager).updateSettings(any());
+ doNothing().when(mPhoneWindowManager).initializeHdmiState();
+ final boolean[] isScreenTurnedOff = { false };
+ final DisplayPolicy displayPolicy = mock(DisplayPolicy.class);
+ doAnswer(invocation -> isScreenTurnedOff[0] = true).when(displayPolicy).screenTurnedOff();
+ doAnswer(invocation -> !isScreenTurnedOff[0]).when(displayPolicy).isScreenOnEarly();
+ doAnswer(invocation -> !isScreenTurnedOff[0]).when(displayPolicy).isScreenOnFully();
+
+ mPhoneWindowManager.mDefaultDisplayPolicy = displayPolicy;
+ mPhoneWindowManager.mDefaultDisplayRotation = mock(DisplayRotation.class);
+ final ActivityTaskManagerInternal.SleepTokenAcquirer tokenAcquirer =
+ mock(ActivityTaskManagerInternal.SleepTokenAcquirer.class);
+ doReturn(tokenAcquirer).when(mAtmInternal).createSleepTokenAcquirer(anyString());
+ final PowerManager pm = mock(PowerManager.class);
+ doReturn(true).when(pm).isInteractive();
+ doReturn(pm).when(mContext).getSystemService(eq(Context.POWER_SERVICE));
+
+ mContext.getMainThreadHandler().runWithScissors(() -> mPhoneWindowManager.init(
+ new PhoneWindowManager.Injector(mContext,
+ mock(WindowManagerPolicy.WindowManagerFuncs.class))), 0);
+ assertThat(isScreenTurnedOff[0]).isFalse();
+ assertThat(mPhoneWindowManager.mIsGoingToSleepDefaultDisplay).isFalse();
+
+ // Skip sleep-token for non-sleep-screen-off.
+ clearInvocations(tokenAcquirer);
+ mPhoneWindowManager.screenTurnedOff(DEFAULT_DISPLAY, true /* isSwappingDisplay */);
+ verify(tokenAcquirer, never()).acquire(anyInt(), anyBoolean());
+ assertThat(isScreenTurnedOff[0]).isTrue();
+
+ // Apply sleep-token for sleep-screen-off.
+ mPhoneWindowManager.startedGoingToSleep(DEFAULT_DISPLAY, 0 /* reason */);
+ assertThat(mPhoneWindowManager.mIsGoingToSleepDefaultDisplay).isTrue();
+ mPhoneWindowManager.screenTurnedOff(DEFAULT_DISPLAY, true /* isSwappingDisplay */);
+ verify(tokenAcquirer).acquire(eq(DEFAULT_DISPLAY), eq(true));
+
+ mPhoneWindowManager.finishedGoingToSleep(DEFAULT_DISPLAY, 0 /* reason */);
+ assertThat(mPhoneWindowManager.mIsGoingToSleepDefaultDisplay).isFalse();
+
+ // Simulate unexpected reversed order: screenTurnedOff -> startedGoingToSleep. The sleep
+ // token can still be acquired.
+ isScreenTurnedOff[0] = false;
+ clearInvocations(tokenAcquirer);
+ mPhoneWindowManager.screenTurnedOff(DEFAULT_DISPLAY, true /* isSwappingDisplay */);
+ verify(tokenAcquirer, never()).acquire(anyInt(), anyBoolean());
+ assertThat(displayPolicy.isScreenOnEarly()).isFalse();
+ assertThat(displayPolicy.isScreenOnFully()).isFalse();
+ mPhoneWindowManager.startedGoingToSleep(DEFAULT_DISPLAY, 0 /* reason */);
+ verify(tokenAcquirer).acquire(eq(DEFAULT_DISPLAY), eq(false));
+ }
+
+ @Test
public void testCheckAddPermission_withoutAccessibilityOverlay_noAccessibilityAppOpLogged() {
mSetFlagsRule.enableFlags(android.view.contentprotection.flags.Flags
.FLAG_CREATE_ACCESSIBILITY_OVERLAY_APP_OP_ENABLED);
@@ -130,11 +208,8 @@
private void mockStartDockOrHome() throws Exception {
doNothing().when(ActivityManager.getService()).stopAppSwitches();
- ActivityTaskManagerInternal mMockActivityTaskManagerInternal =
- mock(ActivityTaskManagerInternal.class);
- when(mMockActivityTaskManagerInternal.startHomeOnDisplay(
+ when(mAtmInternal.startHomeOnDisplay(
anyInt(), anyString(), anyInt(), anyBoolean(), anyBoolean())).thenReturn(false);
- mPhoneWindowManager.mActivityTaskManagerInternal = mMockActivityTaskManagerInternal;
mPhoneWindowManager.mUserManagerInternal = mock(UserManagerInternal.class);
}
}
diff --git a/services/tests/wmtests/src/com/android/server/policy/ShortcutLoggingTests.java b/services/tests/wmtests/src/com/android/server/policy/ShortcutLoggingTests.java
index 0a29dfb..60716cb 100644
--- a/services/tests/wmtests/src/com/android/server/policy/ShortcutLoggingTests.java
+++ b/services/tests/wmtests/src/com/android/server/policy/ShortcutLoggingTests.java
@@ -95,8 +95,6 @@
new int[]{KeyEvent.KEYCODE_NOTIFICATION},
KeyboardLogEvent.TOGGLE_NOTIFICATION_PANEL, KeyEvent.KEYCODE_NOTIFICATION,
0},
- {"Meta + T -> Toggle Taskbar", new int[]{META_KEY, KeyEvent.KEYCODE_T},
- KeyboardLogEvent.TOGGLE_TASKBAR, KeyEvent.KEYCODE_T, META_ON},
{"Meta + Ctrl + S -> Take Screenshot",
new int[]{META_KEY, CTRL_KEY, KeyEvent.KEYCODE_S},
KeyboardLogEvent.TAKE_SCREENSHOT, KeyEvent.KEYCODE_S, META_ON | CTRL_ON},
diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
index daa5a5a..82e55711 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
@@ -3346,7 +3346,7 @@
} else {
verify(app2.mClient, atLeastOnce()).resized(any(), anyBoolean(), any(),
insetsStateCaptor.capture(), anyBoolean(), anyBoolean(), anyInt(), anyInt(),
- anyBoolean());
+ anyBoolean(), any());
}
assertFalse(app2.getInsetsState().isSourceOrDefaultVisible(ID_IME, ime()));
}
diff --git a/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java
index c29547f..b9e87dc 100644
--- a/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java
@@ -633,18 +633,23 @@
@Test
public void testAdjacentFocusInActivityEmbedding() {
mSetFlagsRule.enableFlags(Flags.FLAG_EMBEDDED_ACTIVITY_BACK_NAV_FLAG);
- Task task = createTask(mDefaultDisplay);
- TaskFragment primary = createTaskFragmentWithActivity(task);
- TaskFragment secondary = createTaskFragmentWithActivity(task);
- primary.setAdjacentTaskFragment(secondary);
- secondary.setAdjacentTaskFragment(primary);
+ final Task task = createTask(mDefaultDisplay);
+ final TaskFragment primaryTf = createTaskFragmentWithActivity(task);
+ final TaskFragment secondaryTf = createTaskFragmentWithActivity(task);
+ final ActivityRecord primaryActivity = primaryTf.getTopMostActivity();
+ final ActivityRecord secondaryActivity = secondaryTf.getTopMostActivity();
+ primaryTf.setAdjacentTaskFragment(secondaryTf);
+ secondaryTf.setAdjacentTaskFragment(primaryTf);
- WindowState windowState = mock(WindowState.class);
+ final WindowState windowState = mock(WindowState.class);
+ windowState.mActivityRecord = primaryActivity;
doReturn(windowState).when(mWm).getFocusedWindowLocked();
- doReturn(primary).when(windowState).getTaskFragment();
+ doReturn(primaryTf).when(windowState).getTaskFragment();
+ doReturn(1L).when(primaryActivity).getLastWindowCreateTime();
+ doReturn(2L).when(secondaryActivity).getLastWindowCreateTime();
startBackNavigation();
- verify(mWm).moveFocusToActivity(any());
+ verify(mWm).moveFocusToActivity(eq(secondaryActivity));
}
/**
diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
index 4e360d0..2c88ed2 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
@@ -1068,16 +1068,6 @@
mDisplayContent.getImeTarget(IME_TARGET_LAYERING));
}
- @SetupWindows(addWindows = W_INPUT_METHOD)
- @Test
- public void testInputMethodSet_listenOnDisplayAreaConfigurationChanged() {
- spyOn(mAtm);
- mDisplayContent.setInputMethodWindowLocked(mImeWindow);
-
- verify(mAtm).onImeWindowSetOnDisplayArea(
- mImeWindow.mSession.mPid, mDisplayContent.getImeContainer());
- }
-
@Test
public void testAllowsTopmostFullscreenOrientation() {
final DisplayContent dc = createNewDisplay();
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java
index 897a3da..52485ee 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java
@@ -25,7 +25,7 @@
import static android.view.WindowManager.TRANSIT_NONE;
import static android.view.WindowManager.TRANSIT_OPEN;
import static android.window.TaskFragmentOperation.OP_TYPE_CREATE_TASK_FRAGMENT;
-import static android.window.TaskFragmentOperation.OP_TYPE_CREATE_TASK_FRAGMENT_DECOR_SURFACE;
+import static android.window.TaskFragmentOperation.OP_TYPE_CREATE_OR_MOVE_TASK_FRAGMENT_DECOR_SURFACE;
import static android.window.TaskFragmentOperation.OP_TYPE_DELETE_TASK_FRAGMENT;
import static android.window.TaskFragmentOperation.OP_TYPE_REMOVE_TASK_FRAGMENT_DECOR_SURFACE;
import static android.window.TaskFragmentOperation.OP_TYPE_REORDER_TO_BOTTOM_OF_TASK;
@@ -1835,7 +1835,7 @@
final TaskFragment tf = createTaskFragment(task);
final TaskFragmentOperation operation = new TaskFragmentOperation.Builder(
- OP_TYPE_CREATE_TASK_FRAGMENT_DECOR_SURFACE).build();
+ OP_TYPE_CREATE_OR_MOVE_TASK_FRAGMENT_DECOR_SURFACE).build();
mTransaction.addTaskFragmentOperation(tf.getFragmentToken(), operation);
assertApplyTransactionAllowed(mTransaction);
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java
index 5360a10..6b1bf26bf 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java
@@ -887,20 +887,14 @@
assertEquals(winLeftTop, mDisplayContent.mCurrentFocus);
if (Flags.embeddedActivityBackNavFlag()) {
- // Send request to move the focus to top window from the left window.
- assertTrue(mWm.moveFocusToTopEmbeddedWindow(winLeftTop));
- // The focus should change.
- assertEquals(winRightTop, mDisplayContent.mCurrentFocus);
+ // Move focus if the adjacent activity is more recently active.
+ doReturn(1L).when(appLeftTop).getLastWindowCreateTime();
+ doReturn(2L).when(appRightTop).getLastWindowCreateTime();
+ assertTrue(mWm.moveFocusToAdjacentEmbeddedWindow(winLeftTop));
- // Send request to move the focus to top window from the right window.
- assertFalse(mWm.moveFocusToTopEmbeddedWindow(winRightTop));
- // The focus should NOT change.
- assertEquals(winRightTop, mDisplayContent.mCurrentFocus);
-
- // Do not move focus if the dim is boosted.
- taskFragmentLeft.mDimmerSurfaceBoosted = true;
- assertFalse(mWm.moveFocusToTopEmbeddedWindow(winLeftTop));
- assertEquals(winRightTop, mDisplayContent.mCurrentFocus);
+ // Do not move the focus if the adjacent activity is less recently active.
+ doReturn(3L).when(appLeftTop).getLastWindowCreateTime();
+ assertFalse(mWm.moveFocusToAdjacentEmbeddedWindow(winLeftTop));
}
}
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskTests.java b/services/tests/wmtests/src/com/android/server/wm/TaskTests.java
index 3bd6496..a88680a 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskTests.java
@@ -1945,6 +1945,21 @@
assertEquals(2, finishCount[0]);
}
+ @Test
+ public void testPauseActivityWhenHasEmptyLeafTaskFragment() {
+ // Creating a task that has a RESUMED activity and an empty TaskFragment.
+ final Task task = new TaskBuilder(mSupervisor).setCreateActivity(true).build();
+ final ActivityRecord activity = task.getTopMostActivity();
+ new TaskFragmentBuilder(mAtm).setParentTask(task).build();
+ activity.setState(ActivityRecord.State.RESUMED, "test");
+
+ // Ensure the activity is paused if cannot be resumed.
+ doReturn(false).when(task).canBeResumed(any());
+ mSupervisor.mUserLeaving = true;
+ task.pauseActivityIfNeeded(null /* resuming */, "test");
+ verify(task).startPausing(eq(true) /* userLeaving */, anyBoolean(), any(), any());
+ }
+
private Task getTestTask() {
return new TaskBuilder(mSupervisor).setCreateActivity(true).build();
}
diff --git a/services/tests/wmtests/src/com/android/server/wm/TestIWindow.java b/services/tests/wmtests/src/com/android/server/wm/TestIWindow.java
index 3f8acc6..37de51e 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TestIWindow.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TestIWindow.java
@@ -28,6 +28,7 @@
import android.view.InsetsState;
import android.view.ScrollCaptureResponse;
import android.view.inputmethod.ImeTracker;
+import android.window.ActivityWindowInfo;
import android.window.ClientWindowFrames;
import com.android.internal.os.IResultReceiver;
@@ -46,8 +47,8 @@
@Override
public void resized(ClientWindowFrames frames, boolean reportDraw,
MergedConfiguration mergedConfig, InsetsState insetsState, boolean forceLayout,
- boolean alwaysConsumeSystemBars, int displayId, int seqId, boolean dragResizing)
- throws RemoteException {
+ boolean alwaysConsumeSystemBars, int displayId, int seqId, boolean dragResizing,
+ @Nullable ActivityWindowInfo activityWindowInfo) throws RemoteException {
}
@Override
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java
index 12f46df..48b12f7 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java
@@ -90,6 +90,7 @@
import android.util.MergedConfiguration;
import android.view.ContentRecordingSession;
import android.view.IWindow;
+import android.view.IWindowSession;
import android.view.InputChannel;
import android.view.InsetsSourceControl;
import android.view.InsetsState;
@@ -99,6 +100,7 @@
import android.view.WindowInsets;
import android.view.WindowManager;
import android.view.WindowManager.LayoutParams;
+import android.window.ActivityWindowInfo;
import android.window.ClientWindowFrames;
import android.window.InputTransferToken;
import android.window.ScreenCapture;
@@ -1216,6 +1218,35 @@
mWm.reportKeepClearAreasChanged(session, window, new ArrayList<>(), new ArrayList<>());
}
+ @Test
+ public void testRelayout_appWindowSendActivityWindowInfo() {
+ mSetFlagsRule.enableFlags(Flags.FLAG_ACTIVITY_WINDOW_INFO_FLAG);
+
+ // Skip unnecessary operations of relayout.
+ spyOn(mWm.mWindowPlacerLocked);
+ doNothing().when(mWm.mWindowPlacerLocked).performSurfacePlacement(anyBoolean());
+
+ final Task task = createTask(mDisplayContent);
+ final WindowState win = createAppWindow(task, ACTIVITY_TYPE_STANDARD, "appWindow");
+ mWm.mWindowMap.put(win.mClient.asBinder(), win);
+
+ final int w = 100;
+ final int h = 200;
+ final ClientWindowFrames outFrames = new ClientWindowFrames();
+ final MergedConfiguration outConfig = new MergedConfiguration();
+ final SurfaceControl outSurfaceControl = new SurfaceControl();
+ final InsetsState outInsetsState = new InsetsState();
+ final InsetsSourceControl.Array outControls = new InsetsSourceControl.Array();
+ final Bundle outBundle = new Bundle();
+
+ mWm.relayoutWindow(win.mSession, win.mClient, win.mAttrs, w, h, View.GONE, 0, 0, 0,
+ outFrames, outConfig, outSurfaceControl, outInsetsState, outControls, outBundle);
+
+ final ActivityWindowInfo activityWindowInfo = outBundle.getParcelable(
+ IWindowSession.KEY_RELAYOUT_BUNDLE_ACTIVITY_WINDOW_INFO, ActivityWindowInfo.class);
+ assertEquals(win.mActivityRecord.getActivityWindowInfo(), activityWindowInfo);
+ }
+
class TestResultReceiver implements IResultReceiver {
public android.os.Bundle resultData;
private final IBinder mBinder = mock(IBinder.class);
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java
index c8ad4bd..e20f822 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java
@@ -804,7 +804,8 @@
anyBoolean() /* reportDraw */, any() /* mergedConfig */,
any() /* insetsState */, anyBoolean() /* forceLayout */,
anyBoolean() /* alwaysConsumeSystemBars */, anyInt() /* displayId */,
- anyInt() /* seqId */, anyBoolean() /* dragResizing */);
+ anyInt() /* seqId */, anyBoolean() /* dragResizing */,
+ any() /* activityWindowInfo */);
} catch (RemoteException ignored) {
}
win.reportResized();
diff --git a/services/usage/java/com/android/server/usage/StorageStatsService.java b/services/usage/java/com/android/server/usage/StorageStatsService.java
index 883c702..e9da53a 100644
--- a/services/usage/java/com/android/server/usage/StorageStatsService.java
+++ b/services/usage/java/com/android/server/usage/StorageStatsService.java
@@ -968,22 +968,20 @@
stats.libSize += getDirBytes(new File(sourceDirName + "/lib/"));
// Get dexopt, current profle and reference profile sizes.
- if (SystemProperties.getBoolean("dalvik.vm.features.art_managed_file_stats", false)) {
- ArtManagedFileStats artManagedFileStats;
- try (var snapshot = getPackageManagerLocal().withFilteredSnapshot()) {
- artManagedFileStats =
- getArtManagerLocal().getArtManagedFileStats(snapshot, packageName);
- }
-
- stats.dexoptSize +=
- artManagedFileStats
- .getTotalSizeBytesByType(ArtManagedFileStats.TYPE_DEXOPT_ARTIFACT);
- stats.refProfSize +=
- artManagedFileStats
- .getTotalSizeBytesByType(ArtManagedFileStats.TYPE_REF_PROFILE);
- stats.curProfSize +=
- artManagedFileStats
- .getTotalSizeBytesByType(ArtManagedFileStats.TYPE_CUR_PROFILE);
+ ArtManagedFileStats artManagedFileStats;
+ try (var snapshot = getPackageManagerLocal().withFilteredSnapshot()) {
+ artManagedFileStats =
+ getArtManagerLocal().getArtManagedFileStats(snapshot, packageName);
}
+
+ stats.dexoptSize +=
+ artManagedFileStats
+ .getTotalSizeBytesByType(ArtManagedFileStats.TYPE_DEXOPT_ARTIFACT);
+ stats.refProfSize +=
+ artManagedFileStats
+ .getTotalSizeBytesByType(ArtManagedFileStats.TYPE_REF_PROFILE);
+ stats.curProfSize +=
+ artManagedFileStats
+ .getTotalSizeBytesByType(ArtManagedFileStats.TYPE_CUR_PROFILE);
}
}
diff --git a/telephony/java/android/telephony/satellite/stub/ISatellite.aidl b/telephony/java/android/telephony/satellite/stub/ISatellite.aidl
index 9441fb5..36485c6 100644
--- a/telephony/java/android/telephony/satellite/stub/ISatellite.aidl
+++ b/telephony/java/android/telephony/satellite/stub/ISatellite.aidl
@@ -347,28 +347,6 @@
in IIntegerConsumer callback);
/**
- * Request to get whether satellite communication is allowed for the current location.
- *
- * @param resultCallback The callback to receive the error code result of the operation.
- * This must only be sent when the result is not
- * SatelliteResult#SATELLITE_RESULT_SUCCESS.
- * @param callback If the result is SatelliteResult#SATELLITE_RESULT_SUCCESS, the callback to
- * receive whether satellite communication is allowed for the current location.
- *
- * Valid result codes returned:
- * SatelliteResult:SATELLITE_RESULT_SUCCESS
- * SatelliteResult:SATELLITE_RESULT_SERVICE_ERROR
- * SatelliteResult:SATELLITE_RESULT_MODEM_ERROR
- * SatelliteResult:SATELLITE_RESULT_INVALID_MODEM_STATE
- * SatelliteResult:SATELLITE_RESULT_INVALID_ARGUMENTS
- * SatelliteResult:SATELLITE_RESULT_RADIO_NOT_AVAILABLE
- * SatelliteResult:SATELLITE_RESULT_REQUEST_NOT_SUPPORTED
- * SatelliteResult:SATELLITE_RESULT_NO_RESOURCES
- */
- void requestIsSatelliteCommunicationAllowedForCurrentLocation(
- in IIntegerConsumer resultCallback, in IBooleanConsumer callback);
-
- /**
* Request to get the time after which the satellite will be visible. This is an int
* representing the duration in seconds after which the satellite will be visible.
* This will return 0 if the satellite is currently visible.
diff --git a/telephony/java/android/telephony/satellite/stub/SatelliteImplBase.java b/telephony/java/android/telephony/satellite/stub/SatelliteImplBase.java
index f17ff17..b7dc79f 100644
--- a/telephony/java/android/telephony/satellite/stub/SatelliteImplBase.java
+++ b/telephony/java/android/telephony/satellite/stub/SatelliteImplBase.java
@@ -194,17 +194,6 @@
}
@Override
- public void requestIsSatelliteCommunicationAllowedForCurrentLocation(
- IIntegerConsumer resultCallback, IBooleanConsumer callback)
- throws RemoteException {
- executeMethodAsync(
- () -> SatelliteImplBase.this
- .requestIsSatelliteCommunicationAllowedForCurrentLocation(
- resultCallback, callback),
- "requestIsCommunicationAllowedForCurrentLocation");
- }
-
- @Override
public void requestTimeForNextSatelliteVisibility(IIntegerConsumer resultCallback,
IIntegerConsumer callback) throws RemoteException {
executeMethodAsync(
@@ -638,30 +627,6 @@
}
/**
- * Request to get whether satellite communication is allowed for the current location.
- *
- * @param resultCallback The callback to receive the error code result of the operation.
- * This must only be sent when the result is not
- * SatelliteResult#SATELLITE_RESULT_SUCCESS.
- * @param callback If the result is SatelliteResult#SATELLITE_RESULT_SUCCESS, the callback to
- * receive whether satellite communication is allowed for the current location.
- *
- * Valid result codes returned:
- * SatelliteResult:SATELLITE_RESULT_SUCCESS
- * SatelliteResult:SATELLITE_RESULT_SERVICE_ERROR
- * SatelliteResult:SATELLITE_RESULT_MODEM_ERROR
- * SatelliteResult:SATELLITE_RESULT_INVALID_MODEM_STATE
- * SatelliteResult:SATELLITE_RESULT_INVALID_ARGUMENTS
- * SatelliteResult:SATELLITE_RESULT_RADIO_NOT_AVAILABLE
- * SatelliteResult:SATELLITE_RESULT_REQUEST_NOT_SUPPORTED
- * SatelliteResult:SATELLITE_RESULT_NO_RESOURCES
- */
- public void requestIsSatelliteCommunicationAllowedForCurrentLocation(
- @NonNull IIntegerConsumer resultCallback, @NonNull IBooleanConsumer callback) {
- // stub implementation
- }
-
- /**
* Request to get the time after which the satellite will be visible. This is an int
* representing the duration in seconds after which the satellite will be visible.
* This will return 0 if the satellite is currently visible.
diff --git a/test-base/Android.bp b/test-base/Android.bp
index 70a9540..d65a4e4 100644
--- a/test-base/Android.bp
+++ b/test-base/Android.bp
@@ -14,37 +14,22 @@
// limitations under the License.
//
+package {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
// Build the android.test.base library
// ===================================
// This contains the junit.framework and android.test classes that were in
// Android API level 25 excluding those from android.test.runner.
// Also contains the com.android.internal.util.Predicate[s] classes.
-package {
- // See: http://go/android-license-faq
- // A large-scale-change added 'default_applicable_licenses' to import
- // all of the 'license_kinds' from "frameworks_base_license"
- // to get the below license kinds:
- // SPDX-license-identifier-Apache-2.0
- // SPDX-license-identifier-CPL-1.0
- default_applicable_licenses: ["frameworks_base_test-base_license"],
-}
-
-license {
- name: "frameworks_base_test-base_license",
- visibility: [":__subpackages__"],
- license_kinds: [
- "SPDX-license-identifier-Apache-2.0",
- "SPDX-license-identifier-CPL-1.0",
- ],
- license_text: [
- "src/junit/cpl-v10.html",
- ],
-}
-
java_sdk_library {
name: "android.test.base",
- srcs: [":android-test-base-sources"],
+ srcs: [
+ ":android-test-base-sources",
+ ":frameworks-base-test-junit-framework",
+ ],
errorprone: {
javacflags: ["-Xep:DepAnn:ERROR"],
@@ -84,7 +69,10 @@
],
installable: false,
- srcs: [":android-test-base-sources"],
+ srcs: [
+ ":android-test-base-sources",
+ ":frameworks-base-test-junit-framework",
+ ],
errorprone: {
javacflags: ["-Xep:DepAnn:ERROR"],
@@ -104,8 +92,7 @@
name: "android.test.base-minus-junit",
srcs: [
- "src/android/**/*.java",
- "src/com/**/*.java",
+ "src/**/*.java",
],
sdk_version: "current",
diff --git a/test-base/hiddenapi/Android.bp b/test-base/hiddenapi/Android.bp
index 1466590..4c59b10 100644
--- a/test-base/hiddenapi/Android.bp
+++ b/test-base/hiddenapi/Android.bp
@@ -15,12 +15,7 @@
//
package {
- // See: http://go/android-license-faq
- // A large-scale-change added 'default_applicable_licenses' to import
- // all of the 'license_kinds' from "frameworks_base_license"
- // to get the below license kinds:
- // SPDX-license-identifier-Apache-2.0
- default_applicable_licenses: ["frameworks_base_license"],
+ default_applicable_licenses: ["Android-Apache-2.0"],
}
// Provided solely to contribute information about which hidden parts of the android.test.base
diff --git a/test-junit/Android.bp b/test-junit/Android.bp
new file mode 100644
index 0000000..8d3d439
--- /dev/null
+++ b/test-junit/Android.bp
@@ -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 {
+ default_applicable_licenses: ["frameworks-base-test-junit-license"],
+}
+
+license {
+ name: "frameworks-base-test-junit-license",
+ visibility: [":__subpackages__"],
+ license_kinds: [
+ "SPDX-license-identifier-CPL-1.0",
+ ],
+ license_text: [
+ "src/junit/cpl-v10.html",
+ ],
+}
+
+filegroup {
+ name: "frameworks-base-test-junit-framework",
+ srcs: [
+ "src/junit/framework/**/*.java",
+ ],
+ path: "src",
+ visibility: [
+ "//frameworks/base/test-base",
+ ],
+}
+
+filegroup {
+ name: "frameworks-base-test-junit-runner",
+ srcs: [
+ "src/junit/runner/**/*.java",
+ "src/junit/textui/**/*.java",
+ ],
+ path: "src",
+ visibility: [
+ "//frameworks/base/test-runner",
+ ],
+}
diff --git a/test-base/src/junit/MODULE_LICENSE_CPL b/test-junit/src/junit/MODULE_LICENSE_CPL
similarity index 100%
rename from test-base/src/junit/MODULE_LICENSE_CPL
rename to test-junit/src/junit/MODULE_LICENSE_CPL
diff --git a/test-base/src/junit/README.android b/test-junit/src/junit/README.android
similarity index 100%
rename from test-base/src/junit/README.android
rename to test-junit/src/junit/README.android
diff --git a/test-base/src/junit/cpl-v10.html b/test-junit/src/junit/cpl-v10.html
similarity index 100%
rename from test-base/src/junit/cpl-v10.html
rename to test-junit/src/junit/cpl-v10.html
diff --git a/test-base/src/junit/framework/Assert.java b/test-junit/src/junit/framework/Assert.java
similarity index 100%
rename from test-base/src/junit/framework/Assert.java
rename to test-junit/src/junit/framework/Assert.java
diff --git a/test-base/src/junit/framework/AssertionFailedError.java b/test-junit/src/junit/framework/AssertionFailedError.java
similarity index 100%
rename from test-base/src/junit/framework/AssertionFailedError.java
rename to test-junit/src/junit/framework/AssertionFailedError.java
diff --git a/test-base/src/junit/framework/ComparisonCompactor.java b/test-junit/src/junit/framework/ComparisonCompactor.java
similarity index 100%
rename from test-base/src/junit/framework/ComparisonCompactor.java
rename to test-junit/src/junit/framework/ComparisonCompactor.java
diff --git a/test-base/src/junit/framework/ComparisonFailure.java b/test-junit/src/junit/framework/ComparisonFailure.java
similarity index 100%
rename from test-base/src/junit/framework/ComparisonFailure.java
rename to test-junit/src/junit/framework/ComparisonFailure.java
diff --git a/test-base/src/junit/framework/Protectable.java b/test-junit/src/junit/framework/Protectable.java
similarity index 100%
rename from test-base/src/junit/framework/Protectable.java
rename to test-junit/src/junit/framework/Protectable.java
diff --git a/test-base/src/junit/framework/Test.java b/test-junit/src/junit/framework/Test.java
similarity index 100%
rename from test-base/src/junit/framework/Test.java
rename to test-junit/src/junit/framework/Test.java
diff --git a/test-base/src/junit/framework/TestCase.java b/test-junit/src/junit/framework/TestCase.java
similarity index 100%
rename from test-base/src/junit/framework/TestCase.java
rename to test-junit/src/junit/framework/TestCase.java
diff --git a/test-base/src/junit/framework/TestFailure.java b/test-junit/src/junit/framework/TestFailure.java
similarity index 100%
rename from test-base/src/junit/framework/TestFailure.java
rename to test-junit/src/junit/framework/TestFailure.java
diff --git a/test-base/src/junit/framework/TestListener.java b/test-junit/src/junit/framework/TestListener.java
similarity index 100%
rename from test-base/src/junit/framework/TestListener.java
rename to test-junit/src/junit/framework/TestListener.java
diff --git a/test-base/src/junit/framework/TestResult.java b/test-junit/src/junit/framework/TestResult.java
similarity index 100%
rename from test-base/src/junit/framework/TestResult.java
rename to test-junit/src/junit/framework/TestResult.java
diff --git a/test-base/src/junit/framework/TestSuite.java b/test-junit/src/junit/framework/TestSuite.java
similarity index 100%
rename from test-base/src/junit/framework/TestSuite.java
rename to test-junit/src/junit/framework/TestSuite.java
diff --git a/test-runner/src/junit/runner/BaseTestRunner.java b/test-junit/src/junit/runner/BaseTestRunner.java
similarity index 100%
rename from test-runner/src/junit/runner/BaseTestRunner.java
rename to test-junit/src/junit/runner/BaseTestRunner.java
diff --git a/test-runner/src/junit/runner/StandardTestSuiteLoader.java b/test-junit/src/junit/runner/StandardTestSuiteLoader.java
similarity index 100%
rename from test-runner/src/junit/runner/StandardTestSuiteLoader.java
rename to test-junit/src/junit/runner/StandardTestSuiteLoader.java
diff --git a/test-runner/src/junit/runner/TestRunListener.java b/test-junit/src/junit/runner/TestRunListener.java
similarity index 100%
rename from test-runner/src/junit/runner/TestRunListener.java
rename to test-junit/src/junit/runner/TestRunListener.java
diff --git a/test-runner/src/junit/runner/TestSuiteLoader.java b/test-junit/src/junit/runner/TestSuiteLoader.java
similarity index 100%
rename from test-runner/src/junit/runner/TestSuiteLoader.java
rename to test-junit/src/junit/runner/TestSuiteLoader.java
diff --git a/test-runner/src/junit/runner/Version.java b/test-junit/src/junit/runner/Version.java
similarity index 100%
rename from test-runner/src/junit/runner/Version.java
rename to test-junit/src/junit/runner/Version.java
diff --git a/test-runner/src/junit/runner/package-info.java b/test-junit/src/junit/runner/package-info.java
similarity index 100%
rename from test-runner/src/junit/runner/package-info.java
rename to test-junit/src/junit/runner/package-info.java
diff --git a/test-runner/src/junit/textui/ResultPrinter.java b/test-junit/src/junit/textui/ResultPrinter.java
similarity index 100%
rename from test-runner/src/junit/textui/ResultPrinter.java
rename to test-junit/src/junit/textui/ResultPrinter.java
diff --git a/test-runner/src/junit/textui/TestRunner.java b/test-junit/src/junit/textui/TestRunner.java
similarity index 100%
rename from test-runner/src/junit/textui/TestRunner.java
rename to test-junit/src/junit/textui/TestRunner.java
diff --git a/test-runner/src/junit/textui/package-info.java b/test-junit/src/junit/textui/package-info.java
similarity index 100%
rename from test-runner/src/junit/textui/package-info.java
rename to test-junit/src/junit/textui/package-info.java
diff --git a/test-mock/Android.bp b/test-mock/Android.bp
index f37d2d1..e29d321 100644
--- a/test-mock/Android.bp
+++ b/test-mock/Android.bp
@@ -17,12 +17,7 @@
// Build the android.test.mock library
// ===================================
package {
- // See: http://go/android-license-faq
- // A large-scale-change added 'default_applicable_licenses' to import
- // all of the 'license_kinds' from "frameworks_base_license"
- // to get the below license kinds:
- // SPDX-license-identifier-Apache-2.0
- default_applicable_licenses: ["frameworks_base_license"],
+ default_applicable_licenses: ["Android-Apache-2.0"],
}
java_sdk_library {
diff --git a/test-runner/Android.bp b/test-runner/Android.bp
index 21e09d3..6b5be3cb 100644
--- a/test-runner/Android.bp
+++ b/test-runner/Android.bp
@@ -14,29 +14,19 @@
// limitations under the License.
//
+package {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
// Build the android.test.runner library
// =====================================
-package {
- // See: http://go/android-license-faq
- default_applicable_licenses: ["frameworks_base_test-runner_license"],
-}
-
-license {
- name: "frameworks_base_test-runner_license",
- visibility: [":__subpackages__"],
- license_kinds: [
- "SPDX-license-identifier-Apache-2.0",
- "SPDX-license-identifier-CPL-1.0",
- ],
- license_text: [
- "src/junit/cpl-v10.html",
- ],
-}
-
java_sdk_library {
name: "android.test.runner",
- srcs: [":android-test-runner-sources"],
+ srcs: [
+ ":android-test-runner-sources",
+ ":frameworks-base-test-junit-runner",
+ ],
errorprone: {
javacflags: ["-Xep:DepAnn:ERROR"],
diff --git a/test-runner/src/junit/MODULE_LICENSE_CPL b/test-runner/src/junit/MODULE_LICENSE_CPL
deleted file mode 100644
index e69de29..0000000
--- a/test-runner/src/junit/MODULE_LICENSE_CPL
+++ /dev/null
diff --git a/test-runner/src/junit/README.android b/test-runner/src/junit/README.android
deleted file mode 100644
index 1384a1f..0000000
--- a/test-runner/src/junit/README.android
+++ /dev/null
@@ -1,11 +0,0 @@
-URL: https://github.com/junit-team/junit4
-License: Common Public License Version 1.0
-License File: cpl-v10.html
-
-This is JUnit 4.10 source that was previously part of the Android Public API.
-Where necessary it has been patched to be compatible (according to Android API
-requirements) with JUnit 3.8.
-
-These are copied here to ensure that the android.test.runner target remains
-compatible with the last version of the Android API (25) that contained these
-classes even when external/junit is upgraded to a later version.
diff --git a/test-runner/src/junit/cpl-v10.html b/test-runner/src/junit/cpl-v10.html
deleted file mode 100644
index 36aa208..0000000
--- a/test-runner/src/junit/cpl-v10.html
+++ /dev/null
@@ -1,125 +0,0 @@
-<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN">
-<HTML>
-<HEAD>
-<TITLE>Common Public License - v 1.0</TITLE>
-<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1">
-</HEAD>
-
-<BODY BGCOLOR="#FFFFFF" VLINK="#800000">
-
-
-<P ALIGN="CENTER"><B>Common Public License - v 1.0</B>
-<P><B></B><FONT SIZE="3"></FONT>
-<P><FONT SIZE="3"></FONT><FONT SIZE="2">THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS COMMON PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT.</FONT>
-<P><FONT SIZE="2"></FONT>
-<P><FONT SIZE="2"><B>1. DEFINITIONS</B></FONT>
-<P><FONT SIZE="2">"Contribution" means:</FONT>
-
-<UL><FONT SIZE="2">a) in the case of the initial Contributor, the initial code and documentation distributed under this Agreement, and<BR CLEAR="LEFT">
-b) in the case of each subsequent Contributor:</FONT></UL>
-
-
-<UL><FONT SIZE="2">i) changes to the Program, and</FONT></UL>
-
-
-<UL><FONT SIZE="2">ii) additions to the Program;</FONT></UL>
-
-
-<UL><FONT SIZE="2">where such changes and/or additions to the Program originate from and are distributed by that particular Contributor. </FONT><FONT SIZE="2">A Contribution 'originates' from a Contributor if it was added to the Program by such Contributor itself or anyone acting on such Contributor's behalf. </FONT><FONT SIZE="2">Contributions do not include additions to the Program which: (i) are separate modules of software distributed in conjunction with the Program under their own license agreement, and (ii) are not derivative works of the Program. </FONT></UL>
-
-<P><FONT SIZE="2"></FONT>
-<P><FONT SIZE="2">"Contributor" means any person or entity that distributes the Program.</FONT>
-<P><FONT SIZE="2"></FONT><FONT SIZE="2"></FONT>
-<P><FONT SIZE="2">"Licensed Patents " mean patent claims licensable by a Contributor which are necessarily infringed by the use or sale of its Contribution alone or when combined with the Program. </FONT>
-<P><FONT SIZE="2"></FONT><FONT SIZE="2"></FONT>
-<P><FONT SIZE="2"></FONT><FONT SIZE="2">"Program" means the Contributions distributed in accordance with this Agreement.</FONT>
-<P><FONT SIZE="2"></FONT>
-<P><FONT SIZE="2">"Recipient" means anyone who receives the Program under this Agreement, including all Contributors.</FONT>
-<P><FONT SIZE="2"><B></B></FONT>
-<P><FONT SIZE="2"><B>2. GRANT OF RIGHTS</B></FONT>
-
-<UL><FONT SIZE="2"></FONT><FONT SIZE="2">a) </FONT><FONT SIZE="2">Subject to the terms of this Agreement, each Contributor hereby grants</FONT><FONT SIZE="2"> Recipient a non-exclusive, worldwide, royalty-free copyright license to</FONT><FONT SIZE="2" COLOR="#FF0000"> </FONT><FONT SIZE="2">reproduce, prepare derivative works of, publicly display, publicly perform, distribute and sublicense the Contribution of such Contributor, if any, and such derivative works, in source code and object code form.</FONT></UL>
-
-
-<UL><FONT SIZE="2"></FONT></UL>
-
-
-<UL><FONT SIZE="2"></FONT><FONT SIZE="2">b) Subject to the terms of this Agreement, each Contributor hereby grants </FONT><FONT SIZE="2">Recipient a non-exclusive, worldwide,</FONT><FONT SIZE="2" COLOR="#008000"> </FONT><FONT SIZE="2">royalty-free patent license under Licensed Patents to make, use, sell, offer to sell, import and otherwise transfer the Contribution of such Contributor, if any, in source code and object code form. This patent license shall apply to the combination of the Contribution and the Program if, at the time the Contribution is added by the Contributor, such addition of the Contribution causes such combination to be covered by the Licensed Patents. The patent license shall not apply to any other combinations which include the Contribution. No hardware per se is licensed hereunder. </FONT></UL>
-
-
-<UL><FONT SIZE="2"></FONT></UL>
-
-
-<UL><FONT SIZE="2">c) Recipient understands that although each Contributor grants the licenses to its Contributions set forth herein, no assurances are provided by any Contributor that the Program does not infringe the patent or other intellectual property rights of any other entity. Each Contributor disclaims any liability to Recipient for claims brought by any other entity based on infringement of intellectual property rights or otherwise. As a condition to exercising the rights and licenses granted hereunder, each Recipient hereby assumes sole responsibility to secure any other intellectual property rights needed, if any. For example, if a third party patent license is required to allow Recipient to distribute the Program, it is Recipient's responsibility to acquire that license before distributing the Program.</FONT></UL>
-
-
-<UL><FONT SIZE="2"></FONT></UL>
-
-
-<UL><FONT SIZE="2">d) Each Contributor represents that to its knowledge it has sufficient copyright rights in its Contribution, if any, to grant the copyright license set forth in this Agreement. </FONT></UL>
-
-
-<UL><FONT SIZE="2"></FONT></UL>
-
-<P><FONT SIZE="2"><B>3. REQUIREMENTS</B></FONT>
-<P><FONT SIZE="2"><B></B>A Contributor may choose to distribute the Program in object code form under its own license agreement, provided that:</FONT>
-
-<UL><FONT SIZE="2">a) it complies with the terms and conditions of this Agreement; and</FONT></UL>
-
-
-<UL><FONT SIZE="2">b) its license agreement:</FONT></UL>
-
-
-<UL><FONT SIZE="2">i) effectively disclaims</FONT><FONT SIZE="2"> on behalf of all Contributors all warranties and conditions, express and implied, including warranties or conditions of title and non-infringement, and implied warranties or conditions of merchantability and fitness for a particular purpose; </FONT></UL>
-
-
-<UL><FONT SIZE="2">ii) effectively excludes on behalf of all Contributors all liability for damages, including direct, indirect, special, incidental and consequential damages, such as lost profits; </FONT></UL>
-
-
-<UL><FONT SIZE="2">iii)</FONT><FONT SIZE="2"> states that any provisions which differ from this Agreement are offered by that Contributor alone and not by any other party; and</FONT></UL>
-
-
-<UL><FONT SIZE="2">iv) states that source code for the Program is available from such Contributor, and informs licensees how to obtain it in a reasonable manner on or through a medium customarily used for software exchange.</FONT><FONT SIZE="2" COLOR="#0000FF"> </FONT><FONT SIZE="2" COLOR="#FF0000"></FONT></UL>
-
-
-<UL><FONT SIZE="2" COLOR="#FF0000"></FONT><FONT SIZE="2"></FONT></UL>
-
-<P><FONT SIZE="2">When the Program is made available in source code form:</FONT>
-
-<UL><FONT SIZE="2">a) it must be made available under this Agreement; and </FONT></UL>
-
-
-<UL><FONT SIZE="2">b) a copy of this Agreement must be included with each copy of the Program. </FONT></UL>
-
-<P><FONT SIZE="2"></FONT><FONT SIZE="2" COLOR="#0000FF"><STRIKE></STRIKE></FONT>
-<P><FONT SIZE="2" COLOR="#0000FF"><STRIKE></STRIKE></FONT><FONT SIZE="2">Contributors may not remove or alter any copyright notices contained within the Program. </FONT>
-<P><FONT SIZE="2"></FONT>
-<P><FONT SIZE="2">Each Contributor must identify itself as the originator of its Contribution, if any, in a manner that reasonably allows subsequent Recipients to identify the originator of the Contribution. </FONT>
-<P><FONT SIZE="2"></FONT>
-<P><FONT SIZE="2"><B>4. COMMERCIAL DISTRIBUTION</B></FONT>
-<P><FONT SIZE="2">Commercial distributors of software may accept certain responsibilities with respect to end users, business partners and the like. While this license is intended to facilitate the commercial use of the Program, the Contributor who includes the Program in a commercial product offering should do so in a manner which does not create potential liability for other Contributors. Therefore, if a Contributor includes the Program in a commercial product offering, such Contributor ("Commercial Contributor") hereby agrees to defend and indemnify every other Contributor ("Indemnified Contributor") against any losses, damages and costs (collectively "Losses") arising from claims, lawsuits and other legal actions brought by a third party against the Indemnified Contributor to the extent caused by the acts or omissions of such Commercial Contributor in connection with its distribution of the Program in a commercial product offering. The obligations in this section do not apply to any claims or Losses relating to any actual or alleged intellectual property infringement. In order to qualify, an Indemnified Contributor must: a) promptly notify the Commercial Contributor in writing of such claim, and b) allow the Commercial Contributor to control, and cooperate with the Commercial Contributor in, the defense and any related settlement negotiations. The Indemnified Contributor may participate in any such claim at its own expense.</FONT>
-<P><FONT SIZE="2"></FONT>
-<P><FONT SIZE="2">For example, a Contributor might include the Program in a commercial product offering, Product X. That Contributor is then a Commercial Contributor. If that Commercial Contributor then makes performance claims, or offers warranties related to Product X, those performance claims and warranties are such Commercial Contributor's responsibility alone. Under this section, the Commercial Contributor would have to defend claims against the other Contributors related to those performance claims and warranties, and if a court requires any other Contributor to pay any damages as a result, the Commercial Contributor must pay those damages.</FONT>
-<P><FONT SIZE="2"></FONT><FONT SIZE="2" COLOR="#0000FF"></FONT>
-<P><FONT SIZE="2" COLOR="#0000FF"></FONT><FONT SIZE="2"><B>5. NO WARRANTY</B></FONT>
-<P><FONT SIZE="2">EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Each Recipient is</FONT><FONT SIZE="2"> solely responsible for determining the appropriateness of using and distributing </FONT><FONT SIZE="2">the Program</FONT><FONT SIZE="2"> and assumes all risks associated with its exercise of rights under this Agreement</FONT><FONT SIZE="2">, including but not limited to the risks and costs of program errors, compliance with applicable laws, damage to or loss of data, </FONT><FONT SIZE="2">programs or equipment, and unavailability or interruption of operations</FONT><FONT SIZE="2">. </FONT><FONT SIZE="2"></FONT>
-<P><FONT SIZE="2"></FONT>
-<P><FONT SIZE="2"></FONT><FONT SIZE="2"><B>6. DISCLAIMER OF LIABILITY</B></FONT>
-<P><FONT SIZE="2"></FONT><FONT SIZE="2">EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR ANY CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES </FONT><FONT SIZE="2">(INCLUDING WITHOUT LIMITATION LOST PROFITS),</FONT><FONT SIZE="2"> HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.</FONT>
-<P><FONT SIZE="2"></FONT><FONT SIZE="2"></FONT>
-<P><FONT SIZE="2"><B>7. GENERAL</B></FONT>
-<P><FONT SIZE="2"></FONT><FONT SIZE="2">If any provision of this Agreement is invalid or unenforceable under applicable law, it shall not affect the validity or enforceability of the remainder of the terms of this Agreement, and without further action by the parties hereto, such provision shall be reformed to the minimum extent necessary to make such provision valid and enforceable.</FONT>
-<P><FONT SIZE="2"></FONT>
-<P><FONT SIZE="2">If Recipient institutes patent litigation against a Contributor with respect to a patent applicable to software (including a cross-claim or counterclaim in a lawsuit), then any patent licenses granted by that Contributor to such Recipient under this Agreement shall terminate as of the date such litigation is filed. In addition, if Recipient institutes patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Program itself (excluding combinations of the Program with other software or hardware) infringes such Recipient's patent(s), then such Recipient's rights granted under Section 2(b) shall terminate as of the date such litigation is filed. </FONT><FONT SIZE="2"></FONT>
-<P><FONT SIZE="2"></FONT>
-<P><FONT SIZE="2">All Recipient's rights under this Agreement shall terminate if it fails to comply with any of the material terms or conditions of this Agreement and does not cure such failure in a reasonable period of time after becoming aware of such noncompliance. If all Recipient's rights under this Agreement terminate, Recipient agrees to cease use and distribution of the Program as soon as reasonably practicable. However, Recipient's obligations under this Agreement and any licenses granted by Recipient relating to the Program shall continue and survive. </FONT><FONT SIZE="2"></FONT>
-<P><FONT SIZE="2"></FONT>
-<P><FONT SIZE="2"></FONT><FONT SIZE="2">Everyone is permitted to copy and distribute copies of this Agreement, but in order to avoid inconsistency the Agreement is copyrighted and may only be modified in the following manner. The Agreement Steward reserves the right to </FONT><FONT SIZE="2">publish new versions (including revisions) of this Agreement from time to </FONT><FONT SIZE="2">time. No one other than the Agreement Steward has the right to modify this Agreement. IBM is the initial Agreement Steward. IBM may assign the responsibility to serve as the Agreement Steward to a suitable separate entity. </FONT><FONT SIZE="2">Each new version of the Agreement will be given a distinguishing version number. The Program (including Contributions) may always be distributed subject to the version of the Agreement under which it was received. In addition, after a new version of the Agreement is published, Contributor may elect to distribute the Program (including its Contributions) under the new </FONT><FONT SIZE="2">version. </FONT><FONT SIZE="2">Except as expressly stated in Sections 2(a) and 2(b) above, Recipient receives no rights or licenses to the intellectual property of any Contributor under this Agreement, whether expressly, </FONT><FONT SIZE="2">by implication, estoppel or otherwise</FONT><FONT SIZE="2">.</FONT><FONT SIZE="2"> All rights in the Program not expressly granted under this Agreement are reserved.</FONT>
-<P><FONT SIZE="2"></FONT>
-<P><FONT SIZE="2">This Agreement is governed by the laws of the State of New York and the intellectual property laws of the United States of America. No party to this Agreement will bring a legal action under this Agreement more than one year after the cause of action arose. Each party waives its rights to a jury trial in any resulting litigation.</FONT>
-<P><FONT SIZE="2"></FONT><FONT SIZE="2"></FONT>
-<P><FONT SIZE="2"></FONT>
-
-</BODY>
-
-</HTML>
\ No newline at end of file
diff --git a/test-runner/tests/Android.bp b/test-runner/tests/Android.bp
index ac21bcb..aad2bee 100644
--- a/test-runner/tests/Android.bp
+++ b/test-runner/tests/Android.bp
@@ -13,12 +13,7 @@
// limitations under the License.
package {
- // See: http://go/android-license-faq
- // A large-scale-change added 'default_applicable_licenses' to import
- // all of the 'license_kinds' from "frameworks_base_license"
- // to get the below license kinds:
- // SPDX-license-identifier-Apache-2.0
- default_applicable_licenses: ["frameworks_base_license"],
+ default_applicable_licenses: ["Android-Apache-2.0"],
}
android_test {
diff --git a/tests/ChoreographerTests/src/main/java/android/view/choreographertests/AttachedChoreographerTest.java b/tests/ChoreographerTests/src/main/java/android/view/choreographertests/AttachedChoreographerTest.java
index 5460e4e87..64dbe71 100644
--- a/tests/ChoreographerTests/src/main/java/android/view/choreographertests/AttachedChoreographerTest.java
+++ b/tests/ChoreographerTests/src/main/java/android/view/choreographertests/AttachedChoreographerTest.java
@@ -43,6 +43,7 @@
import org.junit.After;
import org.junit.Before;
+import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -392,6 +393,7 @@
}
@Test
+ @Ignore("Can be enabled only after b/330536267 is ready")
public void testChoreographerDivisorRefreshRate() {
for (int divisor : new int[]{2, 3}) {
CountDownLatch continueLatch = new CountDownLatch(1);
@@ -420,6 +422,7 @@
}
@Test
+ @Ignore("Can be enabled only after b/330536267 is ready")
public void testChoreographerAttachedAfterSetFrameRate() {
Log.i(TAG, "starting testChoreographerAttachedAfterSetFrameRate");
diff --git a/tests/CtsSurfaceControlTestsStaging/src/main/java/android/view/surfacecontroltests/SurfaceControlTest.java b/tests/CtsSurfaceControlTestsStaging/src/main/java/android/view/surfacecontroltests/SurfaceControlTest.java
index caaee63..4d48276 100644
--- a/tests/CtsSurfaceControlTestsStaging/src/main/java/android/view/surfacecontroltests/SurfaceControlTest.java
+++ b/tests/CtsSurfaceControlTestsStaging/src/main/java/android/view/surfacecontroltests/SurfaceControlTest.java
@@ -30,10 +30,12 @@
import org.junit.After;
import org.junit.Before;
+import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
+@Ignore // b/330376055: Write tests for functionality for both dVRR and MRR devices.
@RunWith(AndroidJUnit4.class)
public class SurfaceControlTest {
private static final String TAG = "SurfaceControlTest";
diff --git a/tests/PackageWatchdog/Android.bp b/tests/PackageWatchdog/Android.bp
index e0e6c4c..2c5fdd3 100644
--- a/tests/PackageWatchdog/Android.bp
+++ b/tests/PackageWatchdog/Android.bp
@@ -28,8 +28,10 @@
static_libs: [
"junit",
"mockito-target-extended-minus-junit4",
+ "flag-junit",
"frameworks-base-testutils",
"androidx.test.rules",
+ "PlatformProperties",
"services.core",
"services.net",
"truth",
diff --git a/tests/PackageWatchdog/src/com/android/server/CrashRecoveryTest.java b/tests/PackageWatchdog/src/com/android/server/CrashRecoveryTest.java
new file mode 100644
index 0000000..081da11
--- /dev/null
+++ b/tests/PackageWatchdog/src/com/android/server/CrashRecoveryTest.java
@@ -0,0 +1,644 @@
+/*
+ * 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.server;
+
+import static android.service.watchdog.ExplicitHealthCheckService.PackageConfig;
+
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertFalse;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.Manifest;
+import android.content.Context;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.VersionedPackage;
+import android.content.rollback.PackageRollbackInfo;
+import android.content.rollback.RollbackInfo;
+import android.content.rollback.RollbackManager;
+import android.crashrecovery.flags.Flags;
+import android.net.ConnectivityModuleConnector;
+import android.net.ConnectivityModuleConnector.ConnectivityModuleHealthListener;
+import android.os.Handler;
+import android.os.SystemProperties;
+import android.os.test.TestLooper;
+import android.platform.test.flag.junit.SetFlagsRule;
+import android.provider.DeviceConfig;
+import android.util.AtomicFile;
+
+import androidx.test.InstrumentationRegistry;
+
+import com.android.dx.mockito.inline.extended.ExtendedMockito;
+import com.android.server.RescueParty.RescuePartyObserver;
+import com.android.server.pm.ApexManager;
+import com.android.server.rollback.RollbackPackageHealthObserver;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.mockito.Answers;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.mockito.MockitoSession;
+import org.mockito.quality.Strictness;
+import org.mockito.stubbing.Answer;
+
+import java.io.File;
+import java.lang.reflect.Field;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Consumer;
+
+/**
+ * Test CrashRecovery, integration tests that include PackageWatchdog, RescueParty and
+ * RollbackPackageHealthObserver
+ */
+public class CrashRecoveryTest {
+ private static final String PROP_DEVICE_CONFIG_DISABLE_FLAG =
+ "persist.device_config.configuration.disable_rescue_party";
+
+ private static final String APP_A = "com.package.a";
+ private static final String APP_B = "com.package.b";
+ private static final String APP_C = "com.package.c";
+ private static final long VERSION_CODE = 1L;
+ private static final long SHORT_DURATION = TimeUnit.SECONDS.toMillis(1);
+
+ private static final RollbackInfo ROLLBACK_INFO_LOW = getRollbackInfo(APP_A, VERSION_CODE, 1,
+ PackageManager.ROLLBACK_USER_IMPACT_LOW);
+ private static final RollbackInfo ROLLBACK_INFO_HIGH = getRollbackInfo(APP_B, VERSION_CODE, 2,
+ PackageManager.ROLLBACK_USER_IMPACT_HIGH);
+ private static final RollbackInfo ROLLBACK_INFO_MANUAL = getRollbackInfo(APP_C, VERSION_CODE, 3,
+ PackageManager.ROLLBACK_USER_IMPACT_ONLY_MANUAL);
+
+ @Rule
+ public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
+ private final TestClock mTestClock = new TestClock();
+ private TestLooper mTestLooper;
+ private Context mSpyContext;
+ // Keep track of all created watchdogs to apply device config changes
+ private List<PackageWatchdog> mAllocatedWatchdogs;
+ @Mock
+ private ConnectivityModuleConnector mConnectivityModuleConnector;
+ @Mock
+ private PackageManager mMockPackageManager;
+ @Mock(answer = Answers.RETURNS_DEEP_STUBS)
+ private ApexManager mApexManager;
+ @Mock
+ RollbackManager mRollbackManager;
+ // Mock only sysprop apis
+ private PackageWatchdog.BootThreshold mSpyBootThreshold;
+ @Captor
+ private ArgumentCaptor<ConnectivityModuleHealthListener> mConnectivityModuleCallbackCaptor;
+ private MockitoSession mSession;
+ private HashMap<String, String> mSystemSettingsMap;
+ private HashMap<String, String> mCrashRecoveryPropertiesMap;
+
+ @Before
+ public void setUp() throws Exception {
+ mSetFlagsRule.enableFlags(Flags.FLAG_RECOVERABILITY_DETECTION);
+ MockitoAnnotations.initMocks(this);
+ new File(InstrumentationRegistry.getContext().getFilesDir(),
+ "package-watchdog.xml").delete();
+ adoptShellPermissions(Manifest.permission.READ_DEVICE_CONFIG,
+ Manifest.permission.WRITE_DEVICE_CONFIG);
+ mTestLooper = new TestLooper();
+ mSpyContext = spy(InstrumentationRegistry.getContext());
+ when(mSpyContext.getPackageManager()).thenReturn(mMockPackageManager);
+ when(mMockPackageManager.getPackageInfo(anyString(), anyInt())).then(inv -> {
+ final PackageInfo res = new PackageInfo();
+ res.packageName = inv.getArgument(0);
+ res.setLongVersionCode(VERSION_CODE);
+ return res;
+ });
+ mSession = ExtendedMockito.mockitoSession()
+ .initMocks(this)
+ .strictness(Strictness.LENIENT)
+ .spyStatic(SystemProperties.class)
+ .spyStatic(RescueParty.class)
+ .startMocking();
+ mSystemSettingsMap = new HashMap<>();
+
+ // Mock SystemProperties setter and various getters
+ doAnswer((Answer<Void>) invocationOnMock -> {
+ String key = invocationOnMock.getArgument(0);
+ String value = invocationOnMock.getArgument(1);
+
+ mSystemSettingsMap.put(key, value);
+ return null;
+ }
+ ).when(() -> SystemProperties.set(anyString(), anyString()));
+
+ doAnswer((Answer<Integer>) invocationOnMock -> {
+ String key = invocationOnMock.getArgument(0);
+ int defaultValue = invocationOnMock.getArgument(1);
+
+ String storedValue = mSystemSettingsMap.get(key);
+ return storedValue == null ? defaultValue : Integer.parseInt(storedValue);
+ }
+ ).when(() -> SystemProperties.getInt(anyString(), anyInt()));
+
+ doAnswer((Answer<Long>) invocationOnMock -> {
+ String key = invocationOnMock.getArgument(0);
+ long defaultValue = invocationOnMock.getArgument(1);
+
+ String storedValue = mSystemSettingsMap.get(key);
+ return storedValue == null ? defaultValue : Long.parseLong(storedValue);
+ }
+ ).when(() -> SystemProperties.getLong(anyString(), anyLong()));
+
+ doAnswer((Answer<Boolean>) invocationOnMock -> {
+ String key = invocationOnMock.getArgument(0);
+ boolean defaultValue = invocationOnMock.getArgument(1);
+
+ String storedValue = mSystemSettingsMap.get(key);
+ return storedValue == null ? defaultValue : Boolean.parseBoolean(storedValue);
+ }
+ ).when(() -> SystemProperties.getBoolean(anyString(), anyBoolean()));
+
+ SystemProperties.set(RescueParty.PROP_ENABLE_RESCUE, Boolean.toString(true));
+ SystemProperties.set(PROP_DEVICE_CONFIG_DISABLE_FLAG, Boolean.toString(false));
+
+ DeviceConfig.setProperty(DeviceConfig.NAMESPACE_ROLLBACK,
+ PackageWatchdog.PROPERTY_WATCHDOG_EXPLICIT_HEALTH_CHECK_ENABLED,
+ Boolean.toString(true), false);
+
+ DeviceConfig.setProperty(DeviceConfig.NAMESPACE_ROLLBACK,
+ PackageWatchdog.PROPERTY_WATCHDOG_TRIGGER_FAILURE_COUNT,
+ Integer.toString(PackageWatchdog.DEFAULT_TRIGGER_FAILURE_COUNT), false);
+
+ mAllocatedWatchdogs = new ArrayList<>();
+ RescuePartyObserver.reset();
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ dropShellPermissions();
+ mSession.finishMocking();
+ // Clean up listeners since too many listeners will delay notifications significantly
+ for (PackageWatchdog watchdog : mAllocatedWatchdogs) {
+ watchdog.removePropertyChangedListener();
+ }
+ mAllocatedWatchdogs.clear();
+ }
+
+ @Test
+ public void testBootLoopWithRescueParty() throws Exception {
+ PackageWatchdog watchdog = createWatchdog();
+ RescuePartyObserver rescuePartyObserver = setUpRescuePartyObserver(watchdog);
+
+ verify(rescuePartyObserver, never()).executeBootLoopMitigation(1);
+ int bootCounter = 0;
+ for (int i = 0; i < PackageWatchdog.DEFAULT_BOOT_LOOP_TRIGGER_COUNT; i++) {
+ watchdog.noteBoot();
+ bootCounter += 1;
+ }
+ verify(rescuePartyObserver).executeBootLoopMitigation(1);
+ verify(rescuePartyObserver, never()).executeBootLoopMitigation(2);
+
+ for (int i = 0; i < PackageWatchdog.DEFAULT_BOOT_LOOP_MITIGATION_INCREMENT; i++) {
+ watchdog.noteBoot();
+ bootCounter += 1;
+ }
+ verify(rescuePartyObserver).executeBootLoopMitigation(2);
+ verify(rescuePartyObserver, never()).executeBootLoopMitigation(3);
+
+ int bootLoopThreshold = PackageWatchdog.DEFAULT_BOOT_LOOP_THRESHOLD - bootCounter;
+ for (int i = 0; i < bootLoopThreshold; i++) {
+ watchdog.noteBoot();
+ }
+ verify(rescuePartyObserver).executeBootLoopMitigation(3);
+ verify(rescuePartyObserver, never()).executeBootLoopMitigation(4);
+
+ for (int i = 0; i < PackageWatchdog.DEFAULT_BOOT_LOOP_MITIGATION_INCREMENT; i++) {
+ watchdog.noteBoot();
+ }
+ verify(rescuePartyObserver).executeBootLoopMitigation(4);
+ verify(rescuePartyObserver, never()).executeBootLoopMitigation(5);
+
+ for (int i = 0; i < PackageWatchdog.DEFAULT_BOOT_LOOP_MITIGATION_INCREMENT; i++) {
+ watchdog.noteBoot();
+ }
+ verify(rescuePartyObserver).executeBootLoopMitigation(5);
+ verify(rescuePartyObserver, never()).executeBootLoopMitigation(6);
+
+ for (int i = 0; i < PackageWatchdog.DEFAULT_BOOT_LOOP_MITIGATION_INCREMENT; i++) {
+ watchdog.noteBoot();
+ }
+ verify(rescuePartyObserver).executeBootLoopMitigation(6);
+ verify(rescuePartyObserver, never()).executeBootLoopMitigation(7);
+ }
+
+ @Test
+ public void testBootLoopWithRollbackPackageHealthObserver() throws Exception {
+ PackageWatchdog watchdog = createWatchdog();
+ RollbackPackageHealthObserver rollbackObserver =
+ setUpRollbackPackageHealthObserver(watchdog);
+
+ verify(rollbackObserver, never()).executeBootLoopMitigation(1);
+ int bootCounter = 0;
+ for (int i = 0; i < PackageWatchdog.DEFAULT_BOOT_LOOP_TRIGGER_COUNT; i++) {
+ watchdog.noteBoot();
+ bootCounter += 1;
+ }
+ verify(rollbackObserver).executeBootLoopMitigation(1);
+ verify(rollbackObserver, never()).executeBootLoopMitigation(2);
+
+ // Update the list of available rollbacks after executing bootloop mitigation once
+ when(mRollbackManager.getAvailableRollbacks()).thenReturn(List.of(ROLLBACK_INFO_HIGH,
+ ROLLBACK_INFO_MANUAL));
+
+ int bootLoopThreshold = PackageWatchdog.DEFAULT_BOOT_LOOP_THRESHOLD - bootCounter;
+ for (int i = 0; i < bootLoopThreshold; i++) {
+ watchdog.noteBoot();
+ }
+ verify(rollbackObserver).executeBootLoopMitigation(2);
+ verify(rollbackObserver, never()).executeBootLoopMitigation(3);
+
+ // Update the list of available rollbacks after executing bootloop mitigation once
+ when(mRollbackManager.getAvailableRollbacks()).thenReturn(List.of(ROLLBACK_INFO_MANUAL));
+
+ for (int i = 0; i < PackageWatchdog.DEFAULT_BOOT_LOOP_MITIGATION_INCREMENT; i++) {
+ watchdog.noteBoot();
+ }
+ verify(rollbackObserver, never()).executeBootLoopMitigation(3);
+ }
+
+ @Test
+ public void testBootLoopWithRescuePartyAndRollbackPackageHealthObserver() throws Exception {
+ PackageWatchdog watchdog = createWatchdog();
+ RescuePartyObserver rescuePartyObserver = setUpRescuePartyObserver(watchdog);
+ RollbackPackageHealthObserver rollbackObserver =
+ setUpRollbackPackageHealthObserver(watchdog);
+
+ verify(rescuePartyObserver, never()).executeBootLoopMitigation(1);
+ verify(rollbackObserver, never()).executeBootLoopMitigation(1);
+ int bootCounter = 0;
+ for (int i = 0; i < PackageWatchdog.DEFAULT_BOOT_LOOP_TRIGGER_COUNT; i++) {
+ watchdog.noteBoot();
+ bootCounter += 1;
+ }
+ verify(rescuePartyObserver).executeBootLoopMitigation(1);
+ verify(rescuePartyObserver, never()).executeBootLoopMitigation(2);
+ verify(rollbackObserver, never()).executeBootLoopMitigation(1);
+
+ for (int i = 0; i < PackageWatchdog.DEFAULT_BOOT_LOOP_MITIGATION_INCREMENT; i++) {
+ watchdog.noteBoot();
+ bootCounter += 1;
+ }
+ verify(rescuePartyObserver).executeBootLoopMitigation(2);
+ verify(rescuePartyObserver, never()).executeBootLoopMitigation(3);
+ verify(rollbackObserver, never()).executeBootLoopMitigation(2);
+
+ for (int i = 0; i < PackageWatchdog.DEFAULT_BOOT_LOOP_MITIGATION_INCREMENT; i++) {
+ watchdog.noteBoot();
+ bootCounter += 1;
+ }
+ verify(rescuePartyObserver, never()).executeBootLoopMitigation(3);
+ verify(rollbackObserver).executeBootLoopMitigation(1);
+ verify(rollbackObserver, never()).executeBootLoopMitigation(2);
+ // Update the list of available rollbacks after executing bootloop mitigation once
+ when(mRollbackManager.getAvailableRollbacks()).thenReturn(List.of(ROLLBACK_INFO_HIGH,
+ ROLLBACK_INFO_MANUAL));
+
+ int bootLoopThreshold = PackageWatchdog.DEFAULT_BOOT_LOOP_THRESHOLD - bootCounter;
+ for (int i = 0; i < bootLoopThreshold; i++) {
+ watchdog.noteBoot();
+ }
+ verify(rescuePartyObserver).executeBootLoopMitigation(3);
+ verify(rescuePartyObserver, never()).executeBootLoopMitigation(4);
+ verify(rollbackObserver, never()).executeBootLoopMitigation(2);
+
+ for (int i = 0; i < PackageWatchdog.DEFAULT_BOOT_LOOP_MITIGATION_INCREMENT; i++) {
+ watchdog.noteBoot();
+ }
+ verify(rescuePartyObserver).executeBootLoopMitigation(4);
+ verify(rescuePartyObserver, never()).executeBootLoopMitigation(5);
+ verify(rollbackObserver, never()).executeBootLoopMitigation(2);
+
+ for (int i = 0; i < PackageWatchdog.DEFAULT_BOOT_LOOP_MITIGATION_INCREMENT; i++) {
+ watchdog.noteBoot();
+ }
+ verify(rescuePartyObserver).executeBootLoopMitigation(5);
+ verify(rescuePartyObserver, never()).executeBootLoopMitigation(6);
+ verify(rollbackObserver, never()).executeBootLoopMitigation(2);
+
+ for (int i = 0; i < PackageWatchdog.DEFAULT_BOOT_LOOP_MITIGATION_INCREMENT; i++) {
+ watchdog.noteBoot();
+ }
+ verify(rescuePartyObserver, never()).executeBootLoopMitigation(6);
+ verify(rollbackObserver).executeBootLoopMitigation(2);
+ verify(rollbackObserver, never()).executeBootLoopMitigation(3);
+ // Update the list of available rollbacks after executing bootloop mitigation
+ when(mRollbackManager.getAvailableRollbacks()).thenReturn(List.of(ROLLBACK_INFO_MANUAL));
+
+ for (int i = 0; i < PackageWatchdog.DEFAULT_BOOT_LOOP_MITIGATION_INCREMENT; i++) {
+ watchdog.noteBoot();
+ }
+ verify(rescuePartyObserver).executeBootLoopMitigation(6);
+ verify(rescuePartyObserver, never()).executeBootLoopMitigation(7);
+ verify(rollbackObserver, never()).executeBootLoopMitigation(3);
+ }
+
+ RollbackPackageHealthObserver setUpRollbackPackageHealthObserver(PackageWatchdog watchdog) {
+ RollbackPackageHealthObserver rollbackObserver =
+ spy(new RollbackPackageHealthObserver(mSpyContext, mApexManager));
+ when(mSpyContext.getSystemService(RollbackManager.class)).thenReturn(mRollbackManager);
+ when(mRollbackManager.getAvailableRollbacks()).thenReturn(List.of(ROLLBACK_INFO_LOW,
+ ROLLBACK_INFO_HIGH, ROLLBACK_INFO_MANUAL));
+ when(mSpyContext.getPackageManager()).thenReturn(mMockPackageManager);
+
+ watchdog.registerHealthObserver(rollbackObserver);
+ return rollbackObserver;
+ }
+
+ RescuePartyObserver setUpRescuePartyObserver(PackageWatchdog watchdog) {
+ setCrashRecoveryPropRescueBootCount(0);
+ RescuePartyObserver rescuePartyObserver = spy(RescuePartyObserver.getInstance(mSpyContext));
+ assertFalse(RescueParty.isRebootPropertySet());
+ watchdog.registerHealthObserver(rescuePartyObserver);
+ return rescuePartyObserver;
+ }
+
+ private static RollbackInfo getRollbackInfo(String packageName, long versionCode,
+ int rollbackId, int rollbackUserImpact) {
+ VersionedPackage appFrom = new VersionedPackage(packageName, versionCode + 1);
+ VersionedPackage appTo = new VersionedPackage(packageName, versionCode);
+ PackageRollbackInfo packageRollbackInfo = new PackageRollbackInfo(appFrom, appTo, null,
+ null, false, false, null);
+ RollbackInfo rollbackInfo = new RollbackInfo(rollbackId, List.of(packageRollbackInfo),
+ false, null, 111, rollbackUserImpact);
+ return rollbackInfo;
+ }
+
+ private void adoptShellPermissions(String... permissions) {
+ androidx.test.platform.app.InstrumentationRegistry
+ .getInstrumentation()
+ .getUiAutomation()
+ .adoptShellPermissionIdentity(permissions);
+ }
+
+ private void dropShellPermissions() {
+ androidx.test.platform.app.InstrumentationRegistry
+ .getInstrumentation()
+ .getUiAutomation()
+ .dropShellPermissionIdentity();
+ }
+
+
+ private PackageWatchdog createWatchdog() {
+ return createWatchdog(new TestController(), true /* withPackagesReady */);
+ }
+
+ private PackageWatchdog createWatchdog(TestController controller, boolean withPackagesReady) {
+ AtomicFile policyFile =
+ new AtomicFile(new File(mSpyContext.getFilesDir(), "package-watchdog.xml"));
+ Handler handler = new Handler(mTestLooper.getLooper());
+ PackageWatchdog watchdog =
+ new PackageWatchdog(mSpyContext, policyFile, handler, handler, controller,
+ mConnectivityModuleConnector, mTestClock);
+ mockCrashRecoveryProperties(watchdog);
+
+ // Verify controller is not automatically started
+ assertThat(controller.mIsEnabled).isFalse();
+ if (withPackagesReady) {
+ // Only capture the NetworkStack callback for the latest registered watchdog
+ reset(mConnectivityModuleConnector);
+ watchdog.onPackagesReady();
+ // Verify controller by default is started when packages are ready
+ assertThat(controller.mIsEnabled).isTrue();
+
+ verify(mConnectivityModuleConnector).registerHealthListener(
+ mConnectivityModuleCallbackCaptor.capture());
+ }
+ mAllocatedWatchdogs.add(watchdog);
+ return watchdog;
+ }
+
+ // Mock CrashRecoveryProperties as they cannot be accessed due to SEPolicy restrictions
+ private void mockCrashRecoveryProperties(PackageWatchdog watchdog) {
+ mCrashRecoveryPropertiesMap = new HashMap<>();
+
+ // mock properties in RescueParty
+ try {
+
+ doAnswer((Answer<Boolean>) invocationOnMock -> {
+ String storedValue = mCrashRecoveryPropertiesMap
+ .getOrDefault("crashrecovery.attempting_factory_reset", "false");
+ return Boolean.parseBoolean(storedValue);
+ }).when(() -> RescueParty.isFactoryResetPropertySet());
+ doAnswer((Answer<Void>) invocationOnMock -> {
+ boolean value = invocationOnMock.getArgument(0);
+ mCrashRecoveryPropertiesMap.put("crashrecovery.attempting_factory_reset",
+ Boolean.toString(value));
+ return null;
+ }).when(() -> RescueParty.setFactoryResetProperty(anyBoolean()));
+
+ doAnswer((Answer<Boolean>) invocationOnMock -> {
+ String storedValue = mCrashRecoveryPropertiesMap
+ .getOrDefault("crashrecovery.attempting_reboot", "false");
+ return Boolean.parseBoolean(storedValue);
+ }).when(() -> RescueParty.isRebootPropertySet());
+ doAnswer((Answer<Void>) invocationOnMock -> {
+ boolean value = invocationOnMock.getArgument(0);
+ setCrashRecoveryPropAttemptingReboot(value);
+ return null;
+ }).when(() -> RescueParty.setRebootProperty(anyBoolean()));
+
+ doAnswer((Answer<Long>) invocationOnMock -> {
+ String storedValue = mCrashRecoveryPropertiesMap
+ .getOrDefault("persist.crashrecovery.last_factory_reset", "0");
+ return Long.parseLong(storedValue);
+ }).when(() -> RescueParty.getLastFactoryResetTimeMs());
+ doAnswer((Answer<Void>) invocationOnMock -> {
+ long value = invocationOnMock.getArgument(0);
+ setCrashRecoveryPropLastFactoryReset(value);
+ return null;
+ }).when(() -> RescueParty.setLastFactoryResetTimeMs(anyLong()));
+
+ doAnswer((Answer<Integer>) invocationOnMock -> {
+ String storedValue = mCrashRecoveryPropertiesMap
+ .getOrDefault("crashrecovery.max_rescue_level_attempted", "0");
+ return Integer.parseInt(storedValue);
+ }).when(() -> RescueParty.getMaxRescueLevelAttempted());
+ doAnswer((Answer<Void>) invocationOnMock -> {
+ int value = invocationOnMock.getArgument(0);
+ mCrashRecoveryPropertiesMap.put("crashrecovery.max_rescue_level_attempted",
+ Integer.toString(value));
+ return null;
+ }).when(() -> RescueParty.setMaxRescueLevelAttempted(anyInt()));
+
+ } catch (Exception e) {
+ // tests will fail, just printing the error
+ System.out.println("Error while mocking crashrecovery properties " + e.getMessage());
+ }
+
+ try {
+ if (Flags.recoverabilityDetection()) {
+ mSpyBootThreshold = spy(watchdog.new BootThreshold(
+ PackageWatchdog.DEFAULT_BOOT_LOOP_TRIGGER_COUNT,
+ PackageWatchdog.DEFAULT_BOOT_LOOP_TRIGGER_WINDOW_MS,
+ PackageWatchdog.DEFAULT_BOOT_LOOP_MITIGATION_INCREMENT));
+ } else {
+ mSpyBootThreshold = spy(watchdog.new BootThreshold(
+ PackageWatchdog.DEFAULT_BOOT_LOOP_TRIGGER_COUNT,
+ PackageWatchdog.DEFAULT_BOOT_LOOP_TRIGGER_WINDOW_MS));
+ }
+
+ doAnswer((Answer<Integer>) invocationOnMock -> {
+ String storedValue = mCrashRecoveryPropertiesMap
+ .getOrDefault("crashrecovery.rescue_boot_count", "0");
+ return Integer.parseInt(storedValue);
+ }).when(mSpyBootThreshold).getCount();
+ doAnswer((Answer<Void>) invocationOnMock -> {
+ int count = invocationOnMock.getArgument(0);
+ mCrashRecoveryPropertiesMap.put("crashrecovery.rescue_boot_count",
+ Integer.toString(count));
+ return null;
+ }).when(mSpyBootThreshold).setCount(anyInt());
+
+ doAnswer((Answer<Integer>) invocationOnMock -> {
+ String storedValue = mCrashRecoveryPropertiesMap
+ .getOrDefault("crashrecovery.boot_mitigation_count", "0");
+ return Integer.parseInt(storedValue);
+ }).when(mSpyBootThreshold).getMitigationCount();
+ doAnswer((Answer<Void>) invocationOnMock -> {
+ int count = invocationOnMock.getArgument(0);
+ mCrashRecoveryPropertiesMap.put("crashrecovery.boot_mitigation_count",
+ Integer.toString(count));
+ return null;
+ }).when(mSpyBootThreshold).setMitigationCount(anyInt());
+
+ doAnswer((Answer<Long>) invocationOnMock -> {
+ String storedValue = mCrashRecoveryPropertiesMap
+ .getOrDefault("crashrecovery.rescue_boot_start", "0");
+ return Long.parseLong(storedValue);
+ }).when(mSpyBootThreshold).getStart();
+ doAnswer((Answer<Void>) invocationOnMock -> {
+ long count = invocationOnMock.getArgument(0);
+ mCrashRecoveryPropertiesMap.put("crashrecovery.rescue_boot_start",
+ Long.toString(count));
+ return null;
+ }).when(mSpyBootThreshold).setStart(anyLong());
+
+ doAnswer((Answer<Long>) invocationOnMock -> {
+ String storedValue = mCrashRecoveryPropertiesMap
+ .getOrDefault("crashrecovery.boot_mitigation_start", "0");
+ return Long.parseLong(storedValue);
+ }).when(mSpyBootThreshold).getMitigationStart();
+ doAnswer((Answer<Void>) invocationOnMock -> {
+ long count = invocationOnMock.getArgument(0);
+ mCrashRecoveryPropertiesMap.put("crashrecovery.boot_mitigation_start",
+ Long.toString(count));
+ return null;
+ }).when(mSpyBootThreshold).setMitigationStart(anyLong());
+
+ Field mBootThresholdField = watchdog.getClass().getDeclaredField("mBootThreshold");
+ mBootThresholdField.setAccessible(true);
+ mBootThresholdField.set(watchdog, mSpyBootThreshold);
+ } catch (Exception e) {
+ // tests will fail, just printing the error
+ System.out.println("Error detected while spying BootThreshold" + e.getMessage());
+ }
+ }
+
+ private void setCrashRecoveryPropRescueBootCount(int count) {
+ mCrashRecoveryPropertiesMap.put("crashrecovery.rescue_boot_count",
+ Integer.toString(count));
+ }
+
+ private void setCrashRecoveryPropAttemptingReboot(boolean value) {
+ mCrashRecoveryPropertiesMap.put("crashrecovery.attempting_reboot",
+ Boolean.toString(value));
+ }
+
+ private void setCrashRecoveryPropLastFactoryReset(long value) {
+ mCrashRecoveryPropertiesMap.put("persist.crashrecovery.last_factory_reset",
+ Long.toString(value));
+ }
+
+ private static class TestController extends ExplicitHealthCheckController {
+ TestController() {
+ super(null /* controller */);
+ }
+
+ private boolean mIsEnabled;
+ private List<String> mSupportedPackages = new ArrayList<>();
+ private List<String> mRequestedPackages = new ArrayList<>();
+ private Consumer<List<PackageConfig>> mSupportedConsumer;
+ private List<Set> mSyncRequests = new ArrayList<>();
+
+ @Override
+ public void setEnabled(boolean enabled) {
+ mIsEnabled = enabled;
+ if (!mIsEnabled) {
+ mSupportedPackages.clear();
+ }
+ }
+
+ @Override
+ public void setCallbacks(Consumer<String> passedConsumer,
+ Consumer<List<PackageConfig>> supportedConsumer, Runnable notifySyncRunnable) {
+ mSupportedConsumer = supportedConsumer;
+ }
+
+ @Override
+ public void syncRequests(Set<String> packages) {
+ mSyncRequests.add(packages);
+ mRequestedPackages.clear();
+ if (mIsEnabled) {
+ packages.retainAll(mSupportedPackages);
+ mRequestedPackages.addAll(packages);
+ List<PackageConfig> packageConfigs = new ArrayList<>();
+ for (String packageName: packages) {
+ packageConfigs.add(new PackageConfig(packageName, SHORT_DURATION));
+ }
+ mSupportedConsumer.accept(packageConfigs);
+ } else {
+ mSupportedConsumer.accept(Collections.emptyList());
+ }
+ }
+ }
+
+ private static class TestClock implements PackageWatchdog.SystemClock {
+ // Note 0 is special to the internal clock of PackageWatchdog. We need to start from
+ // a non-zero value in order not to disrupt the logic of PackageWatchdog.
+ private long mUpTimeMillis = 1;
+ @Override
+ public long uptimeMillis() {
+ return mUpTimeMillis;
+ }
+ }
+}
diff --git a/tests/PackageWatchdog/src/com/android/server/PackageWatchdogTest.java b/tests/PackageWatchdog/src/com/android/server/PackageWatchdogTest.java
index 75284c7..4f27e06 100644
--- a/tests/PackageWatchdog/src/com/android/server/PackageWatchdogTest.java
+++ b/tests/PackageWatchdog/src/com/android/server/PackageWatchdogTest.java
@@ -36,11 +36,13 @@
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.VersionedPackage;
+import android.crashrecovery.flags.Flags;
import android.net.ConnectivityModuleConnector;
import android.net.ConnectivityModuleConnector.ConnectivityModuleHealthListener;
import android.os.Handler;
import android.os.SystemProperties;
import android.os.test.TestLooper;
+import android.platform.test.flag.junit.SetFlagsRule;
import android.provider.DeviceConfig;
import android.util.AtomicFile;
import android.util.Xml;
@@ -54,11 +56,13 @@
import com.android.modules.utils.TypedXmlSerializer;
import com.android.server.PackageWatchdog.HealthCheckState;
import com.android.server.PackageWatchdog.MonitoredPackage;
+import com.android.server.PackageWatchdog.ObserverInternal;
import com.android.server.PackageWatchdog.PackageHealthObserver;
import com.android.server.PackageWatchdog.PackageHealthObserverImpact;
import org.junit.After;
import org.junit.Before;
+import org.junit.Rule;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
@@ -99,6 +103,10 @@
private static final String OBSERVER_NAME_4 = "observer4";
private static final long SHORT_DURATION = TimeUnit.SECONDS.toMillis(1);
private static final long LONG_DURATION = TimeUnit.SECONDS.toMillis(5);
+
+ @Rule
+ public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
private final TestClock mTestClock = new TestClock();
private TestLooper mTestLooper;
private Context mSpyContext;
@@ -128,6 +136,7 @@
@Before
public void setUp() throws Exception {
+ mSetFlagsRule.enableFlags(Flags.FLAG_RECOVERABILITY_DETECTION);
MockitoAnnotations.initMocks(this);
new File(InstrumentationRegistry.getContext().getFilesDir(),
"package-watchdog.xml").delete();
@@ -444,6 +453,7 @@
*/
@Test
public void testPackageFailureNotifyAllDifferentImpacts() throws Exception {
+ mSetFlagsRule.disableFlags(Flags.FLAG_RECOVERABILITY_DETECTION);
PackageWatchdog watchdog = createWatchdog();
TestObserver observerNone = new TestObserver(OBSERVER_NAME_1,
PackageHealthObserverImpact.USER_IMPACT_LEVEL_0);
@@ -488,6 +498,52 @@
assertThat(observerLowPackages).containsExactly(APP_A);
}
+ @Test
+ public void testPackageFailureNotifyAllDifferentImpactsRecoverability() throws Exception {
+ PackageWatchdog watchdog = createWatchdog();
+ TestObserver observerNone = new TestObserver(OBSERVER_NAME_1,
+ PackageHealthObserverImpact.USER_IMPACT_LEVEL_0);
+ TestObserver observerHigh = new TestObserver(OBSERVER_NAME_2,
+ PackageHealthObserverImpact.USER_IMPACT_LEVEL_50);
+ TestObserver observerMid = new TestObserver(OBSERVER_NAME_3,
+ PackageHealthObserverImpact.USER_IMPACT_LEVEL_30);
+ TestObserver observerLow = new TestObserver(OBSERVER_NAME_4,
+ PackageHealthObserverImpact.USER_IMPACT_LEVEL_10);
+
+ // Start observing for all impact observers
+ watchdog.startObservingHealth(observerNone, Arrays.asList(APP_A, APP_B, APP_C, APP_D),
+ SHORT_DURATION);
+ watchdog.startObservingHealth(observerHigh, Arrays.asList(APP_A, APP_B, APP_C),
+ SHORT_DURATION);
+ watchdog.startObservingHealth(observerMid, Arrays.asList(APP_A, APP_B),
+ SHORT_DURATION);
+ watchdog.startObservingHealth(observerLow, Arrays.asList(APP_A),
+ SHORT_DURATION);
+
+ // Then fail all apps above the threshold
+ raiseFatalFailureAndDispatch(watchdog,
+ Arrays.asList(new VersionedPackage(APP_A, VERSION_CODE),
+ new VersionedPackage(APP_B, VERSION_CODE),
+ new VersionedPackage(APP_C, VERSION_CODE),
+ new VersionedPackage(APP_D, VERSION_CODE)),
+ PackageWatchdog.FAILURE_REASON_UNKNOWN);
+
+ // Verify least impact observers are notifed of package failures
+ List<String> observerNonePackages = observerNone.mMitigatedPackages;
+ List<String> observerHighPackages = observerHigh.mMitigatedPackages;
+ List<String> observerMidPackages = observerMid.mMitigatedPackages;
+ List<String> observerLowPackages = observerLow.mMitigatedPackages;
+
+ // APP_D failure observed by only observerNone is not caught cos its impact is none
+ assertThat(observerNonePackages).isEmpty();
+ // APP_C failure is caught by observerHigh cos it's the lowest impact observer
+ assertThat(observerHighPackages).containsExactly(APP_C);
+ // APP_B failure is caught by observerMid cos it's the lowest impact observer
+ assertThat(observerMidPackages).containsExactly(APP_B);
+ // APP_A failure is caught by observerLow cos it's the lowest impact observer
+ assertThat(observerLowPackages).containsExactly(APP_A);
+ }
+
/**
* Test package failure and least impact observers are notified successively.
* State transistions:
@@ -501,6 +557,7 @@
*/
@Test
public void testPackageFailureNotifyLeastImpactSuccessively() throws Exception {
+ mSetFlagsRule.disableFlags(Flags.FLAG_RECOVERABILITY_DETECTION);
PackageWatchdog watchdog = createWatchdog();
TestObserver observerFirst = new TestObserver(OBSERVER_NAME_1,
PackageHealthObserverImpact.USER_IMPACT_LEVEL_10);
@@ -563,11 +620,76 @@
assertThat(observerSecond.mMitigatedPackages).isEmpty();
}
+ @Test
+ public void testPackageFailureNotifyLeastImpactSuccessivelyRecoverability() throws Exception {
+ PackageWatchdog watchdog = createWatchdog();
+ TestObserver observerFirst = new TestObserver(OBSERVER_NAME_1,
+ PackageHealthObserverImpact.USER_IMPACT_LEVEL_10);
+ TestObserver observerSecond = new TestObserver(OBSERVER_NAME_2,
+ PackageHealthObserverImpact.USER_IMPACT_LEVEL_30);
+
+ // Start observing for observerFirst and observerSecond with failure handling
+ watchdog.startObservingHealth(observerFirst, Arrays.asList(APP_A), LONG_DURATION);
+ watchdog.startObservingHealth(observerSecond, Arrays.asList(APP_A), LONG_DURATION);
+
+ // Then fail APP_A above the threshold
+ raiseFatalFailureAndDispatch(watchdog,
+ Arrays.asList(new VersionedPackage(APP_A, VERSION_CODE)),
+ PackageWatchdog.FAILURE_REASON_UNKNOWN);
+
+ // Verify only observerFirst is notifed
+ assertThat(observerFirst.mMitigatedPackages).containsExactly(APP_A);
+ assertThat(observerSecond.mMitigatedPackages).isEmpty();
+
+ // After observerFirst handles failure, next action it has is high impact
+ observerFirst.mImpact = PackageHealthObserverImpact.USER_IMPACT_LEVEL_50;
+ observerFirst.mMitigatedPackages.clear();
+ observerSecond.mMitigatedPackages.clear();
+
+ // Then fail APP_A again above the threshold
+ raiseFatalFailureAndDispatch(watchdog,
+ Arrays.asList(new VersionedPackage(APP_A, VERSION_CODE)),
+ PackageWatchdog.FAILURE_REASON_UNKNOWN);
+
+ // Verify only observerSecond is notifed cos it has least impact
+ assertThat(observerSecond.mMitigatedPackages).containsExactly(APP_A);
+ assertThat(observerFirst.mMitigatedPackages).isEmpty();
+
+ // After observerSecond handles failure, it has no further actions
+ observerSecond.mImpact = PackageHealthObserverImpact.USER_IMPACT_LEVEL_0;
+ observerFirst.mMitigatedPackages.clear();
+ observerSecond.mMitigatedPackages.clear();
+
+ // Then fail APP_A again above the threshold
+ raiseFatalFailureAndDispatch(watchdog,
+ Arrays.asList(new VersionedPackage(APP_A, VERSION_CODE)),
+ PackageWatchdog.FAILURE_REASON_UNKNOWN);
+
+ // Verify only observerFirst is notifed cos it has the only action
+ assertThat(observerFirst.mMitigatedPackages).containsExactly(APP_A);
+ assertThat(observerSecond.mMitigatedPackages).isEmpty();
+
+ // After observerFirst handles failure, it too has no further actions
+ observerFirst.mImpact = PackageHealthObserverImpact.USER_IMPACT_LEVEL_0;
+ observerFirst.mMitigatedPackages.clear();
+ observerSecond.mMitigatedPackages.clear();
+
+ // Then fail APP_A again above the threshold
+ raiseFatalFailureAndDispatch(watchdog,
+ Arrays.asList(new VersionedPackage(APP_A, VERSION_CODE)),
+ PackageWatchdog.FAILURE_REASON_UNKNOWN);
+
+ // Verify no observer is notified cos no actions left
+ assertThat(observerFirst.mMitigatedPackages).isEmpty();
+ assertThat(observerSecond.mMitigatedPackages).isEmpty();
+ }
+
/**
* Test package failure and notifies only one observer even with observer impact tie.
*/
@Test
public void testPackageFailureNotifyOneSameImpact() throws Exception {
+ mSetFlagsRule.disableFlags(Flags.FLAG_RECOVERABILITY_DETECTION);
PackageWatchdog watchdog = createWatchdog();
TestObserver observer1 = new TestObserver(OBSERVER_NAME_1,
PackageHealthObserverImpact.USER_IMPACT_LEVEL_100);
@@ -588,6 +710,28 @@
assertThat(observer2.mMitigatedPackages).isEmpty();
}
+ @Test
+ public void testPackageFailureNotifyOneSameImpactRecoverabilityDetection() throws Exception {
+ PackageWatchdog watchdog = createWatchdog();
+ TestObserver observer1 = new TestObserver(OBSERVER_NAME_1,
+ PackageHealthObserverImpact.USER_IMPACT_LEVEL_50);
+ TestObserver observer2 = new TestObserver(OBSERVER_NAME_2,
+ PackageHealthObserverImpact.USER_IMPACT_LEVEL_50);
+
+ // Start observing for observer1 and observer2 with failure handling
+ watchdog.startObservingHealth(observer2, Arrays.asList(APP_A), SHORT_DURATION);
+ watchdog.startObservingHealth(observer1, Arrays.asList(APP_A), SHORT_DURATION);
+
+ // Then fail APP_A above the threshold
+ raiseFatalFailureAndDispatch(watchdog,
+ Arrays.asList(new VersionedPackage(APP_A, VERSION_CODE)),
+ PackageWatchdog.FAILURE_REASON_UNKNOWN);
+
+ // Verify only one observer is notifed
+ assertThat(observer1.mMitigatedPackages).containsExactly(APP_A);
+ assertThat(observer2.mMitigatedPackages).isEmpty();
+ }
+
/**
* Test package passing explicit health checks does not fail and vice versa.
*/
@@ -818,6 +962,7 @@
@Test
public void testNetworkStackFailure() {
+ mSetFlagsRule.disableFlags(Flags.FLAG_RECOVERABILITY_DETECTION);
final PackageWatchdog wd = createWatchdog();
// Start observing with failure handling
@@ -835,6 +980,25 @@
assertThat(observer.mMitigatedPackages).containsExactly(APP_A);
}
+ @Test
+ public void testNetworkStackFailureRecoverabilityDetection() {
+ final PackageWatchdog wd = createWatchdog();
+
+ // Start observing with failure handling
+ TestObserver observer = new TestObserver(OBSERVER_NAME_1,
+ PackageHealthObserverImpact.USER_IMPACT_LEVEL_100);
+ wd.startObservingHealth(observer, Collections.singletonList(APP_A), SHORT_DURATION);
+
+ // Notify of NetworkStack failure
+ mConnectivityModuleCallbackCaptor.getValue().onNetworkStackFailure(APP_A);
+
+ // Run handler so package failures are dispatched to observers
+ mTestLooper.dispatchAll();
+
+ // Verify the NetworkStack observer is notified
+ assertThat(observer.mMitigatedPackages).isEmpty();
+ }
+
/** Test default values are used when device property is invalid. */
@Test
public void testInvalidConfig_watchdogTriggerFailureCount() {
@@ -1045,6 +1209,7 @@
/** Ensure that boot loop mitigation is done when the number of boots meets the threshold. */
@Test
public void testBootLoopDetection_meetsThreshold() {
+ mSetFlagsRule.disableFlags(Flags.FLAG_RECOVERABILITY_DETECTION);
PackageWatchdog watchdog = createWatchdog();
TestObserver bootObserver = new TestObserver(OBSERVER_NAME_1);
watchdog.registerHealthObserver(bootObserver);
@@ -1054,6 +1219,16 @@
assertThat(bootObserver.mitigatedBootLoop()).isTrue();
}
+ @Test
+ public void testBootLoopDetection_meetsThresholdRecoverability() {
+ PackageWatchdog watchdog = createWatchdog();
+ TestObserver bootObserver = new TestObserver(OBSERVER_NAME_1);
+ watchdog.registerHealthObserver(bootObserver);
+ for (int i = 0; i < PackageWatchdog.DEFAULT_BOOT_LOOP_THRESHOLD; i++) {
+ watchdog.noteBoot();
+ }
+ assertThat(bootObserver.mitigatedBootLoop()).isTrue();
+ }
/**
* Ensure that boot loop mitigation is not done when the number of boots does not meet the
@@ -1071,10 +1246,43 @@
}
/**
+ * Ensure that boot loop mitigation is not done when the number of boots does not meet the
+ * threshold.
+ */
+ @Test
+ public void testBootLoopDetection_doesNotMeetThresholdRecoverabilityLowImpact() {
+ PackageWatchdog watchdog = createWatchdog();
+ TestObserver bootObserver = new TestObserver(OBSERVER_NAME_1,
+ PackageHealthObserverImpact.USER_IMPACT_LEVEL_30);
+ watchdog.registerHealthObserver(bootObserver);
+ for (int i = 0; i < PackageWatchdog.DEFAULT_BOOT_LOOP_TRIGGER_COUNT - 1; i++) {
+ watchdog.noteBoot();
+ }
+ assertThat(bootObserver.mitigatedBootLoop()).isFalse();
+ }
+
+ /**
+ * Ensure that boot loop mitigation is not done when the number of boots does not meet the
+ * threshold.
+ */
+ @Test
+ public void testBootLoopDetection_doesNotMeetThresholdRecoverabilityHighImpact() {
+ PackageWatchdog watchdog = createWatchdog();
+ TestObserver bootObserver = new TestObserver(OBSERVER_NAME_1,
+ PackageHealthObserverImpact.USER_IMPACT_LEVEL_80);
+ watchdog.registerHealthObserver(bootObserver);
+ for (int i = 0; i < PackageWatchdog.DEFAULT_BOOT_LOOP_THRESHOLD - 1; i++) {
+ watchdog.noteBoot();
+ }
+ assertThat(bootObserver.mitigatedBootLoop()).isFalse();
+ }
+
+ /**
* Ensure that boot loop mitigation is done for the observer with the lowest user impact
*/
@Test
public void testBootLoopMitigationDoneForLowestUserImpact() {
+ mSetFlagsRule.disableFlags(Flags.FLAG_RECOVERABILITY_DETECTION);
PackageWatchdog watchdog = createWatchdog();
TestObserver bootObserver1 = new TestObserver(OBSERVER_NAME_1);
bootObserver1.setImpact(PackageHealthObserverImpact.USER_IMPACT_LEVEL_10);
@@ -1089,11 +1297,28 @@
assertThat(bootObserver2.mitigatedBootLoop()).isFalse();
}
+ @Test
+ public void testBootLoopMitigationDoneForLowestUserImpactRecoverability() {
+ PackageWatchdog watchdog = createWatchdog();
+ TestObserver bootObserver1 = new TestObserver(OBSERVER_NAME_1);
+ bootObserver1.setImpact(PackageHealthObserverImpact.USER_IMPACT_LEVEL_10);
+ TestObserver bootObserver2 = new TestObserver(OBSERVER_NAME_2);
+ bootObserver2.setImpact(PackageHealthObserverImpact.USER_IMPACT_LEVEL_30);
+ watchdog.registerHealthObserver(bootObserver1);
+ watchdog.registerHealthObserver(bootObserver2);
+ for (int i = 0; i < PackageWatchdog.DEFAULT_BOOT_LOOP_THRESHOLD; i++) {
+ watchdog.noteBoot();
+ }
+ assertThat(bootObserver1.mitigatedBootLoop()).isTrue();
+ assertThat(bootObserver2.mitigatedBootLoop()).isFalse();
+ }
+
/**
* Ensure that the correct mitigation counts are sent to the boot loop observer.
*/
@Test
public void testMultipleBootLoopMitigation() {
+ mSetFlagsRule.disableFlags(Flags.FLAG_RECOVERABILITY_DETECTION);
PackageWatchdog watchdog = createWatchdog();
TestObserver bootObserver = new TestObserver(OBSERVER_NAME_1);
watchdog.registerHealthObserver(bootObserver);
@@ -1114,6 +1339,64 @@
assertThat(bootObserver.mBootMitigationCounts).isEqualTo(List.of(1, 2, 3, 4, 1, 2, 3, 4));
}
+ @Test
+ public void testMultipleBootLoopMitigationRecoverabilityLowImpact() {
+ PackageWatchdog watchdog = createWatchdog();
+ TestObserver bootObserver = new TestObserver(OBSERVER_NAME_1,
+ PackageHealthObserverImpact.USER_IMPACT_LEVEL_30);
+ watchdog.registerHealthObserver(bootObserver);
+ for (int j = 0; j < PackageWatchdog.DEFAULT_BOOT_LOOP_TRIGGER_COUNT - 1; j++) {
+ watchdog.noteBoot();
+ }
+ for (int i = 0; i < 4; i++) {
+ for (int j = 0; j < PackageWatchdog.DEFAULT_BOOT_LOOP_MITIGATION_INCREMENT; j++) {
+ watchdog.noteBoot();
+ }
+ }
+
+ moveTimeForwardAndDispatch(PackageWatchdog.DEFAULT_DEESCALATION_WINDOW_MS + 1);
+
+ for (int j = 0; j < PackageWatchdog.DEFAULT_BOOT_LOOP_TRIGGER_COUNT - 1; j++) {
+ watchdog.noteBoot();
+ }
+ for (int i = 0; i < 4; i++) {
+ for (int j = 0; j < PackageWatchdog.DEFAULT_BOOT_LOOP_MITIGATION_INCREMENT; j++) {
+ watchdog.noteBoot();
+ }
+ }
+
+ assertThat(bootObserver.mBootMitigationCounts).isEqualTo(List.of(1, 2, 3, 4, 1, 2, 3, 4));
+ }
+
+ @Test
+ public void testMultipleBootLoopMitigationRecoverabilityHighImpact() {
+ PackageWatchdog watchdog = createWatchdog();
+ TestObserver bootObserver = new TestObserver(OBSERVER_NAME_1,
+ PackageHealthObserverImpact.USER_IMPACT_LEVEL_80);
+ watchdog.registerHealthObserver(bootObserver);
+ for (int j = 0; j < PackageWatchdog.DEFAULT_BOOT_LOOP_THRESHOLD - 1; j++) {
+ watchdog.noteBoot();
+ }
+ for (int i = 0; i < 4; i++) {
+ for (int j = 0; j < PackageWatchdog.DEFAULT_BOOT_LOOP_MITIGATION_INCREMENT; j++) {
+ watchdog.noteBoot();
+ }
+ }
+
+ moveTimeForwardAndDispatch(PackageWatchdog.DEFAULT_DEESCALATION_WINDOW_MS + 1);
+
+ for (int j = 0; j < PackageWatchdog.DEFAULT_BOOT_LOOP_THRESHOLD - 1; j++) {
+ watchdog.noteBoot();
+ }
+ for (int i = 0; i < 4; i++) {
+ for (int j = 0; j < PackageWatchdog.DEFAULT_BOOT_LOOP_MITIGATION_INCREMENT; j++) {
+ watchdog.noteBoot();
+ }
+ }
+
+ assertThat(bootObserver.mBootMitigationCounts).isEqualTo(List.of(1, 2, 3, 4, 1, 2, 3, 4));
+ }
+
/**
* Ensure that passing a null list of failed packages does not cause any mitigation logic to
* execute.
@@ -1304,6 +1587,78 @@
}
/**
+ * Ensure that a {@link ObserverInternal} may be correctly written and read in order to persist
+ * across reboots.
+ */
+ @Test
+ @SuppressWarnings("GuardedBy")
+ public void testWritingAndReadingObserverInternalRecoverability() throws Exception {
+ PackageWatchdog watchdog = createWatchdog();
+
+ LongArrayQueue mitigationCalls = new LongArrayQueue();
+ mitigationCalls.addLast(1000);
+ mitigationCalls.addLast(2000);
+ mitigationCalls.addLast(3000);
+ MonitoredPackage writePkg = watchdog.newMonitoredPackage(
+ "test.package", 1000, 2000, true, mitigationCalls);
+ final int bootMitigationCount = 4;
+ ObserverInternal writeObserver = new ObserverInternal("test", List.of(writePkg),
+ bootMitigationCount);
+
+ // Write the observer
+ File tmpFile = File.createTempFile("observer-watchdog-test", ".xml");
+ AtomicFile testFile = new AtomicFile(tmpFile);
+ FileOutputStream stream = testFile.startWrite();
+ TypedXmlSerializer outputSerializer = Xml.resolveSerializer(stream);
+ outputSerializer.startDocument(null, true);
+ writeObserver.writeLocked(outputSerializer);
+ outputSerializer.endDocument();
+ testFile.finishWrite(stream);
+
+ // Read the observer
+ TypedXmlPullParser parser = Xml.resolvePullParser(testFile.openRead());
+ XmlUtils.beginDocument(parser, "observer");
+ ObserverInternal readObserver = ObserverInternal.read(parser, watchdog);
+
+ assertThat(readObserver.name).isEqualTo(writeObserver.name);
+ assertThat(readObserver.getBootMitigationCount()).isEqualTo(bootMitigationCount);
+ }
+
+ /**
+ * Ensure that boot mitigation counts may be correctly written and read as metadata
+ * in order to persist across reboots.
+ */
+ @Test
+ @SuppressWarnings("GuardedBy")
+ public void testWritingAndReadingMetadataBootMitigationCountRecoverability() throws Exception {
+ PackageWatchdog watchdog = createWatchdog();
+ String filePath = InstrumentationRegistry.getContext().getFilesDir().toString()
+ + "metadata_file.txt";
+
+ ObserverInternal observer1 = new ObserverInternal("test1", List.of(), 1);
+ ObserverInternal observer2 = new ObserverInternal("test2", List.of(), 2);
+ watchdog.registerObserverInternal(observer1);
+ watchdog.registerObserverInternal(observer2);
+
+ mSpyBootThreshold = spy(watchdog.new BootThreshold(
+ PackageWatchdog.DEFAULT_BOOT_LOOP_TRIGGER_COUNT,
+ PackageWatchdog.DEFAULT_BOOT_LOOP_TRIGGER_WINDOW_MS,
+ PackageWatchdog.DEFAULT_BOOT_LOOP_MITIGATION_INCREMENT));
+
+ watchdog.saveAllObserversBootMitigationCountToMetadata(filePath);
+
+ observer1.setBootMitigationCount(0);
+ observer2.setBootMitigationCount(0);
+ assertThat(observer1.getBootMitigationCount()).isEqualTo(0);
+ assertThat(observer2.getBootMitigationCount()).isEqualTo(0);
+
+ mSpyBootThreshold.readAllObserversBootMitigationCountIfNecessary(filePath);
+
+ assertThat(observer1.getBootMitigationCount()).isEqualTo(1);
+ assertThat(observer2.getBootMitigationCount()).isEqualTo(2);
+ }
+
+ /**
* Tests device config changes are propagated correctly.
*/
@Test
@@ -1440,11 +1795,19 @@
// Mock CrashRecoveryProperties as they cannot be accessed due to SEPolicy restrictions
private void mockCrashRecoveryProperties(PackageWatchdog watchdog) {
+ mCrashRecoveryPropertiesMap = new HashMap<>();
+
try {
- mSpyBootThreshold = spy(watchdog.new BootThreshold(
- PackageWatchdog.DEFAULT_BOOT_LOOP_TRIGGER_COUNT,
- PackageWatchdog.DEFAULT_BOOT_LOOP_TRIGGER_WINDOW_MS));
- mCrashRecoveryPropertiesMap = new HashMap<>();
+ if (Flags.recoverabilityDetection()) {
+ mSpyBootThreshold = spy(watchdog.new BootThreshold(
+ PackageWatchdog.DEFAULT_BOOT_LOOP_TRIGGER_COUNT,
+ PackageWatchdog.DEFAULT_BOOT_LOOP_TRIGGER_WINDOW_MS,
+ PackageWatchdog.DEFAULT_BOOT_LOOP_MITIGATION_INCREMENT));
+ } else {
+ mSpyBootThreshold = spy(watchdog.new BootThreshold(
+ PackageWatchdog.DEFAULT_BOOT_LOOP_TRIGGER_COUNT,
+ PackageWatchdog.DEFAULT_BOOT_LOOP_TRIGGER_WINDOW_MS));
+ }
doAnswer((Answer<Integer>) invocationOnMock -> {
String storedValue = mCrashRecoveryPropertiesMap
diff --git a/tools/app_metadata_bundles/Android.bp b/tools/app_metadata_bundles/Android.bp
new file mode 100644
index 0000000..be6bea6
--- /dev/null
+++ b/tools/app_metadata_bundles/Android.bp
@@ -0,0 +1,26 @@
+package {
+ // See: http://go/android-license-faq
+ // A large-scale-change added 'default_applicable_licenses' to import
+ // all of the 'license_kinds' from "frameworks_base_license"
+ // to get the below license kinds:
+ // SPDX-license-identifier-Apache-2.0
+ default_applicable_licenses: ["frameworks_base_license"],
+}
+
+java_library_host {
+ name: "asllib",
+ srcs: [
+ "src/lib/java/**/*.java",
+ ],
+}
+
+java_binary_host {
+ name: "aslgen",
+ manifest: "src/aslgen/aslgen.mf",
+ srcs: [
+ "src/aslgen/java/**/*.java",
+ ],
+ static_libs: [
+ "asllib",
+ ],
+}
diff --git a/tools/app_metadata_bundles/OWNERS b/tools/app_metadata_bundles/OWNERS
new file mode 100644
index 0000000..a2a250b
--- /dev/null
+++ b/tools/app_metadata_bundles/OWNERS
@@ -0,0 +1,2 @@
+wenhaowang@google.com
+mloh@google.com
diff --git a/tools/app_metadata_bundles/README.md b/tools/app_metadata_bundles/README.md
new file mode 100644
index 0000000..6e8d287
--- /dev/null
+++ b/tools/app_metadata_bundles/README.md
@@ -0,0 +1,9 @@
+# App metadata bundles
+
+This project delivers a comprehensive toolchain solution for developers
+to efficiently manage app metadata bundles.
+
+The project consists of two subprojects:
+
+ * A pure Java library, and
+ * A pure Java command-line tool.
diff --git a/tools/app_metadata_bundles/src/aslgen/aslgen.mf b/tools/app_metadata_bundles/src/aslgen/aslgen.mf
new file mode 100644
index 0000000..fc656e2
--- /dev/null
+++ b/tools/app_metadata_bundles/src/aslgen/aslgen.mf
@@ -0,0 +1 @@
+Main-Class: com.android.aslgen.Main
\ No newline at end of file
diff --git a/tools/app_metadata_bundles/src/aslgen/java/com/android/aslgen/Main.java b/tools/app_metadata_bundles/src/aslgen/java/com/android/aslgen/Main.java
new file mode 100644
index 0000000..fb7a6ab
--- /dev/null
+++ b/tools/app_metadata_bundles/src/aslgen/java/com/android/aslgen/Main.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.aslgen;
+
+import com.android.asllib.AndroidSafetyLabel;
+import com.android.asllib.AndroidSafetyLabel.Format;
+import com.android.asllib.util.MalformedXmlException;
+
+import org.xml.sax.SAXException;
+
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.transform.TransformerException;
+
+public class Main {
+
+ /** Takes the options to make file conversion. */
+ public static void main(String[] args)
+ throws IOException,
+ ParserConfigurationException,
+ SAXException,
+ TransformerException,
+ MalformedXmlException {
+
+ String inFile = null;
+ String outFile = null;
+ Format inFormat = Format.NULL;
+ Format outFormat = Format.NULL;
+
+
+ // Except for "--help", all arguments require a value currently.
+ // So just make sure we have an even number and
+ // then process them all two at a time.
+ if (args.length == 1 && "--help".equals(args[0])) {
+ showUsage();
+ return;
+ }
+ if (args.length % 2 != 0) {
+ throw new IllegalArgumentException("Argument is missing corresponding value");
+ }
+ for (int i = 0; i < args.length - 1; i += 2) {
+ final String arg = args[i].trim();
+ final String argValue = args[i + 1].trim();
+ if ("--in-path".equals(arg)) {
+ inFile = argValue;
+ } else if ("--out-path".equals(arg)) {
+ outFile = argValue;
+ } else if ("--in-format".equals(arg)) {
+ inFormat = getFormat(argValue);
+ } else if ("--out-format".equals(arg)) {
+ outFormat = getFormat(argValue);
+ } else {
+ throw new IllegalArgumentException("Unknown argument: " + arg);
+ }
+ }
+
+ if (inFile == null) {
+ throw new IllegalArgumentException("input file is required");
+ }
+
+ if (outFile == null) {
+ throw new IllegalArgumentException("output file is required");
+ }
+
+ if (inFormat == Format.NULL) {
+ throw new IllegalArgumentException("input format is required");
+ }
+
+ if (outFormat == Format.NULL) {
+ throw new IllegalArgumentException("output format is required");
+ }
+
+ System.out.println("in path: " + inFile);
+ System.out.println("out path: " + outFile);
+ System.out.println("in format: " + inFormat);
+ System.out.println("out format: " + outFormat);
+
+ var asl = AndroidSafetyLabel.readFromStream(new FileInputStream(inFile), inFormat);
+ asl.writeToStream(new FileOutputStream(outFile), outFormat);
+ }
+
+ private static Format getFormat(String argValue) {
+ if ("hr".equals(argValue)) {
+ return Format.HUMAN_READABLE;
+ } else if ("od".equals(argValue)) {
+ return Format.ON_DEVICE;
+ } else {
+ return Format.NULL;
+ }
+ }
+
+ private static void showUsage() {
+ AndroidSafetyLabel.test();
+ System.err.println(
+ "Usage:\n"
+ );
+ }
+}
diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/AndroidSafetyLabel.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/AndroidSafetyLabel.java
new file mode 100644
index 0000000..bc8063e
--- /dev/null
+++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/AndroidSafetyLabel.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.asllib;
+
+import com.android.asllib.util.MalformedXmlException;
+
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.xml.sax.SAXException;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.List;
+
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.transform.OutputKeys;
+import javax.xml.transform.Transformer;
+import javax.xml.transform.TransformerException;
+import javax.xml.transform.TransformerFactory;
+import javax.xml.transform.dom.DOMSource;
+import javax.xml.transform.stream.StreamResult;
+
+public class AndroidSafetyLabel implements AslMarshallable {
+
+ public enum Format {
+ NULL, HUMAN_READABLE, ON_DEVICE;
+ }
+
+ private final SafetyLabels mSafetyLabels;
+
+ public SafetyLabels getSafetyLabels() {
+ return mSafetyLabels;
+ }
+
+ public AndroidSafetyLabel(SafetyLabels safetyLabels) {
+ this.mSafetyLabels = safetyLabels;
+ }
+
+ /** Reads a {@link AndroidSafetyLabel} from an {@link InputStream}. */
+ // TODO(b/329902686): Support parsing from on-device.
+ public static AndroidSafetyLabel readFromStream(InputStream in, Format format)
+ throws IOException, ParserConfigurationException, SAXException, MalformedXmlException {
+ DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
+ factory.setNamespaceAware(true);
+ Document document = factory.newDocumentBuilder().parse(in);
+
+ switch (format) {
+ case HUMAN_READABLE:
+ Element appMetadataBundles =
+ XmlUtils.getSingleElement(document, XmlUtils.HR_TAG_APP_METADATA_BUNDLES);
+
+ return new AndroidSafetyLabelFactory()
+ .createFromHrElements(
+ List.of(
+ XmlUtils.getSingleElement(
+ document, XmlUtils.HR_TAG_APP_METADATA_BUNDLES)));
+ case ON_DEVICE:
+ throw new IllegalArgumentException(
+ "Parsing from on-device format is not supported at this time.");
+ default:
+ throw new IllegalStateException("Unrecognized input format.");
+ }
+ }
+
+ /** Write the content of the {@link AndroidSafetyLabel} to a {@link OutputStream}. */
+ // TODO(b/329902686): Support outputting human-readable format.
+ public void writeToStream(OutputStream out, Format format)
+ throws IOException, ParserConfigurationException, TransformerException {
+ var docBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
+ var document = docBuilder.newDocument();
+
+ switch (format) {
+ case HUMAN_READABLE:
+ throw new IllegalArgumentException(
+ "Outputting human-readable format is not supported at this time.");
+ case ON_DEVICE:
+ for (var child : this.toOdDomElements(document)) {
+ document.appendChild(child);
+ }
+ break;
+ default:
+ throw new IllegalStateException("Unrecognized input format.");
+ }
+
+ TransformerFactory transformerFactory = TransformerFactory.newInstance();
+ Transformer transformer = transformerFactory.newTransformer();
+ transformer.setOutputProperty(OutputKeys.INDENT, "yes");
+ transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
+ transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
+ StreamResult streamResult = new StreamResult(out); // out
+ DOMSource domSource = new DOMSource(document);
+ transformer.transform(domSource, streamResult);
+ }
+
+ /** Creates an on-device DOM element from an {@link AndroidSafetyLabel} */
+ @Override
+ public List<Element> toOdDomElements(Document doc) {
+ Element aslEle = doc.createElement(XmlUtils.OD_TAG_BUNDLE);
+ XmlUtils.appendChildren(aslEle, mSafetyLabels.toOdDomElements(doc));
+ return List.of(aslEle);
+ }
+
+ public static void test() {
+ // TODO(b/329902686): Add tests.
+ }
+}
diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/AndroidSafetyLabelFactory.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/AndroidSafetyLabelFactory.java
new file mode 100644
index 0000000..7e7fcf9
--- /dev/null
+++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/AndroidSafetyLabelFactory.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.asllib;
+
+import com.android.asllib.util.MalformedXmlException;
+
+import org.w3c.dom.Element;
+
+import java.util.List;
+
+public class AndroidSafetyLabelFactory implements AslMarshallableFactory<AndroidSafetyLabel> {
+
+ /** Creates an {@link AndroidSafetyLabel} from human-readable DOM element */
+ @Override
+ public AndroidSafetyLabel createFromHrElements(List<Element> appMetadataBundles)
+ throws MalformedXmlException {
+ Element appMetadataBundlesEle = XmlUtils.getSingleElement(appMetadataBundles);
+ Element safetyLabelsEle =
+ XmlUtils.getSingleChildElement(
+ appMetadataBundlesEle, XmlUtils.HR_TAG_SAFETY_LABELS);
+ SafetyLabels safetyLabels =
+ new SafetyLabelsFactory().createFromHrElements(List.of(safetyLabelsEle));
+ return new AndroidSafetyLabel(safetyLabels);
+ }
+}
diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/AslMarshallable.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/AslMarshallable.java
new file mode 100644
index 0000000..4e64ab0
--- /dev/null
+++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/AslMarshallable.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.asllib;
+
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+
+import java.util.List;
+
+public interface AslMarshallable {
+
+ /** Creates the on-device DOM element from the AslMarshallable Java Object. */
+ List<Element> toOdDomElements(Document doc);
+}
diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/AslMarshallableFactory.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/AslMarshallableFactory.java
new file mode 100644
index 0000000..b8f9f0e
--- /dev/null
+++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/AslMarshallableFactory.java
@@ -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.asllib;
+
+import com.android.asllib.util.MalformedXmlException;
+
+import org.w3c.dom.Element;
+
+import java.util.List;
+
+public interface AslMarshallableFactory<T extends AslMarshallable> {
+
+ /** Creates an {@link AslMarshallableFactory} from human-readable DOM element */
+ T createFromHrElements(List<Element> elements) throws MalformedXmlException;
+}
diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataCategory.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataCategory.java
new file mode 100644
index 0000000..e5ed63b
--- /dev/null
+++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataCategory.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.asllib;
+
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Data usage category representation containing one or more {@link DataType}. Valid category keys
+ * are defined in {@link DataCategoryConstants}, each category has a valid set of types {@link
+ * DataType}, which are mapped in {@link DataTypeConstants}
+ */
+public class DataCategory implements AslMarshallable {
+ private final String mCategoryName;
+ private final Map<String, DataType> mDataTypes;
+
+ public DataCategory(String categoryName, Map<String, DataType> dataTypes) {
+ this.mCategoryName = categoryName;
+ this.mDataTypes = dataTypes;
+ }
+
+ public String getCategoryName() {
+ return mCategoryName;
+ }
+
+ /** Return the type {@link Map} of String type key to {@link DataType} */
+
+ public Map<String, DataType> getDataTypes() {
+ return mDataTypes;
+ }
+
+ /** Creates on-device DOM element(s) from the {@link DataCategory}. */
+ @Override
+ public List<Element> toOdDomElements(Document doc) {
+ Element dataCategoryEle = XmlUtils.createPbundleEleWithName(doc, this.getCategoryName());
+ for (DataType dataType : mDataTypes.values()) {
+ XmlUtils.appendChildren(dataCategoryEle, dataType.toOdDomElements(doc));
+ }
+ return List.of(dataCategoryEle);
+ }
+}
diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataCategoryConstants.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataCategoryConstants.java
new file mode 100644
index 0000000..b364c8b
--- /dev/null
+++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataCategoryConstants.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.asllib;
+
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Constants for determining valid {@link String} data types for usage within {@link SafetyLabels},
+ * {@link DataCategory}, and {@link DataType}
+ */
+public class DataCategoryConstants {
+
+ public static final String CATEGORY_PERSONAL = "personal";
+ public static final String CATEGORY_FINANCIAL = "financial";
+ public static final String CATEGORY_LOCATION = "location";
+ public static final String CATEGORY_EMAIL_TEXT_MESSAGE = "email_text_message";
+ public static final String CATEGORY_PHOTO_VIDEO = "photo_video";
+ public static final String CATEGORY_AUDIO = "audio";
+ public static final String CATEGORY_STORAGE = "storage";
+ public static final String CATEGORY_HEALTH_FITNESS = "health_fitness";
+ public static final String CATEGORY_CONTACTS = "contacts";
+ public static final String CATEGORY_CALENDAR = "calendar";
+ public static final String CATEGORY_IDENTIFIERS = "identifiers";
+ public static final String CATEGORY_APP_PERFORMANCE = "app_performance";
+ public static final String CATEGORY_ACTIONS_IN_APP = "actions_in_app";
+ public static final String CATEGORY_SEARCH_AND_BROWSING = "search_and_browsing";
+
+ /** Set of valid categories */
+ public static final Set<String> VALID_CATEGORIES =
+ Collections.unmodifiableSet(
+ new HashSet<>(
+ Arrays.asList(
+ CATEGORY_PERSONAL,
+ CATEGORY_FINANCIAL,
+ CATEGORY_LOCATION,
+ CATEGORY_EMAIL_TEXT_MESSAGE,
+ CATEGORY_PHOTO_VIDEO,
+ CATEGORY_AUDIO,
+ CATEGORY_STORAGE,
+ CATEGORY_HEALTH_FITNESS,
+ CATEGORY_CONTACTS,
+ CATEGORY_CALENDAR,
+ CATEGORY_IDENTIFIERS,
+ CATEGORY_APP_PERFORMANCE,
+ CATEGORY_ACTIONS_IN_APP,
+ CATEGORY_SEARCH_AND_BROWSING)));
+
+ /** Returns {@link Set} of valid {@link String} category keys */
+ public static Set<String> getValidDataCategories() {
+ return VALID_CATEGORIES;
+ }
+
+ private DataCategoryConstants() {
+ /* do nothing - hide constructor */
+ }
+}
diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataCategoryFactory.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataCategoryFactory.java
new file mode 100644
index 0000000..d946345
--- /dev/null
+++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataCategoryFactory.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.asllib;
+
+import com.android.asllib.util.MalformedXmlException;
+
+import org.w3c.dom.Element;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class DataCategoryFactory implements AslMarshallableFactory<DataCategory> {
+ @Override
+ public DataCategory createFromHrElements(List<Element> elements) throws MalformedXmlException {
+ String categoryName = null;
+ Map<String, DataType> dataTypeMap = new HashMap<String, DataType>();
+ for (Element ele : elements) {
+ categoryName = ele.getAttribute(XmlUtils.HR_ATTR_DATA_CATEGORY);
+ String dataTypeName = ele.getAttribute(XmlUtils.HR_ATTR_DATA_TYPE);
+ if (!DataTypeConstants.getValidDataTypes().contains(dataTypeName)) {
+ throw new MalformedXmlException(
+ String.format("Unrecognized data type name: %s", dataTypeName));
+ }
+ dataTypeMap.put(dataTypeName, new DataTypeFactory().createFromHrElements(List.of(ele)));
+ }
+
+ return new DataCategory(categoryName, dataTypeMap);
+ }
+}
diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataLabels.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataLabels.java
new file mode 100644
index 0000000..d2fffc0
--- /dev/null
+++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataLabels.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.asllib;
+
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Data label representation with data shared and data collected maps containing zero or more {@link
+ * DataCategory}
+ */
+public class DataLabels implements AslMarshallable {
+ private final Map<String, DataCategory> mDataAccessed;
+ private final Map<String, DataCategory> mDataCollected;
+ private final Map<String, DataCategory> mDataShared;
+
+ public DataLabels(
+ Map<String, DataCategory> dataAccessed,
+ Map<String, DataCategory> dataCollected,
+ Map<String, DataCategory> dataShared) {
+ mDataAccessed = dataAccessed;
+ mDataCollected = dataCollected;
+ mDataShared = dataShared;
+ }
+
+ /**
+ * Returns the data accessed {@link Map} of {@link com.android.asllib.DataCategoryConstants} to
+ * {@link DataCategory}
+ */
+ public Map<String, DataCategory> getDataAccessed() {
+ return mDataAccessed;
+ }
+
+ /**
+ * Returns the data collected {@link Map} of {@link com.android.asllib.DataCategoryConstants} to
+ * {@link DataCategory}
+ */
+ public Map<String, DataCategory> getDataCollected() {
+ return mDataCollected;
+ }
+
+ /**
+ * Returns the data shared {@link Map} of {@link com.android.asllib.DataCategoryConstants} to
+ * {@link DataCategory}
+ */
+ public Map<String, DataCategory> getDataShared() {
+ return mDataShared;
+ }
+
+ /** Gets the on-device DOM element for the {@link DataLabels}. */
+ @Override
+ public List<Element> toOdDomElements(Document doc) {
+ Element dataLabelsEle =
+ XmlUtils.createPbundleEleWithName(doc, XmlUtils.OD_NAME_DATA_LABELS);
+
+ maybeAppendDataUsages(doc, dataLabelsEle, mDataCollected, XmlUtils.OD_NAME_DATA_ACCESSED);
+ maybeAppendDataUsages(doc, dataLabelsEle, mDataCollected, XmlUtils.OD_NAME_DATA_COLLECTED);
+ maybeAppendDataUsages(doc, dataLabelsEle, mDataShared, XmlUtils.OD_NAME_DATA_SHARED);
+
+ return List.of(dataLabelsEle);
+ }
+
+ private void maybeAppendDataUsages(
+ Document doc,
+ Element dataLabelsEle,
+ Map<String, DataCategory> dataCategoriesMap,
+ String dataUsageTypeName) {
+ if (dataCategoriesMap.isEmpty()) {
+ return;
+ }
+ Element dataUsageEle = XmlUtils.createPbundleEleWithName(doc, dataUsageTypeName);
+
+ for (String dataCategoryName : dataCategoriesMap.keySet()) {
+ Element dataCategoryEle = XmlUtils.createPbundleEleWithName(doc, dataCategoryName);
+ DataCategory dataCategory = dataCategoriesMap.get(dataCategoryName);
+ for (String dataTypeName : dataCategory.getDataTypes().keySet()) {
+ DataType dataType = dataCategory.getDataTypes().get(dataTypeName);
+ XmlUtils.appendChildren(dataCategoryEle, dataType.toOdDomElements(doc));
+ }
+ dataUsageEle.appendChild(dataCategoryEle);
+ }
+ dataLabelsEle.appendChild(dataUsageEle);
+ }
+}
diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataLabelsFactory.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataLabelsFactory.java
new file mode 100644
index 0000000..1adb140
--- /dev/null
+++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataLabelsFactory.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 com.android.asllib;
+
+import com.android.asllib.util.MalformedXmlException;
+
+import org.w3c.dom.Element;
+import org.w3c.dom.NodeList;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+public class DataLabelsFactory implements AslMarshallableFactory<DataLabels> {
+
+ /** Creates a {@link DataLabels} from the human-readable DOM element. */
+ @Override
+ public DataLabels createFromHrElements(List<Element> elements) throws MalformedXmlException {
+ Element ele = XmlUtils.getSingleElement(elements);
+ Map<String, DataCategory> dataAccessed =
+ getDataCategoriesWithTag(ele, XmlUtils.HR_TAG_DATA_ACCESSED);
+ Map<String, DataCategory> dataCollected =
+ getDataCategoriesWithTag(ele, XmlUtils.HR_TAG_DATA_COLLECTED);
+ Map<String, DataCategory> dataShared =
+ getDataCategoriesWithTag(ele, XmlUtils.HR_TAG_DATA_SHARED);
+
+ // Validate booleans such as isCollectionOptional, isSharingOptional.
+ for (DataCategory dataCategory : dataAccessed.values()) {
+ for (DataType dataType : dataCategory.getDataTypes().values()) {
+ if (dataType.getIsSharingOptional() != null) {
+ throw new MalformedXmlException(
+ String.format(
+ "isSharingOptional was unexpectedly defined on a DataType"
+ + " belonging to data accessed: %s",
+ dataType.getDataTypeName()));
+ }
+ if (dataType.getIsCollectionOptional() != null) {
+ throw new MalformedXmlException(
+ String.format(
+ "isCollectionOptional was unexpectedly defined on a DataType"
+ + " belonging to data accessed: %s",
+ dataType.getDataTypeName()));
+ }
+ }
+ }
+ for (DataCategory dataCategory : dataCollected.values()) {
+ for (DataType dataType : dataCategory.getDataTypes().values()) {
+ if (dataType.getIsSharingOptional() != null) {
+ throw new MalformedXmlException(
+ String.format(
+ "isSharingOptional was unexpectedly defined on a DataType"
+ + " belonging to data collected: %s",
+ dataType.getDataTypeName()));
+ }
+ }
+ }
+ for (DataCategory dataCategory : dataShared.values()) {
+ for (DataType dataType : dataCategory.getDataTypes().values()) {
+ if (dataType.getIsCollectionOptional() != null) {
+ throw new MalformedXmlException(
+ String.format(
+ "isCollectionOptional was unexpectedly defined on a DataType"
+ + " belonging to data shared: %s",
+ dataType.getDataTypeName()));
+ }
+ }
+ }
+
+ return new DataLabels(dataAccessed, dataCollected, dataShared);
+ }
+
+ private static Map<String, DataCategory> getDataCategoriesWithTag(
+ Element dataLabelsEle, String dataCategoryUsageTypeTag) throws MalformedXmlException {
+ NodeList dataUsedNodeList = dataLabelsEle.getElementsByTagName(dataCategoryUsageTypeTag);
+ Map<String, DataCategory> dataCategoryMap = new HashMap<String, DataCategory>();
+
+ Set<String> dataCategoryNames = new HashSet<String>();
+ for (int i = 0; i < dataUsedNodeList.getLength(); i++) {
+ Element dataUsedEle = (Element) dataUsedNodeList.item(i);
+ String dataCategoryName = dataUsedEle.getAttribute(XmlUtils.HR_ATTR_DATA_CATEGORY);
+ if (!DataCategoryConstants.getValidDataCategories().contains(dataCategoryName)) {
+ throw new MalformedXmlException(
+ String.format("Unrecognized category name: %s", dataCategoryName));
+ }
+ dataCategoryNames.add(dataCategoryName);
+ }
+ for (String dataCategoryName : dataCategoryNames) {
+ var dataCategoryElements =
+ XmlUtils.asElementList(dataUsedNodeList).stream()
+ .filter(
+ ele ->
+ ele.getAttribute(XmlUtils.HR_ATTR_DATA_CATEGORY)
+ .equals(dataCategoryName))
+ .toList();
+ DataCategory dataCategory =
+ new DataCategoryFactory().createFromHrElements(dataCategoryElements);
+ dataCategoryMap.put(dataCategoryName, dataCategory);
+ }
+ return dataCategoryMap;
+ }
+}
diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataType.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataType.java
new file mode 100644
index 0000000..5ba2975
--- /dev/null
+++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataType.java
@@ -0,0 +1,176 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.asllib;
+
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Data usage type representation. Types are specific to a {@link DataCategory} and contains
+ * metadata related to the data usage purpose.
+ */
+public class DataType implements AslMarshallable {
+
+ public enum Purpose {
+ PURPOSE_APP_FUNCTIONALITY(1),
+ PURPOSE_ANALYTICS(2),
+ PURPOSE_DEVELOPER_COMMUNICATIONS(3),
+ PURPOSE_FRAUD_PREVENTION_SECURITY(4),
+ PURPOSE_ADVERTISING(5),
+ PURPOSE_PERSONALIZATION(6),
+ PURPOSE_ACCOUNT_MANAGEMENT(7);
+
+ private static final String PURPOSE_PREFIX = "PURPOSE_";
+
+ private final int mValue;
+
+ Purpose(int value) {
+ this.mValue = value;
+ }
+
+ /** Get the int value associated with the Purpose. */
+ public int getValue() {
+ return mValue;
+ }
+
+ /** Get the Purpose associated with the int value. */
+ public static Purpose forValue(int value) {
+ for (Purpose e : values()) {
+ if (e.getValue() == value) {
+ return e;
+ }
+ }
+ throw new IllegalArgumentException("No enum for value: " + value);
+ }
+
+ /** Get the Purpose associated with the human-readable String. */
+ public static Purpose forString(String s) {
+ for (Purpose e : values()) {
+ if (e.toString().equals(s)) {
+ return e;
+ }
+ }
+ throw new IllegalArgumentException("No enum for str: " + s);
+ }
+
+ /** Human-readable String representation of Purpose. */
+ public String toString() {
+ if (!this.name().startsWith(PURPOSE_PREFIX)) {
+ return this.name();
+ }
+ return this.name().substring(PURPOSE_PREFIX.length()).toLowerCase();
+ }
+ }
+
+ private final String mDataTypeName;
+
+ private final Set<Purpose> mPurposeSet;
+ private final Boolean mIsCollectionOptional;
+ private final Boolean mIsSharingOptional;
+ private final Boolean mEphemeral;
+
+ public DataType(
+ String dataTypeName,
+ Set<Purpose> purposeSet,
+ Boolean isCollectionOptional,
+ Boolean isSharingOptional,
+ Boolean ephemeral) {
+ this.mDataTypeName = dataTypeName;
+ this.mPurposeSet = purposeSet;
+ this.mIsCollectionOptional = isCollectionOptional;
+ this.mIsSharingOptional = isSharingOptional;
+ this.mEphemeral = ephemeral;
+ }
+
+ public String getDataTypeName() {
+ return mDataTypeName;
+ }
+
+ /**
+ * Returns {@link Set} of valid {@link Integer} purposes for using the associated data category
+ * and type
+ */
+ public Set<Purpose> getPurposeSet() {
+ return mPurposeSet;
+ }
+
+ /**
+ * For data-collected, returns {@code true} if data usage is user optional and {@code false} if
+ * data usage is required. Should return {@code null} for data-accessed and data-shared.
+ */
+ public Boolean getIsCollectionOptional() {
+ return mIsCollectionOptional;
+ }
+
+ /**
+ * For data-shared, returns {@code true} if data usage is user optional and {@code false} if
+ * data usage is required. Should return {@code null} for data-accessed and data-collected.
+ */
+ public Boolean getIsSharingOptional() {
+ return mIsSharingOptional;
+ }
+
+ /**
+ * For data-collected, returns {@code true} if data usage is user optional and {@code false} if
+ * data usage is processed ephemerally. Should return {@code null} for data-shared.
+ */
+ public Boolean getEphemeral() {
+ return mEphemeral;
+ }
+
+ @Override
+ public List<Element> toOdDomElements(Document doc) {
+ Element dataTypeEle = XmlUtils.createPbundleEleWithName(doc, this.getDataTypeName());
+ if (!this.getPurposeSet().isEmpty()) {
+ Element purposesEle = doc.createElement(XmlUtils.OD_TAG_INT_ARRAY);
+ purposesEle.setAttribute(XmlUtils.OD_ATTR_NAME, XmlUtils.OD_NAME_PURPOSES);
+ purposesEle.setAttribute(
+ XmlUtils.OD_ATTR_NUM, String.valueOf(this.getPurposeSet().size()));
+ for (DataType.Purpose purpose : this.getPurposeSet()) {
+ Element purposeEle = doc.createElement(XmlUtils.OD_TAG_ITEM);
+ purposeEle.setAttribute(XmlUtils.OD_ATTR_VALUE, String.valueOf(purpose.getValue()));
+ purposesEle.appendChild(purposeEle);
+ }
+ dataTypeEle.appendChild(purposesEle);
+ }
+
+ maybeAddBoolToOdElement(
+ doc,
+ dataTypeEle,
+ this.getIsCollectionOptional(),
+ XmlUtils.OD_NAME_IS_COLLECTION_OPTIONAL);
+ maybeAddBoolToOdElement(
+ doc,
+ dataTypeEle,
+ this.getIsSharingOptional(),
+ XmlUtils.OD_NAME_IS_SHARING_OPTIONAL);
+ maybeAddBoolToOdElement(doc, dataTypeEle, this.getEphemeral(), XmlUtils.OD_NAME_EPHEMERAL);
+ return List.of(dataTypeEle);
+ }
+
+ private static void maybeAddBoolToOdElement(
+ Document doc, Element parentEle, Boolean b, String odName) {
+ if (b == null) {
+ return;
+ }
+ Element ele = XmlUtils.createOdBooleanEle(doc, odName, b);
+ parentEle.appendChild(ele);
+ }
+}
diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataTypeConstants.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataTypeConstants.java
new file mode 100644
index 0000000..a0a7537
--- /dev/null
+++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataTypeConstants.java
@@ -0,0 +1,156 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.asllib;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Constants for determining valid {@link String} data types for usage within {@link SafetyLabels},
+ * {@link DataCategory}, and {@link DataType}
+ */
+public class DataTypeConstants {
+ /** Data types for {@link DataCategoryConstants.CATEGORY_PERSONAL} */
+ public static final String TYPE_NAME = "name";
+
+ public static final String TYPE_EMAIL_ADDRESS = "email_address";
+ public static final String TYPE_PHONE_NUMBER = "phone_number";
+ public static final String TYPE_RACE_ETHNICITY = "race_ethnicity";
+ public static final String TYPE_POLITICAL_OR_RELIGIOUS_BELIEFS =
+ "political_or_religious_beliefs";
+ public static final String TYPE_SEXUAL_ORIENTATION_OR_GENDER_IDENTITY =
+ "sexual_orientation_or_gender_identity";
+ public static final String TYPE_PERSONAL_IDENTIFIERS = "personal_identifiers";
+ public static final String TYPE_OTHER = "other";
+
+ /** Data types for {@link DataCategoryConstants.CATEGORY_FINANCIAL} */
+ public static final String TYPE_CARD_BANK_ACCOUNT = "card_bank_account";
+
+ public static final String TYPE_PURCHASE_HISTORY = "purchase_history";
+ public static final String TYPE_CREDIT_SCORE = "credit_score";
+ public static final String TYPE_FINANCIAL_OTHER = "other";
+
+ /** Data types for {@link DataCategoryConstants.CATEGORY_LOCATION} */
+ public static final String TYPE_APPROX_LOCATION = "approx_location";
+
+ public static final String TYPE_PRECISE_LOCATION = "precise_location";
+
+ /** Data types for {@link DataCategoryConstants.CATEGORY_EMAIL_TEXT_MESSAGE} */
+ public static final String TYPE_EMAILS = "emails";
+
+ public static final String TYPE_TEXT_MESSAGES = "text_messages";
+ public static final String TYPE_EMAIL_TEXT_MESSAGE_OTHER = "other";
+
+ /** Data types for {@link DataCategoryConstants.CATEGORY_PHOTO_VIDEO} */
+ public static final String TYPE_PHOTOS = "photos";
+
+ public static final String TYPE_VIDEOS = "videos";
+
+ /** Data types for {@link DataCategoryConstants.CATEGORY_AUDIO} */
+ public static final String TYPE_SOUND_RECORDINGS = "sound_recordings";
+
+ public static final String TYPE_MUSIC_FILES = "music_files";
+ public static final String TYPE_AUDIO_OTHER = "other";
+
+ /** Data types for {@link DataCategoryConstants.CATEGORY_STORAGE} */
+ public static final String TYPE_FILES_DOCS = "files_docs";
+
+ /** Data types for {@link DataCategoryConstants.CATEGORY_HEALTH_FITNESS} */
+ public static final String TYPE_HEALTH = "health";
+
+ public static final String TYPE_FITNESS = "fitness";
+
+ /** Data types for {@link DataCategoryConstants.CATEGORY_CONTACTS} */
+ public static final String TYPE_CONTACTS = "contacts";
+
+ /** Data types for {@link DataCategoryConstants.CATEGORY_CALENDAR} */
+ public static final String TYPE_CALENDAR = "calendar";
+
+ /** Data types for {@link DataCategoryConstants.CATEGORY_IDENTIFIERS} */
+ public static final String TYPE_IDENTIFIERS_OTHER = "other";
+
+ /** Data types for {@link DataCategoryConstants.CATEGORY_APP_PERFORMANCE} */
+ public static final String TYPE_CRASH_LOGS = "crash_logs";
+
+ public static final String TYPE_PERFORMANCE_DIAGNOSTICS = "performance_diagnostics";
+ public static final String TYPE_APP_PERFORMANCE_OTHER = "other";
+
+ /** Data types for {@link DataCategoryConstants.CATEGORY_ACTIONS_IN_APP} */
+ public static final String TYPE_USER_INTERACTION = "user_interaction";
+
+ public static final String TYPE_IN_APP_SEARCH_HISTORY = "in_app_search_history";
+ public static final String TYPE_INSTALLED_APPS = "installed_apps";
+ public static final String TYPE_USER_GENERATED_CONTENT = "user_generated_content";
+ public static final String TYPE_ACTIONS_IN_APP_OTHER = "other";
+
+ /** Data types for {@link DataCategoryConstants.CATEGORY_SEARCH_AND_BROWSING} */
+ public static final String TYPE_WEB_BROWSING_HISTORY = "web_browsing_history";
+
+ /** Set of valid categories */
+ public static final Set<String> VALID_TYPES =
+ Collections.unmodifiableSet(
+ new HashSet<>(
+ Arrays.asList(
+ TYPE_NAME,
+ TYPE_EMAIL_ADDRESS,
+ TYPE_PHONE_NUMBER,
+ TYPE_RACE_ETHNICITY,
+ TYPE_POLITICAL_OR_RELIGIOUS_BELIEFS,
+ TYPE_SEXUAL_ORIENTATION_OR_GENDER_IDENTITY,
+ TYPE_PERSONAL_IDENTIFIERS,
+ TYPE_OTHER,
+ TYPE_CARD_BANK_ACCOUNT,
+ TYPE_PURCHASE_HISTORY,
+ TYPE_CREDIT_SCORE,
+ TYPE_FINANCIAL_OTHER,
+ TYPE_APPROX_LOCATION,
+ TYPE_PRECISE_LOCATION,
+ TYPE_EMAILS,
+ TYPE_TEXT_MESSAGES,
+ TYPE_EMAIL_TEXT_MESSAGE_OTHER,
+ TYPE_PHOTOS,
+ TYPE_VIDEOS,
+ TYPE_SOUND_RECORDINGS,
+ TYPE_MUSIC_FILES,
+ TYPE_AUDIO_OTHER,
+ TYPE_FILES_DOCS,
+ TYPE_HEALTH,
+ TYPE_FITNESS,
+ TYPE_CONTACTS,
+ TYPE_CALENDAR,
+ TYPE_IDENTIFIERS_OTHER,
+ TYPE_CRASH_LOGS,
+ TYPE_PERFORMANCE_DIAGNOSTICS,
+ TYPE_APP_PERFORMANCE_OTHER,
+ TYPE_USER_INTERACTION,
+ TYPE_IN_APP_SEARCH_HISTORY,
+ TYPE_INSTALLED_APPS,
+ TYPE_USER_GENERATED_CONTENT,
+ TYPE_ACTIONS_IN_APP_OTHER,
+ TYPE_WEB_BROWSING_HISTORY)));
+
+ /** Returns {@link Set} of valid {@link String} category keys */
+ public static Set<String> getValidDataTypes() {
+ return VALID_TYPES;
+ }
+
+ private DataTypeConstants() {
+ /* do nothing - hide constructor */
+ }
+}
diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataTypeFactory.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataTypeFactory.java
new file mode 100644
index 0000000..e3d1587
--- /dev/null
+++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataTypeFactory.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.asllib;
+
+import org.w3c.dom.Element;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+public class DataTypeFactory implements AslMarshallableFactory<DataType> {
+ /** Creates a {@link DataType} from the human-readable DOM element. */
+ @Override
+ public DataType createFromHrElements(List<Element> elements) {
+ Element hrDataTypeEle = XmlUtils.getSingleElement(elements);
+ String dataTypeName = hrDataTypeEle.getAttribute(XmlUtils.HR_ATTR_DATA_TYPE);
+ Set<DataType.Purpose> purposeSet =
+ Arrays.stream(hrDataTypeEle.getAttribute(XmlUtils.HR_ATTR_PURPOSES).split("\\|"))
+ .map(DataType.Purpose::forString)
+ .collect(Collectors.toUnmodifiableSet());
+ Boolean isCollectionOptional =
+ XmlUtils.fromString(
+ hrDataTypeEle.getAttribute(XmlUtils.HR_ATTR_IS_COLLECTION_OPTIONAL));
+ Boolean isSharingOptional =
+ XmlUtils.fromString(
+ hrDataTypeEle.getAttribute(XmlUtils.HR_ATTR_IS_SHARING_OPTIONAL));
+ Boolean ephemeral =
+ XmlUtils.fromString(hrDataTypeEle.getAttribute(XmlUtils.HR_ATTR_EPHEMERAL));
+ return new DataType(
+ dataTypeName, purposeSet, isCollectionOptional, isSharingOptional, ephemeral);
+ }
+}
diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/SafetyLabels.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/SafetyLabels.java
new file mode 100644
index 0000000..f06522f
--- /dev/null
+++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/SafetyLabels.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.asllib;
+
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+
+import java.util.List;
+
+/** Safety Label representation containing zero or more {@link DataCategory} for data shared */
+public class SafetyLabels implements AslMarshallable {
+
+ private final Long mVersion;
+ private final DataLabels mDataLabels;
+
+ public SafetyLabels(Long version, DataLabels dataLabels) {
+ this.mVersion = version;
+ this.mDataLabels = dataLabels;
+ }
+
+ /** Returns the data label for the safety label */
+ public DataLabels getDataLabel() {
+ return mDataLabels;
+ }
+
+ /** Gets the version of the {@link SafetyLabels}. */
+ public Long getVersion() {
+ return mVersion;
+ }
+
+ /** Creates an on-device DOM element from the {@link SafetyLabels}. */
+ @Override
+ public List<Element> toOdDomElements(Document doc) {
+ Element safetyLabelsEle =
+ XmlUtils.createPbundleEleWithName(doc, XmlUtils.OD_NAME_SAFETY_LABELS);
+ XmlUtils.appendChildren(safetyLabelsEle, mDataLabels.toOdDomElements(doc));
+ return List.of(safetyLabelsEle);
+ }
+}
diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/SafetyLabelsFactory.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/SafetyLabelsFactory.java
new file mode 100644
index 0000000..80b9f57
--- /dev/null
+++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/SafetyLabelsFactory.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.asllib;
+
+import com.android.asllib.util.MalformedXmlException;
+
+import org.w3c.dom.Element;
+
+import java.util.List;
+
+public class SafetyLabelsFactory implements AslMarshallableFactory<SafetyLabels> {
+
+ /** Creates a {@link SafetyLabels} from the human-readable DOM element. */
+ @Override
+ public SafetyLabels createFromHrElements(List<Element> elements) throws MalformedXmlException {
+ Element safetyLabelsEle = XmlUtils.getSingleElement(elements);
+ Long version;
+ try {
+ version = Long.parseLong(safetyLabelsEle.getAttribute(XmlUtils.HR_ATTR_VERSION));
+ } catch (Exception e) {
+ throw new IllegalArgumentException(
+ "Malformed or missing required version in safety labels.");
+ }
+
+ DataLabels dataLabels =
+ new DataLabelsFactory()
+ .createFromHrElements(
+ List.of(
+ XmlUtils.getSingleChildElement(
+ safetyLabelsEle, XmlUtils.HR_TAG_DATA_LABELS)));
+ return new SafetyLabels(version, dataLabels);
+ }
+}
diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/XmlUtils.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/XmlUtils.java
new file mode 100644
index 0000000..3bc9ccc
--- /dev/null
+++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/XmlUtils.java
@@ -0,0 +1,158 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.asllib;
+
+import com.android.asllib.util.MalformedXmlException;
+
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.NodeList;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class XmlUtils {
+ public static final String HR_TAG_APP_METADATA_BUNDLES = "app-metadata-bundles";
+ public static final String HR_TAG_SAFETY_LABELS = "safety-labels";
+ public static final String HR_TAG_DATA_LABELS = "data-labels";
+ public static final String HR_TAG_DATA_ACCESSED = "data-accessed";
+ public static final String HR_TAG_DATA_COLLECTED = "data-collected";
+ public static final String HR_TAG_DATA_SHARED = "data-shared";
+
+ public static final String HR_ATTR_DATA_CATEGORY = "dataCategory";
+ public static final String HR_ATTR_DATA_TYPE = "dataType";
+ public static final String HR_ATTR_IS_COLLECTION_OPTIONAL = "isCollectionOptional";
+ public static final String HR_ATTR_IS_SHARING_OPTIONAL = "isSharingOptional";
+ public static final String HR_ATTR_EPHEMERAL = "ephemeral";
+ public static final String HR_ATTR_PURPOSES = "purposes";
+ public static final String HR_ATTR_VERSION = "version";
+
+ public static final String OD_TAG_BUNDLE = "bundle";
+ public static final String OD_TAG_PBUNDLE_AS_MAP = "pbundle_as_map";
+ public static final String OD_TAG_BOOLEAN = "boolean";
+ public static final String OD_TAG_INT_ARRAY = "int-array";
+ public static final String OD_TAG_ITEM = "item";
+ public static final String OD_ATTR_NAME = "name";
+ public static final String OD_ATTR_VALUE = "value";
+ public static final String OD_ATTR_NUM = "num";
+ public static final String OD_NAME_SAFETY_LABELS = "safety_labels";
+ public static final String OD_NAME_DATA_LABELS = "data_labels";
+ public static final String OD_NAME_DATA_ACCESSED = "data_accessed";
+ public static final String OD_NAME_DATA_COLLECTED = "data_collected";
+ public static final String OD_NAME_DATA_SHARED = "data_shared";
+ public static final String OD_NAME_PURPOSES = "purposes";
+ public static final String OD_NAME_IS_COLLECTION_OPTIONAL = "is_collection_optional";
+ public static final String OD_NAME_IS_SHARING_OPTIONAL = "is_sharing_optional";
+ public static final String OD_NAME_EPHEMERAL = "ephemeral";
+
+ public static final String TRUE_STR = "true";
+ public static final String FALSE_STR = "false";
+
+ /** Gets the single top-level {@link Element} having the {@param tagName}. */
+ public static Element getSingleElement(Document doc, String tagName)
+ throws MalformedXmlException {
+ var elements = doc.getElementsByTagName(tagName);
+ return getSingleElement(elements, tagName);
+ }
+
+ /**
+ * Gets the single {@link Element} within {@param parentEle} and having the {@param tagName}.
+ */
+ public static Element getSingleChildElement(Element parentEle, String tagName)
+ throws MalformedXmlException {
+ var elements = parentEle.getElementsByTagName(tagName);
+ return getSingleElement(elements, tagName);
+ }
+
+ /** Gets the single {@link Element} from {@param elements} */
+ public static Element getSingleElement(NodeList elements, String tagName)
+ throws MalformedXmlException {
+ if (elements.getLength() != 1) {
+ throw new MalformedXmlException(
+ String.format(
+ "Expected 1 element \"%s\" in NodeList but got %s.",
+ tagName, elements.getLength()));
+ }
+ var elementAsNode = elements.item(0);
+ if (!(elementAsNode instanceof Element)) {
+ throw new MalformedXmlException(
+ String.format("%s was not a valid XML element.", tagName));
+ }
+ return ((Element) elementAsNode);
+ }
+
+ /** Gets the single {@link Element} within {@param elements}. */
+ public static Element getSingleElement(List<Element> elements) {
+ if (elements.size() != 1) {
+ throw new IllegalStateException(
+ String.format("Expected 1 element in list but got %s.", elements.size()));
+ }
+ return elements.get(0);
+ }
+
+ /** Converts {@param nodeList} into List of {@link Element}. */
+ public static List<Element> asElementList(NodeList nodeList) {
+ List<Element> elementList = new ArrayList<Element>();
+ for (int i = 0; i < nodeList.getLength(); i++) {
+ var elementAsNode = nodeList.item(0);
+ if (elementAsNode instanceof Element) {
+ elementList.add(((Element) elementAsNode));
+ }
+ }
+ return elementList;
+ }
+
+ /** Appends {@param children} to the {@param ele}. */
+ public static void appendChildren(Element ele, List<Element> children) {
+ for (Element c : children) {
+ ele.appendChild(c);
+ }
+ }
+
+ /** Gets the Boolean from the String value. */
+ public static Boolean fromString(String s) {
+ if (s == null) {
+ return null;
+ }
+ if (s.equals(TRUE_STR)) {
+ return true;
+ } else if (s.equals(FALSE_STR)) {
+ return false;
+ }
+ return null;
+ }
+
+ /** Creates an on-device PBundle DOM Element with the given attribute name. */
+ public static Element createPbundleEleWithName(Document doc, String name) {
+ var ele = doc.createElement(XmlUtils.OD_TAG_PBUNDLE_AS_MAP);
+ ele.setAttribute(XmlUtils.OD_ATTR_NAME, name);
+ return ele;
+ }
+
+ /** Create an on-device Boolean DOM Element with the given attribute name. */
+ public static Element createOdBooleanEle(Document doc, String name, boolean b) {
+ var ele = doc.createElement(XmlUtils.OD_TAG_BOOLEAN);
+ ele.setAttribute(XmlUtils.OD_ATTR_NAME, name);
+ ele.setAttribute(XmlUtils.OD_ATTR_VALUE, String.valueOf(b));
+ return ele;
+ }
+
+ /** Returns whether the String is null or empty. */
+ public static boolean isNullOrEmpty(String s) {
+ return s == null || s.isEmpty();
+ }
+}
diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/util/MalformedXmlException.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/util/MalformedXmlException.java
new file mode 100644
index 0000000..216df56
--- /dev/null
+++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/util/MalformedXmlException.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.asllib.util;
+
+public class MalformedXmlException extends Exception {
+ /** Constructs an {@code MalformedXmlException} with no detail message. */
+ public MalformedXmlException() {
+ super();
+ }
+
+ /**
+ * Constructs an {@code MalformedXmlException} with the specified detail message.
+ *
+ * @param s the detail message.
+ */
+ public MalformedXmlException(String s) {
+ super(s);
+ }
+}
diff --git a/wifi/wifi.aconfig b/wifi/wifi.aconfig
index 6ac986e..6c4e4c3 100644
--- a/wifi/wifi.aconfig
+++ b/wifi/wifi.aconfig
@@ -2,6 +2,7 @@
flag {
name: "get_device_cross_akm_roaming_support"
+ is_exported: true
namespace: "wifi"
description: "Add new API to get the device support for CROSS-AKM roaming"
bug: "313038031"