Merge "Allow cross user package suspension" 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/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/core/api/current.txt b/core/api/current.txt
index 4d3ca13..8a61f4a 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -10764,6 +10764,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 +20236,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
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..a2b847e 100644
--- a/core/api/test-current.txt
+++ b/core/api/test-current.txt
@@ -2460,6 +2460,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/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/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/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/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/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/content/Context.java b/core/java/android/content/Context.java
index 89300e3..284e318 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,
@@ -5930,9 +5930,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/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/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/credentials/flags.aconfig b/core/java/android/credentials/flags.aconfig
index 47edba6..16ca31f 100644
--- a/core/java/android/credentials/flags.aconfig
+++ b/core/java/android/credentials/flags.aconfig
@@ -47,6 +47,7 @@
name: "configurable_selector_ui_enabled"
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/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/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/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/Trace.java b/core/java/android/os/Trace.java
index bebb912..edb3a64 100644
--- a/core/java/android/os/Trace.java
+++ b/core/java/android/os/Trace.java
@@ -125,15 +125,15 @@
@UnsupportedAppUsage
@CriticalNative
@android.ravenwood.annotation.RavenwoodReplace
- private static native long nativeGetEnabledTags();
+ private static native boolean nativeIsTagEnabled(long tag);
@android.ravenwood.annotation.RavenwoodReplace
private static native void nativeSetAppTracingAllowed(boolean allowed);
@android.ravenwood.annotation.RavenwoodReplace
private static native void nativeSetTracingEnabled(boolean allowed);
- private static long nativeGetEnabledTags$ravenwood() {
+ private static boolean nativeIsTagEnabled$ravenwood(long traceTag) {
// Tracing currently completely disabled under Ravenwood
- return 0;
+ return false;
}
private static void nativeSetAppTracingAllowed$ravenwood(boolean allowed) {
@@ -181,8 +181,7 @@
@UnsupportedAppUsage
@SystemApi(client = MODULE_LIBRARIES)
public static boolean isTagEnabled(long traceTag) {
- long tags = nativeGetEnabledTags();
- return (tags & traceTag) != 0;
+ return nativeIsTagEnabled(traceTag);
}
/**
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/provider/Settings.java b/core/java/android/provider/Settings.java
index e26dc73..25c2b0e 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,13 @@
*/
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.
+ *
+ * @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/service/chooser/flags.aconfig b/core/java/android/service/chooser/flags.aconfig
index d72441f..00236df 100644
--- a/core/java/android/service/chooser/flags.aconfig
+++ b/core/java/android/service/chooser/flags.aconfig
@@ -27,14 +27,3 @@
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/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/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/text/flags/flags.aconfig b/core/java/android/text/flags/flags.aconfig
index aff1d4a..30b1a2e 100644
--- a/core/java/android/text/flags/flags.aconfig
+++ b/core/java/android/text/flags/flags.aconfig
@@ -121,8 +121,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/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/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/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/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/windowing_frontend.aconfig b/core/java/android/window/flags/windowing_frontend.aconfig
index 14fb17c..65bf241 100644
--- a/core/java/android/window/flags/windowing_frontend.aconfig
+++ b/core/java/android/window/flags/windowing_frontend.aconfig
@@ -38,6 +38,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"
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/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/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/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_media_AudioSystem.cpp b/core/jni/android_media_AudioSystem.cpp
index 5223798..d48cdc4 100644
--- a/core/jni/android_media_AudioSystem.cpp
+++ b/core/jni/android_media_AudioSystem.cpp
@@ -161,6 +161,7 @@
jfieldID mMixType;
jfieldID mCallbackFlags;
jfieldID mToken;
+ jfieldID mVirtualDeviceId;
} gAudioMixFields;
static jclass gAudioFormatClass;
@@ -2312,7 +2313,7 @@
jstring deviceAddress = env->NewStringUTF(nAudioMix.mDeviceAddress.c_str());
*jAudioMix = env->NewObject(gAudioMixClass, gAudioMixCstor, jAudioMixingRule, jAudioFormat,
nAudioMix.mRouteFlags, nAudioMix.mCbFlags, nAudioMix.mDeviceType,
- deviceAddress, jBinderToken);
+ deviceAddress, jBinderToken, nAudioMix.mVirtualDeviceId);
return AUDIO_JAVA_SUCCESS;
}
@@ -2347,6 +2348,7 @@
aiBinder(AIBinder_fromJavaBinder(env, jToken), &AIBinder_decStrong);
nAudioMix->mToken = AIBinder_toPlatformBinder(aiBinder.get());
+ nAudioMix->mVirtualDeviceId = env->GetIntField(jAudioMix, gAudioMixFields.mVirtualDeviceId);
jint status = convertAudioMixingRuleToNative(env, jRule, &(nAudioMix->mCriteria));
env->DeleteLocalRef(jRule);
@@ -3676,7 +3678,7 @@
gAudioMixCstor =
GetMethodIDOrDie(env, audioMixClass, "<init>",
"(Landroid/media/audiopolicy/AudioMixingRule;Landroid/"
- "media/AudioFormat;IIILjava/lang/String;Landroid/os/IBinder;)V");
+ "media/AudioFormat;IIILjava/lang/String;Landroid/os/IBinder;I)V");
}
gAudioMixFields.mRule = GetFieldIDOrDie(env, audioMixClass, "mRule",
"Landroid/media/audiopolicy/AudioMixingRule;");
@@ -3689,6 +3691,7 @@
gAudioMixFields.mMixType = GetFieldIDOrDie(env, audioMixClass, "mMixType", "I");
gAudioMixFields.mCallbackFlags = GetFieldIDOrDie(env, audioMixClass, "mCallbackFlags", "I");
gAudioMixFields.mToken = GetFieldIDOrDie(env, audioMixClass, "mToken", "Landroid/os/IBinder;");
+ gAudioMixFields.mVirtualDeviceId = GetFieldIDOrDie(env, audioMixClass, "mVirtualDeviceId", "I");
jclass audioFormatClass = FindClassOrDie(env, "android/media/AudioFormat");
gAudioFormatClass = MakeGlobalRefOrDie(env, audioFormatClass);
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 b579daf..4387a4c 100644
--- a/core/jni/android_os_Trace.cpp
+++ b/core/jni/android_os_Trace.cpp
@@ -124,8 +124,8 @@
});
}
-static jlong android_os_Trace_nativeGetEnabledTags(JNIEnv* env) {
- return tracing_perfetto::getEnabledCategories();
+static jboolean android_os_Trace_nativeIsTagEnabled(jlong tag) {
+ return tracing_perfetto::isTagEnabled(tag);
}
static void android_os_Trace_nativeRegisterWithPerfetto(JNIEnv* env) {
@@ -157,7 +157,7 @@
{"nativeRegisterWithPerfetto", "()V", (void*)android_os_Trace_nativeRegisterWithPerfetto},
// ----------- @CriticalNative ----------------
- {"nativeGetEnabledTags", "()J", (void*)android_os_Trace_nativeGetEnabledTags},
+ {"nativeIsTagEnabled", "(J)Z", (void*)android_os_Trace_nativeIsTagEnabled},
};
int register_android_os_Trace(JNIEnv* env) {
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/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/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 efba709..89ac81e 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -6419,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 668a88c..2e029b2 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -3122,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" />
@@ -5017,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" />
@@ -5336,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/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 9576e8d..2da6227 100644
--- a/data/keyboards/Vendor_054c_Product_05c4.idc
+++ b/data/keyboards/Vendor_054c_Product_05c4.idc
@@ -45,14 +45,15 @@
# 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, which causes the finger's single smooth movement to
-# be treated as many small movements of consecutive touches, which are then
-# inhibited by the click wiggle filter.
+# 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 does not seem vulnerable to click wiggle, we can safely
-# disable drumroll detection due to speed changes (by setting the speed change
-# threshold very high, since there's no boolean control property).
-gestureProp.Drumroll_Max_Speed_Change_Factor = 1000000000
+# 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
diff --git a/data/keyboards/Vendor_054c_Product_09cc.idc b/data/keyboards/Vendor_054c_Product_09cc.idc
index 9576e8d..2a1a4fc 100644
--- a/data/keyboards/Vendor_054c_Product_09cc.idc
+++ b/data/keyboards/Vendor_054c_Product_09cc.idc
@@ -45,14 +45,15 @@
# 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, which causes the finger's single smooth movement to
-# be treated as many small movements of consecutive touches, which are then
-# inhibited by the click wiggle filter.
+# 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 does not seem vulnerable to click wiggle, we can safely
-# disable drumroll detection due to speed changes (by setting the speed change
-# threshold very high, since there's no boolean control property).
-gestureProp.Drumroll_Max_Speed_Change_Factor = 1000000000
+# 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
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/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 43ce166..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 -->
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/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/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/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/media/java/android/media/AudioManager.java b/media/java/android/media/AudioManager.java
index 6f7024a..1fe3c2e 100644
--- a/media/java/android/media/AudioManager.java
+++ b/media/java/android/media/AudioManager.java
@@ -5453,7 +5453,8 @@
String regId = service.registerAudioPolicy(policy.getConfig(), policy.cb(),
policy.hasFocusListener(), policy.isFocusPolicy(), policy.isTestFocusPolicy(),
policy.isVolumeController(),
- projection == null ? null : projection.getProjection());
+ projection == null ? null : projection.getProjection(),
+ policy.getAttributionSource());
if (regId == null) {
return ERROR;
} else {
diff --git a/media/java/android/media/AudioRecord.java b/media/java/android/media/AudioRecord.java
index 447d3bb..80e5719 100644
--- a/media/java/android/media/AudioRecord.java
+++ b/media/java/android/media/AudioRecord.java
@@ -789,7 +789,7 @@
private @NonNull AudioRecord buildAudioPlaybackCaptureRecord() {
AudioMix audioMix = mAudioPlaybackCaptureConfiguration.createAudioMix(mFormat);
MediaProjection projection = mAudioPlaybackCaptureConfiguration.getMediaProjection();
- AudioPolicy audioPolicy = new AudioPolicy.Builder(/*context=*/ null)
+ AudioPolicy audioPolicy = new AudioPolicy.Builder(/*context=*/ mContext)
.setMediaProjection(projection)
.addMix(audioMix).build();
@@ -853,7 +853,7 @@
.setFormat(mFormat)
.setRouteFlags(AudioMix.ROUTE_FLAG_LOOP_BACK)
.build();
- AudioPolicy audioPolicy = new AudioPolicy.Builder(null).addMix(audioMix).build();
+ AudioPolicy audioPolicy = new AudioPolicy.Builder(mContext).addMix(audioMix).build();
if (AudioManager.registerAudioPolicyStatic(audioPolicy) != 0) {
throw new UnsupportedOperationException("Error: could not register audio policy");
}
diff --git a/media/java/android/media/AudioTrack.java b/media/java/android/media/AudioTrack.java
index 194da21..73deb17 100644
--- a/media/java/android/media/AudioTrack.java
+++ b/media/java/android/media/AudioTrack.java
@@ -1353,7 +1353,8 @@
.setRouteFlags(AudioMix.ROUTE_FLAG_LOOP_BACK)
.build();
AudioPolicy audioPolicy =
- new AudioPolicy.Builder(/*context=*/ null).addMix(audioMix).build();
+ new AudioPolicy.Builder(/*context=*/ mContext).addMix(audioMix).build();
+
if (AudioManager.registerAudioPolicyStatic(audioPolicy) != 0) {
throw new UnsupportedOperationException("Error: could not register audio policy");
}
diff --git a/media/java/android/media/IAudioService.aidl b/media/java/android/media/IAudioService.aidl
index 98bd3ca..e612645 100644
--- a/media/java/android/media/IAudioService.aidl
+++ b/media/java/android/media/IAudioService.aidl
@@ -18,6 +18,7 @@
import android.bluetooth.BluetoothDevice;
import android.content.ComponentName;
+import android.content.AttributionSource;
import android.media.AudioAttributes;
import android.media.AudioDeviceAttributes;
import android.media.AudioFormat;
@@ -361,7 +362,8 @@
String registerAudioPolicy(in AudioPolicyConfig policyConfig,
in IAudioPolicyCallback pcb, boolean hasFocusListener, boolean isFocusPolicy,
boolean isTestFocusPolicy,
- boolean isVolumeController, in IMediaProjection projection);
+ boolean isVolumeController, in IMediaProjection projection,
+ in AttributionSource attributionSource);
oneway void unregisterAudioPolicyAsync(in IAudioPolicyCallback pcb);
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/audiopolicy/AudioMix.java b/media/java/android/media/audiopolicy/AudioMix.java
index a53a8ce..e4eaaa3 100644
--- a/media/java/android/media/audiopolicy/AudioMix.java
+++ b/media/java/android/media/audiopolicy/AudioMix.java
@@ -24,6 +24,7 @@
import android.annotation.Nullable;
import android.annotation.SystemApi;
import android.compat.annotation.UnsupportedAppUsage;
+import android.content.Context;
import android.media.AudioDeviceInfo;
import android.media.AudioFormat;
import android.media.AudioSystem;
@@ -67,12 +68,19 @@
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
final int mDeviceSystemType; // an AudioSystem.DEVICE_* value, not AudioDeviceInfo.TYPE_*
+ // The (virtual) device ID that this AudioMix was registered for. This value is overwritten
+ // when registering this AudioMix with an AudioPolicy or attaching this AudioMix to an
+ // AudioPolicy to match the AudioPolicy attribution. Does not imply that it only modifies
+ // audio routing for this device ID.
+ private int mVirtualDeviceId;
+
/**
* All parameters are guaranteed valid through the Builder.
*/
private AudioMix(@NonNull AudioMixingRule rule, @NonNull AudioFormat format,
int routeFlags, int callbackFlags,
- int deviceType, @Nullable String deviceAddress, IBinder token) {
+ int deviceType, @Nullable String deviceAddress, IBinder token,
+ int virtualDeviceId) {
mRule = Objects.requireNonNull(rule);
mFormat = Objects.requireNonNull(format);
mRouteFlags = routeFlags;
@@ -81,6 +89,7 @@
mDeviceSystemType = deviceType;
mDeviceAddress = (deviceAddress == null) ? new String("") : deviceAddress;
mToken = token;
+ mVirtualDeviceId = virtualDeviceId;
}
// CALLBACK_FLAG_* values: keep in sync with AudioMix::kCbFlag* values defined
@@ -269,6 +278,11 @@
}
/** @hide */
+ public boolean matchesVirtualDeviceId(int deviceId) {
+ return mVirtualDeviceId == deviceId;
+ }
+
+ /** @hide */
@Override
public boolean equals(Object o) {
if (this == o) return true;
@@ -311,6 +325,7 @@
mFormat.writeToParcel(dest, flags);
mRule.writeToParcel(dest, flags);
dest.writeStrongBinder(mToken);
+ dest.writeInt(mVirtualDeviceId);
}
public static final @NonNull Parcelable.Creator<AudioMix> CREATOR = new Parcelable.Creator<>() {
@@ -331,6 +346,7 @@
mixBuilder.setFormat(AudioFormat.CREATOR.createFromParcel(p));
mixBuilder.setMixingRule(AudioMixingRule.CREATOR.createFromParcel(p));
mixBuilder.setToken(p.readStrongBinder());
+ mixBuilder.setVirtualDeviceId(p.readInt());
return mixBuilder.build();
}
@@ -339,6 +355,15 @@
}
};
+ /**
+ * Updates the deviceId of the AudioMix to match with the AudioPolicy the mix is registered
+ * through.
+ * @hide
+ */
+ public void setVirtualDeviceId(int virtualDeviceId) {
+ mVirtualDeviceId = virtualDeviceId;
+ }
+
/** @hide */
@IntDef(flag = true,
value = { ROUTE_FLAG_RENDER, ROUTE_FLAG_LOOP_BACK } )
@@ -354,6 +379,7 @@
private int mRouteFlags = 0;
private int mCallbackFlags = 0;
private IBinder mToken = null;
+ private int mVirtualDeviceId = Context.DEVICE_ID_DEFAULT;
// an AudioSystem.DEVICE_* value, not AudioDeviceInfo.TYPE_*
private int mDeviceSystemType = AudioSystem.DEVICE_NONE;
private String mDeviceAddress = null;
@@ -404,6 +430,15 @@
/**
* @hide
+ * Only used by AudioMix internally.
+ */
+ Builder setVirtualDeviceId(int virtualDeviceId) {
+ mVirtualDeviceId = virtualDeviceId;
+ return this;
+ }
+
+ /**
+ * @hide
* Only used by AudioPolicyConfig, not a public API.
* @param callbackFlags which callbacks are called from native
* @return the same Builder instance.
@@ -570,7 +605,7 @@
}
return new AudioMix(mRule, mFormat, mRouteFlags, mCallbackFlags, mDeviceSystemType,
- mDeviceAddress, mToken);
+ mDeviceAddress, mToken, mVirtualDeviceId);
}
private int getLoopbackDeviceSystemTypeForAudioMixingRule(AudioMixingRule rule) {
diff --git a/media/java/android/media/audiopolicy/AudioPolicy.java b/media/java/android/media/audiopolicy/AudioPolicy.java
index 508c0a2b..293a8f8 100644
--- a/media/java/android/media/audiopolicy/AudioPolicy.java
+++ b/media/java/android/media/audiopolicy/AudioPolicy.java
@@ -27,6 +27,7 @@
import android.annotation.TestApi;
import android.annotation.UserIdInt;
import android.app.ActivityManager;
+import android.content.AttributionSource;
import android.content.Context;
import android.content.pm.PackageManager;
import android.media.AudioAttributes;
@@ -146,6 +147,16 @@
return mProjection;
}
+ /** @hide */
+ public AttributionSource getAttributionSource() {
+ return getAttributionSource(mContext);
+ }
+
+ private static AttributionSource getAttributionSource(Context context) {
+ return context == null
+ ? AttributionSource.myAttributionSource() : context.getAttributionSource();
+ }
+
/**
* The parameters are guaranteed non-null through the Builder
*/
@@ -208,6 +219,9 @@
if (mix == null) {
throw new IllegalArgumentException("Illegal null AudioMix argument");
}
+ if (android.permission.flags.Flags.deviceAwarePermissionApisEnabled()) {
+ mix.setVirtualDeviceId(getAttributionSource(mContext).getDeviceId());
+ }
mMixes.add(mix);
return this;
}
@@ -358,6 +372,9 @@
if (mix == null) {
throw new IllegalArgumentException("Illegal null AudioMix in attachMixes");
} else {
+ if (android.permission.flags.Flags.deviceAwarePermissionApisEnabled()) {
+ mix.setVirtualDeviceId(getAttributionSource(mContext).getDeviceId());
+ }
zeMixes.add(mix);
}
}
@@ -400,6 +417,9 @@
if (mix == null) {
throw new IllegalArgumentException("Illegal null AudioMix in detachMixes");
} else {
+ if (android.permission.flags.Flags.deviceAwarePermissionApisEnabled()) {
+ mix.setVirtualDeviceId(getAttributionSource(mContext).getDeviceId());
+ }
zeMixes.add(mix);
}
}
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/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/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_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..a4c6ac7 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,10 +76,11 @@
return;
}
mPackageManager = getPackageManager();
+ mPackageInstaller = mPackageManager.getPackageInstaller();
mUserManager = getSystemService(UserManager.class);
Intent intent = getIntent();
- String callingPackage = getCallingPackage();
+ String callingPackage = getLaunchedFromPackage();
String callingAttributionTag = null;
// Uid of the source package, coming from ActivityManager
@@ -87,31 +89,33 @@
Log.w(TAG, "Could not determine the launching uid.");
}
+ // The UID of the origin of the installation. Note that it can be different than the
+ // "installer" of the session. For instance, if a 3P caller launched PIA with an ACTION_VIEW
+ // intent, the originatingUid is the 3P caller, but the "installer" in this case would
+ // be PIA.
+ int originatingUid = callingUid;
+
final boolean isSessionInstall =
PackageInstaller.ACTION_CONFIRM_PRE_APPROVAL.equals(intent.getAction())
|| PackageInstaller.ACTION_CONFIRM_INSTALL.equals(intent.getAction());
- // If the activity was started via a PackageInstaller session, we retrieve the calling
- // package from that session
+ // If the activity was started via a PackageInstaller session, we retrieve the originating
+ // UID from that session
final int sessionId = (isSessionInstall
- ? intent.getIntExtra(PackageInstaller.EXTRA_SESSION_ID, -1)
- : -1);
- int originatingUidFromSession = callingUid;
- if (callingPackage == null && sessionId != -1) {
- PackageInstaller packageInstaller = getPackageManager().getPackageInstaller();
- PackageInstaller.SessionInfo sessionInfo = packageInstaller.getSessionInfo(sessionId);
+ ? intent.getIntExtra(PackageInstaller.EXTRA_SESSION_ID, SessionInfo.INVALID_ID)
+ : SessionInfo.INVALID_ID);
+ if (sessionId != SessionInfo.INVALID_ID) {
+ PackageInstaller.SessionInfo sessionInfo = mPackageInstaller.getSessionInfo(sessionId);
if (sessionInfo != null) {
- callingPackage = sessionInfo.getInstallerPackageName();
callingAttributionTag = sessionInfo.getInstallerAttributionTag();
- originatingUidFromSession = sessionInfo.getOriginatingUid();
+ if (sessionInfo.getOriginatingUid() != Process.INVALID_UID) {
+ originatingUid = sessionInfo.getOriginatingUid();
+ }
}
}
final ApplicationInfo sourceInfo = getSourceInfo(callingPackage);
- // Uid of the source package, with a preference to uid from ApplicationInfo
- final int originatingUid = sourceInfo != null ? sourceInfo.uid : callingUid;
-
if (callingUid == Process.INVALID_UID && sourceInfo == null) {
Log.e(TAG, "Cannot determine caller since UID is invalid and sourceInfo is null");
mAbortInstall = true;
@@ -124,28 +128,28 @@
boolean isTrustedSource = false;
if (sourceInfo != null && sourceInfo.isPrivilegedApp()) {
isTrustedSource = intent.getBooleanExtra(Intent.EXTRA_NOT_UNKNOWN_SOURCE, false) || (
- originatingUid != Process.INVALID_UID && checkPermission(
- Manifest.permission.INSTALL_PACKAGES, -1 /* pid */, originatingUid)
- == PackageManager.PERMISSION_GRANTED);
+ callingUid != Process.INVALID_UID && checkPermission(
+ Manifest.permission.INSTALL_PACKAGES, -1 /* pid */, callingUid)
+ == PackageManager.PERMISSION_GRANTED);
}
if (!isTrustedSource && !isSystemDownloadsProvider && !isDocumentsManager
- && originatingUid != Process.INVALID_UID) {
- final int targetSdkVersion = getMaxTargetSdkVersionForUid(this, originatingUid);
+ && callingUid != Process.INVALID_UID) {
+ final int targetSdkVersion = getMaxTargetSdkVersionForUid(this, callingUid);
if (targetSdkVersion < 0) {
- Log.e(TAG, "Cannot get target sdk version for uid " + originatingUid);
+ Log.e(TAG, "Cannot get target sdk version for uid " + callingUid);
// Invalid originating uid supplied. Abort install.
mAbortInstall = true;
} else if (targetSdkVersion >= Build.VERSION_CODES.O && !isUidRequestingPermission(
- originatingUid, Manifest.permission.REQUEST_INSTALL_PACKAGES)) {
- Log.e(TAG, "Requesting uid " + originatingUid + " needs to declare permission "
+ callingUid, Manifest.permission.REQUEST_INSTALL_PACKAGES)) {
+ Log.e(TAG, "Requesting uid " + callingUid + " needs to declare permission "
+ Manifest.permission.REQUEST_INSTALL_PACKAGES);
mAbortInstall = true;
}
}
- if (sessionId != -1 && !isCallerSessionOwner(originatingUid, sessionId)) {
- Log.e(TAG, "UID " + originatingUid + " is not the owner of session " +
+ if (sessionId != -1 && !isCallerSessionOwner(callingUid, sessionId)) {
+ Log.e(TAG, "CallingUid " + callingUid + " is not the owner of session " +
sessionId);
mAbortInstall = true;
}
@@ -155,10 +159,9 @@
final String installerPackageNameFromIntent = getIntent().getStringExtra(
Intent.EXTRA_INSTALLER_PACKAGE_NAME);
if (installerPackageNameFromIntent != null) {
- final String callingPkgName = getLaunchedFromPackage();
- if (!TextUtils.equals(installerPackageNameFromIntent, callingPkgName)
+ if (!TextUtils.equals(installerPackageNameFromIntent, callingPackage)
&& mPackageManager.checkPermission(Manifest.permission.INSTALL_PACKAGES,
- callingPkgName) != PackageManager.PERMISSION_GRANTED) {
+ callingPackage) != PackageManager.PERMISSION_GRANTED) {
Log.e(TAG, "The given installer package name " + installerPackageNameFromIntent
+ " is invalid. Remove it.");
EventLog.writeEvent(0x534e4554, "236687884", getLaunchedFromUid(),
@@ -186,8 +189,7 @@
callingAttributionTag);
nextActivity.putExtra(PackageInstallerActivity.EXTRA_ORIGINAL_SOURCE_INFO, sourceInfo);
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));
@@ -291,17 +291,16 @@
return false;
}
- private boolean isCallerSessionOwner(int originatingUid, int sessionId) {
- if (originatingUid == Process.ROOT_UID) {
+ private boolean isCallerSessionOwner(int callingUid, int sessionId) {
+ if (callingUid == 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;
}
int installerUid = sessionInfo.getInstallerUid();
- return originatingUid == installerUid;
+ return callingUid == installerUid;
}
private void checkDevicePolicyRestrictions() {
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/PackageInstallerActivity.java b/packages/PackageInstaller/src/com/android/packageinstaller/PackageInstallerActivity.java
index 45bfe54..8bed945 100644
--- a/packages/PackageInstaller/src/com/android/packageinstaller/PackageInstallerActivity.java
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/PackageInstallerActivity.java
@@ -84,8 +84,7 @@
static final String EXTRA_ORIGINAL_SOURCE_INFO = "EXTRA_ORIGINAL_SOURCE_INFO";
static final String EXTRA_STAGED_SESSION_ID = "EXTRA_STAGED_SESSION_ID";
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";
@@ -98,10 +97,6 @@
* The package name corresponding to #mOriginatingUid
*/
private String mOriginatingPackage;
- /**
- * The package name corresponding to the app updater in the update-ownership confirmation dialog
- */
- private String mOriginatingPackageFromSessionInfo;
private int mActivityResultCode = Activity.RESULT_CANCELED;
private int mPendingUserActionReason = -1;
@@ -154,8 +149,7 @@
viewToEnable = mDialog.requireViewById(R.id.install_confirm_question_update);
final CharSequence existingUpdateOwnerLabel = getExistingUpdateOwnerLabel();
- final CharSequence requestedUpdateOwnerLabel =
- getApplicationLabel(mOriginatingPackageFromSessionInfo);
+ final CharSequence requestedUpdateOwnerLabel = getApplicationLabel(mOriginatingPackage);
if (!TextUtils.isEmpty(existingUpdateOwnerLabel)
&& mPendingUserActionReason == PackageInstaller.REASON_REMIND_OWNERSHIP) {
String updateOwnerString =
@@ -304,21 +298,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
@@ -384,15 +363,9 @@
mCallingPackage = intent.getStringExtra(EXTRA_CALLING_PACKAGE);
mCallingAttributionTag = intent.getStringExtra(EXTRA_CALLING_ATTRIBUTION_TAG);
mSourceInfo = intent.getParcelableExtra(EXTRA_ORIGINAL_SOURCE_INFO);
- mOriginatingUid = intent.getIntExtra(Intent.EXTRA_ORIGINATING_UID,
- Process.INVALID_UID);
+ mOriginatingUid = intent.getIntExtra(Intent.EXTRA_ORIGINATING_UID, Process.INVALID_UID);
mOriginatingPackage = (mOriginatingUid != Process.INVALID_UID)
? getPackageNameForUid(mOriginatingUid) : null;
- int originatingUidFromSessionInfo =
- intent.getIntExtra(EXTRA_ORIGINATING_UID_FROM_SESSION_INFO, Process.INVALID_UID);
- mOriginatingPackageFromSessionInfo = (originatingUidFromSessionInfo != Process.INVALID_UID)
- ? getPackageNameForUid(originatingUidFromSessionInfo) : mCallingPackage;
-
final Object packageSource;
if (PackageInstaller.ACTION_CONFIRM_INSTALL.equals(action)) {
@@ -557,7 +530,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 32795e4..e48c0f4 100644
--- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallRepository.kt
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallRepository.kt
@@ -96,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
@@ -148,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
@@ -282,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) {
@@ -338,6 +339,7 @@
}
private fun createSessionParams(
+ originatingUid: Int,
intent: Intent,
pfd: ParcelFileDescriptor?,
debugPathName: String,
@@ -354,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.
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/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/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/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/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..a155dc4 100644
--- a/packages/SystemUI/aconfig/systemui.aconfig
+++ b/packages/SystemUI/aconfig/systemui.aconfig
@@ -104,6 +104,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 +431,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/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..1da6c1e 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))
@@ -548,26 +552,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/keyguard/ui/composable/section/DefaultClockSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/DefaultClockSection.kt
index 82e19e7c..1d86b15 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
@@ -58,7 +58,6 @@
if (currentClock?.smallClock?.view == null) {
return
}
- viewModel.clock = currentClock
val context = LocalContext.current
MovableElement(key = smallClockElementKey, modifier = modifier) {
@@ -89,7 +88,6 @@
@Composable
fun SceneScope.LargeClock(modifier: Modifier = Modifier) {
val currentClock by viewModel.currentClock.collectAsState()
- viewModel.clock = currentClock
if (currentClock?.largeClock?.view == null) {
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/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/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..248dfee 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
@@ -18,6 +18,7 @@
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.basicMarquee
+import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.size
import androidx.compose.material3.IconButton
@@ -27,6 +28,7 @@
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
+import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.semantics.ProgressBarRangeInfo
@@ -38,6 +40,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
@@ -86,18 +89,11 @@
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,
+ )
}
}
},
@@ -127,3 +123,32 @@
}
)
}
+
+@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/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..ebc9099 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
@@ -131,9 +131,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 +436,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/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/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/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/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/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/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/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/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/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 77d1484..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] -->
@@ -1994,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] -->
@@ -2013,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/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/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/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/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/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/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/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/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/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/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 9cb920a..2de14dd 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;
@@ -1018,7 +1018,7 @@
instantCollapse();
} else {
mView.animate().cancel();
- if (!migrateClocksToBlueprint()) {
+ if (!MigrateClocksToBlueprint.isEnabled()) {
mView.animate()
.alpha(0f)
.setStartDelay(0)
@@ -1075,7 +1075,7 @@
mQsController.init();
mShadeHeadsUpTracker.addTrackingHeadsUpListener(
mNotificationStackScrollLayoutController::setTrackingHeadsUp);
- if (!keyguardBottomAreaRefactor()) {
+ if (!KeyguardBottomAreaRefactor.isEnabled()) {
setKeyguardBottomArea(mView.findViewById(R.id.keyguard_bottom_area));
}
@@ -1154,7 +1154,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 +1165,7 @@
// Lockscreen->Dreaming
collectFlow(mView, mKeyguardTransitionInteractor.getLockscreenToDreamingTransition(),
mLockscreenToDreamingTransition, mMainDispatcher);
- if (!migrateClocksToBlueprint()) {
+ if (!MigrateClocksToBlueprint.isEnabled()) {
collectFlow(mView, mLockscreenToDreamingTransitionViewModel.getLockscreenAlpha(),
setDreamLockscreenTransitionAlpha(mNotificationStackScrollLayoutController),
mMainDispatcher);
@@ -1177,7 +1177,7 @@
// Gone->Dreaming
collectFlow(mView, mKeyguardTransitionInteractor.getGoneToDreamingTransition(),
mGoneToDreamingTransition, mMainDispatcher);
- if (!migrateClocksToBlueprint()) {
+ if (!MigrateClocksToBlueprint.isEnabled()) {
collectFlow(mView, mGoneToDreamingTransitionViewModel.getLockscreenAlpha(),
setTransitionAlpha(mNotificationStackScrollLayoutController), mMainDispatcher);
}
@@ -1188,7 +1188,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 +1196,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 +1280,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 +1332,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 +1359,7 @@
}
updateClockAppearance();
mQsController.updateQsState();
- if (!migrateClocksToBlueprint() && !FooterViewRefactor.isEnabled()) {
+ if (!MigrateClocksToBlueprint.isEnabled() && !FooterViewRefactor.isEnabled()) {
mNotificationStackScrollLayoutController.updateFooter();
}
}
@@ -1391,7 +1391,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 +1430,7 @@
updateViewControllers(userAvatarView, keyguardUserSwitcherView);
- if (!keyguardBottomAreaRefactor()) {
+ if (!KeyguardBottomAreaRefactor.isEnabled()) {
// Update keyguard bottom area
int index = mView.indexOfChild(mKeyguardBottomArea);
mView.removeView(mKeyguardBottomArea);
@@ -1449,7 +1449,7 @@
mStatusBarStateListener.onDozeAmountChanged(mStatusBarStateController.getDozeAmount(),
mStatusBarStateController.getInterpolatedDozeAmount());
- if (!migrateClocksToBlueprint()) {
+ if (!MigrateClocksToBlueprint.isEnabled()) {
mKeyguardStatusViewController.setKeyguardStatusViewVisibility(
mBarState,
false,
@@ -1471,7 +1471,7 @@
mBarState);
}
- if (!keyguardBottomAreaRefactor()) {
+ if (!KeyguardBottomAreaRefactor.isEnabled()) {
setKeyguardBottomAreaVisibility(mBarState, false);
}
@@ -1480,14 +1480,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 +1513,7 @@
}
private void updateMaxDisplayedNotifications(boolean recompute) {
- if (migrateClocksToBlueprint()) {
+ if (MigrateClocksToBlueprint.isEnabled()) {
return;
}
@@ -1630,7 +1630,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 +1671,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 +1683,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 +1740,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 +1758,7 @@
private void updateKeyguardStatusViewAlignment(boolean animate) {
boolean shouldBeCentered = shouldKeyguardStatusViewBeCentered();
- if (migrateClocksToBlueprint()) {
+ if (MigrateClocksToBlueprint.isEnabled()) {
mKeyguardInteractor.setClockShouldBeCentered(shouldBeCentered);
return;
}
@@ -1941,7 +1941,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 +2498,7 @@
}
if (!mKeyguardBypassController.getBypassEnabled()) {
- if (migrateClocksToBlueprint() && !mSplitShadeEnabled) {
+ if (MigrateClocksToBlueprint.isEnabled() && !mSplitShadeEnabled) {
return (int) mKeyguardInteractor.getNotificationContainerBounds()
.getValue().getTop();
}
@@ -2531,7 +2531,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 +2712,7 @@
return;
}
- if (!migrateClocksToBlueprint()) {
+ if (!MigrateClocksToBlueprint.isEnabled()) {
float alpha = 1f;
if (mClosingWithAlphaFadeOut && !mExpandingFromHeadsUp
&& !mHeadsUpManager.hasPinnedHeadsUp()) {
@@ -2748,7 +2748,7 @@
}
private void updateKeyguardBottomAreaAlpha() {
- if (migrateClocksToBlueprint()) {
+ if (MigrateClocksToBlueprint.isEnabled()) {
return;
}
if (mIsOcclusionTransitionRunning) {
@@ -2766,7 +2766,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 +2978,7 @@
}
private void updateDozingVisibilities(boolean animate) {
- if (keyguardBottomAreaRefactor()) {
+ if (KeyguardBottomAreaRefactor.isEnabled()) {
mKeyguardInteractor.setAnimateDozingTransitions(animate);
} else {
mKeyguardBottomAreaInteractor.setAnimateDozingTransitions(animate);
@@ -2990,7 +2990,7 @@
@Override
public void onScreenTurningOn() {
- if (!migrateClocksToBlueprint()) {
+ if (!MigrateClocksToBlueprint.isEnabled()) {
mKeyguardStatusViewController.dozeTimeTick();
}
}
@@ -3189,7 +3189,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 +3245,7 @@
public void dozeTimeTick() {
mLockIconViewController.dozeTimeTick();
- if (!migrateClocksToBlueprint()) {
+ if (!MigrateClocksToBlueprint.isEnabled()) {
mKeyguardStatusViewController.dozeTimeTick();
}
if (mInterpolatedDarkAmount > 0) {
@@ -3324,7 +3324,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
@@ -3348,7 +3348,7 @@
@Override
public void startFoldToAodAnimation(Runnable startAction, Runnable endAction,
Runnable cancelAction) {
- if (migrateClocksToBlueprint()) {
+ if (MigrateClocksToBlueprint.isEnabled()) {
return;
}
final ViewPropertyAnimator viewAnimator = mView.animate();
@@ -3386,7 +3386,7 @@
/** Cancels fold to AOD transition and resets view state. */
@Override
public void cancelFoldToAodAnimation() {
- if (migrateClocksToBlueprint()) {
+ if (MigrateClocksToBlueprint.isEnabled()) {
return;
}
cancelAnimation();
@@ -4460,7 +4460,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,
@@ -4469,7 +4469,7 @@
}
}
- if (!migrateClocksToBlueprint()) {
+ if (!MigrateClocksToBlueprint.isEnabled()) {
mKeyguardStatusViewController.setKeyguardStatusViewVisibility(
statusBarState,
keyguardFadingAway,
@@ -4477,7 +4477,7 @@
mBarState);
}
- if (!keyguardBottomAreaRefactor()) {
+ if (!KeyguardBottomAreaRefactor.isEnabled()) {
setKeyguardBottomAreaVisibility(statusBarState, goingToFullShade);
}
@@ -4582,7 +4582,7 @@
setDozing(true /* dozing */, false /* animate */);
mStatusBarStateController.setUpcomingState(KEYGUARD);
- if (migrateClocksToBlueprint()) {
+ if (MigrateClocksToBlueprint.isEnabled()) {
mStatusBarStateController.setState(KEYGUARD);
} else {
mStatusBarStateListener.onStateChanged(KEYGUARD);
@@ -4645,7 +4645,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());
}
@@ -4746,7 +4746,7 @@
stackScroller.setMaxAlphaForKeyguard(alpha, "NPVC.setTransitionAlpha()");
}
- if (keyguardBottomAreaRefactor()) {
+ if (KeyguardBottomAreaRefactor.isEnabled()) {
mKeyguardInteractor.setAlpha(alpha);
} else {
mKeyguardBottomAreaInteractor.setAlpha(alpha);
@@ -4765,7 +4765,7 @@
private Consumer<Float> setTransitionY(
NotificationStackScrollLayoutController stackScroller) {
return (Float translationY) -> {
- if (!migrateClocksToBlueprint()) {
+ if (!MigrateClocksToBlueprint.isEnabled()) {
mKeyguardStatusViewController.setTranslationY(translationY,
/* excludeMedia= */false);
stackScroller.setTranslationY(translationY);
@@ -4807,7 +4807,7 @@
*/
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
- if (migrateClocksToBlueprint() && !mUseExternalTouch) {
+ if (MigrateClocksToBlueprint.isEnabled() && !mUseExternalTouch) {
return false;
}
@@ -4878,7 +4878,7 @@
switch (event.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
- if (!migrateClocksToBlueprint()) {
+ if (!MigrateClocksToBlueprint.isEnabled()) {
mCentralSurfaces.userActivity();
}
mAnimatingOnDown = mHeightAnimator != null && !mIsSpringBackAnimation;
@@ -4979,7 +4979,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/ShadeControllerSceneImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt
index c20efea..6bb1df7 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt
@@ -248,7 +248,7 @@
}
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()
}
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/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/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/row/ExpandableNotificationRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
index c05c3c3..b8b4a03 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) {
@@ -2050,7 +2054,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 +3439,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 +3496,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/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/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/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..f2c593d 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();
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..2f577d0 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;
@@ -2090,7 +2090,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/StackScrollAlgorithm.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java
index 9b1952b..b42c07d 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() {
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/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/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/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..86bb844 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java
@@ -41,6 +41,7 @@
import com.android.systemui.statusbar.notification.collection.provider.VisualStabilityProvider;
import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManager;
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;
@@ -94,6 +95,7 @@
@Override
public HeadsUpEntryPhone acquire() {
+ NotificationsHeadsUpRefactor.assertInLegacyMode();
if (!mPoolObjects.isEmpty()) {
return mPoolObjects.pop();
}
@@ -102,6 +104,7 @@
@Override
public boolean release(@NonNull HeadsUpEntryPhone instance) {
+ NotificationsHeadsUpRefactor.assertInLegacyMode();
mPoolObjects.push(instance);
return true;
}
@@ -371,15 +374,24 @@
///////////////////////////////////////////////////////////////////////////////////////////////
// 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 onEntryRemoved(HeadsUpEntry headsUpEntry) {
super.onEntryRemoved(headsUpEntry);
- mEntryPool.release((HeadsUpEntryPhone) headsUpEntry);
+ if (!NotificationsHeadsUpRefactor.isEnabled()) {
+ mEntryPool.release((HeadsUpEntryPhone) headsUpEntry);
+ }
}
@Override
@@ -439,14 +451,22 @@
*/
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
+ 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 +480,6 @@
removeEntry(entry.getKey());
}
};
-
- setEntry(entry, removeHeadsUpRunnable);
}
@Override
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..6f7e046 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
@@ -375,7 +372,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) {
@@ -395,7 +392,11 @@
mHeadsUpEntryMap.remove(key);
onEntryRemoved(headsUpEntry);
entry.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
- headsUpEntry.reset();
+ if (NotificationsHeadsUpRefactor.isEnabled()) {
+ headsUpEntry.cancelAutoRemovalCallbacks("removeEntry");
+ } else {
+ headsUpEntry.reset();
+ }
};
mAvalancheController.delete(headsUpEntry, runnable, "removeEntry");
}
@@ -657,8 +658,8 @@
}
@NonNull
- protected HeadsUpEntry createHeadsUpEntry() {
- return new HeadsUpEntry();
+ protected HeadsUpEntry createHeadsUpEntry(NotificationEntry entry) {
+ return new HeadsUpEntry(entry);
}
/**
@@ -694,11 +695,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;
@@ -847,6 +860,7 @@
}
public void reset() {
+ NotificationsHeadsUpRefactor.assertInLegacyMode();
cancelAutoRemovalCallbacks("reset()");
mEntry = null;
mRemoveRunnable = null;
@@ -919,6 +933,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/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/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/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/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/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..ee2eb80 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);
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/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/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/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/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/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/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/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/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
index 880a687..3e7682a 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
@@ -2703,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);
@@ -2767,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/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/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 cc40940..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;
@@ -205,6 +207,10 @@
if (sensitiveNotificationAppProtection()) {
updateAppsThatShouldBlockScreenCapture();
}
+
+ if (sensitiveContentAppProtection() && mPackagesShowingSensitiveContent.size() > 0) {
+ mWindowManager.addBlockScreenCaptureForApps(mPackagesShowingSensitiveContent);
+ }
}
}
@@ -354,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) {
@@ -392,6 +408,12 @@
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 {
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/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 e8c05c6..53c0f58 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -73,6 +73,7 @@
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothHeadset;
import android.bluetooth.BluetoothProfile;
+import android.content.AttributionSource;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.ContentResolver;
@@ -6853,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:
@@ -6870,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;
@@ -6894,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) {
@@ -12207,7 +12193,9 @@
//==========================================================================================
public String registerAudioPolicy(AudioPolicyConfig policyConfig, IAudioPolicyCallback pcb,
boolean hasFocusListener, boolean isFocusPolicy, boolean isTestFocusPolicy,
- boolean isVolumeController, IMediaProjection projection) {
+ boolean isVolumeController, IMediaProjection projection,
+ AttributionSource attributionSource) {
+ Objects.requireNonNull(attributionSource);
AudioSystem.setDynamicPolicyCallback(mDynPolicyCallback);
if (!isPolicyRegisterAllowed(policyConfig,
@@ -12228,7 +12216,8 @@
}
try {
AudioPolicyProxy app = new AudioPolicyProxy(policyConfig, pcb, hasFocusListener,
- isFocusPolicy, isTestFocusPolicy, isVolumeController, projection);
+ isFocusPolicy, isTestFocusPolicy, isVolumeController, projection,
+ attributionSource);
pcb.asBinder().linkToDeath(app, 0/*flags*/);
// logging after registration so we have the registration id
@@ -13200,6 +13189,7 @@
public class AudioPolicyProxy extends AudioPolicyConfig implements IBinder.DeathRecipient {
private static final String TAG = "AudioPolicyProxy";
final IAudioPolicyCallback mPolicyCallback;
+ final AttributionSource mAttributionSource;
final boolean mHasFocusListener;
final boolean mIsVolumeController;
final HashMap<Integer, AudioDeviceArray> mUidDeviceAffinities =
@@ -13239,10 +13229,12 @@
AudioPolicyProxy(AudioPolicyConfig config, IAudioPolicyCallback token,
boolean hasFocusListener, boolean isFocusPolicy, boolean isTestFocusPolicy,
- boolean isVolumeController, IMediaProjection projection) {
+ boolean isVolumeController, IMediaProjection projection,
+ AttributionSource attributionSource) {
super(config);
setRegistration(new String(config.hashCode() + ":ap:" + mAudioPolicyCounter++));
mPolicyCallback = token;
+ mAttributionSource = attributionSource;
mHasFocusListener = hasFocusListener;
mIsVolumeController = isVolumeController;
mProjection = projection;
@@ -13370,6 +13362,7 @@
if (android.media.audiopolicy.Flags.audioMixOwnership()) {
for (AudioMix mix : mixes) {
setMixRegistration(mix);
+ mix.setVirtualDeviceId(mAttributionSource.getDeviceId());
}
int result = mAudioSystem.registerPolicyMixes(mixes, true);
@@ -13393,6 +13386,9 @@
@AudioSystem.AudioSystemError int connectMixes() {
final long identity = Binder.clearCallingIdentity();
try {
+ for (AudioMix mix : mMixes) {
+ mix.setVirtualDeviceId(mAttributionSource.getDeviceId());
+ }
return mAudioSystem.registerPolicyMixes(mMixes, true);
} finally {
Binder.restoreCallingIdentity(identity);
@@ -13406,6 +13402,9 @@
Objects.requireNonNull(mixesToUpdate);
Objects.requireNonNull(updatedMixingRules);
+ for (AudioMix mix : mixesToUpdate) {
+ mix.setVirtualDeviceId(mAttributionSource.getDeviceId());
+ }
if (mixesToUpdate.length != updatedMixingRules.length) {
Log.e(TAG, "Provided list of audio mixes to update and corresponding mixing rules "
+ "have mismatching length (mixesToUpdate.length = " + mixesToUpdate.length
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..04e7f77 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,9 @@
@Nullable
private HdrBrightnessData mHdrBrightnessData;
+ @Nullable
+ public LowBrightnessData mLowBrightnessData;
+
/**
* Maximum screen brightness setting when screen brightness capped in Wear Bedtime mode.
*/
@@ -1814,6 +1839,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 +1986,8 @@
+ "mUsiVersion= " + mHostUsiVersion + "\n"
+ "mHdrBrightnessData= " + mHdrBrightnessData + "\n"
+ "mBrightnessCapForWearBedtimeMode= " + mBrightnessCapForWearBedtimeMode
+ + "\n"
+ + (mLowBrightnessData != null ? mLowBrightnessData.toString() : "")
+ "}";
}
@@ -2002,6 +2038,9 @@
loadDensityMapping(config);
loadBrightnessDefaultFromDdcXml(config);
loadBrightnessConstraintsFromConfigXml();
+ if (mFlags.isEvenDimmerEnabled()) {
+ mLowBrightnessData = LowBrightnessData.loadConfig(config);
+ }
loadBrightnessMap(config);
loadThermalThrottlingConfig(config);
loadPowerThrottlingConfigData(config);
@@ -2793,6 +2832,18 @@
// These splines are used to convert from the system brightness value to the HAL backlight
// value
private void createBacklightConversionSplines() {
+ if (mLowBrightnessData != null) {
+ mBrightnessToBacklightSpline = mLowBrightnessData.mBrightnessToBacklight;
+ mBacklightToBrightnessSpline = mLowBrightnessData.mBacklightToBrightness;
+ mBacklightToNitsSpline = mLowBrightnessData.mBacklightToNits;
+ mNitsToBacklightSpline = mLowBrightnessData.mNitsToBacklight;
+
+ mNits = mLowBrightnessData.mNits;
+ mBrightness = mLowBrightnessData.mBrightness;
+ mBacklight = mLowBrightnessData.mBacklight;
+ return;
+ }
+
mBrightness = new float[mBacklight.length];
for (int i = 0; i < mBrightness.length; i++) {
mBrightness[i] = MathUtils.map(mBacklight[0],
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/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/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 37023e1..953300a 100644
--- a/services/core/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceManagerService.java
+++ b/services/core/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceManagerService.java
@@ -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,7 +358,7 @@
};
}
- private void ensureRemoteIntelligenceServiceInitialized() throws RemoteException {
+ private void ensureRemoteIntelligenceServiceInitialized() {
synchronized (mLock) {
if (mRemoteOnDeviceIntelligenceService == null) {
String serviceName = getServiceNames()[0];
@@ -388,25 +390,15 @@
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];
@@ -457,34 +449,38 @@
};
}
- private static void validateServiceElevated(String serviceName, boolean checkIsolated)
- throws RemoteException {
- if (TextUtils.isEmpty(serviceName)) {
- throw new IllegalArgumentException("Received null/empty service name : " + serviceName);
- }
- 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);
}
}
@@ -542,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/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/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java
index 7a83eb5..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;
@@ -3479,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()) {
@@ -3507,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;
}
@@ -5477,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);
@@ -5500,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
@@ -5535,7 +5546,7 @@
}
EventLogTags.writeScreenToggled(1);
-
+ mIsGoingToSleepDefaultDisplay = false;
mDefaultDisplayPolicy.setAwake(true);
// Since goToSleep performs these functions synchronously, we must
@@ -5637,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/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/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/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 4c282bd..18d2718 100644
--- a/services/core/java/com/android/server/wm/Task.java
+++ b/services/core/java/com/android/server/wm/Task.java
@@ -6822,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/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/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index 2934574..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, " ");
@@ -10068,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 c0cf97d..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;
@@ -1898,6 +1899,10 @@
}
boolean isSecureLocked() {
+ if (mWmService.getDisableSecureWindows()) {
+ return false;
+ }
+
if ((mAttrs.flags & WindowManager.LayoutParams.FLAG_SECURE) != 0) {
return true;
}
@@ -3692,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/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/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/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java
index 53c460c..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;
@@ -885,7 +884,6 @@
}
@Test
- @RequiresFlagsEnabled(FLAG_CLEANUP_ACCESSIBILITY_WARNING_DIALOG)
public void testIsAccessibilityServiceWarningRequired_requiredByDefault() {
mockManageAccessibilityGranted(mTestableContext);
final AccessibilityServiceInfo info = mockAccessibilityServiceInfo(COMPONENT_NAME);
@@ -894,7 +892,6 @@
}
@Test
- @RequiresFlagsEnabled(FLAG_CLEANUP_ACCESSIBILITY_WARNING_DIALOG)
public void testIsAccessibilityServiceWarningRequired_notRequiredIfAlreadyEnabled() {
mockManageAccessibilityGranted(mTestableContext);
final AccessibilityServiceInfo info_a = mockAccessibilityServiceInfo(COMPONENT_NAME);
@@ -909,7 +906,6 @@
}
@Test
- @RequiresFlagsEnabled(FLAG_CLEANUP_ACCESSIBILITY_WARNING_DIALOG)
public void testIsAccessibilityServiceWarningRequired_notRequiredIfExistingShortcut() {
mockManageAccessibilityGranted(mTestableContext);
final AccessibilityServiceInfo info_a = mockAccessibilityServiceInfo(
@@ -930,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(
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/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/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/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/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/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/tests/inputmethod/ConcurrentMultiSessionImeTest/Android.bp b/tests/inputmethod/ConcurrentMultiSessionImeTest/Android.bp
index 0e0d212..8d05a97 100644
--- a/tests/inputmethod/ConcurrentMultiSessionImeTest/Android.bp
+++ b/tests/inputmethod/ConcurrentMultiSessionImeTest/Android.bp
@@ -26,11 +26,6 @@
"platform-test-annotations",
"platform-test-rules",
"truth",
-
- // beadstead
- "Nene",
- "Harrier",
- "TestApp",
],
test_suites: [
"general-tests",
diff --git a/tests/inputmethod/ConcurrentMultiSessionImeTest/src/com/android/server/inputmethod/multisessiontest/ConcurrentMultiUserTest.java b/tests/inputmethod/ConcurrentMultiSessionImeTest/src/com/android/server/inputmethod/multisessiontest/ConcurrentMultiUserTest.java
index 867c0a6..b66ceba 100644
--- a/tests/inputmethod/ConcurrentMultiSessionImeTest/src/com/android/server/inputmethod/multisessiontest/ConcurrentMultiUserTest.java
+++ b/tests/inputmethod/ConcurrentMultiSessionImeTest/src/com/android/server/inputmethod/multisessiontest/ConcurrentMultiUserTest.java
@@ -23,20 +23,14 @@
import androidx.test.platform.app.InstrumentationRegistry;
-import com.android.bedstead.harrier.BedsteadJUnit4;
-import com.android.bedstead.harrier.DeviceState;
-
import org.junit.Before;
-import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
-@RunWith(BedsteadJUnit4.class)
+@RunWith(JUnit4.class)
public final class ConcurrentMultiUserTest {
- @Rule
- public static final DeviceState sDeviceState = new DeviceState();
-
@Before
public void doBeforeEachTest() {
// No op
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..df003b6
--- /dev/null
+++ b/tools/app_metadata_bundles/src/aslgen/java/com/android/aslgen/Main.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.aslgen;
+
+import com.android.asllib.AndroidSafetyLabel;
+import com.android.asllib.AndroidSafetyLabel.Format;
+
+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 {
+
+ 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..0f7ce68
--- /dev/null
+++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/AndroidSafetyLabel.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.asllib;
+
+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 {
+ 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(
+ XmlUtils.asElementList(
+ document.getElementsByTagName(
+ 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..9b0f05b
--- /dev/null
+++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/AndroidSafetyLabelFactory.java
@@ -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.asllib;
+
+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) {
+ 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..b607353
--- /dev/null
+++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/AslMarshallableFactory.java
@@ -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.asllib;
+
+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);
+}
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..5a52591
--- /dev/null
+++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataCategoryFactory.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.asllib;
+
+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) {
+ 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);
+ 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..c758ab9
--- /dev/null
+++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataLabelsFactory.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS 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 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) {
+ 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);
+ return new DataLabels(dataAccessed, dataCollected, dataShared);
+ }
+
+ private static Map<String, DataCategory> getDataCategoriesWithTag(
+ Element dataLabelsEle, String dataCategoryUsageTypeTag) {
+ Map<String, Map<String, DataType>> dataTypeMap =
+ new HashMap<String, Map<String, DataType>>();
+ 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);
+ 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..99f8a8b
--- /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_SHARING_OPTIONAL));
+ Boolean isSharingOptional =
+ XmlUtils.fromString(
+ hrDataTypeEle.getAttribute(XmlUtils.HR_ATTR_IS_COLLECTION_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..68e83fe
--- /dev/null
+++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/SafetyLabelsFactory.java
@@ -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.asllib;
+
+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) {
+ 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..3c89a30
--- /dev/null
+++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/XmlUtils.java
@@ -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.asllib;
+
+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) {
+ var elements = doc.getElementsByTagName(tagName);
+ return getSingleElement(elements);
+ }
+
+ /**
+ * Gets the single {@link Element} within {@param parentEle} and having the {@param tagName}.
+ */
+ public static Element getSingleChildElement(Element parentEle, String tagName) {
+ var elements = parentEle.getElementsByTagName(tagName);
+ return getSingleElement(elements);
+ }
+
+ /** Gets the single {@link Element} from {@param elements} */
+ public static Element getSingleElement(NodeList elements) {
+ if (elements.getLength() != 1) {
+ throw new IllegalArgumentException(
+ String.format(
+ "Expected 1 element in NodeList but got %s.", elements.getLength()));
+ }
+ var elementAsNode = elements.item(0);
+ if (!(elementAsNode instanceof Element)) {
+ throw new IllegalStateException(
+ String.format("%s was not an element.", elementAsNode.getNodeName()));
+ }
+ 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();
+ }
+}