Merge "Remove usages of TimestampedValue<Long>"
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index f221eca..134b71a4 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -167,6 +167,7 @@
field public static final String MANAGE_CONTENT_SUGGESTIONS = "android.permission.MANAGE_CONTENT_SUGGESTIONS";
field public static final String MANAGE_DEBUGGING = "android.permission.MANAGE_DEBUGGING";
field public static final String MANAGE_DEVICE_ADMINS = "android.permission.MANAGE_DEVICE_ADMINS";
+ field public static final String MANAGE_DEVICE_POLICY_APP_EXEMPTIONS = "android.permission.MANAGE_DEVICE_POLICY_APP_EXEMPTIONS";
field public static final String MANAGE_ETHERNET_NETWORKS = "android.permission.MANAGE_ETHERNET_NETWORKS";
field public static final String MANAGE_FACTORY_RESET_PROTECTION = "android.permission.MANAGE_FACTORY_RESET_PROTECTION";
field public static final String MANAGE_GAME_ACTIVITY = "android.permission.MANAGE_GAME_ACTIVITY";
@@ -2789,6 +2790,8 @@
public final class VirtualDeviceManager {
method @NonNull @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public android.companion.virtual.VirtualDeviceManager.VirtualDevice createVirtualDevice(int, @NonNull android.companion.virtual.VirtualDeviceParams);
+ field public static final int DEFAULT_DEVICE_ID = 0; // 0x0
+ field public static final int INVALID_DEVICE_ID = -1; // 0xffffffff
field public static final int LAUNCH_FAILURE_NO_ACTIVITY = 2; // 0x2
field public static final int LAUNCH_FAILURE_PENDING_INTENT_CANCELED = 1; // 0x1
field public static final int LAUNCH_SUCCESS = 0; // 0x0
@@ -2808,6 +2811,7 @@
method @NonNull @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public android.hardware.input.VirtualKeyboard createVirtualKeyboard(@NonNull android.hardware.display.VirtualDisplay, @NonNull String, int, int);
method @NonNull @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public android.hardware.input.VirtualMouse createVirtualMouse(@NonNull android.hardware.display.VirtualDisplay, @NonNull String, int, int);
method @NonNull @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public android.hardware.input.VirtualTouchscreen createVirtualTouchscreen(@NonNull android.hardware.display.VirtualDisplay, @NonNull String, int, int);
+ method public int getDeviceId();
method public void launchPendingIntent(int, @NonNull android.app.PendingIntent, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.IntConsumer);
method public void removeActivityListener(@NonNull android.companion.virtual.VirtualDeviceManager.ActivityListener);
method @NonNull @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void setShowPointerIcon(boolean);
diff --git a/core/java/android/app/IActivityManager.aidl b/core/java/android/app/IActivityManager.aidl
index b4abd3c..6404a1f 100644
--- a/core/java/android/app/IActivityManager.aidl
+++ b/core/java/android/app/IActivityManager.aidl
@@ -524,9 +524,30 @@
@UnsupportedAppUsage(maxTargetSdk = 30, trackingBug = 170729553)
void suppressResizeConfigChanges(boolean suppress);
+
+ /**
+ * @deprecated Use {@link #unlockUser2(int, IProgressListener)} instead, since the token and
+ * secret arguments no longer do anything. This method still exists only because it is marked
+ * with {@code @UnsupportedAppUsage}, so it might not be safe to remove it or change its
+ * signature.
+ */
@UnsupportedAppUsage(maxTargetSdk = 30, trackingBug = 170729553)
boolean unlockUser(int userid, in byte[] token, in byte[] secret,
in IProgressListener listener);
+
+ /**
+ * Tries to unlock the given user.
+ * <p>
+ * This will succeed only if the user's CE storage key is already unlocked or if the user
+ * doesn't have a lockscreen credential set.
+ *
+ * @param userId The ID of the user to unlock.
+ * @param listener An optional progress listener.
+ *
+ * @return true if the user was successfully unlocked, otherwise false.
+ */
+ boolean unlockUser2(int userId, in IProgressListener listener);
+
void killPackageDependents(in String packageName, int userId);
void makePackageIdle(String packageName, int userId);
int getMemoryTrimLevel();
diff --git a/core/java/android/app/SystemServiceRegistry.java b/core/java/android/app/SystemServiceRegistry.java
index d5b85cd..4ddfdb6 100644
--- a/core/java/android/app/SystemServiceRegistry.java
+++ b/core/java/android/app/SystemServiceRegistry.java
@@ -81,6 +81,8 @@
import android.content.pm.verify.domain.IDomainVerificationManager;
import android.content.res.Resources;
import android.content.rollback.RollbackManagerFrameworkInitializer;
+import android.credentials.CredentialManager;
+import android.credentials.ICredentialManager;
import android.debug.AdbManager;
import android.debug.IAdbManager;
import android.graphics.fonts.FontManager;
@@ -1138,6 +1140,19 @@
return new AutofillManager(ctx.getOuterContext(), service);
}});
+ registerService(Context.CREDENTIAL_SERVICE, CredentialManager.class,
+ new CachedServiceFetcher<CredentialManager>() {
+ @Override
+ public CredentialManager createService(ContextImpl ctx)
+ throws ServiceNotFoundException {
+ IBinder b = ServiceManager.getService(Context.CREDENTIAL_SERVICE);
+ ICredentialManager service = ICredentialManager.Stub.asInterface(b);
+ if (service != null) {
+ return new CredentialManager(ctx.getOuterContext(), service);
+ }
+ return null;
+ }});
+
registerService(Context.MUSIC_RECOGNITION_SERVICE, MusicRecognitionManager.class,
new CachedServiceFetcher<MusicRecognitionManager>() {
@Override
diff --git a/core/java/android/companion/virtual/IVirtualDevice.aidl b/core/java/android/companion/virtual/IVirtualDevice.aidl
index 9c99da5..e7f19166 100644
--- a/core/java/android/companion/virtual/IVirtualDevice.aidl
+++ b/core/java/android/companion/virtual/IVirtualDevice.aidl
@@ -43,6 +43,11 @@
int getAssociationId();
/**
+ * Returns the unique device ID for this virtual device.
+ */
+ int getDeviceId();
+
+ /**
* Closes the virtual device and frees all associated resources.
*/
void close();
diff --git a/core/java/android/companion/virtual/VirtualDeviceManager.java b/core/java/android/companion/virtual/VirtualDeviceManager.java
index fadfa5c..08bee25 100644
--- a/core/java/android/companion/virtual/VirtualDeviceManager.java
+++ b/core/java/android/companion/virtual/VirtualDeviceManager.java
@@ -78,6 +78,16 @@
| DisplayManager.VIRTUAL_DISPLAY_FLAG_SUPPORTS_TOUCH
| DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP;
+ /**
+ * The default device ID, which is the ID of the primary (non-virtual) device.
+ */
+ public static final int DEFAULT_DEVICE_ID = 0;
+
+ /**
+ * Invalid device ID.
+ */
+ public static final int INVALID_DEVICE_ID = -1;
+
/** @hide */
@Retention(RetentionPolicy.SOURCE)
@IntDef(
@@ -204,6 +214,17 @@
}
/**
+ * Returns the unique ID of this virtual device.
+ */
+ public int getDeviceId() {
+ try {
+ return mVirtualDevice.getDeviceId();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
* Launches a given pending intent on the give display ID.
*
* @param displayId The display to launch the pending intent on. This display must be
diff --git a/core/java/android/credentials/CreateCredentialRequest.java b/core/java/android/credentials/CreateCredentialRequest.java
index 333c837..22ef230 100644
--- a/core/java/android/credentials/CreateCredentialRequest.java
+++ b/core/java/android/credentials/CreateCredentialRequest.java
@@ -45,6 +45,11 @@
private final Bundle mData;
/**
+ * Determines whether or not the request must only be fulfilled by a system provider.
+ */
+ private final boolean mRequireSystemProvider;
+
+ /**
* Returns the requested credential type.
*/
@NonNull
@@ -60,10 +65,21 @@
return mData;
}
+ /**
+ * Returns true if the request must only be fulfilled by a system provider, and false
+ * otherwise.
+ *
+ * @hide
+ */
+ public boolean requireSystemProvider() {
+ return mRequireSystemProvider;
+ }
+
@Override
public void writeToParcel(@NonNull Parcel dest, int flags) {
dest.writeString8(mType);
dest.writeBundle(mData);
+ dest.writeBoolean(mRequireSystemProvider);
}
@Override
@@ -73,30 +89,56 @@
@Override
public String toString() {
- return "CreateCredentialRequest {" + "type=" + mType + ", data=" + mData + "}";
+ return "CreateCredentialRequest {"
+ + "type=" + mType
+ + ", data=" + mData
+ + ", requireSystemProvider=" + mRequireSystemProvider
+ + "}";
}
/**
* Constructs a {@link CreateCredentialRequest}.
*
- * @param type the requested credential type.
- * @param data the request data.
+ * @param type the requested credential type
+ * @param data the request data
*
- * @throws IllegalArgumentException If type is empty.
+ * @throws IllegalArgumentException If type is empty
*/
public CreateCredentialRequest(@NonNull String type, @NonNull Bundle data) {
+ this(type, data, /*requireSystemProvider=*/ false);
+ }
+
+ /**
+ * Constructs a {@link CreateCredentialRequest}.
+ *
+ * @param type the requested credential type
+ * @param data the request data
+ * @param requireSystemProvider whether or not the request must only be fulfilled by a system
+ * provider
+ *
+ * @throws IllegalArgumentException If type is empty.
+ *
+ * @hide
+ */
+ public CreateCredentialRequest(
+ @NonNull String type,
+ @NonNull Bundle data,
+ boolean requireSystemProvider) {
mType = Preconditions.checkStringNotEmpty(type, "type must not be empty");
mData = requireNonNull(data, "data must not be null");
+ mRequireSystemProvider = requireSystemProvider;
}
private CreateCredentialRequest(@NonNull Parcel in) {
String type = in.readString8();
Bundle data = in.readBundle();
+ boolean requireSystemProvider = in.readBoolean();
mType = type;
AnnotationValidations.validate(NonNull.class, null, mType);
mData = data;
AnnotationValidations.validate(NonNull.class, null, mData);
+ mRequireSystemProvider = requireSystemProvider;
}
public static final @NonNull Parcelable.Creator<CreateCredentialRequest> CREATOR =
diff --git a/core/java/android/credentials/CredentialManagerException.java b/core/java/android/credentials/CredentialManagerException.java
index 116395c..8369649 100644
--- a/core/java/android/credentials/CredentialManagerException.java
+++ b/core/java/android/credentials/CredentialManagerException.java
@@ -23,6 +23,20 @@
/** Indicates that an unknown error was encountered. */
public static final int ERROR_UNKNOWN = 0;
+ /**
+ * The given CredentialManager operation is cancelled by the user.
+ *
+ * @hide
+ */
+ public static final int ERROR_USER_CANCELLED = 1;
+
+ /**
+ * No appropriate provider is found to support the target credential type(s).
+ *
+ * @hide
+ */
+ public static final int ERROR_PROVIDER_NOT_FOUND = 2;
+
public final int errorCode;
public CredentialManagerException(int errorCode, @Nullable String message) {
diff --git a/core/java/android/credentials/GetCredentialOption.java b/core/java/android/credentials/GetCredentialOption.java
index af58b08..a0d3c0b 100644
--- a/core/java/android/credentials/GetCredentialOption.java
+++ b/core/java/android/credentials/GetCredentialOption.java
@@ -44,6 +44,11 @@
private final Bundle mData;
/**
+ * Determines whether or not the request must only be fulfilled by a system provider.
+ */
+ private final boolean mRequireSystemProvider;
+
+ /**
* Returns the requested credential type.
*/
@NonNull
@@ -59,10 +64,21 @@
return mData;
}
+ /**
+ * Returns true if the request must only be fulfilled by a system provider, and false
+ * otherwise.
+ *
+ * @hide
+ */
+ public boolean requireSystemProvider() {
+ return mRequireSystemProvider;
+ }
+
@Override
public void writeToParcel(@NonNull Parcel dest, int flags) {
dest.writeString8(mType);
dest.writeBundle(mData);
+ dest.writeBoolean(mRequireSystemProvider);
}
@Override
@@ -72,30 +88,56 @@
@Override
public String toString() {
- return "GetCredentialOption {" + "type=" + mType + ", data=" + mData + "}";
+ return "GetCredentialOption {"
+ + "type=" + mType
+ + ", data=" + mData
+ + ", requireSystemProvider=" + mRequireSystemProvider
+ + "}";
}
/**
* Constructs a {@link GetCredentialOption}.
*
- * @param type the requested credential type.
- * @param data the request data.
+ * @param type the requested credential type
+ * @param data the request data
*
- * @throws IllegalArgumentException If type is empty.
+ * @throws IllegalArgumentException If type is empty
*/
public GetCredentialOption(@NonNull String type, @NonNull Bundle data) {
+ this(type, data, /*requireSystemProvider=*/ false);
+ }
+
+ /**
+ * Constructs a {@link GetCredentialOption}.
+ *
+ * @param type the requested credential type
+ * @param data the request data
+ * @param requireSystemProvider whether or not the request must only be fulfilled by a system
+ * provider
+ *
+ * @throws IllegalArgumentException If type is empty.
+ *
+ * @hide
+ */
+ public GetCredentialOption(
+ @NonNull String type,
+ @NonNull Bundle data,
+ boolean requireSystemProvider) {
mType = Preconditions.checkStringNotEmpty(type, "type must not be empty");
mData = requireNonNull(data, "data must not be null");
+ mRequireSystemProvider = requireSystemProvider;
}
private GetCredentialOption(@NonNull Parcel in) {
String type = in.readString8();
Bundle data = in.readBundle();
+ boolean requireSystemProvider = in.readBoolean();
mType = type;
AnnotationValidations.validate(NonNull.class, null, mType);
mData = data;
AnnotationValidations.validate(NonNull.class, null, mData);
+ mRequireSystemProvider = requireSystemProvider;
}
public static final @NonNull Parcelable.Creator<GetCredentialOption> CREATOR =
diff --git a/core/java/android/os/PowerManager.java b/core/java/android/os/PowerManager.java
index c0e2864..9bc7ffd 100644
--- a/core/java/android/os/PowerManager.java
+++ b/core/java/android/os/PowerManager.java
@@ -2913,6 +2913,7 @@
private int mFlags;
@UnsupportedAppUsage
private String mTag;
+ private int mTagHash;
private final String mPackageName;
private final IBinder mToken;
private int mInternalCount;
@@ -2921,7 +2922,6 @@
private boolean mHeld;
private WorkSource mWorkSource;
private String mHistoryTag;
- private final String mTraceName;
private final int mDisplayId;
private WakeLockStateListener mListener;
private IWakeLockCallback mCallback;
@@ -2931,9 +2931,9 @@
WakeLock(int flags, String tag, String packageName, int displayId) {
mFlags = flags;
mTag = tag;
+ mTagHash = mTag.hashCode();
mPackageName = packageName;
mToken = new Binder();
- mTraceName = "WakeLock (" + mTag + ")";
mDisplayId = displayId;
}
@@ -2942,7 +2942,8 @@
synchronized (mToken) {
if (mHeld) {
Log.wtf(TAG, "WakeLock finalized while still held: " + mTag);
- Trace.asyncTraceEnd(Trace.TRACE_TAG_POWER, mTraceName, 0);
+ Trace.asyncTraceForTrackEnd(Trace.TRACE_TAG_POWER,
+ "WakeLocks", mTagHash);
try {
mService.releaseWakeLock(mToken, 0);
} catch (RemoteException e) {
@@ -3012,7 +3013,8 @@
// should immediately acquire the wake lock once again despite never having
// been explicitly released by the keyguard.
mHandler.removeCallbacks(mReleaser);
- Trace.asyncTraceBegin(Trace.TRACE_TAG_POWER, mTraceName, 0);
+ Trace.asyncTraceForTrackBegin(Trace.TRACE_TAG_POWER,
+ "WakeLocks", mTag, mTagHash);
try {
mService.acquireWakeLock(mToken, mFlags, mTag, mPackageName, mWorkSource,
mHistoryTag, mDisplayId, mCallback);
@@ -3060,7 +3062,8 @@
if (!mRefCounted || mInternalCount == 0) {
mHandler.removeCallbacks(mReleaser);
if (mHeld) {
- Trace.asyncTraceEnd(Trace.TRACE_TAG_POWER, mTraceName, 0);
+ Trace.asyncTraceForTrackEnd(Trace.TRACE_TAG_POWER,
+ "WakeLocks", mTagHash);
try {
mService.releaseWakeLock(mToken, flags);
} catch (RemoteException e) {
@@ -3137,6 +3140,7 @@
/** @hide */
public void setTag(String tag) {
mTag = tag;
+ mTagHash = mTag.hashCode();
}
/** @hide */
diff --git a/core/java/android/os/Process.java b/core/java/android/os/Process.java
index c943a3d..e483328 100644
--- a/core/java/android/os/Process.java
+++ b/core/java/android/os/Process.java
@@ -1482,6 +1482,18 @@
public static final native int killProcessGroup(int uid, int pid);
/**
+ * Freeze the cgroup for the given UID.
+ * This cgroup may contain child cgroups which will also be frozen. If this cgroup or its
+ * children contain processes with Binder interfaces, those interfaces should be frozen before
+ * the cgroup to avoid blocking synchronous callers indefinitely.
+ *
+ * @param uid The UID to be frozen
+ * @param freeze true = freeze; false = unfreeze
+ * @hide
+ */
+ public static final native void freezeCgroupUid(int uid, boolean freeze);
+
+ /**
* Remove all process groups. Expected to be called when ActivityManager
* is restarted.
* @hide
diff --git a/core/java/android/os/storage/IStorageManager.aidl b/core/java/android/os/storage/IStorageManager.aidl
index df0bee7..bc52744 100644
--- a/core/java/android/os/storage/IStorageManager.aidl
+++ b/core/java/android/os/storage/IStorageManager.aidl
@@ -137,6 +137,7 @@
void createUserKey(int userId, int serialNumber, boolean ephemeral) = 61;
@EnforcePermission("STORAGE_INTERNAL")
void destroyUserKey(int userId) = 62;
+ @EnforcePermission("STORAGE_INTERNAL")
void unlockUserKey(int userId, int serialNumber, in byte[] secret) = 63;
@EnforcePermission("STORAGE_INTERNAL")
void lockUserKey(int userId) = 64;
@@ -146,9 +147,7 @@
@EnforcePermission("STORAGE_INTERNAL")
void destroyUserStorage(in String volumeUuid, int userId, int flags) = 67;
@EnforcePermission("STORAGE_INTERNAL")
- void addUserKeyAuth(int userId, int serialNumber, in byte[] secret) = 70;
- @EnforcePermission("STORAGE_INTERNAL")
- void fixateNewestUserKeyAuth(int userId) = 71;
+ void setUserKeyProtection(int userId, in byte[] secret) = 70;
@EnforcePermission("MOUNT_FORMAT_FILESYSTEMS")
void fstrim(int flags, IVoldTaskListener listener) = 72;
AppFuseMount mountProxyFileDescriptorBridge() = 73;
@@ -165,8 +164,6 @@
@EnforcePermission("MOUNT_FORMAT_FILESYSTEMS")
boolean needsCheckpoint() = 86;
void abortChanges(in String message, boolean retry) = 87;
- @EnforcePermission("STORAGE_INTERNAL")
- void clearUserKeyAuth(int userId, int serialNumber, in byte[] secret) = 88;
void fixupAppDir(in String path) = 89;
void disableAppDataIsolation(in String pkgName, int pid, int userId) = 90;
PendingIntent getManageSpaceActivityIntent(in String packageName, int requestCode) = 91;
diff --git a/core/java/android/os/storage/StorageManager.java b/core/java/android/os/storage/StorageManager.java
index c1606e8..38ac984 100644
--- a/core/java/android/os/storage/StorageManager.java
+++ b/core/java/android/os/storage/StorageManager.java
@@ -1606,15 +1606,6 @@
}
/** {@hide} */
- public void unlockUserKey(int userId, int serialNumber, byte[] secret) {
- try {
- mStorageManager.unlockUserKey(userId, serialNumber, secret);
- } catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
- }
- }
-
- /** {@hide} */
public void lockUserKey(int userId) {
try {
mStorageManager.lockUserKey(userId);
diff --git a/core/java/android/provider/DeviceConfig.java b/core/java/android/provider/DeviceConfig.java
index 99b9156..7095d1b 100644
--- a/core/java/android/provider/DeviceConfig.java
+++ b/core/java/android/provider/DeviceConfig.java
@@ -377,6 +377,14 @@
public static final String NAMESPACE_REBOOT_READINESS = "reboot_readiness";
/**
+ * Namespace for Remote Key Provisioning related features.
+ *
+ * @hide
+ */
+ public static final String NAMESPACE_REMOTE_KEY_PROVISIONING_NATIVE =
+ "remote_key_provisioning_native";
+
+ /**
* Namespace for Rollback flags that are applied immediately.
*
* @hide
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index cab6acb..00633a2 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -393,6 +393,21 @@
"android.settings.ACCESSIBILITY_DETAILS_SETTINGS";
/**
+ * Activity Action: Show settings to allow configuration of accessibility color and motion.
+ * <p>
+ * In some cases, a matching Activity may not exist, so ensure you
+ * safeguard against this.
+ * <p>
+ * Input: Nothing.
+ * <p>
+ * Output: Nothing.
+ * @hide
+ */
+ @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
+ public static final String ACTION_ACCESSIBILITY_COLOR_MOTION_SETTINGS =
+ "android.settings.ACCESSIBILITY_COLOR_MOTION_SETTINGS";
+
+ /**
* Activity Action: Show settings to allow configuration of Reduce Bright Colors.
* <p>
* In some cases, a matching Activity may not exist, so ensure you
@@ -438,6 +453,21 @@
"android.settings.COLOR_INVERSION_SETTINGS";
/**
+ * Activity Action: Show settings to allow configuration of text reading.
+ * <p>
+ * In some cases, a matching Activity may not exist, so ensure you
+ * safeguard against this.
+ * <p>
+ * Input: Nothing.
+ * <p>
+ * Output: Nothing.
+ * @hide
+ */
+ @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
+ public static final String ACTION_TEXT_READING_SETTINGS =
+ "android.settings.TEXT_READING_SETTINGS";
+
+ /**
* Activity Action: Show settings to control access to usage information.
* <p>
* In some cases, a matching Activity may not exist, so ensure you
diff --git a/core/java/android/security/keymaster/KeymasterDefs.java b/core/java/android/security/keymaster/KeymasterDefs.java
index 8efc5eb..e720f1a 100644
--- a/core/java/android/security/keymaster/KeymasterDefs.java
+++ b/core/java/android/security/keymaster/KeymasterDefs.java
@@ -65,6 +65,7 @@
public static final int KM_TAG_PADDING = Tag.PADDING; // KM_ENUM_REP | 6;
public static final int KM_TAG_CALLER_NONCE = Tag.CALLER_NONCE; // KM_BOOL | 7;
public static final int KM_TAG_MIN_MAC_LENGTH = Tag.MIN_MAC_LENGTH; // KM_UINT | 8;
+ public static final int KM_TAG_EC_CURVE = Tag.EC_CURVE; // KM_ENUM | 10;
public static final int KM_TAG_RSA_PUBLIC_EXPONENT = Tag.RSA_PUBLIC_EXPONENT; // KM_ULONG | 200;
public static final int KM_TAG_INCLUDE_UNIQUE_ID = Tag.INCLUDE_UNIQUE_ID; // KM_BOOL | 202;
diff --git a/core/java/android/service/dreams/Sandman.java b/core/java/android/service/dreams/Sandman.java
index fae72a2..ced2a01 100644
--- a/core/java/android/service/dreams/Sandman.java
+++ b/core/java/android/service/dreams/Sandman.java
@@ -20,13 +20,13 @@
import android.content.Context;
import android.content.Intent;
import android.os.PowerManager;
+import android.os.RemoteException;
+import android.os.ServiceManager;
import android.os.SystemClock;
import android.os.UserHandle;
import android.provider.Settings;
import android.util.Slog;
-import com.android.server.LocalServices;
-
/**
* Internal helper for launching dreams to ensure consistency between the
* <code>UiModeManagerService</code> system service and the <code>Somnambulator</code> activity.
@@ -75,28 +75,32 @@
}
private static void startDream(Context context, boolean docked) {
- DreamManagerInternal dreamManagerService =
- LocalServices.getService(DreamManagerInternal.class);
- if (dreamManagerService != null && !dreamManagerService.isDreaming()) {
- if (docked) {
- Slog.i(TAG, "Activating dream while docked.");
+ try {
+ IDreamManager dreamManagerService = IDreamManager.Stub.asInterface(
+ ServiceManager.getService(DreamService.DREAM_SERVICE));
+ if (dreamManagerService != null && !dreamManagerService.isDreaming()) {
+ if (docked) {
+ Slog.i(TAG, "Activating dream while docked.");
- // Wake up.
- // The power manager will wake up the system automatically when it starts
- // receiving power from a dock but there is a race between that happening
- // and the UI mode manager starting a dream. We want the system to already
- // be awake by the time this happens. Otherwise the dream may not start.
- PowerManager powerManager =
- context.getSystemService(PowerManager.class);
- powerManager.wakeUp(SystemClock.uptimeMillis(),
- PowerManager.WAKE_REASON_PLUGGED_IN,
- "android.service.dreams:DREAM");
- } else {
- Slog.i(TAG, "Activating dream by user request.");
+ // Wake up.
+ // The power manager will wake up the system automatically when it starts
+ // receiving power from a dock but there is a race between that happening
+ // and the UI mode manager starting a dream. We want the system to already
+ // be awake by the time this happens. Otherwise the dream may not start.
+ PowerManager powerManager =
+ context.getSystemService(PowerManager.class);
+ powerManager.wakeUp(SystemClock.uptimeMillis(),
+ PowerManager.WAKE_REASON_PLUGGED_IN,
+ "android.service.dreams:DREAM");
+ } else {
+ Slog.i(TAG, "Activating dream by user request.");
+ }
+
+ // Dream.
+ dreamManagerService.dream();
}
-
- // Dream.
- dreamManagerService.requestDream();
+ } catch (RemoteException ex) {
+ Slog.e(TAG, "Could not start dream when docked.", ex);
}
}
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index 455f258..6c61d4bd 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -1098,6 +1098,10 @@
mInputQueueCallback.onInputQueueCreated(mInputQueue);
}
}
+
+ // Update the last resource config in case the resource configuration was changed while
+ // activity relaunched.
+ mLastConfigurationFromResources.setTo(getConfiguration());
}
private Configuration getConfiguration() {
@@ -8540,6 +8544,10 @@
if (mLocalSyncState != LOCAL_SYNC_NONE) {
writer.println(innerPrefix + "mLocalSyncState=" + mLocalSyncState);
}
+ writer.println(innerPrefix + "mLastReportedMergedConfiguration="
+ + mLastReportedMergedConfiguration);
+ writer.println(innerPrefix + "mLastConfigurationFromResources="
+ + mLastConfigurationFromResources);
writer.println(innerPrefix + "mIsAmbientMode=" + mIsAmbientMode);
writer.println(innerPrefix + "mUnbufferedInputSource="
+ Integer.toHexString(mUnbufferedInputSource));
diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java
index da786f3..b339d76 100644
--- a/core/java/android/widget/TextView.java
+++ b/core/java/android/widget/TextView.java
@@ -9358,10 +9358,11 @@
// - The deleted text is at the end of the text
// e.g. "one [deleted]" -> "one |" -> "one|"
// (The pipe | indicates the cursor position.)
- while (start > 0 && TextUtils.isWhitespaceExceptNewline(codePointBeforeStart)) {
+ do {
start -= Character.charCount(codePointBeforeStart);
+ if (start == 0) break;
codePointBeforeStart = Character.codePointBefore(mText, start);
- }
+ } while (TextUtils.isWhitespaceExceptNewline(codePointBeforeStart));
} else if (TextUtils.isWhitespaceExceptNewline(codePointAtEnd)
&& (TextUtils.isWhitespace(codePointBeforeStart)
|| TextUtils.isPunctuation(codePointBeforeStart))) {
@@ -9373,11 +9374,11 @@
// - The deleted text is at the start of the text
// e.g. "[deleted] two" -> "| two" -> "|two"
// (The pipe | indicates the cursor position.)
- while (end < mText.length()
- && TextUtils.isWhitespaceExceptNewline(codePointAtEnd)) {
+ do {
end += Character.charCount(codePointAtEnd);
+ if (end == mText.length()) break;
codePointAtEnd = Character.codePointAt(mText, end);
- }
+ } while (TextUtils.isWhitespaceExceptNewline(codePointAtEnd));
}
}
@@ -9487,11 +9488,19 @@
}
int endOffset = startOffset;
- while (startOffset > 0 && Character.isWhitespace(mText.charAt(startOffset - 1))) {
- startOffset--;
+ while (startOffset > 0) {
+ int codePointBeforeStart = Character.codePointBefore(mText, startOffset);
+ if (!TextUtils.isWhitespace(codePointBeforeStart)) {
+ break;
+ }
+ startOffset -= Character.charCount(codePointBeforeStart);
}
- while (endOffset < mText.length() && Character.isWhitespace(mText.charAt(endOffset))) {
- endOffset++;
+ while (endOffset < mText.length()) {
+ int codePointAtEnd = Character.codePointAt(mText, endOffset);
+ if (!TextUtils.isWhitespace(codePointAtEnd)) {
+ break;
+ }
+ endOffset += Character.charCount(codePointAtEnd);
}
if (startOffset < endOffset) {
getEditableText().delete(startOffset, endOffset);
diff --git a/core/java/android/window/IWindowOrganizerController.aidl b/core/java/android/window/IWindowOrganizerController.aidl
index 3c7cd02..36eaf49 100644
--- a/core/java/android/window/IWindowOrganizerController.aidl
+++ b/core/java/android/window/IWindowOrganizerController.aidl
@@ -51,16 +51,19 @@
in IWindowContainerTransactionCallback callback);
/**
- * Starts a transition.
+ * Starts a new transition.
* @param type The transition type.
- * @param transitionToken A token associated with the transition to start. If null, a new
- * transition will be created of the provided type.
* @param t Operations that are part of the transition.
- * @return a token representing the transition. This will just be transitionToken if it was
- * non-null.
+ * @return a token representing the transition.
*/
- IBinder startTransition(int type, in @nullable IBinder transitionToken,
- in @nullable WindowContainerTransaction t);
+ IBinder startNewTransition(int type, in @nullable WindowContainerTransaction t);
+
+ /**
+ * Starts the given transition.
+ * @param transitionToken A token associated with the transition to start.
+ * @param t Operations that are part of the transition.
+ */
+ oneway void startTransition(IBinder transitionToken, in @nullable WindowContainerTransaction t);
/**
* Starts a legacy transition.
diff --git a/core/java/android/window/WindowOrganizer.java b/core/java/android/window/WindowOrganizer.java
index 4ea5ea5..2a80d02 100644
--- a/core/java/android/window/WindowOrganizer.java
+++ b/core/java/android/window/WindowOrganizer.java
@@ -84,9 +84,8 @@
}
/**
- * Start a transition.
+ * Starts a new transition, don't use this to start an already created one.
* @param type The type of the transition. This is ignored if a transitionToken is provided.
- * @param transitionToken An existing transition to start. If null, a new transition is created.
* @param t The set of window operations that are part of this transition.
* @return A token identifying the transition. This will be the same as transitionToken if it
* was provided.
@@ -94,10 +93,24 @@
*/
@RequiresPermission(android.Manifest.permission.MANAGE_ACTIVITY_TASKS)
@NonNull
- public IBinder startTransition(int type, @Nullable IBinder transitionToken,
+ public IBinder startNewTransition(int type, @Nullable WindowContainerTransaction t) {
+ try {
+ return getWindowOrganizerController().startNewTransition(type, t);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Starts an already created transition.
+ * @param transitionToken An existing transition to start.
+ * @hide
+ */
+ @RequiresPermission(android.Manifest.permission.MANAGE_ACTIVITY_TASKS)
+ public void startTransition(@NonNull IBinder transitionToken,
@Nullable WindowContainerTransaction t) {
try {
- return getWindowOrganizerController().startTransition(type, transitionToken, t);
+ getWindowOrganizerController().startTransition(transitionToken, t);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
diff --git a/core/java/com/android/internal/app/ChooserListAdapter.java b/core/java/com/android/internal/app/ChooserListAdapter.java
index 1ec5325..4f74ca7 100644
--- a/core/java/com/android/internal/app/ChooserListAdapter.java
+++ b/core/java/com/android/internal/app/ChooserListAdapter.java
@@ -86,7 +86,6 @@
private final ChooserActivityLogger mChooserActivityLogger;
private int mNumShortcutResults = 0;
- private Map<DisplayResolveInfo, LoadIconTask> mIconLoaders = new HashMap<>();
private boolean mApplySharingAppLimits;
// Reserve spots for incoming direct share targets by adding placeholders
@@ -265,31 +264,20 @@
return;
}
- if (!(info instanceof DisplayResolveInfo)) {
- holder.bindLabel(info.getDisplayLabel(), info.getExtendedInfo(), alwaysShowSubLabel());
- holder.bindIcon(info);
-
- if (info instanceof SelectableTargetInfo) {
- // direct share targets should append the application name for a better readout
- DisplayResolveInfo rInfo = ((SelectableTargetInfo) info).getDisplayResolveInfo();
- CharSequence appName = rInfo != null ? rInfo.getDisplayLabel() : "";
- CharSequence extendedInfo = info.getExtendedInfo();
- String contentDescription = String.join(" ", info.getDisplayLabel(),
- extendedInfo != null ? extendedInfo : "", appName);
- holder.updateContentDescription(contentDescription);
- }
- } else {
+ holder.bindLabel(info.getDisplayLabel(), info.getExtendedInfo(), alwaysShowSubLabel());
+ holder.bindIcon(info);
+ if (info instanceof SelectableTargetInfo) {
+ // direct share targets should append the application name for a better readout
+ DisplayResolveInfo rInfo = ((SelectableTargetInfo) info).getDisplayResolveInfo();
+ CharSequence appName = rInfo != null ? rInfo.getDisplayLabel() : "";
+ CharSequence extendedInfo = info.getExtendedInfo();
+ String contentDescription = String.join(" ", info.getDisplayLabel(),
+ extendedInfo != null ? extendedInfo : "", appName);
+ holder.updateContentDescription(contentDescription);
+ } else if (info instanceof DisplayResolveInfo) {
DisplayResolveInfo dri = (DisplayResolveInfo) info;
- holder.bindLabel(dri.getDisplayLabel(), dri.getExtendedInfo(), alwaysShowSubLabel());
- LoadIconTask task = mIconLoaders.get(dri);
- if (task == null) {
- task = new LoadIconTask(dri, holder);
- mIconLoaders.put(dri, task);
- task.execute();
- } else {
- // The holder was potentially changed as the underlying items were
- // reshuffled, so reset the target holder
- task.setViewHolder(holder);
+ if (!dri.hasDisplayIcon()) {
+ loadIcon(dri);
}
}
diff --git a/core/java/com/android/internal/app/ResolverActivity.java b/core/java/com/android/internal/app/ResolverActivity.java
index c8bc204..822393f 100644
--- a/core/java/com/android/internal/app/ResolverActivity.java
+++ b/core/java/com/android/internal/app/ResolverActivity.java
@@ -55,6 +55,7 @@
import android.content.res.Configuration;
import android.content.res.TypedArray;
import android.graphics.Insets;
+import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
@@ -1475,14 +1476,21 @@
mMultiProfilePagerAdapter.getActiveListAdapter().mDisplayList.get(0);
boolean inWorkProfile = getCurrentProfile() == PROFILE_WORK;
- ResolverListAdapter inactiveAdapter = mMultiProfilePagerAdapter.getInactiveListAdapter();
- DisplayResolveInfo otherProfileResolveInfo = inactiveAdapter.mDisplayList.get(0);
+ final ResolverListAdapter inactiveAdapter =
+ mMultiProfilePagerAdapter.getInactiveListAdapter();
+ final DisplayResolveInfo otherProfileResolveInfo = inactiveAdapter.mDisplayList.get(0);
// Load the icon asynchronously
ImageView icon = findViewById(R.id.icon);
- ResolverListAdapter.LoadIconTask iconTask = inactiveAdapter.new LoadIconTask(
- otherProfileResolveInfo, new ResolverListAdapter.ViewHolder(icon));
- iconTask.execute();
+ inactiveAdapter.new LoadIconTask(otherProfileResolveInfo) {
+ @Override
+ protected void onPostExecute(Drawable drawable) {
+ if (!isDestroyed()) {
+ otherProfileResolveInfo.setDisplayIcon(drawable);
+ new ResolverListAdapter.ViewHolder(icon).bindIcon(otherProfileResolveInfo);
+ }
+ }
+ }.execute();
((TextView) findViewById(R.id.open_cross_profile)).setText(
getResources().getString(
diff --git a/core/java/com/android/internal/app/ResolverListAdapter.java b/core/java/com/android/internal/app/ResolverListAdapter.java
index 66fff5c..f6075b0 100644
--- a/core/java/com/android/internal/app/ResolverListAdapter.java
+++ b/core/java/com/android/internal/app/ResolverListAdapter.java
@@ -58,7 +58,10 @@
import com.android.internal.app.chooser.TargetInfo;
import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
import java.util.List;
+import java.util.Map;
public class ResolverListAdapter extends BaseAdapter {
private static final String TAG = "ResolverListAdapter";
@@ -87,6 +90,8 @@
private Runnable mPostListReadyRunnable;
private final boolean mIsAudioCaptureDevice;
private boolean mIsTabLoaded;
+ private final Map<DisplayResolveInfo, LoadIconTask> mIconLoaders = new HashMap<>();
+ private final Map<DisplayResolveInfo, LoadLabelTask> mLabelLoaders = new HashMap<>();
public ResolverListAdapter(Context context, List<Intent> payloadIntents,
Intent[] initialIntents, List<ResolveInfo> rList,
@@ -636,26 +641,47 @@
if (info == null) {
holder.icon.setImageDrawable(
mContext.getDrawable(R.drawable.resolver_icon_placeholder));
+ holder.bindLabel("", "", false);
return;
}
- if (info instanceof DisplayResolveInfo
- && !((DisplayResolveInfo) info).hasDisplayLabel()) {
- getLoadLabelTask((DisplayResolveInfo) info, holder).execute();
- } else {
- holder.bindLabel(info.getDisplayLabel(), info.getExtendedInfo(), alwaysShowSubLabel());
- }
-
- if (info instanceof DisplayResolveInfo
- && !((DisplayResolveInfo) info).hasDisplayIcon()) {
- new LoadIconTask((DisplayResolveInfo) info, holder).execute();
- } else {
+ if (info instanceof DisplayResolveInfo) {
+ DisplayResolveInfo dri = (DisplayResolveInfo) info;
+ boolean hasLabel = dri.hasDisplayLabel();
+ holder.bindLabel(
+ dri.getDisplayLabel(),
+ dri.getExtendedInfo(),
+ hasLabel && alwaysShowSubLabel());
holder.bindIcon(info);
+ if (!hasLabel) {
+ loadLabel(dri);
+ }
+ if (!dri.hasDisplayIcon()) {
+ loadIcon(dri);
+ }
}
}
- protected LoadLabelTask getLoadLabelTask(DisplayResolveInfo info, ViewHolder holder) {
- return new LoadLabelTask(info, holder);
+ protected final void loadIcon(DisplayResolveInfo info) {
+ LoadIconTask task = mIconLoaders.get(info);
+ if (task == null) {
+ task = new LoadIconTask((DisplayResolveInfo) info);
+ mIconLoaders.put(info, task);
+ task.execute();
+ }
+ }
+
+ private void loadLabel(DisplayResolveInfo info) {
+ LoadLabelTask task = mLabelLoaders.get(info);
+ if (task == null) {
+ task = createLoadLabelTask(info);
+ mLabelLoaders.put(info, task);
+ task.execute();
+ }
+ }
+
+ protected LoadLabelTask createLoadLabelTask(DisplayResolveInfo info) {
+ return new LoadLabelTask(info);
}
public void onDestroy() {
@@ -666,6 +692,16 @@
if (mResolverListController != null) {
mResolverListController.destroy();
}
+ cancelTasks(mIconLoaders.values());
+ cancelTasks(mLabelLoaders.values());
+ mIconLoaders.clear();
+ mLabelLoaders.clear();
+ }
+
+ private <T extends AsyncTask> void cancelTasks(Collection<T> tasks) {
+ for (T task: tasks) {
+ task.cancel(false);
+ }
}
private static ColorMatrixColorFilter getSuspendedColorMatrix() {
@@ -883,11 +919,9 @@
protected class LoadLabelTask extends AsyncTask<Void, Void, CharSequence[]> {
private final DisplayResolveInfo mDisplayResolveInfo;
- private final ViewHolder mHolder;
- protected LoadLabelTask(DisplayResolveInfo dri, ViewHolder holder) {
+ protected LoadLabelTask(DisplayResolveInfo dri) {
mDisplayResolveInfo = dri;
- mHolder = holder;
}
@Override
@@ -925,21 +959,22 @@
@Override
protected void onPostExecute(CharSequence[] result) {
+ if (mDisplayResolveInfo.hasDisplayLabel()) {
+ return;
+ }
mDisplayResolveInfo.setDisplayLabel(result[0]);
mDisplayResolveInfo.setExtendedInfo(result[1]);
- mHolder.bindLabel(result[0], result[1], alwaysShowSubLabel());
+ notifyDataSetChanged();
}
}
class LoadIconTask extends AsyncTask<Void, Void, Drawable> {
protected final DisplayResolveInfo mDisplayResolveInfo;
private final ResolveInfo mResolveInfo;
- private ViewHolder mHolder;
- LoadIconTask(DisplayResolveInfo dri, ViewHolder holder) {
+ LoadIconTask(DisplayResolveInfo dri) {
mDisplayResolveInfo = dri;
mResolveInfo = dri.getResolveInfo();
- mHolder = holder;
}
@Override
@@ -953,17 +988,9 @@
mResolverListCommunicator.updateProfileViewButton();
} else if (!mDisplayResolveInfo.hasDisplayIcon()) {
mDisplayResolveInfo.setDisplayIcon(d);
- mHolder.bindIcon(mDisplayResolveInfo);
- // Notify in case view is already bound to resolve the race conditions on
- // low end devices
notifyDataSetChanged();
}
}
-
- public void setViewHolder(ViewHolder holder) {
- mHolder = holder;
- mHolder.bindIcon(mDisplayResolveInfo);
- }
}
/**
diff --git a/core/java/com/android/internal/widget/LockPatternUtils.java b/core/java/com/android/internal/widget/LockPatternUtils.java
index 65c2d00..55a26fe 100644
--- a/core/java/com/android/internal/widget/LockPatternUtils.java
+++ b/core/java/com/android/internal/widget/LockPatternUtils.java
@@ -26,6 +26,7 @@
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.annotation.UserIdInt;
import android.app.PropertyInvalidatedCache;
import android.app.admin.DevicePolicyManager;
import android.app.admin.PasswordMetrics;
@@ -1784,4 +1785,16 @@
re.rethrowFromSystemServer();
}
}
+
+ public void unlockUserKeyIfUnsecured(@UserIdInt int userId) {
+ getLockSettingsInternal().unlockUserKeyIfUnsecured(userId);
+ }
+
+ public void createNewUser(@UserIdInt int userId, int userSerialNumber) {
+ getLockSettingsInternal().createNewUser(userId, userSerialNumber);
+ }
+
+ public void removeUser(@UserIdInt int userId) {
+ getLockSettingsInternal().removeUser(userId);
+ }
}
diff --git a/core/java/com/android/internal/widget/LockSettingsInternal.java b/core/java/com/android/internal/widget/LockSettingsInternal.java
index 0a2c18f8..5b08bb1 100644
--- a/core/java/com/android/internal/widget/LockSettingsInternal.java
+++ b/core/java/com/android/internal/widget/LockSettingsInternal.java
@@ -18,6 +18,7 @@
import android.annotation.IntDef;
import android.annotation.Nullable;
+import android.annotation.UserIdInt;
import android.app.admin.PasswordMetrics;
import java.lang.annotation.Retention;
@@ -53,6 +54,37 @@
// TODO(b/183140900) split store escrow key errors into detailed ones.
/**
+ * Unlocks the credential-encrypted storage for the given user if the user is not secured, i.e.
+ * doesn't have an LSKF.
+ * <p>
+ * This doesn't throw an exception on failure; whether the storage has been unlocked can be
+ * determined by {@link StorageManager#isUserKeyUnlocked()}.
+ *
+ * @param userId the ID of the user whose storage to unlock
+ */
+ public abstract void unlockUserKeyIfUnsecured(@UserIdInt int userId);
+
+ /**
+ * Creates the locksettings state for a new user.
+ * <p>
+ * This includes creating a synthetic password and protecting it with an empty LSKF.
+ *
+ * @param userId the ID of the new user
+ * @param userSerialNumber the serial number of the new user
+ */
+ public abstract void createNewUser(@UserIdInt int userId, int userSerialNumber);
+
+ /**
+ * Removes the locksettings state for the given user.
+ * <p>
+ * This includes removing the user's synthetic password and any protectors that are protecting
+ * it.
+ *
+ * @param userId the ID of the user being removed
+ */
+ public abstract void removeUser(@UserIdInt int userId);
+
+ /**
* Create an escrow token for the current user, which can later be used to unlock FBE
* or change user password.
*
diff --git a/core/jni/android_media_AudioVolumeGroups.cpp b/core/jni/android_media_AudioVolumeGroups.cpp
index 7098451..1252e89 100644
--- a/core/jni/android_media_AudioVolumeGroups.cpp
+++ b/core/jni/android_media_AudioVolumeGroups.cpp
@@ -94,6 +94,11 @@
for (size_t j = 0; j < static_cast<size_t>(numAttributes); j++) {
auto attributes = group.getAudioAttributes()[j];
+ // Native & Java audio attributes default initializers are not aligned for the source.
+ // Given the volume group class concerns only playback, this field must be equal to the
+ // default java initializer.
+ attributes.source = AUDIO_SOURCE_INVALID;
+
jStatus = JNIAudioAttributeHelper::nativeToJava(env, &jAudioAttribute, attributes);
if (jStatus != AUDIO_JAVA_SUCCESS) {
goto exit;
diff --git a/core/jni/android_util_Process.cpp b/core/jni/android_util_Process.cpp
index b9d5ee4..9501c8d 100644
--- a/core/jni/android_util_Process.cpp
+++ b/core/jni/android_util_Process.cpp
@@ -1252,6 +1252,20 @@
return fd;
}
+void android_os_Process_freezeCgroupUID(JNIEnv* env, jobject clazz, jint uid, jboolean freeze) {
+ bool success = true;
+
+ if (freeze) {
+ success = SetUserProfiles(uid, {"Frozen"});
+ } else {
+ success = SetUserProfiles(uid, {"Unfrozen"});
+ }
+
+ if (!success) {
+ jniThrowRuntimeException(env, "Could not apply user profile");
+ }
+}
+
static const JNINativeMethod methods[] = {
{"getUidForName", "(Ljava/lang/String;)I", (void*)android_os_Process_getUidForName},
{"getGidForName", "(Ljava/lang/String;)I", (void*)android_os_Process_getGidForName},
@@ -1293,6 +1307,7 @@
{"killProcessGroup", "(II)I", (void*)android_os_Process_killProcessGroup},
{"removeAllProcessGroups", "()V", (void*)android_os_Process_removeAllProcessGroups},
{"nativePidFdOpen", "(II)I", (void*)android_os_Process_nativePidFdOpen},
+ {"freezeCgroupUid", "(IZ)V", (void*)android_os_Process_freezeCgroupUID},
};
int register_android_os_Process(JNIEnv* env)
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index ded1b89..f0b1b2a 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -3051,6 +3051,10 @@
<permission android:name="android.permission.QUERY_ADMIN_POLICY"
android:protectionLevel="signature|role" />
+ <!-- @SystemApi @hide Allows an application to exempt apps from platform restrictions.-->
+ <permission android:name="android.permission.MANAGE_DEVICE_POLICY_APP_EXEMPTIONS"
+ android:protectionLevel="signature|role" />
+
<!-- @SystemApi @hide Allows an application to set a device owner on retail demo devices.-->
<permission android:name="android.permission.PROVISION_DEMO_DEVICE"
android:protectionLevel="signature|setup" />
diff --git a/core/tests/coretests/src/com/android/internal/app/ResolverWrapperAdapter.java b/core/tests/coretests/src/com/android/internal/app/ResolverWrapperAdapter.java
index 56a7070..2861428 100644
--- a/core/tests/coretests/src/com/android/internal/app/ResolverWrapperAdapter.java
+++ b/core/tests/coretests/src/com/android/internal/app/ResolverWrapperAdapter.java
@@ -46,14 +46,14 @@
}
@Override
- protected LoadLabelTask getLoadLabelTask(DisplayResolveInfo info, ViewHolder holder) {
- return new LoadLabelWrapperTask(info, holder);
+ protected LoadLabelTask createLoadLabelTask(DisplayResolveInfo info) {
+ return new LoadLabelWrapperTask(info);
}
class LoadLabelWrapperTask extends LoadLabelTask {
- protected LoadLabelWrapperTask(DisplayResolveInfo dri, ViewHolder holder) {
- super(dri, holder);
+ protected LoadLabelWrapperTask(DisplayResolveInfo dri) {
+ super(dri);
}
@Override
diff --git a/keystore/java/android/security/KeyStore2.java b/keystore/java/android/security/KeyStore2.java
index c2cd6ff..f507d76 100644
--- a/keystore/java/android/security/KeyStore2.java
+++ b/keystore/java/android/security/KeyStore2.java
@@ -32,6 +32,7 @@
import android.util.Log;
import java.util.Calendar;
+import java.util.Objects;
/**
* @hide This should not be made public in its present form because it
@@ -137,13 +138,13 @@
return new KeyStore2();
}
- private synchronized IKeystoreService getService(boolean retryLookup) {
+ @NonNull private synchronized IKeystoreService getService(boolean retryLookup) {
if (mBinder == null || retryLookup) {
mBinder = IKeystoreService.Stub.asInterface(ServiceManager
.getService(KEYSTORE2_SERVICE_NAME));
Binder.allowBlocking(mBinder.asBinder());
}
- return mBinder;
+ return Objects.requireNonNull(mBinder);
}
void delete(KeyDescriptor descriptor) throws KeyStoreException {
diff --git a/keystore/java/android/security/keystore2/AndroidKeyStoreECPublicKey.java b/keystore/java/android/security/keystore2/AndroidKeyStoreECPublicKey.java
index b631999..4e73bd9 100644
--- a/keystore/java/android/security/keystore2/AndroidKeyStoreECPublicKey.java
+++ b/keystore/java/android/security/keystore2/AndroidKeyStoreECPublicKey.java
@@ -18,13 +18,19 @@
import android.annotation.NonNull;
import android.security.KeyStoreSecurityLevel;
+import android.security.keymaster.KeymasterDefs;
import android.security.keystore.KeyProperties;
+import android.system.keystore2.Authorization;
import android.system.keystore2.KeyDescriptor;
import android.system.keystore2.KeyMetadata;
+import java.security.AlgorithmParameters;
+import java.security.NoSuchAlgorithmException;
import java.security.interfaces.ECPublicKey;
+import java.security.spec.ECGenParameterSpec;
import java.security.spec.ECParameterSpec;
import java.security.spec.ECPoint;
+import java.security.spec.InvalidParameterSpecException;
/**
* {@link ECPublicKey} backed by keystore.
@@ -56,11 +62,45 @@
}
}
+ private static String getEcCurveFromKeymaster(int ecCurve) {
+ switch (ecCurve) {
+ case android.hardware.security.keymint.EcCurve.P_224:
+ return "secp224r1";
+ case android.hardware.security.keymint.EcCurve.P_256:
+ return "secp256r1";
+ case android.hardware.security.keymint.EcCurve.P_384:
+ return "secp384r1";
+ case android.hardware.security.keymint.EcCurve.P_521:
+ return "secp521r1";
+ }
+ return "";
+ }
+
+ private ECParameterSpec getCurveSpec(String name)
+ throws NoSuchAlgorithmException, InvalidParameterSpecException {
+ AlgorithmParameters parameters = AlgorithmParameters.getInstance("EC");
+ parameters.init(new ECGenParameterSpec(name));
+ return parameters.getParameterSpec(ECParameterSpec.class);
+ }
+
@Override
public AndroidKeyStorePrivateKey getPrivateKey() {
+ ECParameterSpec params = mParams;
+ for (Authorization a : getAuthorizations()) {
+ try {
+ if (a.keyParameter.tag == KeymasterDefs.KM_TAG_EC_CURVE) {
+ params = getCurveSpec(getEcCurveFromKeymaster(
+ a.keyParameter.value.getEcCurve()));
+ break;
+ }
+ } catch (Exception e) {
+ throw new RuntimeException("Unable to parse EC curve "
+ + a.keyParameter.value.getEcCurve());
+ }
+ }
return new AndroidKeyStoreECPrivateKey(
getUserKeyDescriptor(), getKeyIdDescriptor().nspace, getAuthorizations(),
- getSecurityLevel(), mParams);
+ getSecurityLevel(), params);
}
@Override
diff --git a/keystore/java/android/security/keystore2/AndroidKeyStoreKeyAgreementSpi.java b/keystore/java/android/security/keystore2/AndroidKeyStoreKeyAgreementSpi.java
index b1338d1..4caa47f 100644
--- a/keystore/java/android/security/keystore2/AndroidKeyStoreKeyAgreementSpi.java
+++ b/keystore/java/android/security/keystore2/AndroidKeyStoreKeyAgreementSpi.java
@@ -31,6 +31,8 @@
import java.security.ProviderException;
import java.security.PublicKey;
import java.security.SecureRandom;
+import java.security.interfaces.ECKey;
+import java.security.interfaces.XECKey;
import java.security.spec.AlgorithmParameterSpec;
import java.util.ArrayList;
import java.util.List;
@@ -132,6 +134,15 @@
throw new InvalidKeyException("key == null");
} else if (!(key instanceof PublicKey)) {
throw new InvalidKeyException("Only public keys supported. Key: " + key);
+ } else if (!(mKey instanceof ECKey && key instanceof ECKey)
+ && !(mKey instanceof XECKey && key instanceof XECKey)) {
+ throw new InvalidKeyException(
+ "Public and Private key should be of the same type:");
+ } else if (mKey instanceof ECKey
+ && !((ECKey) key).getParams().getCurve()
+ .equals(((ECKey) mKey).getParams().getCurve())) {
+ throw new InvalidKeyException(
+ "Public and Private key parameters should be same.");
} else if (!lastPhase) {
throw new IllegalStateException(
"Only one other party supported. lastPhase must be set to true.");
diff --git a/keystore/java/android/security/keystore2/AndroidKeyStoreXDHPrivateKey.java b/keystore/java/android/security/keystore2/AndroidKeyStoreXDHPrivateKey.java
index 42589640..e392c8d 100644
--- a/keystore/java/android/security/keystore2/AndroidKeyStoreXDHPrivateKey.java
+++ b/keystore/java/android/security/keystore2/AndroidKeyStoreXDHPrivateKey.java
@@ -22,16 +22,18 @@
import android.system.keystore2.KeyDescriptor;
import java.security.PrivateKey;
-import java.security.interfaces.EdECKey;
+import java.security.interfaces.XECPrivateKey;
import java.security.spec.NamedParameterSpec;
+import java.util.Optional;
/**
* X25519 Private Key backed by Keystore.
- * instance of {@link PrivateKey} and {@link EdECKey}
+ * instance of {@link PrivateKey} and {@link XECPrivateKey}
*
* @hide
*/
-public class AndroidKeyStoreXDHPrivateKey extends AndroidKeyStorePrivateKey implements EdECKey {
+public class AndroidKeyStoreXDHPrivateKey extends AndroidKeyStorePrivateKey
+ implements XECPrivateKey {
public AndroidKeyStoreXDHPrivateKey(
@NonNull KeyDescriptor descriptor, long keyId,
@NonNull Authorization[] authorizations,
@@ -44,4 +46,12 @@
public NamedParameterSpec getParams() {
return NamedParameterSpec.X25519;
}
+
+ @Override
+ public Optional<byte[]> getScalar() {
+ /* An empty Optional if the scalar cannot be extracted (e.g. if the provider is a hardware
+ * token and the private key is not allowed to leave the crypto boundary).
+ */
+ return Optional.empty();
+ }
}
diff --git a/libs/WindowManager/Shell/res/layout/tv_pip_menu.xml b/libs/WindowManager/Shell/res/layout/tv_pip_menu.xml
index afd3aac..70755e6 100644
--- a/libs/WindowManager/Shell/res/layout/tv_pip_menu.xml
+++ b/libs/WindowManager/Shell/res/layout/tv_pip_menu.xml
@@ -119,7 +119,7 @@
<!-- Temporarily extending the background to show an edu text hint for opening the menu -->
<FrameLayout
- android:id="@+id/tv_pip_menu_edu_text_container"
+ android:id="@+id/tv_pip_menu_edu_text_drawer_placeholder"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@+id/tv_pip"
@@ -127,23 +127,8 @@
android:layout_alignStart="@+id/tv_pip"
android:layout_alignEnd="@+id/tv_pip"
android:background="@color/tv_pip_menu_background"
- android:clipChildren="true">
-
- <TextView
- android:id="@+id/tv_pip_menu_edu_text"
- android:layout_width="wrap_content"
- android:layout_height="@dimen/pip_menu_edu_text_view_height"
- android:layout_gravity="bottom|center"
- android:gravity="center"
- android:clickable="false"
- android:paddingBottom="@dimen/pip_menu_border_width"
- android:text="@string/pip_edu_text"
- android:singleLine="true"
- android:ellipsize="marquee"
- android:marqueeRepeatLimit="1"
- android:scrollHorizontally="true"
- android:textAppearance="@style/TvPipEduText"/>
- </FrameLayout>
+ android:paddingBottom="@dimen/pip_menu_border_width"
+ android:paddingTop="@dimen/pip_menu_border_width"/>
<!-- Frame around the PiP content + edu text hint - used to highlight open menu -->
<View
diff --git a/libs/WindowManager/Shell/res/values-tvdpi/dimen.xml b/libs/WindowManager/Shell/res/values-tvdpi/dimen.xml
index b45b9ec..9833a88 100644
--- a/libs/WindowManager/Shell/res/values-tvdpi/dimen.xml
+++ b/libs/WindowManager/Shell/res/values-tvdpi/dimen.xml
@@ -41,8 +41,10 @@
<dimen name="pip_menu_edu_text_view_height">24dp</dimen>
<dimen name="pip_menu_edu_text_home_icon">9sp</dimen>
<dimen name="pip_menu_edu_text_home_icon_outline">14sp</dimen>
- <integer name="pip_edu_text_show_duration_ms">10500</integer>
- <integer name="pip_edu_text_window_exit_animation_duration_ms">1000</integer>
- <integer name="pip_edu_text_view_exit_animation_duration_ms">300</integer>
+ <integer name="pip_edu_text_scroll_times">2</integer>
+ <integer name="pip_edu_text_non_scroll_show_duration">10500</integer>
+ <integer name="pip_edu_text_start_scroll_delay">2000</integer>
+ <integer name="pip_edu_text_window_exit_animation_duration">1000</integer>
+ <integer name="pip_edu_text_view_exit_animation_duration">300</integer>
</resources>
diff --git a/libs/WindowManager/Shell/res/values/strings_tv.xml b/libs/WindowManager/Shell/res/values/strings_tv.xml
index 2b7a13e..8f806cf 100644
--- a/libs/WindowManager/Shell/res/values/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values/strings_tv.xml
@@ -42,8 +42,8 @@
<!-- Educative text instructing the user to double press the HOME button to access the pip
controls menu [CHAR LIMIT=50] -->
- <string name="pip_edu_text"> Double press <annotation icon="home_icon"> HOME </annotation> for
- controls </string>
+ <string name="pip_edu_text">Double press <annotation icon="home_icon">HOME</annotation> for
+ controls</string>
<!-- Accessibility announcement when opening the PiP menu. [CHAR LIMIT=NONE] -->
<string name="a11y_pip_menu_entered">Picture-in-Picture menu.</string>
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DockStateReader.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DockStateReader.java
new file mode 100644
index 0000000..e029358
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DockStateReader.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.common;
+
+import static android.content.Intent.EXTRA_DOCK_STATE;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+
+import com.android.wm.shell.dagger.WMSingleton;
+
+import javax.inject.Inject;
+
+/**
+ * Provides information about the docked state of the device.
+ */
+@WMSingleton
+public class DockStateReader {
+
+ private static final IntentFilter DOCK_INTENT_FILTER = new IntentFilter(
+ Intent.ACTION_DOCK_EVENT);
+
+ private final Context mContext;
+
+ @Inject
+ public DockStateReader(Context context) {
+ mContext = context;
+ }
+
+ /**
+ * @return True if the device is docked and false otherwise.
+ */
+ public boolean isDocked() {
+ Intent dockStatus = mContext.registerReceiver(/* receiver */ null, DOCK_INTENT_FILTER);
+ if (dockStatus != null) {
+ int dockState = dockStatus.getIntExtra(EXTRA_DOCK_STATE,
+ Intent.EXTRA_DOCK_STATE_UNDOCKED);
+ return dockState != Intent.EXTRA_DOCK_STATE_UNDOCKED;
+ }
+ return false;
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java
index 235fd9c..6627de5 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java
@@ -37,6 +37,7 @@
import com.android.wm.shell.common.DisplayInsetsController;
import com.android.wm.shell.common.DisplayInsetsController.OnInsetsChangedListener;
import com.android.wm.shell.common.DisplayLayout;
+import com.android.wm.shell.common.DockStateReader;
import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.common.SyncTransactionQueue;
import com.android.wm.shell.compatui.CompatUIWindowManager.CompatUIHintsState;
@@ -109,6 +110,7 @@
private final SyncTransactionQueue mSyncQueue;
private final ShellExecutor mMainExecutor;
private final Lazy<Transitions> mTransitionsLazy;
+ private final DockStateReader mDockStateReader;
private CompatUICallback mCallback;
@@ -127,7 +129,8 @@
DisplayImeController imeController,
SyncTransactionQueue syncQueue,
ShellExecutor mainExecutor,
- Lazy<Transitions> transitionsLazy) {
+ Lazy<Transitions> transitionsLazy,
+ DockStateReader dockStateReader) {
mContext = context;
mShellController = shellController;
mDisplayController = displayController;
@@ -138,6 +141,7 @@
mTransitionsLazy = transitionsLazy;
mCompatUIHintsState = new CompatUIHintsState();
shellInit.addInitCallback(this::onInit, this);
+ mDockStateReader = dockStateReader;
}
private void onInit() {
@@ -315,7 +319,8 @@
return new LetterboxEduWindowManager(context, taskInfo,
mSyncQueue, taskListener, mDisplayController.getDisplayLayout(taskInfo.displayId),
mTransitionsLazy.get(),
- this::onLetterboxEduDismissed);
+ this::onLetterboxEduDismissed,
+ mDockStateReader);
}
private void onLetterboxEduDismissed() {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduWindowManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduWindowManager.java
index 35f1038..867d0ef 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduWindowManager.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduWindowManager.java
@@ -34,6 +34,7 @@
import com.android.wm.shell.R;
import com.android.wm.shell.ShellTaskOrganizer;
import com.android.wm.shell.common.DisplayLayout;
+import com.android.wm.shell.common.DockStateReader;
import com.android.wm.shell.common.SyncTransactionQueue;
import com.android.wm.shell.compatui.CompatUIWindowManagerAbstract;
import com.android.wm.shell.transition.Transitions;
@@ -88,19 +89,21 @@
*/
private final int mDialogVerticalMargin;
+ private final DockStateReader mDockStateReader;
+
public LetterboxEduWindowManager(Context context, TaskInfo taskInfo,
SyncTransactionQueue syncQueue, ShellTaskOrganizer.TaskListener taskListener,
DisplayLayout displayLayout, Transitions transitions,
- Runnable onDismissCallback) {
+ Runnable onDismissCallback, DockStateReader dockStateReader) {
this(context, taskInfo, syncQueue, taskListener, displayLayout, transitions,
- onDismissCallback, new LetterboxEduAnimationController(context));
+ onDismissCallback, new LetterboxEduAnimationController(context), dockStateReader);
}
@VisibleForTesting
LetterboxEduWindowManager(Context context, TaskInfo taskInfo,
SyncTransactionQueue syncQueue, ShellTaskOrganizer.TaskListener taskListener,
DisplayLayout displayLayout, Transitions transitions, Runnable onDismissCallback,
- LetterboxEduAnimationController animationController) {
+ LetterboxEduAnimationController animationController, DockStateReader dockStateReader) {
super(context, taskInfo, syncQueue, taskListener, displayLayout);
mTransitions = transitions;
mOnDismissCallback = onDismissCallback;
@@ -111,6 +114,7 @@
Context.MODE_PRIVATE);
mDialogVerticalMargin = (int) mContext.getResources().getDimension(
R.dimen.letterbox_education_dialog_margin);
+ mDockStateReader = dockStateReader;
}
@Override
@@ -130,13 +134,15 @@
@Override
protected boolean eligibleToShowLayout() {
+ // - The letterbox education should not be visible if the device is docked.
// - If taskbar education is showing, the letterbox education shouldn't be shown for the
// given task until the taskbar education is dismissed and the compat info changes (then
// the controller will create a new instance of this class since this one isn't eligible).
// - If the layout isn't null then it was previously showing, and we shouldn't check if the
// user has seen the letterbox education before.
- return mEligibleForLetterboxEducation && !isTaskbarEduShowing() && (mLayout != null
- || !getHasSeenLetterboxEducation());
+ return mEligibleForLetterboxEducation && !isTaskbarEduShowing()
+ && (mLayout != null || !getHasSeenLetterboxEducation())
+ && !mDockStateReader.isDocked();
}
@Override
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
index c4accde..2214a98 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
@@ -46,6 +46,7 @@
import com.android.wm.shell.common.DisplayImeController;
import com.android.wm.shell.common.DisplayInsetsController;
import com.android.wm.shell.common.DisplayLayout;
+import com.android.wm.shell.common.DockStateReader;
import com.android.wm.shell.common.FloatingContentCoordinator;
import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.common.SyncTransactionQueue;
@@ -69,7 +70,6 @@
import com.android.wm.shell.freeform.FreeformComponents;
import com.android.wm.shell.fullscreen.FullscreenTaskListener;
import com.android.wm.shell.hidedisplaycutout.HideDisplayCutoutController;
-import com.android.wm.shell.kidsmode.KidsModeTaskOrganizer;
import com.android.wm.shell.onehanded.OneHanded;
import com.android.wm.shell.onehanded.OneHandedController;
import com.android.wm.shell.pip.Pip;
@@ -192,33 +192,16 @@
@WMSingleton
@Provides
- static KidsModeTaskOrganizer provideKidsModeTaskOrganizer(
- Context context,
- ShellInit shellInit,
- ShellCommandHandler shellCommandHandler,
- SyncTransactionQueue syncTransactionQueue,
- DisplayController displayController,
- DisplayInsetsController displayInsetsController,
- Optional<UnfoldAnimationController> unfoldAnimationController,
- Optional<RecentTasksController> recentTasksOptional,
- @ShellMainThread ShellExecutor mainExecutor,
- @ShellMainThread Handler mainHandler
- ) {
- return new KidsModeTaskOrganizer(context, shellInit, shellCommandHandler,
- syncTransactionQueue, displayController, displayInsetsController,
- unfoldAnimationController, recentTasksOptional, mainExecutor, mainHandler);
- }
-
- @WMSingleton
- @Provides
static CompatUIController provideCompatUIController(Context context,
ShellInit shellInit,
ShellController shellController,
DisplayController displayController, DisplayInsetsController displayInsetsController,
DisplayImeController imeController, SyncTransactionQueue syncQueue,
- @ShellMainThread ShellExecutor mainExecutor, Lazy<Transitions> transitionsLazy) {
+ @ShellMainThread ShellExecutor mainExecutor, Lazy<Transitions> transitionsLazy,
+ DockStateReader dockStateReader) {
return new CompatUIController(context, shellInit, shellController, displayController,
- displayInsetsController, imeController, syncQueue, mainExecutor, transitionsLazy);
+ displayInsetsController, imeController, syncQueue, mainExecutor, transitionsLazy,
+ dockStateReader);
}
@WMSingleton
@@ -782,7 +765,6 @@
DisplayInsetsController displayInsetsController,
DragAndDropController dragAndDropController,
ShellTaskOrganizer shellTaskOrganizer,
- KidsModeTaskOrganizer kidsModeTaskOrganizer,
Optional<BubbleController> bubblesOptional,
Optional<SplitScreenController> splitScreenOptional,
Optional<Pip> pipOptional,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
index 37a50b6..47b6659 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
@@ -56,6 +56,7 @@
import com.android.wm.shell.freeform.FreeformTaskTransitionHandler;
import com.android.wm.shell.freeform.FreeformTaskTransitionObserver;
import com.android.wm.shell.fullscreen.FullscreenTaskListener;
+import com.android.wm.shell.kidsmode.KidsModeTaskOrganizer;
import com.android.wm.shell.onehanded.OneHandedController;
import com.android.wm.shell.pip.Pip;
import com.android.wm.shell.pip.PipAnimationController;
@@ -620,6 +621,28 @@
}
//
+ // Kids mode
+ //
+ @WMSingleton
+ @Provides
+ static KidsModeTaskOrganizer provideKidsModeTaskOrganizer(
+ Context context,
+ ShellInit shellInit,
+ ShellCommandHandler shellCommandHandler,
+ SyncTransactionQueue syncTransactionQueue,
+ DisplayController displayController,
+ DisplayInsetsController displayInsetsController,
+ Optional<UnfoldAnimationController> unfoldAnimationController,
+ Optional<RecentTasksController> recentTasksOptional,
+ @ShellMainThread ShellExecutor mainExecutor,
+ @ShellMainThread Handler mainHandler
+ ) {
+ return new KidsModeTaskOrganizer(context, shellInit, shellCommandHandler,
+ syncTransactionQueue, displayController, displayInsetsController,
+ unfoldAnimationController, recentTasksOptional, mainExecutor, mainHandler);
+ }
+
+ //
// Misc
//
@@ -630,6 +653,7 @@
@Provides
static Object provideIndependentShellComponentsToCreate(
DefaultMixedHandler defaultMixedHandler,
+ KidsModeTaskOrganizer kidsModeTaskOrganizer,
Optional<DesktopModeController> desktopModeController) {
return new Object();
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/IPip.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/IPip.aidl
index 4def15d..2624ee5 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/IPip.aidl
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/IPip.aidl
@@ -59,10 +59,15 @@
/**
* Sets listener to get pinned stack animation callbacks.
*/
- oneway void setPinnedStackAnimationListener(IPipAnimationListener listener) = 3;
+ oneway void setPipAnimationListener(IPipAnimationListener listener) = 3;
/**
* Sets the shelf height and visibility.
*/
oneway void setShelfHeight(boolean visible, int shelfHeight) = 4;
+
+ /**
+ * Sets the next pip animation type to be the alpha animation.
+ */
+ oneway void setPipAnimationTypeToAlpha() = 5;
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/Pip.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/Pip.java
index c06881a..72b9dd3 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/Pip.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/Pip.java
@@ -51,15 +51,6 @@
}
/**
- * Sets both shelf visibility and its height.
- *
- * @param visible visibility of shelf.
- * @param height to specify the height for shelf.
- */
- default void setShelfHeight(boolean visible, int height) {
- }
-
- /**
* Set the callback when {@link PipTaskOrganizer#isInPip()} state is changed.
*
* @param callback The callback accepts the result of {@link PipTaskOrganizer#isInPip()}
@@ -68,14 +59,6 @@
default void setOnIsInPipStateChangedListener(Consumer<Boolean> callback) {}
/**
- * Set the pinned stack with {@link PipAnimationController.AnimationType}
- *
- * @param animationType The pre-defined {@link PipAnimationController.AnimationType}
- */
- default void setPinnedStackAnimationType(int animationType) {
- }
-
- /**
* Called when showing Pip menu.
*/
default void showPictureInPictureMenu() {}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java
index af47666..3345b1b 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java
@@ -23,6 +23,7 @@
import static com.android.internal.jank.InteractionJankMonitor.CUJ_PIP_TRANSITION;
import static com.android.wm.shell.common.ExecutorUtils.executeRemoteCallWithTaskPermission;
+import static com.android.wm.shell.pip.PipAnimationController.ANIM_TYPE_ALPHA;
import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_EXPAND_OR_UNEXPAND;
import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_LEAVE_PIP;
import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_LEAVE_PIP_TO_SPLIT_SCREEN;
@@ -1065,13 +1066,6 @@
}
@Override
- public void setShelfHeight(boolean visible, int height) {
- mMainExecutor.execute(() -> {
- PipController.this.setShelfHeight(visible, height);
- });
- }
-
- @Override
public void setOnIsInPipStateChangedListener(Consumer<Boolean> callback) {
mMainExecutor.execute(() -> {
PipController.this.setOnIsInPipStateChangedListener(callback);
@@ -1079,13 +1073,6 @@
}
@Override
- public void setPinnedStackAnimationType(int animationType) {
- mMainExecutor.execute(() -> {
- PipController.this.setPinnedStackAnimationType(animationType);
- });
- }
-
- @Override
public void addPipExclusionBoundsChangeListener(Consumer<Rect> listener) {
mMainExecutor.execute(() -> {
mPipBoundsState.addPipExclusionBoundsChangeCallback(listener);
@@ -1178,8 +1165,8 @@
}
@Override
- public void setPinnedStackAnimationListener(IPipAnimationListener listener) {
- executeRemoteCallWithTaskPermission(mController, "setPinnedStackAnimationListener",
+ public void setPipAnimationListener(IPipAnimationListener listener) {
+ executeRemoteCallWithTaskPermission(mController, "setPipAnimationListener",
(controller) -> {
if (listener != null) {
mListener.register(listener);
@@ -1188,5 +1175,13 @@
}
});
}
+
+ @Override
+ public void setPipAnimationTypeToAlpha() {
+ executeRemoteCallWithTaskPermission(mController, "setPipAnimationTypeToAlpha",
+ (controller) -> {
+ controller.setPinnedStackAnimationType(ANIM_TYPE_ALPHA);
+ });
+ }
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipDoubleTapHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipDoubleTapHelper.java
index acc0caf..d7d335b 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipDoubleTapHelper.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipDoubleTapHelper.java
@@ -43,16 +43,16 @@
* <p>MAX - maximum allowed screen size</p>
*/
@IntDef(value = {
- SIZE_SPEC_CUSTOM,
SIZE_SPEC_DEFAULT,
- SIZE_SPEC_MAX
+ SIZE_SPEC_MAX,
+ SIZE_SPEC_CUSTOM
})
@Retention(RetentionPolicy.SOURCE)
@interface PipSizeSpec {}
- static final int SIZE_SPEC_CUSTOM = 2;
static final int SIZE_SPEC_DEFAULT = 0;
static final int SIZE_SPEC_MAX = 1;
+ static final int SIZE_SPEC_CUSTOM = 2;
/**
* Returns MAX or DEFAULT {@link PipSizeSpec} to toggle to/from.
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java
index 4c26224..3e8de45 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java
@@ -127,7 +127,7 @@
private int mPipForceCloseDelay;
private int mResizeAnimationDuration;
- private int mEduTextWindowExitAnimationDurationMs;
+ private int mEduTextWindowExitAnimationDuration;
public static Pip create(
Context context,
@@ -371,10 +371,10 @@
}
@Override
- public void onPipTargetBoundsChange(Rect newTargetBounds, int animationDuration) {
- mPipTaskOrganizer.scheduleAnimateResizePip(newTargetBounds,
+ public void onPipTargetBoundsChange(Rect targetBounds, int animationDuration) {
+ mPipTaskOrganizer.scheduleAnimateResizePip(targetBounds,
animationDuration, rect -> mTvPipMenuController.updateExpansionState());
- mTvPipMenuController.onPipTransitionStarted(newTargetBounds);
+ mTvPipMenuController.onPipTransitionToTargetBoundsStarted(targetBounds);
}
/**
@@ -411,7 +411,7 @@
@Override
public void closeEduText() {
- updatePinnedStackBounds(mEduTextWindowExitAnimationDurationMs, false);
+ updatePinnedStackBounds(mEduTextWindowExitAnimationDuration, false);
}
private void registerSessionListenerForCurrentUser() {
@@ -453,27 +453,30 @@
}
@Override
- public void onPipTransitionStarted(int direction, Rect pipBounds) {
+ public void onPipTransitionStarted(int direction, Rect currentPipBounds) {
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
- "%s: onPipTransition_Started(), state=%s", TAG, stateToName(mState));
- mTvPipMenuController.notifyPipAnimating(true);
+ "%s: onPipTransition_Started(), state=%s, direction=%d",
+ TAG, stateToName(mState), direction);
}
@Override
public void onPipTransitionCanceled(int direction) {
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
"%s: onPipTransition_Canceled(), state=%s", TAG, stateToName(mState));
- mTvPipMenuController.notifyPipAnimating(false);
+ mTvPipMenuController.onPipTransitionFinished(
+ PipAnimationController.isInPipDirection(direction));
}
@Override
public void onPipTransitionFinished(int direction) {
- if (PipAnimationController.isInPipDirection(direction) && mState == STATE_NO_PIP) {
+ final boolean enterPipTransition = PipAnimationController.isInPipDirection(direction);
+ if (enterPipTransition && mState == STATE_NO_PIP) {
setState(STATE_PIP);
}
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
- "%s: onPipTransition_Finished(), state=%s", TAG, stateToName(mState));
- mTvPipMenuController.notifyPipAnimating(false);
+ "%s: onPipTransition_Finished(), state=%s, direction=%d",
+ TAG, stateToName(mState), direction);
+ mTvPipMenuController.onPipTransitionFinished(enterPipTransition);
}
private void setState(@State int state) {
@@ -487,8 +490,8 @@
final Resources res = mContext.getResources();
mResizeAnimationDuration = res.getInteger(R.integer.config_pipResizeAnimationDuration);
mPipForceCloseDelay = res.getInteger(R.integer.config_pipForceCloseDelay);
- mEduTextWindowExitAnimationDurationMs =
- res.getInteger(R.integer.pip_edu_text_window_exit_animation_duration_ms);
+ mEduTextWindowExitAnimationDuration =
+ res.getInteger(R.integer.pip_edu_text_window_exit_animation_duration);
}
private void registerTaskStackListenerCallback(TaskStackListenerImpl taskStackListener) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java
index 176fdfe..ab7edbf 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java
@@ -60,9 +60,6 @@
private final SystemWindows mSystemWindows;
private final TvPipBoundsState mTvPipBoundsState;
private final Handler mMainHandler;
- private final int mPipMenuBorderWidth;
- private final int mPipEduTextShowDurationMs;
- private final int mPipEduTextHeight;
private Delegate mDelegate;
private SurfaceControl mLeash;
@@ -85,8 +82,6 @@
RectF mTmpDestinationRectF = new RectF();
Matrix mMoveTransform = new Matrix();
- private final Runnable mCloseEduTextRunnable = this::closeEduText;
-
public TvPipMenuController(Context context, TvPipBoundsState tvPipBoundsState,
SystemWindows systemWindows, PipMediaController pipMediaController,
Handler mainHandler) {
@@ -109,12 +104,6 @@
pipMediaController.addActionListener(this::onMediaActionsChanged);
- mPipEduTextShowDurationMs = context.getResources()
- .getInteger(R.integer.pip_edu_text_show_duration_ms);
- mPipEduTextHeight = context.getResources()
- .getDimensionPixelSize(R.dimen.pip_menu_edu_text_view_height);
- mPipMenuBorderWidth = context.getResources()
- .getDimensionPixelSize(R.dimen.pip_menu_border_width);
}
void setDelegate(Delegate delegate) {
@@ -152,15 +141,17 @@
attachPipBackgroundView();
attachPipMenuView();
- mTvPipBoundsState.setPipMenuPermanentDecorInsets(Insets.of(-mPipMenuBorderWidth,
- -mPipMenuBorderWidth, -mPipMenuBorderWidth, -mPipMenuBorderWidth));
- mTvPipBoundsState.setPipMenuTemporaryDecorInsets(Insets.of(0, 0, 0, -mPipEduTextHeight));
- mMainHandler.postDelayed(mCloseEduTextRunnable, mPipEduTextShowDurationMs);
+ int pipEduTextHeight = mContext.getResources()
+ .getDimensionPixelSize(R.dimen.pip_menu_edu_text_view_height);
+ int pipMenuBorderWidth = mContext.getResources()
+ .getDimensionPixelSize(R.dimen.pip_menu_border_width);
+ mTvPipBoundsState.setPipMenuPermanentDecorInsets(Insets.of(-pipMenuBorderWidth,
+ -pipMenuBorderWidth, -pipMenuBorderWidth, -pipMenuBorderWidth));
+ mTvPipBoundsState.setPipMenuTemporaryDecorInsets(Insets.of(0, 0, 0, -pipEduTextHeight));
}
private void attachPipMenuView() {
- mPipMenuView = new TvPipMenuView(mContext);
- mPipMenuView.setListener(this);
+ mPipMenuView = new TvPipMenuView(mContext, mMainHandler, this);
setUpViewSurfaceZOrder(mPipMenuView, 1);
addPipMenuViewToSystemWindows(mPipMenuView, MENU_WINDOW_TITLE);
maybeUpdateMenuViewActions();
@@ -192,11 +183,15 @@
0 /* height */), 0 /* displayId */, SHELL_ROOT_LAYER_PIP);
}
- void notifyPipAnimating(boolean animating) {
- mPipMenuView.setEduTextActive(!animating);
- if (!animating) {
- mPipMenuView.onPipTransitionFinished(mTvPipBoundsState.isTvPipExpanded());
- }
+ void onPipTransitionFinished(boolean enterTransition) {
+ // There is a race between when this is called and when the last frame of the pip transition
+ // is drawn. To ensure that view updates are applied only when the animation has fully drawn
+ // and the menu view has been fully remeasured and relaid out, we add a small delay here by
+ // posting on the handler.
+ mMainHandler.post(() -> {
+ mPipMenuView.onPipTransitionFinished(
+ enterTransition, mTvPipBoundsState.isTvPipExpanded());
+ });
}
void showMovementMenuOnly() {
@@ -219,7 +214,6 @@
if (mPipMenuView == null) {
return;
}
- maybeCloseEduText();
maybeUpdateMenuViewActions();
updateExpansionState();
@@ -232,25 +226,12 @@
mPipMenuView.updateBounds(mTvPipBoundsState.getBounds());
}
- void onPipTransitionStarted(Rect finishBounds) {
+ void onPipTransitionToTargetBoundsStarted(Rect targetBounds) {
if (mPipMenuView != null) {
- mPipMenuView.onPipTransitionStarted(finishBounds);
+ mPipMenuView.onPipTransitionToTargetBoundsStarted(targetBounds);
}
}
- private void maybeCloseEduText() {
- if (mMainHandler.hasCallbacks(mCloseEduTextRunnable)) {
- mMainHandler.removeCallbacks(mCloseEduTextRunnable);
- mCloseEduTextRunnable.run();
- }
- }
-
- private void closeEduText() {
- mTvPipBoundsState.setPipMenuTemporaryDecorInsets(Insets.NONE);
- mPipMenuView.hideEduText();
- mDelegate.closeEduText();
- }
-
void updateGravity(int gravity) {
mPipMenuView.showMovementHints(gravity);
}
@@ -332,7 +313,6 @@
@Override
public void detach() {
closeMenu();
- mMainHandler.removeCallbacks(mCloseEduTextRunnable);
detachPipMenu();
mLeash = null;
}
@@ -578,6 +558,12 @@
mDelegate.togglePipExpansion();
}
+ @Override
+ public void onCloseEduText() {
+ mTvPipBoundsState.setPipMenuTemporaryDecorInsets(Insets.NONE);
+ mDelegate.closeEduText();
+ }
+
interface Delegate {
void movePipToFullscreen();
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuEduTextDrawer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuEduTextDrawer.java
new file mode 100644
index 0000000..6eef225
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuEduTextDrawer.java
@@ -0,0 +1,280 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.pip.tv;
+
+import static android.view.Gravity.BOTTOM;
+import static android.view.Gravity.CENTER;
+import static android.view.View.GONE;
+import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
+
+import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE;
+
+import android.animation.ValueAnimator;
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.os.Handler;
+import android.text.Annotation;
+import android.text.Spannable;
+import android.text.SpannableString;
+import android.text.SpannedString;
+import android.text.TextUtils;
+import android.view.ViewGroup;
+import android.view.ViewTreeObserver;
+import android.widget.FrameLayout;
+import android.widget.FrameLayout.LayoutParams;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+
+import com.android.internal.protolog.common.ProtoLog;
+import com.android.wm.shell.R;
+
+import java.util.Arrays;
+
+/**
+ * The edu text drawer shows the user a hint for how to access the Picture-in-Picture menu.
+ * It displays a text in a drawer below the Picture-in-Picture window. The drawer has the same
+ * width as the Picture-in-Picture window. Depending on the Picture-in-Picture mode, there might
+ * not be enough space to fit the whole educational text in the available space. In such cases we
+ * apply a marquee animation to the TextView inside the drawer.
+ *
+ * The drawer is shown temporarily giving the user enough time to read it, after which it slides
+ * shut. We show the text for a duration calculated based on whether the text is marqueed or not.
+ */
+class TvPipMenuEduTextDrawer extends FrameLayout {
+ private static final String TAG = "TvPipMenuEduTextDrawer";
+
+ private static final float MARQUEE_DP_PER_SECOND = 30; // Copy of TextView.MARQUEE_DP_PER_SECOND
+ private static final int MARQUEE_RESTART_DELAY = 1200; // Copy of TextView.MARQUEE_DELAY
+ private final float mMarqueeAnimSpeed; // pixels per ms
+
+ private final Runnable mCloseDrawerRunnable = this::closeDrawer;
+ private final Runnable mStartScrollEduTextRunnable = this::startScrollEduText;
+
+ private final Handler mMainHandler;
+ private final Listener mListener;
+ private final TextView mEduTextView;
+
+ TvPipMenuEduTextDrawer(@NonNull Context context, Handler mainHandler, Listener listener) {
+ super(context, null, 0, 0);
+
+ mListener = listener;
+ mMainHandler = mainHandler;
+
+ // Taken from TextView.Marquee calculation
+ mMarqueeAnimSpeed =
+ (MARQUEE_DP_PER_SECOND * context.getResources().getDisplayMetrics().density) / 1000f;
+
+ mEduTextView = new TextView(mContext);
+ setupDrawer();
+ }
+
+ private void setupDrawer() {
+ final int eduTextHeight = mContext.getResources().getDimensionPixelSize(
+ R.dimen.pip_menu_edu_text_view_height);
+ final int marqueeRepeatLimit = mContext.getResources()
+ .getInteger(R.integer.pip_edu_text_scroll_times);
+
+ mEduTextView.setLayoutParams(
+ new LayoutParams(MATCH_PARENT, eduTextHeight, BOTTOM | CENTER));
+ mEduTextView.setGravity(CENTER);
+ mEduTextView.setClickable(false);
+ mEduTextView.setText(createEduTextString());
+ mEduTextView.setSingleLine();
+ mEduTextView.setTextAppearance(R.style.TvPipEduText);
+ mEduTextView.setEllipsize(TextUtils.TruncateAt.MARQUEE);
+ mEduTextView.setMarqueeRepeatLimit(marqueeRepeatLimit);
+ mEduTextView.setHorizontallyScrolling(true);
+ mEduTextView.setHorizontalFadingEdgeEnabled(true);
+ mEduTextView.setSelected(false);
+ addView(mEduTextView);
+
+ setLayoutParams(new LayoutParams(MATCH_PARENT, eduTextHeight, CENTER));
+ setClipChildren(true);
+ }
+
+ /**
+ * Initializes the edu text. Should only be called once when the PiP is entered
+ */
+ void init() {
+ ProtoLog.i(WM_SHELL_PICTURE_IN_PICTURE, "%s: init()", TAG);
+ scheduleLifecycleEvents();
+ }
+
+ private void scheduleLifecycleEvents() {
+ final int startScrollDelay = mContext.getResources().getInteger(
+ R.integer.pip_edu_text_start_scroll_delay);
+ if (isEduTextMarqueed()) {
+ mMainHandler.postDelayed(mStartScrollEduTextRunnable, startScrollDelay);
+ }
+ mMainHandler.postDelayed(mCloseDrawerRunnable, startScrollDelay + getEduTextShowDuration());
+ mEduTextView.getViewTreeObserver().addOnWindowAttachListener(
+ new ViewTreeObserver.OnWindowAttachListener() {
+ @Override
+ public void onWindowAttached() {
+ }
+
+ @Override
+ public void onWindowDetached() {
+ mEduTextView.getViewTreeObserver().removeOnWindowAttachListener(this);
+ mMainHandler.removeCallbacks(mStartScrollEduTextRunnable);
+ mMainHandler.removeCallbacks(mCloseDrawerRunnable);
+ }
+ });
+ }
+
+ private int getEduTextShowDuration() {
+ int eduTextShowDuration;
+ if (isEduTextMarqueed()) {
+ // Calculate the time it takes to fully scroll the text once: time = distance / speed
+ final float singleMarqueeDuration =
+ getMarqueeAnimEduTextLineWidth() / mMarqueeAnimSpeed;
+ // The TextView adds a delay between each marquee repetition. Take that into account
+ final float durationFromStartToStart = singleMarqueeDuration + MARQUEE_RESTART_DELAY;
+ // Finally, multiply by the number of times we repeat the marquee animation
+ eduTextShowDuration =
+ (int) durationFromStartToStart * mEduTextView.getMarqueeRepeatLimit();
+ } else {
+ eduTextShowDuration = mContext.getResources()
+ .getInteger(R.integer.pip_edu_text_non_scroll_show_duration);
+ }
+
+ ProtoLog.d(WM_SHELL_PICTURE_IN_PICTURE, "%s: getEduTextShowDuration(), showDuration=%d",
+ TAG, eduTextShowDuration);
+ return eduTextShowDuration;
+ }
+
+ /**
+ * Returns true if the edu text width is bigger than the width of the text view, which indicates
+ * that the edu text will be marqueed
+ */
+ private boolean isEduTextMarqueed() {
+ final int availableWidth = (int) mEduTextView.getWidth()
+ - mEduTextView.getCompoundPaddingLeft()
+ - mEduTextView.getCompoundPaddingRight();
+ return availableWidth < getEduTextWidth();
+ }
+
+ /**
+ * Returns the width of a single marquee repetition of the edu text in pixels.
+ * This is the width from the start of the edu text to the start of the next edu
+ * text when it is marqueed.
+ *
+ * This is calculated based on the TextView.Marquee#start calculations
+ */
+ private float getMarqueeAnimEduTextLineWidth() {
+ // When the TextView has a marquee animation, it puts a gap between the text end and the
+ // start of the next edu text repetition. The space is equal to a third of the TextView
+ // width
+ final float gap = mEduTextView.getWidth() / 3.0f;
+ return getEduTextWidth() + gap;
+ }
+
+ private void startScrollEduText() {
+ ProtoLog.d(WM_SHELL_PICTURE_IN_PICTURE, "%s: startScrollEduText(), repeat=%d",
+ TAG, mEduTextView.getMarqueeRepeatLimit());
+ mEduTextView.setSelected(true);
+ }
+
+ /**
+ * Returns the width of the edu text irrespective of the TextView width
+ */
+ private int getEduTextWidth() {
+ return (int) mEduTextView.getLayout().getLineWidth(0);
+ }
+
+ /**
+ * Closes the edu text drawer if it hasn't been closed yet
+ */
+ void closeIfNeeded() {
+ if (mMainHandler.hasCallbacks(mCloseDrawerRunnable)) {
+ ProtoLog.d(WM_SHELL_PICTURE_IN_PICTURE,
+ "%s: close(), closing the edu text drawer because of user action", TAG);
+ mMainHandler.removeCallbacks(mCloseDrawerRunnable);
+ mCloseDrawerRunnable.run();
+ } else {
+ // Do nothing, the drawer has already been closed
+ }
+ }
+
+ private void closeDrawer() {
+ ProtoLog.i(WM_SHELL_PICTURE_IN_PICTURE, "%s: closeDrawer()", TAG);
+ final int eduTextFadeExitAnimationDuration = mContext.getResources().getInteger(
+ R.integer.pip_edu_text_view_exit_animation_duration);
+ final int eduTextSlideExitAnimationDuration = mContext.getResources().getInteger(
+ R.integer.pip_edu_text_window_exit_animation_duration);
+
+ // Start fading out the edu text
+ mEduTextView.animate()
+ .alpha(0f)
+ .setInterpolator(TvPipInterpolators.EXIT)
+ .setDuration(eduTextFadeExitAnimationDuration)
+ .start();
+
+ // Start animation to close the drawer by animating its height to 0
+ final ValueAnimator heightAnimation = ValueAnimator.ofInt(getHeight(), 0);
+ heightAnimation.setDuration(eduTextSlideExitAnimationDuration);
+ heightAnimation.setInterpolator(TvPipInterpolators.BROWSE);
+ heightAnimation.addUpdateListener(animator -> {
+ final ViewGroup.LayoutParams params = getLayoutParams();
+ params.height = (int) animator.getAnimatedValue();
+ setLayoutParams(params);
+ if (params.height == 0) {
+ setVisibility(GONE);
+ }
+ });
+ heightAnimation.start();
+
+ mListener.onCloseEduText();
+ }
+
+ /**
+ * Creates the educational text that will be displayed to the user. Here we replace the
+ * HOME annotation in the String with an icon
+ */
+ private CharSequence createEduTextString() {
+ final SpannedString eduText = (SpannedString) getResources().getText(R.string.pip_edu_text);
+ final SpannableString spannableString = new SpannableString(eduText);
+ Arrays.stream(eduText.getSpans(0, eduText.length(), Annotation.class)).findFirst()
+ .ifPresent(annotation -> {
+ final Drawable icon =
+ getResources().getDrawable(R.drawable.home_icon, mContext.getTheme());
+ if (icon != null) {
+ icon.mutate();
+ icon.setBounds(0, 0, icon.getIntrinsicWidth(), icon.getIntrinsicHeight());
+ spannableString.setSpan(new CenteredImageSpan(icon),
+ eduText.getSpanStart(annotation),
+ eduText.getSpanEnd(annotation),
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+ });
+
+ return spannableString;
+ }
+
+ /**
+ * A listener for edu text drawer event states.
+ */
+ interface Listener {
+ /**
+ * The edu text closing impacts the size of the Picture-in-Picture window and influences
+ * how it is positioned on the screen.
+ */
+ void onCloseEduText();
+ }
+
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuView.java
index 9cd05b0..57e95c4 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuView.java
@@ -25,18 +25,11 @@
import static android.view.KeyEvent.KEYCODE_DPAD_UP;
import static android.view.KeyEvent.KEYCODE_ENTER;
-import android.animation.ValueAnimator;
import android.app.PendingIntent;
import android.app.RemoteAction;
import android.content.Context;
import android.graphics.Rect;
-import android.graphics.drawable.Drawable;
import android.os.Handler;
-import android.text.Annotation;
-import android.text.Spannable;
-import android.text.SpannableString;
-import android.text.SpannedString;
-import android.util.AttributeSet;
import android.view.Gravity;
import android.view.KeyEvent;
import android.view.SurfaceControl;
@@ -49,7 +42,6 @@
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.ScrollView;
-import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -61,7 +53,6 @@
import com.android.wm.shell.protolog.ShellProtoLogGroup;
import java.util.ArrayList;
-import java.util.Arrays;
import java.util.List;
/**
@@ -74,21 +65,16 @@
private static final int FIRST_CUSTOM_ACTION_POSITION = 3;
- @Nullable
- private Listener mListener;
+ private final Listener mListener;
private final LinearLayout mActionButtonsContainer;
private final View mMenuFrameView;
private final List<TvWindowMenuActionButton> mAdditionalButtons = new ArrayList<>();
private final View mPipFrameView;
private final View mPipView;
- private final TextView mEduTextView;
- private final View mEduTextContainerView;
+ private final TvPipMenuEduTextDrawer mEduTextDrawer;
private final int mPipMenuOuterSpace;
private final int mPipMenuBorderWidth;
- private final int mEduTextFadeExitAnimationDurationMs;
- private final int mEduTextSlideExitAnimationDurationMs;
- private int mEduTextHeight;
private final ImageView mArrowUp;
private final ImageView mArrowRight;
@@ -116,25 +102,17 @@
private final int mResizeAnimationDuration;
private final AccessibilityManager mA11yManager;
+ private final Handler mMainHandler;
- public TvPipMenuView(@NonNull Context context) {
- this(context, null);
- }
-
- public TvPipMenuView(@NonNull Context context, @Nullable AttributeSet attrs) {
- this(context, attrs, 0);
- }
-
- public TvPipMenuView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
- this(context, attrs, defStyleAttr, 0);
- }
-
- public TvPipMenuView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr,
- int defStyleRes) {
- super(context, attrs, defStyleAttr, defStyleRes);
+ public TvPipMenuView(@NonNull Context context, @NonNull Handler mainHandler,
+ @NonNull Listener listener) {
+ super(context, null, 0, 0);
inflate(context, R.layout.tv_pip_menu, this);
+ mMainHandler = mainHandler;
+ mListener = listener;
+
mA11yManager = context.getSystemService(AccessibilityManager.class);
mActionButtonsContainer = findViewById(R.id.tv_pip_menu_action_buttons);
@@ -166,9 +144,6 @@
mArrowLeft = findViewById(R.id.tv_pip_menu_arrow_left);
mA11yDoneButton = findViewById(R.id.tv_pip_menu_done_button);
- mEduTextView = findViewById(R.id.tv_pip_menu_edu_text);
- mEduTextContainerView = findViewById(R.id.tv_pip_menu_edu_text_container);
-
mResizeAnimationDuration = context.getResources().getInteger(
R.integer.config_pipResizeAnimationDuration);
mPipMenuFadeAnimationDuration = context.getResources()
@@ -178,63 +153,18 @@
.getDimensionPixelSize(R.dimen.pip_menu_outer_space);
mPipMenuBorderWidth = context.getResources()
.getDimensionPixelSize(R.dimen.pip_menu_border_width);
- mEduTextHeight = context.getResources()
- .getDimensionPixelSize(R.dimen.pip_menu_edu_text_view_height);
- mEduTextFadeExitAnimationDurationMs = context.getResources()
- .getInteger(R.integer.pip_edu_text_view_exit_animation_duration_ms);
- mEduTextSlideExitAnimationDurationMs = context.getResources()
- .getInteger(R.integer.pip_edu_text_window_exit_animation_duration_ms);
- initEduText();
+ mEduTextDrawer = new TvPipMenuEduTextDrawer(mContext, mainHandler, mListener);
+ ((FrameLayout) findViewById(R.id.tv_pip_menu_edu_text_drawer_placeholder))
+ .addView(mEduTextDrawer);
}
- void initEduText() {
- final SpannedString eduText = (SpannedString) getResources().getText(R.string.pip_edu_text);
- final SpannableString spannableString = new SpannableString(eduText);
- Arrays.stream(eduText.getSpans(0, eduText.length(), Annotation.class)).findFirst()
- .ifPresent(annotation -> {
- final Drawable icon =
- getResources().getDrawable(R.drawable.home_icon, mContext.getTheme());
- if (icon != null) {
- icon.mutate();
- icon.setBounds(0, 0, icon.getIntrinsicWidth(), icon.getIntrinsicHeight());
- spannableString.setSpan(new CenteredImageSpan(icon),
- eduText.getSpanStart(annotation),
- eduText.getSpanEnd(annotation),
- Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
- }
- });
-
- mEduTextView.setText(spannableString);
- }
-
- void setEduTextActive(boolean active) {
- mEduTextView.setSelected(active);
- }
-
- void hideEduText() {
- final ValueAnimator heightAnimation = ValueAnimator.ofInt(mEduTextHeight, 0);
- heightAnimation.setDuration(mEduTextSlideExitAnimationDurationMs);
- heightAnimation.setInterpolator(TvPipInterpolators.BROWSE);
- heightAnimation.addUpdateListener(animator -> {
- mEduTextHeight = (int) animator.getAnimatedValue();
- });
- mEduTextView.animate()
- .alpha(0f)
- .setInterpolator(TvPipInterpolators.EXIT)
- .setDuration(mEduTextFadeExitAnimationDurationMs)
- .withEndAction(() -> {
- mEduTextContainerView.setVisibility(GONE);
- }).start();
- heightAnimation.start();
- }
-
- void onPipTransitionStarted(Rect finishBounds) {
+ void onPipTransitionToTargetBoundsStarted(Rect targetBounds) {
// Fade out content by fading in view on top.
- if (mCurrentPipBounds != null && finishBounds != null) {
+ if (mCurrentPipBounds != null && targetBounds != null) {
boolean ratioChanged = PipUtils.aspectRatioChanged(
mCurrentPipBounds.width() / (float) mCurrentPipBounds.height(),
- finishBounds.width() / (float) finishBounds.height());
+ targetBounds.width() / (float) targetBounds.height());
if (ratioChanged) {
mPipBackground.animate()
.alpha(1f)
@@ -245,11 +175,12 @@
}
// Update buttons.
- final boolean vertical = finishBounds.height() > finishBounds.width();
+ final boolean vertical = targetBounds.height() > targetBounds.width();
final boolean orientationChanged =
vertical != (mActionButtonsContainer.getOrientation() == LinearLayout.VERTICAL);
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
- "%s: onPipTransitionStarted(), orientation changed %b", TAG, orientationChanged);
+ "%s: onPipTransitionToTargetBoundsStarted(), orientation changed %b",
+ TAG, orientationChanged);
if (!orientationChanged) {
return;
}
@@ -261,18 +192,18 @@
.setInterpolator(TvPipInterpolators.EXIT)
.setDuration(mResizeAnimationDuration / 2)
.withEndAction(() -> {
- changeButtonScrollOrientation(finishBounds);
- updateButtonGravity(finishBounds);
+ changeButtonScrollOrientation(targetBounds);
+ updateButtonGravity(targetBounds);
// Only make buttons visible again in onPipTransitionFinished to keep in
// sync with PiP content alpha animation.
});
} else {
- changeButtonScrollOrientation(finishBounds);
- updateButtonGravity(finishBounds);
+ changeButtonScrollOrientation(targetBounds);
+ updateButtonGravity(targetBounds);
}
}
- void onPipTransitionFinished(boolean isTvPipExpanded) {
+ void onPipTransitionFinished(boolean enterTransition, boolean isTvPipExpanded) {
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
"%s: onPipTransitionFinished()", TAG);
@@ -283,6 +214,10 @@
.setInterpolator(TvPipInterpolators.ENTER)
.start();
+ if (enterTransition) {
+ mEduTextDrawer.init();
+ }
+
setIsExpanded(isTvPipExpanded);
// Update buttons.
@@ -409,7 +344,7 @@
Rect getPipMenuContainerBounds(Rect pipBounds) {
final Rect menuUiBounds = new Rect(pipBounds);
menuUiBounds.inset(-mPipMenuOuterSpace, -mPipMenuOuterSpace);
- menuUiBounds.bottom += mEduTextHeight;
+ menuUiBounds.bottom += mEduTextDrawer.getHeight();
return menuUiBounds;
}
@@ -438,10 +373,6 @@
}
- void setListener(@Nullable Listener listener) {
- mListener = listener;
- }
-
void setExpandedModeEnabled(boolean enabled) {
mExpandButton.setVisibility(enabled ? VISIBLE : GONE);
}
@@ -460,21 +391,19 @@
*/
void showMoveMenu(int gravity) {
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: showMoveMenu()", TAG);
- mButtonMenuIsVisible = false;
- mMoveMenuIsVisible = true;
showButtonsMenu(false);
showMovementHints(gravity);
setFrameHighlighted(true);
mHorizontalScrollView.setFocusable(false);
mScrollView.setFocusable(false);
+
+ mEduTextDrawer.closeIfNeeded();
}
void showButtonsMenu() {
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
"%s: showButtonsMenu()", TAG);
- mButtonMenuIsVisible = true;
- mMoveMenuIsVisible = false;
showButtonsMenu(true);
hideMovementHints();
setFrameHighlighted(true);
@@ -501,8 +430,6 @@
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
"%s: hideAllUserControls()", TAG);
mFocusedButton = null;
- mButtonMenuIsVisible = false;
- mMoveMenuIsVisible = false;
showButtonsMenu(false);
hideMovementHints();
setFrameHighlighted(false);
@@ -632,8 +559,6 @@
@Override
public void onClick(View v) {
- if (mListener == null) return;
-
final int id = v.getId();
if (id == R.id.tv_pip_menu_fullscreen_button) {
mListener.onFullscreenButtonClick();
@@ -662,7 +587,7 @@
@Override
public boolean dispatchKeyEvent(KeyEvent event) {
- if (mListener != null && event.getAction() == ACTION_UP) {
+ if (event.getAction() == ACTION_UP) {
if (!mMoveMenuIsVisible) {
mFocusedButton = mActionButtonsContainer.getFocusedChild();
}
@@ -700,6 +625,11 @@
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
"%s: showMovementHints(), position: %s", TAG, Gravity.toString(gravity));
+ if (mMoveMenuIsVisible) {
+ return;
+ }
+ mMoveMenuIsVisible = true;
+
animateAlphaTo(checkGravity(gravity, Gravity.BOTTOM) ? 1f : 0f, mArrowUp);
animateAlphaTo(checkGravity(gravity, Gravity.TOP) ? 1f : 0f, mArrowDown);
animateAlphaTo(checkGravity(gravity, Gravity.RIGHT) ? 1f : 0f, mArrowLeft);
@@ -714,9 +644,7 @@
animateAlphaTo(a11yEnabled ? 1f : 0f, mA11yDoneButton);
if (a11yEnabled) {
mA11yDoneButton.setOnClickListener(v -> {
- if (mListener != null) {
- mListener.onExitMoveMode();
- }
+ mListener.onExitMoveMode();
});
}
}
@@ -725,9 +653,7 @@
arrowView.setClickable(enabled);
if (enabled) {
arrowView.setOnClickListener(v -> {
- if (mListener != null) {
- mListener.onPipMovement(keycode);
- }
+ mListener.onPipMovement(keycode);
});
}
}
@@ -743,6 +669,11 @@
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
"%s: hideMovementHints()", TAG);
+ if (!mMoveMenuIsVisible) {
+ return;
+ }
+ mMoveMenuIsVisible = false;
+
animateAlphaTo(0, mArrowUp);
animateAlphaTo(0, mArrowRight);
animateAlphaTo(0, mArrowDown);
@@ -756,19 +687,25 @@
public void showButtonsMenu(boolean show) {
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
"%s: showUserActions: %b", TAG, show);
+ if (mButtonMenuIsVisible == show) {
+ return;
+ }
+ mButtonMenuIsVisible = show;
+
if (show) {
mActionButtonsContainer.setVisibility(VISIBLE);
refocusPreviousButton();
}
animateAlphaTo(show ? 1 : 0, mActionButtonsContainer);
animateAlphaTo(show ? 1 : 0, mDimLayer);
+ mEduTextDrawer.closeIfNeeded();
}
private void setFrameHighlighted(boolean highlighted) {
mMenuFrameView.setActivated(highlighted);
}
- interface Listener {
+ interface Listener extends TvPipMenuEduTextDrawer.Listener {
void onBackPress();
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 d2e8624..fb5a504 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
@@ -716,8 +716,8 @@
null /* newDisplayAreaInfo */);
}
}
- active.mToken = mOrganizer.startTransition(
- request.getType(), transitionToken, wct);
+ mOrganizer.startTransition(transitionToken, wct != null && wct.isEmpty() ? null : wct);
+ active.mToken = transitionToken;
mActiveTransitions.add(active);
}
@@ -726,7 +726,7 @@
@NonNull WindowContainerTransaction wct, @Nullable TransitionHandler handler) {
final ActiveTransition active = new ActiveTransition();
active.mHandler = handler;
- active.mToken = mOrganizer.startTransition(type, null /* token */, wct);
+ active.mToken = mOrganizer.startNewTransition(type, wct);
mActiveTransitions.add(active);
return active.mToken;
}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/BaseTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/BaseTest.kt
index c9c8943..6370df4 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/BaseTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/BaseTest.kt
@@ -32,14 +32,15 @@
import com.android.server.wm.flicker.statusBarWindowIsAlwaysVisible
import com.android.server.wm.flicker.taskBarLayerIsVisibleAtStartAndEnd
import com.android.server.wm.flicker.taskBarWindowIsAlwaysVisible
+import com.android.server.wm.traces.common.ComponentNameMatcher
import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper
import org.junit.Assume
import org.junit.Test
/**
- * Base test class containing common assertions for [ComponentMatcher.NAV_BAR],
- * [ComponentMatcher.TASK_BAR], [ComponentMatcher.STATUS_BAR], and general assertions (layers
- * visible in consecutive states, entire screen covered, etc.)
+ * Base test class containing common assertions for [ComponentNameMatcher.NAV_BAR],
+ * [ComponentNameMatcher.TASK_BAR], [ComponentNameMatcher.STATUS_BAR], and general assertions
+ * (layers visible in consecutive states, entire screen covered, etc.)
*/
abstract class BaseTest
@JvmOverloads
@@ -73,9 +74,11 @@
}
/** Checks that all parts of the screen are covered during the transition */
- open fun entireScreenCovered() = testSpec.entireScreenCovered()
+ @Presubmit @Test open fun entireScreenCovered() = testSpec.entireScreenCovered()
- /** Checks that the [ComponentMatcher.NAV_BAR] layer is visible during the whole transition */
+ /**
+ * Checks that the [ComponentNameMatcher.NAV_BAR] layer is visible during the whole transition
+ */
@Presubmit
@Test
open fun navBarLayerIsVisibleAtStartAndEnd() {
@@ -84,7 +87,8 @@
}
/**
- * Checks the position of the [ComponentMatcher.NAV_BAR] at the start and end of the transition
+ * Checks the position of the [ComponentNameMatcher.NAV_BAR] at the start and end of the
+ * transition
*/
@Presubmit
@Test
@@ -94,7 +98,7 @@
}
/**
- * Checks that the [ComponentMatcher.NAV_BAR] window is visible during the whole transition
+ * Checks that the [ComponentNameMatcher.NAV_BAR] window is visible during the whole transition
*
* Note: Phones only
*/
@@ -105,7 +109,9 @@
testSpec.navBarWindowIsAlwaysVisible()
}
- /** Checks that the [ComponentMatcher.TASK_BAR] layer is visible during the whole transition */
+ /**
+ * Checks that the [ComponentNameMatcher.TASK_BAR] layer is visible during the whole transition
+ */
@Presubmit
@Test
open fun taskBarLayerIsVisibleAtStartAndEnd() {
@@ -114,7 +120,7 @@
}
/**
- * Checks that the [ComponentMatcher.TASK_BAR] window is visible during the whole transition
+ * Checks that the [ComponentNameMatcher.TASK_BAR] window is visible during the whole transition
*
* Note: Large screen only
*/
@@ -126,7 +132,8 @@
}
/**
- * Checks that the [ComponentMatcher.STATUS_BAR] layer is visible during the whole transition
+ * Checks that the [ComponentNameMatcher.STATUS_BAR] layer is visible during the whole
+ * transition
*/
@Presubmit
@Test
@@ -134,7 +141,7 @@
testSpec.statusBarLayerIsVisibleAtStartAndEnd()
/**
- * Checks the position of the [ComponentMatcher.STATUS_BAR] at the start and end of the
+ * Checks the position of the [ComponentNameMatcher.STATUS_BAR] at the start and end of the
* transition
*/
@Presubmit
@@ -142,7 +149,8 @@
open fun statusBarLayerPositionAtStartAndEnd() = testSpec.statusBarLayerPositionAtStartAndEnd()
/**
- * Checks that the [ComponentMatcher.STATUS_BAR] window is visible during the whole transition
+ * Checks that the [ComponentNameMatcher.STATUS_BAR] window is visible during the whole
+ * transition
*/
@Presubmit
@Test
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExpandPipOnPinchOpenTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExpandPipOnPinchOpenTest.kt
new file mode 100644
index 0000000..bcd01a4
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExpandPipOnPinchOpenTest.kt
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.flicker.pip
+
+import android.platform.test.annotations.Postsubmit
+import android.view.Surface
+import androidx.test.filters.RequiresDevice
+import com.android.server.wm.flicker.FlickerParametersRunnerFactory
+import com.android.server.wm.flicker.FlickerTestParameter
+import com.android.server.wm.flicker.FlickerTestParameterFactory
+import com.android.server.wm.flicker.dsl.FlickerBuilder
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.MethodSorters
+import org.junit.runners.Parameterized
+
+/**
+ * Test expanding a pip window via pinch out gesture.
+ */
+@RequiresDevice
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+class ExpandPipOnPinchOpenTest(testSpec: FlickerTestParameter) : PipTransition(testSpec) {
+ override val transition: FlickerBuilder.() -> Unit
+ get() = buildTransition {
+ transitions {
+ pipApp.pinchOpenPipWindow(wmHelper, 0.4f, 30)
+ }
+ }
+
+ /**
+ * Checks that the visible region area of [pipApp] always increases during the animation.
+ */
+ @Postsubmit
+ @Test
+ fun pipLayerAreaIncreases() {
+ testSpec.assertLayers {
+ val pipLayerList = this.layers { pipApp.layerMatchesAnyOf(it) && it.isVisible }
+ pipLayerList.zipWithNext { previous, current ->
+ previous.visibleRegion.notBiggerThan(current.visibleRegion.region)
+ }
+ }
+ }
+
+ companion object {
+ /**
+ * Creates the test configurations.
+ *
+ * See [FlickerTestParameterFactory.getConfigNonRotationTests] for configuring
+ * repetitions, screen orientation and navigation modes.
+ */
+ @Parameterized.Parameters(name = "{0}")
+ @JvmStatic
+ fun getParams(): List<FlickerTestParameter> {
+ return FlickerTestParameterFactory.getInstance()
+ .getConfigNonRotationTests(
+ supportedRotations = listOf(Surface.ROTATION_0)
+ )
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java
index 6292130..2fc0914 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java
@@ -51,6 +51,7 @@
import com.android.wm.shell.common.DisplayInsetsController;
import com.android.wm.shell.common.DisplayInsetsController.OnInsetsChangedListener;
import com.android.wm.shell.common.DisplayLayout;
+import com.android.wm.shell.common.DockStateReader;
import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.common.SyncTransactionQueue;
import com.android.wm.shell.compatui.letterboxedu.LetterboxEduWindowManager;
@@ -93,6 +94,7 @@
private @Mock Lazy<Transitions> mMockTransitionsLazy;
private @Mock CompatUIWindowManager mMockCompatLayout;
private @Mock LetterboxEduWindowManager mMockLetterboxEduLayout;
+ private @Mock DockStateReader mDockStateReader;
@Captor
ArgumentCaptor<OnInsetsChangedListener> mOnInsetsChangedListenerCaptor;
@@ -113,7 +115,7 @@
mShellInit = spy(new ShellInit(mMockExecutor));
mController = new CompatUIController(mContext, mShellInit, mMockShellController,
mMockDisplayController, mMockDisplayInsetsController, mMockImeController,
- mMockSyncQueue, mMockExecutor, mMockTransitionsLazy) {
+ mMockSyncQueue, mMockExecutor, mMockTransitionsLazy, mDockStateReader) {
@Override
CompatUIWindowManager createCompatUiWindowManager(Context context, TaskInfo taskInfo,
ShellTaskOrganizer.TaskListener taskListener) {
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduWindowManagerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduWindowManagerTest.java
index f3a8cf4..16517c0 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduWindowManagerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduWindowManagerTest.java
@@ -54,6 +54,7 @@
import com.android.wm.shell.ShellTaskOrganizer;
import com.android.wm.shell.ShellTestCase;
import com.android.wm.shell.common.DisplayLayout;
+import com.android.wm.shell.common.DockStateReader;
import com.android.wm.shell.common.SyncTransactionQueue;
import com.android.wm.shell.transition.Transitions;
@@ -103,6 +104,7 @@
@Mock private SurfaceControlViewHost mViewHost;
@Mock private Transitions mTransitions;
@Mock private Runnable mOnDismissCallback;
+ @Mock private DockStateReader mDockStateReader;
private SharedPreferences mSharedPreferences;
@Nullable
@@ -153,6 +155,16 @@
}
@Test
+ public void testCreateLayout_eligibleAndDocked_doesNotCreateLayout() {
+ LetterboxEduWindowManager windowManager = createWindowManager(/* eligible= */
+ true, /* isDocked */ true);
+
+ assertFalse(windowManager.createLayout(/* canShow= */ true));
+
+ assertNull(windowManager.mLayout);
+ }
+
+ @Test
public void testCreateLayout_taskBarEducationIsShowing_doesNotCreateLayout() {
LetterboxEduWindowManager windowManager = createWindowManager(/* eligible= */
true, USER_ID_1, /* isTaskbarEduShowing= */ true);
@@ -382,17 +394,27 @@
return createWindowManager(eligible, USER_ID_1, /* isTaskbarEduShowing= */ false);
}
+ private LetterboxEduWindowManager createWindowManager(boolean eligible, boolean isDocked) {
+ return createWindowManager(eligible, USER_ID_1, /* isTaskbarEduShowing= */
+ false, isDocked);
+ }
+
private LetterboxEduWindowManager createWindowManager(boolean eligible,
int userId, boolean isTaskbarEduShowing) {
+ return createWindowManager(eligible, userId, isTaskbarEduShowing, /* isDocked */false);
+ }
+
+ private LetterboxEduWindowManager createWindowManager(boolean eligible,
+ int userId, boolean isTaskbarEduShowing, boolean isDocked) {
+ doReturn(isDocked).when(mDockStateReader).isDocked();
LetterboxEduWindowManager windowManager = new LetterboxEduWindowManager(mContext,
createTaskInfo(eligible, userId), mSyncTransactionQueue, mTaskListener,
createDisplayLayout(), mTransitions, mOnDismissCallback,
- mAnimationController);
+ mAnimationController, mDockStateReader);
spyOn(windowManager);
doReturn(mViewHost).when(windowManager).createSurfaceViewHost();
doReturn(isTaskbarEduShowing).when(windowManager).isTaskbarEduShowing();
-
return windowManager;
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java
index c6492be..db9136d 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java
@@ -45,7 +45,6 @@
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
-import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.Mockito.clearInvocations;
@@ -67,10 +66,12 @@
import android.view.WindowManager;
import android.window.IRemoteTransition;
import android.window.IRemoteTransitionFinishedCallback;
+import android.window.IWindowContainerToken;
import android.window.RemoteTransition;
import android.window.TransitionFilter;
import android.window.TransitionInfo;
import android.window.TransitionRequestInfo;
+import android.window.WindowContainerToken;
import android.window.WindowContainerTransaction;
import android.window.WindowOrganizer;
@@ -117,7 +118,7 @@
@Before
public void setUp() {
doAnswer(invocation -> invocation.getArguments()[1])
- .when(mOrganizer).startTransition(anyInt(), any(), any());
+ .when(mOrganizer).startTransition(any(), any());
}
@Test
@@ -136,7 +137,7 @@
IBinder transitToken = new Binder();
transitions.requestStartTransition(transitToken,
new TransitionRequestInfo(TRANSIT_OPEN, null /* trigger */, null /* remote */));
- verify(mOrganizer, times(1)).startTransition(eq(TRANSIT_OPEN), eq(transitToken), any());
+ verify(mOrganizer, times(1)).startTransition(eq(transitToken), any());
TransitionInfo info = new TransitionInfoBuilder(TRANSIT_OPEN)
.addChange(TRANSIT_OPEN).addChange(TRANSIT_CLOSE).build();
transitions.onTransitionReady(transitToken, info, mock(SurfaceControl.Transaction.class),
@@ -188,7 +189,7 @@
// Make a request that will be rejected by the testhandler.
transitions.requestStartTransition(transitToken,
new TransitionRequestInfo(TRANSIT_OPEN, null /* trigger */, null /* remote */));
- verify(mOrganizer, times(1)).startTransition(eq(TRANSIT_OPEN), eq(transitToken), isNull());
+ verify(mOrganizer, times(1)).startTransition(eq(transitToken), isNull());
transitions.onTransitionReady(transitToken, open, mock(SurfaceControl.Transaction.class),
mock(SurfaceControl.Transaction.class));
assertEquals(1, mDefaultHandler.activeCount());
@@ -199,10 +200,12 @@
// Make a request that will be handled by testhandler but not animated by it.
RunningTaskInfo mwTaskInfo =
createTaskInfo(1, WINDOWING_MODE_MULTI_WINDOW, ACTIVITY_TYPE_STANDARD);
+ // Make the wct non-empty.
+ handlerWCT.setFocusable(new WindowContainerToken(mock(IWindowContainerToken.class)), true);
transitions.requestStartTransition(transitToken,
new TransitionRequestInfo(TRANSIT_OPEN, mwTaskInfo, null /* remote */));
verify(mOrganizer, times(1)).startTransition(
- eq(TRANSIT_OPEN), eq(transitToken), eq(handlerWCT));
+ eq(transitToken), eq(handlerWCT));
transitions.onTransitionReady(transitToken, open, mock(SurfaceControl.Transaction.class),
mock(SurfaceControl.Transaction.class));
assertEquals(1, mDefaultHandler.activeCount());
@@ -217,8 +220,8 @@
transitions.addHandler(topHandler);
transitions.requestStartTransition(transitToken,
new TransitionRequestInfo(TRANSIT_CHANGE, mwTaskInfo, null /* remote */));
- verify(mOrganizer, times(1)).startTransition(
- eq(TRANSIT_CHANGE), eq(transitToken), eq(handlerWCT));
+ verify(mOrganizer, times(2)).startTransition(
+ eq(transitToken), eq(handlerWCT));
TransitionInfo change = new TransitionInfoBuilder(TRANSIT_CHANGE)
.addChange(TRANSIT_CHANGE).build();
transitions.onTransitionReady(transitToken, change, mock(SurfaceControl.Transaction.class),
@@ -256,7 +259,7 @@
transitions.requestStartTransition(transitToken,
new TransitionRequestInfo(TRANSIT_OPEN, null /* trigger */,
new RemoteTransition(testRemote)));
- verify(mOrganizer, times(1)).startTransition(eq(TRANSIT_OPEN), eq(transitToken), any());
+ verify(mOrganizer, times(1)).startTransition(eq(transitToken), any());
TransitionInfo info = new TransitionInfoBuilder(TRANSIT_OPEN)
.addChange(TRANSIT_OPEN).addChange(TRANSIT_CLOSE).build();
transitions.onTransitionReady(transitToken, info, mock(SurfaceControl.Transaction.class),
@@ -406,7 +409,7 @@
IBinder transitToken = new Binder();
transitions.requestStartTransition(transitToken,
new TransitionRequestInfo(TRANSIT_OPEN, null /* trigger */, null /* remote */));
- verify(mOrganizer, times(1)).startTransition(eq(TRANSIT_OPEN), eq(transitToken), any());
+ verify(mOrganizer, times(1)).startTransition(eq(transitToken), any());
TransitionInfo info = new TransitionInfoBuilder(TRANSIT_OPEN)
.addChange(TRANSIT_OPEN).addChange(TRANSIT_CLOSE).build();
transitions.onTransitionReady(transitToken, info, mock(SurfaceControl.Transaction.class),
diff --git a/media/tests/AudioPolicyTest/Android.bp b/media/tests/AudioPolicyTest/Android.bp
index 95d1c6c..fcf91f0 100644
--- a/media/tests/AudioPolicyTest/Android.bp
+++ b/media/tests/AudioPolicyTest/Android.bp
@@ -10,15 +10,11 @@
android_test {
name: "audiopolicytest",
srcs: ["**/*.java"],
- libs: [
- "android.test.runner",
- "android.test.base",
- ],
static_libs: [
- "mockito-target-minus-junit4",
+ "androidx.test.ext.junit",
"androidx.test.rules",
- "android-ex-camera2",
- "testng",
+ "guava",
+ "platform-test-annotations",
],
platform_apis: true,
certificate: "platform",
diff --git a/media/tests/AudioPolicyTest/AndroidManifest.xml b/media/tests/AudioPolicyTest/AndroidManifest.xml
index a7ab828..f696735 100644
--- a/media/tests/AudioPolicyTest/AndroidManifest.xml
+++ b/media/tests/AudioPolicyTest/AndroidManifest.xml
@@ -24,7 +24,7 @@
<application>
<uses-library android:name="android.test.runner" />
- <activity android:label="@string/app_name" android:name="AudioPolicyTest"
+ <activity android:label="@string/app_name" android:name="AudioPolicyTestActivity"
android:screenOrientation="landscape" android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
@@ -33,11 +33,6 @@
</activity>
</application>
- <!--instrumentation android:name=".AudioPolicyTestRunner"
- android:targetPackage="com.android.audiopolicytest"
- android:label="AudioManager policy oriented integration tests InstrumentationRunner">
- </instrumentation-->
-
<instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
android:targetPackage="com.android.audiopolicytest"
android:label="AudioManager policy oriented integration tests InstrumentationRunner">
diff --git a/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioManagerTest.java b/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioManagerTest.java
index 27cf943..94df40d 100644
--- a/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioManagerTest.java
+++ b/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioManagerTest.java
@@ -16,28 +16,57 @@
package com.android.audiopolicytest;
+import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
+
+import static com.android.audiopolicytest.AudioVolumeTestUtil.DEFAULT_ATTRIBUTES;
+import static com.android.audiopolicytest.AudioVolumeTestUtil.incrementVolumeIndex;
+
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
-import static org.testng.Assert.assertThrows;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
import android.media.AudioAttributes;
import android.media.AudioManager;
import android.media.AudioSystem;
import android.media.audiopolicy.AudioProductStrategy;
import android.media.audiopolicy.AudioVolumeGroup;
+import android.platform.test.annotations.Presubmit;
import android.util.Log;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
import com.google.common.primitives.Ints;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
import java.util.List;
-public class AudioManagerTest extends AudioVolumesTestBase {
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+public class AudioManagerTest {
private static final String TAG = "AudioManagerTest";
+ private AudioManager mAudioManager;
+
+ @Rule
+ public final AudioVolumesTestRule rule = new AudioVolumesTestRule();
+
+ @Before
+ public void setUp() {
+ mAudioManager = getApplicationContext().getSystemService(AudioManager.class);
+ }
+
//-----------------------------------------------------------------
// Test getAudioProductStrategies and validate strategies
//-----------------------------------------------------------------
- public void testGetAndValidateProductStrategies() throws Exception {
+ @Test
+ public void testGetAndValidateProductStrategies() {
List<AudioProductStrategy> audioProductStrategies =
mAudioManager.getAudioProductStrategies();
assertTrue(audioProductStrategies.size() > 0);
@@ -101,8 +130,8 @@
//-----------------------------------------------------------------
// Test getAudioVolumeGroups and validate volume groups
//-----------------------------------------------------------------
-
- public void testGetAndValidateVolumeGroups() throws Exception {
+ @Test
+ public void testGetAndValidateVolumeGroups() {
List<AudioVolumeGroup> audioVolumeGroups = mAudioManager.getAudioVolumeGroups();
assertTrue(audioVolumeGroups.size() > 0);
@@ -118,7 +147,7 @@
// for each volume group attributes, find the matching product strategy and ensure
// it is linked the considered volume group
for (final AudioAttributes aa : avgAttributes) {
- if (aa.equals(sDefaultAttributes)) {
+ if (aa.equals(DEFAULT_ATTRIBUTES)) {
// Some volume groups may not have valid attributes, used for internal
// volume management like patch/rerouting
// so bailing out strategy retrieval from attributes
@@ -180,6 +209,7 @@
//-----------------------------------------------------------------
// Test Volume per Attributes setter/getters
//-----------------------------------------------------------------
+ @Test
public void testSetGetVolumePerAttributesWithInvalidAttributes() throws Exception {
AudioAttributes nullAttributes = null;
@@ -197,7 +227,8 @@
nullAttributes, 0 /*index*/, 0/*flags*/));
}
- public void testSetGetVolumePerAttributes() throws Exception {
+ @Test
+ public void testSetGetVolumePerAttributes() {
for (int usage : AudioAttributes.SDK_USAGES) {
if (usage == AudioAttributes.USAGE_UNKNOWN) {
continue;
@@ -248,12 +279,14 @@
//-----------------------------------------------------------------
// Test register/unregister VolumeGroupCallback
//-----------------------------------------------------------------
- public void testVolumeGroupCallback() throws Exception {
+ @Test
+ public void testVolumeGroupCallback() {
List<AudioVolumeGroup> audioVolumeGroups = mAudioManager.getAudioVolumeGroups();
assertTrue(audioVolumeGroups.size() > 0);
AudioVolumeGroupCallbackHelper vgCbReceiver = new AudioVolumeGroupCallbackHelper();
- mAudioManager.registerVolumeGroupCallback(mContext.getMainExecutor(), vgCbReceiver);
+ mAudioManager.registerVolumeGroupCallback(getApplicationContext().getMainExecutor(),
+ vgCbReceiver);
final List<Integer> publicStreams = Ints.asList(AudioManager.getPublicStreamTypes());
try {
@@ -273,7 +306,7 @@
// Set the volume per attributes (if valid) and wait the callback
for (final AudioAttributes aa : avgAttributes) {
- if (aa.equals(sDefaultAttributes)) {
+ if (aa.equals(DEFAULT_ATTRIBUTES)) {
// Some volume groups may not have valid attributes, used for internal
// volume management like patch/rerouting
// so bailing out strategy retrieval from attributes
diff --git a/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioPolicyTest.java b/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioPolicyTestActivity.java
similarity index 90%
rename from media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioPolicyTest.java
rename to media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioPolicyTestActivity.java
index e0c7b22..e31c01a 100644
--- a/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioPolicyTest.java
+++ b/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioPolicyTestActivity.java
@@ -19,9 +19,9 @@
import android.app.Activity;
import android.os.Bundle;
-public class AudioPolicyTest extends Activity {
+public class AudioPolicyTestActivity extends Activity {
- public AudioPolicyTest() {
+ public AudioPolicyTestActivity() {
}
/** Called when the activity is first created. */
diff --git a/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioProductStrategyTest.java b/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioProductStrategyTest.java
index 0e918d1..b66545a 100644
--- a/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioProductStrategyTest.java
+++ b/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioProductStrategyTest.java
@@ -16,25 +16,43 @@
package com.android.audiopolicytest;
+import static com.android.audiopolicytest.AudioVolumeTestUtil.INVALID_ATTRIBUTES;
+
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
import android.media.AudioAttributes;
import android.media.AudioManager;
import android.media.AudioSystem;
import android.media.audiopolicy.AudioProductStrategy;
import android.media.audiopolicy.AudioVolumeGroup;
+import android.platform.test.annotations.Presubmit;
import android.util.Log;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
import java.util.List;
-public class AudioProductStrategyTest extends AudioVolumesTestBase {
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+public class AudioProductStrategyTest {
private static final String TAG = "AudioProductStrategyTest";
+ @Rule
+ public final AudioVolumesTestRule rule = new AudioVolumesTestRule();
+
//-----------------------------------------------------------------
// Test getAudioProductStrategies and validate strategies
//-----------------------------------------------------------------
- public void testGetProductStrategies() throws Exception {
+ @Test
+ public void testGetProductStrategies() {
List<AudioProductStrategy> audioProductStrategies =
AudioProductStrategy.getAudioProductStrategies();
@@ -65,6 +83,7 @@
//-----------------------------------------------------------------
// Test stream to/from attributes conversion
//-----------------------------------------------------------------
+ @Test
public void testAudioAttributesFromStreamTypes() throws Exception {
List<AudioProductStrategy> audioProductStrategies =
AudioProductStrategy.getAudioProductStrategies();
@@ -81,7 +100,7 @@
// hosting this stream type; Bailing out the test, just ensure that any request
// for reciproque API with the unknown attributes would return default stream
// for volume control, aka STREAM_MUSIC.
- if (aaFromStreamType.equals(sInvalidAttributes)) {
+ if (aaFromStreamType.equals(INVALID_ATTRIBUTES)) {
assertEquals(AudioSystem.STREAM_MUSIC,
AudioProductStrategy.getLegacyStreamTypeForStrategyWithAudioAttributes(
aaFromStreamType));
@@ -139,7 +158,8 @@
}
}
- public void testAudioAttributesToStreamTypes() throws Exception {
+ @Test
+ public void testAudioAttributesToStreamTypes() {
List<AudioProductStrategy> audioProductStrategies =
AudioProductStrategy.getAudioProductStrategies();
diff --git a/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioVolumeGroupChangeHandlerTest.java b/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioVolumeGroupChangeHandlerTest.java
index 221f1f7..82394a2 100644
--- a/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioVolumeGroupChangeHandlerTest.java
+++ b/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioVolumeGroupChangeHandlerTest.java
@@ -16,21 +16,48 @@
package com.android.audiopolicytest;
+import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
+
+import static com.android.audiopolicytest.AudioVolumeTestUtil.DEFAULT_ATTRIBUTES;
+import static com.android.audiopolicytest.AudioVolumeTestUtil.incrementVolumeIndex;
+
import static org.junit.Assert.assertEquals;
-import static org.testng.Assert.assertThrows;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
import android.media.AudioAttributes;
import android.media.AudioManager;
import android.media.audiopolicy.AudioVolumeGroup;
import android.media.audiopolicy.AudioVolumeGroupChangeHandler;
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
import java.util.ArrayList;
import java.util.List;
-public class AudioVolumeGroupChangeHandlerTest extends AudioVolumesTestBase {
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+public class AudioVolumeGroupChangeHandlerTest {
private static final String TAG = "AudioVolumeGroupChangeHandlerTest";
- public void testRegisterInvalidCallback() throws Exception {
+ @Rule
+ public final AudioVolumesTestRule rule = new AudioVolumesTestRule();
+
+ private AudioManager mAudioManager;
+
+ @Before
+ public void setUp() {
+ mAudioManager = getApplicationContext().getSystemService(AudioManager.class);
+ }
+
+ @Test
+ public void testRegisterInvalidCallback() {
final AudioVolumeGroupChangeHandler audioAudioVolumeGroupChangedHandler =
new AudioVolumeGroupChangeHandler();
@@ -42,7 +69,8 @@
});
}
- public void testUnregisterInvalidCallback() throws Exception {
+ @Test
+ public void testUnregisterInvalidCallback() {
final AudioVolumeGroupChangeHandler audioAudioVolumeGroupChangedHandler =
new AudioVolumeGroupChangeHandler();
@@ -58,7 +86,8 @@
audioAudioVolumeGroupChangedHandler.unregisterListener(cb);
}
- public void testRegisterUnregisterCallback() throws Exception {
+ @Test
+ public void testRegisterUnregisterCallback() {
final AudioVolumeGroupChangeHandler audioAudioVolumeGroupChangedHandler =
new AudioVolumeGroupChangeHandler();
@@ -72,7 +101,8 @@
audioAudioVolumeGroupChangedHandler.unregisterListener(validCb);
}
- public void testCallbackReceived() throws Exception {
+ @Test
+ public void testCallbackReceived() {
final AudioVolumeGroupChangeHandler audioAudioVolumeGroupChangedHandler =
new AudioVolumeGroupChangeHandler();
@@ -90,7 +120,7 @@
List<AudioAttributes> avgAttributes = audioVolumeGroup.getAudioAttributes();
// Set the volume per attributes (if valid) and wait the callback
- if (avgAttributes.size() == 0 || avgAttributes.get(0).equals(sDefaultAttributes)) {
+ if (avgAttributes.size() == 0 || avgAttributes.get(0).equals(DEFAULT_ATTRIBUTES)) {
// Some volume groups may not have valid attributes, used for internal
// volume management like patch/rerouting
// so bailing out strategy retrieval from attributes
@@ -118,7 +148,8 @@
}
}
- public void testMultipleCallbackReceived() throws Exception {
+ @Test
+ public void testMultipleCallbackReceived() {
final AudioVolumeGroupChangeHandler audioAudioVolumeGroupChangedHandler =
new AudioVolumeGroupChangeHandler();
@@ -144,7 +175,7 @@
List<AudioAttributes> avgAttributes = audioVolumeGroup.getAudioAttributes();
// Set the volume per attributes (if valid) and wait the callback
- if (avgAttributes.size() == 0 || avgAttributes.get(0).equals(sDefaultAttributes)) {
+ if (avgAttributes.size() == 0 || avgAttributes.get(0).equals(DEFAULT_ATTRIBUTES)) {
// Some volume groups may not have valid attributes, used for internal
// volume management like patch/rerouting
// so bailing out strategy retrieval from attributes
diff --git a/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioVolumeGroupTest.java b/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioVolumeGroupTest.java
index 84b24b8..1880983 100644
--- a/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioVolumeGroupTest.java
+++ b/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioVolumeGroupTest.java
@@ -16,22 +16,51 @@
package com.android.audiopolicytest;
+import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
+
+import static com.android.audiopolicytest.AudioVolumeTestUtil.DEFAULT_ATTRIBUTES;
+
+import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
import android.media.AudioAttributes;
+import android.media.AudioManager;
import android.media.AudioSystem;
import android.media.audiopolicy.AudioProductStrategy;
import android.media.audiopolicy.AudioVolumeGroup;
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
import java.util.List;
-public class AudioVolumeGroupTest extends AudioVolumesTestBase {
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+public class AudioVolumeGroupTest {
private static final String TAG = "AudioVolumeGroupTest";
+ @Rule
+ public final AudioVolumesTestRule rule = new AudioVolumesTestRule();
+
+ private AudioManager mAudioManager;
+
+ @Before
+ public void setUp() {
+ mAudioManager = getApplicationContext().getSystemService(AudioManager.class);
+ }
+
//-----------------------------------------------------------------
// Test getAudioVolumeGroups and validate groud id
//-----------------------------------------------------------------
- public void testGetVolumeGroupsFromNonServiceCaller() throws Exception {
+ @Test
+ public void testGetVolumeGroupsFromNonServiceCaller() {
// The transaction behind getAudioVolumeGroups will fail. Check is done at binder level
// with policy service. Error is not reported, the list is just empty.
// Request must come from service components
@@ -44,7 +73,8 @@
//-----------------------------------------------------------------
// Test getAudioVolumeGroups and validate groud id
//-----------------------------------------------------------------
- public void testGetVolumeGroups() throws Exception {
+ @Test
+ public void testGetVolumeGroups() {
// Through AudioManager, the transaction behind getAudioVolumeGroups will succeed
final List<AudioVolumeGroup> audioVolumeGroup = mAudioManager.getAudioVolumeGroups();
assertNotNull(audioVolumeGroup);
@@ -67,7 +97,7 @@
// for each volume group attributes, find the matching product strategy and ensure
// it is linked the considered volume group
for (final AudioAttributes aa : avgAttributes) {
- if (aa.equals(sDefaultAttributes)) {
+ if (aa.equals(DEFAULT_ATTRIBUTES)) {
// Some volume groups may not have valid attributes, used for internal
// volume management like patch/rerouting
// so bailing out strategy retrieval from attributes
diff --git a/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioVolumeTestUtil.java b/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioVolumeTestUtil.java
new file mode 100644
index 0000000..42b249f
--- /dev/null
+++ b/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioVolumeTestUtil.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.audiopolicytest;
+
+import android.media.AudioAttributes;
+import android.media.audiopolicy.AudioProductStrategy;
+
+class AudioVolumeTestUtil {
+ // Default matches the invalid (empty) attributes from native.
+ // The difference is the input source default which is not aligned between native and java
+ public static final AudioAttributes DEFAULT_ATTRIBUTES =
+ AudioProductStrategy.getDefaultAttributes();
+ public static final AudioAttributes INVALID_ATTRIBUTES = new AudioAttributes.Builder().build();
+
+ public static int resetVolumeIndex(int indexMin, int indexMax) {
+ return (indexMax + indexMin) / 2;
+ }
+
+ public static int incrementVolumeIndex(int index, int indexMin, int indexMax) {
+ return (index + 1 > indexMax) ? resetVolumeIndex(indexMin, indexMax) : ++index;
+ }
+
+}
diff --git a/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioVolumesTestBase.java b/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioVolumesTestRule.java
similarity index 74%
rename from media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioVolumesTestBase.java
rename to media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioVolumesTestRule.java
index b30ef30..fc3b198 100644
--- a/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioVolumesTestBase.java
+++ b/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioVolumesTestRule.java
@@ -16,35 +16,36 @@
package com.android.audiopolicytest;
+import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
+
+import static com.android.audiopolicytest.AudioVolumeTestUtil.DEFAULT_ATTRIBUTES;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
import android.content.Context;
import android.content.pm.PackageManager;
import android.media.AudioAttributes;
import android.media.AudioManager;
import android.media.audiopolicy.AudioProductStrategy;
import android.media.audiopolicy.AudioVolumeGroup;
-import android.test.ActivityInstrumentationTestCase2;
+
+import androidx.test.core.app.ActivityScenario;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.rules.ExternalResource;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
-public class AudioVolumesTestBase extends ActivityInstrumentationTestCase2<AudioPolicyTest> {
- public AudioManager mAudioManager;
- Context mContext;
+final class AudioVolumesTestRule extends ExternalResource {
+ private AudioManager mAudioManager;
+ private Context mContext;
private Map<Integer, Integer> mOriginalStreamVolumes = new HashMap<>();
private Map<Integer, Integer> mOriginalVolumeGroupVolumes = new HashMap<>();
- // Default matches the invalid (empty) attributes from native.
- // The difference is the input source default which is not aligned between native and java
- public static final AudioAttributes sDefaultAttributes =
- AudioProductStrategy.getDefaultAttributes();
-
- public static final AudioAttributes sInvalidAttributes = new AudioAttributes.Builder().build();
-
- public AudioVolumesTestBase() {
- super("com.android.audiopolicytest", AudioPolicyTest.class);
- }
-
/**
* <p>Note: must be called with shell permission (MODIFY_AUDIO_ROUTING)
*/
@@ -56,14 +57,14 @@
// like rerouting/patch since these groups are internal to audio policy manager
continue;
}
- AudioAttributes avgAttributes = sDefaultAttributes;
+ AudioAttributes avgAttributes = DEFAULT_ATTRIBUTES;
for (final AudioAttributes aa : avg.getAudioAttributes()) {
if (!aa.equals(AudioProductStrategy.getDefaultAttributes())) {
avgAttributes = aa;
break;
}
}
- if (avgAttributes.equals(sDefaultAttributes)) {
+ if (avgAttributes.equals(DEFAULT_ATTRIBUTES)) {
// This shall not happen, however, not purpose of this base class.
// so bailing out.
continue;
@@ -82,14 +83,14 @@
for (final AudioVolumeGroup avg : audioVolumeGroups) {
if (avg.getId() == e.getKey()) {
assertTrue(!avg.getAudioAttributes().isEmpty());
- AudioAttributes avgAttributes = sDefaultAttributes;
+ AudioAttributes avgAttributes = DEFAULT_ATTRIBUTES;
for (final AudioAttributes aa : avg.getAudioAttributes()) {
if (!aa.equals(AudioProductStrategy.getDefaultAttributes())) {
avgAttributes = aa;
break;
}
}
- assertTrue(!avgAttributes.equals(sDefaultAttributes));
+ assertTrue(!avgAttributes.equals(DEFAULT_ATTRIBUTES));
mAudioManager.setVolumeIndexForAttributes(
avgAttributes, e.getValue(), AudioManager.FLAG_ALLOW_RINGER_MODES);
}
@@ -97,11 +98,11 @@
}
}
- @Override
- protected void setUp() throws Exception {
- super.setUp();
+ @Before
+ public void setUp() throws Exception {
+ ActivityScenario.launch(AudioPolicyTestActivity.class);
- mContext = getActivity();
+ mContext = getApplicationContext();
mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
assertEquals(PackageManager.PERMISSION_GRANTED,
@@ -117,10 +118,8 @@
storeAllVolumes();
}
- @Override
- protected void tearDown() throws Exception {
- super.tearDown();
-
+ @After
+ public void tearDown() throws Exception {
// Recover the volume and the ringer mode that the test may have overwritten.
for (Map.Entry<Integer, Integer> e : mOriginalStreamVolumes.entrySet()) {
mAudioManager.setStreamVolume(e.getKey(), e.getValue(),
@@ -131,11 +130,5 @@
restoreAllVolumes();
}
- public static int resetVolumeIndex(int indexMin, int indexMax) {
- return (indexMax + indexMin) / 2;
- }
- public static int incrementVolumeIndex(int index, int indexMin, int indexMax) {
- return (index + 1 > indexMax) ? resetVolumeIndex(indexMin, indexMax) : ++index;
- }
}
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreatePasskeyComponents.kt b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreatePasskeyComponents.kt
index 60a8e4b..fbec1bc 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreatePasskeyComponents.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreatePasskeyComponents.kt
@@ -79,10 +79,8 @@
sheetShape = Shapes.medium,
) {}
LaunchedEffect(state.currentValue) {
- when (state.currentValue) {
- ModalBottomSheetValue.Hidden -> {
- cancelActivity()
- }
+ if (state.currentValue == ModalBottomSheetValue.Hidden) {
+ cancelActivity()
}
}
}
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt
index 4b957e8..1ca70ed 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt
@@ -64,10 +64,8 @@
sheetShape = Shapes.medium,
) {}
LaunchedEffect(state.currentValue) {
- when (state.currentValue) {
- ModalBottomSheetValue.Hidden -> {
- cancelActivity()
- }
+ if (state.currentValue == ModalBottomSheetValue.Hidden) {
+ cancelActivity()
}
}
}
diff --git a/packages/SettingsProvider/src/android/provider/settings/backup/SystemSettings.java b/packages/SettingsProvider/src/android/provider/settings/backup/SystemSettings.java
index f501682..1a76943 100644
--- a/packages/SettingsProvider/src/android/provider/settings/backup/SystemSettings.java
+++ b/packages/SettingsProvider/src/android/provider/settings/backup/SystemSettings.java
@@ -44,8 +44,6 @@
Settings.System.DIM_SCREEN,
Settings.System.SCREEN_OFF_TIMEOUT,
Settings.System.SCREEN_BRIGHTNESS_MODE,
- Settings.System.SCREEN_AUTO_BRIGHTNESS_ADJ,
- Settings.System.SCREEN_BRIGHTNESS_FOR_VR,
Settings.System.ADAPTIVE_SLEEP, // moved to secure
Settings.System.APPLY_RAMPING_RINGER,
Settings.System.VIBRATE_INPUT_DEVICES,
diff --git a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
index b1979c9..8f6924c 100644
--- a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
+++ b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
@@ -100,7 +100,9 @@
Settings.System.MIN_REFRESH_RATE, // depends on hardware capabilities
Settings.System.PEAK_REFRESH_RATE, // depends on hardware capabilities
Settings.System.SCREEN_BRIGHTNESS_FLOAT,
+ Settings.System.SCREEN_BRIGHTNESS_FOR_VR,
Settings.System.SCREEN_BRIGHTNESS_FOR_VR_FLOAT,
+ Settings.System.SCREEN_AUTO_BRIGHTNESS_ADJ,
Settings.System.MULTI_AUDIO_FOCUS_ENABLED // form-factor/OEM specific
);
diff --git a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/NonInjectedServiceDetector.kt b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/NonInjectedServiceDetector.kt
new file mode 100644
index 0000000..4eb7c7d
--- /dev/null
+++ b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/NonInjectedServiceDetector.kt
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.systemui.lint
+
+import com.android.tools.lint.detector.api.Category
+import com.android.tools.lint.detector.api.Detector
+import com.android.tools.lint.detector.api.Implementation
+import com.android.tools.lint.detector.api.Issue
+import com.android.tools.lint.detector.api.JavaContext
+import com.android.tools.lint.detector.api.Scope
+import com.android.tools.lint.detector.api.Severity
+import com.android.tools.lint.detector.api.SourceCodeScanner
+import com.intellij.psi.PsiMethod
+import org.jetbrains.uast.UCallExpression
+
+/** Detects usage of Context.getSystemService() and suggests to use an injected instance instead. */
+@Suppress("UnstableApiUsage")
+class NonInjectedServiceDetector : Detector(), SourceCodeScanner {
+
+ override fun getApplicableMethodNames(): List<String> {
+ return listOf("getSystemService")
+ }
+
+ override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) {
+ val evaluator = context.evaluator
+ if (
+ !evaluator.isStatic(method) &&
+ method.name == "getSystemService" &&
+ method.containingClass?.qualifiedName == "android.content.Context"
+ ) {
+ context.report(
+ ISSUE,
+ method,
+ context.getNameLocation(node),
+ "Use @Inject to get the handle to a system-level services instead of using " +
+ "Context.getSystemService()"
+ )
+ }
+ }
+
+ companion object {
+ @JvmField
+ val ISSUE: Issue =
+ Issue.create(
+ id = "NonInjectedService",
+ briefDescription =
+ "System-level services should be retrieved using " +
+ "@Inject instead of Context.getSystemService().",
+ explanation =
+ "Context.getSystemService() should be avoided because it makes testing " +
+ "difficult. Instead, use an injected service. For example, " +
+ "instead of calling Context.getSystemService(UserManager.class), " +
+ "use @Inject and add UserManager to the constructor",
+ category = Category.CORRECTNESS,
+ priority = 8,
+ severity = Severity.WARNING,
+ implementation =
+ Implementation(NonInjectedServiceDetector::class.java, Scope.JAVA_FILE_SCOPE)
+ )
+ }
+}
diff --git a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/RegisterReceiverViaContextDetector.kt b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/RegisterReceiverViaContextDetector.kt
index b72d03d..eb71d32 100644
--- a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/RegisterReceiverViaContextDetector.kt
+++ b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/RegisterReceiverViaContextDetector.kt
@@ -27,6 +27,7 @@
import com.intellij.psi.PsiMethod
import org.jetbrains.uast.UCallExpression
+@Suppress("UnstableApiUsage")
class RegisterReceiverViaContextDetector : Detector(), SourceCodeScanner {
override fun getApplicableMethodNames(): List<String> {
diff --git a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SystemUIIssueRegistry.kt b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SystemUIIssueRegistry.kt
index 4879883..312810b 100644
--- a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SystemUIIssueRegistry.kt
+++ b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SystemUIIssueRegistry.kt
@@ -35,6 +35,7 @@
GetMainLooperViaContextDetector.ISSUE,
RegisterReceiverViaContextDetector.ISSUE,
SoftwareBitmapDetector.ISSUE,
+ NonInjectedServiceDetector.ISSUE,
)
override val api: Int
diff --git a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/AndroidStubs.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/AndroidStubs.kt
new file mode 100644
index 0000000..26bd8d0
--- /dev/null
+++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/AndroidStubs.kt
@@ -0,0 +1,156 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.systemui.lint
+
+import com.android.tools.lint.checks.infrastructure.LintDetectorTest.java
+
+/*
+ * This file contains stubs of framework APIs and System UI classes for testing purposes only. The
+ * stubs are not used in the lint detectors themselves.
+ */
+@Suppress("UnstableApiUsage")
+internal val androidStubs =
+ arrayOf(
+ java(
+ """
+package android.app;
+
+public class ActivityManager {
+ public static int getCurrentUser() {}
+}
+"""
+ ),
+ java(
+ """
+package android.os;
+import android.content.pm.UserInfo;
+import android.annotation.UserIdInt;
+
+public class UserManager {
+ public UserInfo getUserInfo(@UserIdInt int userId) {}
+}
+"""
+ ),
+ java("""
+package android.annotation;
+
+public @interface UserIdInt {}
+"""),
+ java("""
+package android.content.pm;
+
+public class UserInfo {}
+"""),
+ java("""
+package android.os;
+
+public class Looper {}
+"""),
+ java("""
+package android.os;
+
+public class Handler {}
+"""),
+ java("""
+package android.content;
+
+public class ServiceConnection {}
+"""),
+ java("""
+package android.os;
+
+public enum UserHandle {
+ ALL
+}
+"""),
+ java(
+ """
+package android.content;
+import android.os.UserHandle;
+import android.os.Handler;
+import android.os.Looper;
+import java.util.concurrent.Executor;
+
+public class Context {
+ public void registerReceiver(BroadcastReceiver receiver, IntentFilter filter, int flags) {}
+ public void registerReceiverAsUser(
+ BroadcastReceiver receiver, UserHandle user, IntentFilter filter,
+ String broadcastPermission, Handler scheduler) {}
+ public void registerReceiverForAllUsers(
+ BroadcastReceiver receiver, IntentFilter filter, String broadcastPermission,
+ Handler scheduler) {}
+ public void sendBroadcast(Intent intent) {}
+ public void sendBroadcast(Intent intent, String receiverPermission) {}
+ public void sendBroadcastAsUser(Intent intent, UserHandle userHandle, String permission) {}
+ public void bindService(Intent intent) {}
+ public void bindServiceAsUser(
+ Intent intent, ServiceConnection connection, int flags, UserHandle userHandle) {}
+ public void unbindService(ServiceConnection connection) {}
+ public Looper getMainLooper() { return null; }
+ public Executor getMainExecutor() { return null; }
+ public Handler getMainThreadHandler() { return null; }
+ public final @Nullable <T> T getSystemService(@NonNull Class<T> serviceClass) { return null; }
+ public abstract @Nullable Object getSystemService(@ServiceName @NonNull String name);
+}
+"""
+ ),
+ java(
+ """
+package android.app;
+import android.content.Context;
+
+public class Activity extends Context {}
+"""
+ ),
+ java(
+ """
+package android.graphics;
+
+public class Bitmap {
+ public enum Config {
+ ARGB_8888,
+ RGB_565,
+ HARDWARE
+ }
+ public static Bitmap createBitmap(int width, int height, Config config) {
+ return null;
+ }
+}
+"""
+ ),
+ java("""
+package android.content;
+
+public class BroadcastReceiver {}
+"""),
+ java("""
+package android.content;
+
+public class IntentFilter {}
+"""),
+ java(
+ """
+package com.android.systemui.settings;
+import android.content.pm.UserInfo;
+
+public interface UserTracker {
+ int getUserId();
+ UserInfo getUserInfo();
+}
+"""
+ ),
+ )
diff --git a/packages/SystemUI/checks/tests/com/android/systemui/lint/BindServiceViaContextDetectorTest.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/BindServiceViaContextDetectorTest.kt
similarity index 61%
rename from packages/SystemUI/checks/tests/com/android/systemui/lint/BindServiceViaContextDetectorTest.kt
rename to packages/SystemUI/checks/tests/com/android/internal/systemui/lint/BindServiceViaContextDetectorTest.kt
index bf685f7..564afcb 100644
--- a/packages/SystemUI/checks/tests/com/android/systemui/lint/BindServiceViaContextDetectorTest.kt
+++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/BindServiceViaContextDetectorTest.kt
@@ -17,26 +17,26 @@
package com.android.internal.systemui.lint
import com.android.tools.lint.checks.infrastructure.LintDetectorTest
-import com.android.tools.lint.checks.infrastructure.TestFile
import com.android.tools.lint.checks.infrastructure.TestFiles
import com.android.tools.lint.checks.infrastructure.TestLintTask
import com.android.tools.lint.detector.api.Detector
import com.android.tools.lint.detector.api.Issue
import org.junit.Test
+@Suppress("UnstableApiUsage")
class BindServiceViaContextDetectorTest : LintDetectorTest() {
override fun getDetector(): Detector = BindServiceViaContextDetector()
override fun lint(): TestLintTask = super.lint().allowMissingSdk(true)
- override fun getIssues(): List<Issue> = listOf(
- BindServiceViaContextDetector.ISSUE)
+ override fun getIssues(): List<Issue> = listOf(BindServiceViaContextDetector.ISSUE)
private val explanation = "Binding or unbinding services are synchronous calls"
@Test
fun testBindService() {
- lint().files(
+ lint()
+ .files(
TestFiles.java(
"""
package test.pkg;
@@ -49,17 +49,20 @@
}
}
"""
- ).indented(),
- *stubs)
- .issues(BindServiceViaContextDetector.ISSUE)
- .run()
- .expectWarningCount(1)
- .expectContains(explanation)
+ )
+ .indented(),
+ *stubs
+ )
+ .issues(BindServiceViaContextDetector.ISSUE)
+ .run()
+ .expectWarningCount(1)
+ .expectContains(explanation)
}
@Test
fun testBindServiceAsUser() {
- lint().files(
+ lint()
+ .files(
TestFiles.java(
"""
package test.pkg;
@@ -73,17 +76,20 @@
}
}
"""
- ).indented(),
- *stubs)
- .issues(BindServiceViaContextDetector.ISSUE)
- .run()
- .expectWarningCount(1)
- .expectContains(explanation)
+ )
+ .indented(),
+ *stubs
+ )
+ .issues(BindServiceViaContextDetector.ISSUE)
+ .run()
+ .expectWarningCount(1)
+ .expectContains(explanation)
}
@Test
fun testUnbindService() {
- lint().files(
+ lint()
+ .files(
TestFiles.java(
"""
package test.pkg;
@@ -96,45 +102,15 @@
}
}
"""
- ).indented(),
- *stubs)
- .issues(BindServiceViaContextDetector.ISSUE)
- .run()
- .expectWarningCount(1)
- .expectContains(explanation)
+ )
+ .indented(),
+ *stubs
+ )
+ .issues(BindServiceViaContextDetector.ISSUE)
+ .run()
+ .expectWarningCount(1)
+ .expectContains(explanation)
}
- private val contextStub: TestFile = java(
- """
- package android.content;
- import android.os.UserHandle;
-
- public class Context {
- public void bindService(Intent intent) {};
- public void bindServiceAsUser(Intent intent, ServiceConnection connection, int flags,
- UserHandle userHandle) {};
- public void unbindService(ServiceConnection connection) {};
- }
- """
- )
-
- private val serviceConnectionStub: TestFile = java(
- """
- package android.content;
-
- public class ServiceConnection {}
- """
- )
-
- private val userHandleStub: TestFile = java(
- """
- package android.os;
-
- public enum UserHandle {
- ALL
- }
- """
- )
-
- private val stubs = arrayOf(contextStub, serviceConnectionStub, userHandleStub)
+ private val stubs = androidStubs
}
diff --git a/packages/SystemUI/checks/tests/com/android/systemui/lint/BroadcastSentViaContextDetectorTest.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/BroadcastSentViaContextDetectorTest.kt
similarity index 62%
rename from packages/SystemUI/checks/tests/com/android/systemui/lint/BroadcastSentViaContextDetectorTest.kt
rename to packages/SystemUI/checks/tests/com/android/internal/systemui/lint/BroadcastSentViaContextDetectorTest.kt
index da010212f2..06aee8e 100644
--- a/packages/SystemUI/checks/tests/com/android/systemui/lint/BroadcastSentViaContextDetectorTest.kt
+++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/BroadcastSentViaContextDetectorTest.kt
@@ -1,26 +1,43 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
package com.android.internal.systemui.lint
import com.android.tools.lint.checks.infrastructure.LintDetectorTest
import com.android.tools.lint.checks.infrastructure.TestFiles
-import com.android.tools.lint.checks.infrastructure.TestFile
import com.android.tools.lint.checks.infrastructure.TestLintTask
import com.android.tools.lint.detector.api.Detector
import com.android.tools.lint.detector.api.Issue
import org.junit.Test
+@Suppress("UnstableApiUsage")
class BroadcastSentViaContextDetectorTest : LintDetectorTest() {
override fun getDetector(): Detector = BroadcastSentViaContextDetector()
override fun lint(): TestLintTask = super.lint().allowMissingSdk(true)
- override fun getIssues(): List<Issue> = listOf(
- BroadcastSentViaContextDetector.ISSUE)
+ override fun getIssues(): List<Issue> = listOf(BroadcastSentViaContextDetector.ISSUE)
@Test
fun testSendBroadcast() {
- lint().files(
- TestFiles.java(
- """
+ println(stubs.size)
+ lint()
+ .files(
+ TestFiles.java(
+ """
package test.pkg;
import android.content.Context;
@@ -31,21 +48,25 @@
}
}
"""
- ).indented(),
- *stubs)
+ )
+ .indented(),
+ *stubs
+ )
.issues(BroadcastSentViaContextDetector.ISSUE)
.run()
.expectWarningCount(1)
.expectContains(
- "Please don't call sendBroadcast/sendBroadcastAsUser directly on " +
- "Context, use com.android.systemui.broadcast.BroadcastSender instead.")
+ "Please don't call sendBroadcast/sendBroadcastAsUser directly on " +
+ "Context, use com.android.systemui.broadcast.BroadcastSender instead."
+ )
}
@Test
fun testSendBroadcastAsUser() {
- lint().files(
- TestFiles.java(
- """
+ lint()
+ .files(
+ TestFiles.java(
+ """
package test.pkg;
import android.content.Context;
import android.os.UserHandle;
@@ -56,21 +77,26 @@
context.sendBroadcastAsUser(intent, UserHandle.ALL, "permission");
}
}
- """).indented(),
- *stubs)
+ """
+ )
+ .indented(),
+ *stubs
+ )
.issues(BroadcastSentViaContextDetector.ISSUE)
.run()
.expectWarningCount(1)
.expectContains(
- "Please don't call sendBroadcast/sendBroadcastAsUser directly on " +
- "Context, use com.android.systemui.broadcast.BroadcastSender instead.")
+ "Please don't call sendBroadcast/sendBroadcastAsUser directly on " +
+ "Context, use com.android.systemui.broadcast.BroadcastSender instead."
+ )
}
@Test
fun testSendBroadcastInActivity() {
- lint().files(
- TestFiles.java(
- """
+ lint()
+ .files(
+ TestFiles.java(
+ """
package test.pkg;
import android.app.Activity;
import android.os.UserHandle;
@@ -82,21 +108,26 @@
}
}
- """).indented(),
- *stubs)
+ """
+ )
+ .indented(),
+ *stubs
+ )
.issues(BroadcastSentViaContextDetector.ISSUE)
.run()
.expectWarningCount(1)
.expectContains(
- "Please don't call sendBroadcast/sendBroadcastAsUser directly on " +
- "Context, use com.android.systemui.broadcast.BroadcastSender instead.")
+ "Please don't call sendBroadcast/sendBroadcastAsUser directly on " +
+ "Context, use com.android.systemui.broadcast.BroadcastSender instead."
+ )
}
@Test
fun testNoopIfNoCall() {
- lint().files(
- TestFiles.java(
- """
+ lint()
+ .files(
+ TestFiles.java(
+ """
package test.pkg;
import android.content.Context;
@@ -106,45 +137,15 @@
context.startActivity(intent);
}
}
- """).indented(),
- *stubs)
+ """
+ )
+ .indented(),
+ *stubs
+ )
.issues(BroadcastSentViaContextDetector.ISSUE)
.run()
.expectClean()
}
- private val contextStub: TestFile = java(
- """
- package android.content;
- import android.os.UserHandle;
-
- public class Context {
- public void sendBroadcast(Intent intent) {};
- public void sendBroadcast(Intent intent, String receiverPermission) {};
- public void sendBroadcastAsUser(Intent intent, UserHandle userHandle,
- String permission) {};
- }
- """
- )
-
- private val activityStub: TestFile = java(
- """
- package android.app;
- import android.content.Context;
-
- public class Activity extends Context {}
- """
- )
-
- private val userHandleStub: TestFile = java(
- """
- package android.os;
-
- public enum UserHandle {
- ALL
- }
- """
- )
-
- private val stubs = arrayOf(contextStub, activityStub, userHandleStub)
+ private val stubs = androidStubs
}
diff --git a/packages/SystemUI/checks/tests/com/android/systemui/lint/GetMainLooperViaContextDetectorTest.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/GetMainLooperViaContextDetectorTest.kt
similarity index 64%
rename from packages/SystemUI/checks/tests/com/android/systemui/lint/GetMainLooperViaContextDetectorTest.kt
rename to packages/SystemUI/checks/tests/com/android/internal/systemui/lint/GetMainLooperViaContextDetectorTest.kt
index ec761cd..c55f399 100644
--- a/packages/SystemUI/checks/tests/com/android/systemui/lint/GetMainLooperViaContextDetectorTest.kt
+++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/GetMainLooperViaContextDetectorTest.kt
@@ -17,13 +17,13 @@
package com.android.internal.systemui.lint
import com.android.tools.lint.checks.infrastructure.LintDetectorTest
-import com.android.tools.lint.checks.infrastructure.TestFile
import com.android.tools.lint.checks.infrastructure.TestFiles
import com.android.tools.lint.checks.infrastructure.TestLintTask
import com.android.tools.lint.detector.api.Detector
import com.android.tools.lint.detector.api.Issue
import org.junit.Test
+@Suppress("UnstableApiUsage")
class GetMainLooperViaContextDetectorTest : LintDetectorTest() {
override fun getDetector(): Detector = GetMainLooperViaContextDetector()
@@ -35,7 +35,8 @@
@Test
fun testGetMainThreadHandler() {
- lint().files(
+ lint()
+ .files(
TestFiles.java(
"""
package test.pkg;
@@ -48,17 +49,20 @@
}
}
"""
- ).indented(),
- *stubs)
- .issues(GetMainLooperViaContextDetector.ISSUE)
- .run()
- .expectWarningCount(1)
- .expectContains(explanation)
+ )
+ .indented(),
+ *stubs
+ )
+ .issues(GetMainLooperViaContextDetector.ISSUE)
+ .run()
+ .expectWarningCount(1)
+ .expectContains(explanation)
}
@Test
fun testGetMainLooper() {
- lint().files(
+ lint()
+ .files(
TestFiles.java(
"""
package test.pkg;
@@ -71,17 +75,20 @@
}
}
"""
- ).indented(),
- *stubs)
- .issues(GetMainLooperViaContextDetector.ISSUE)
- .run()
- .expectWarningCount(1)
- .expectContains(explanation)
+ )
+ .indented(),
+ *stubs
+ )
+ .issues(GetMainLooperViaContextDetector.ISSUE)
+ .run()
+ .expectWarningCount(1)
+ .expectContains(explanation)
}
@Test
fun testGetMainExecutor() {
- lint().files(
+ lint()
+ .files(
TestFiles.java(
"""
package test.pkg;
@@ -94,42 +101,15 @@
}
}
"""
- ).indented(),
- *stubs)
- .issues(GetMainLooperViaContextDetector.ISSUE)
- .run()
- .expectWarningCount(1)
- .expectContains(explanation)
+ )
+ .indented(),
+ *stubs
+ )
+ .issues(GetMainLooperViaContextDetector.ISSUE)
+ .run()
+ .expectWarningCount(1)
+ .expectContains(explanation)
}
- private val contextStub: TestFile = java(
- """
- package android.content;
- import android.os.Handler;import android.os.Looper;import java.util.concurrent.Executor;
-
- public class Context {
- public Looper getMainLooper() { return null; };
- public Executor getMainExecutor() { return null; };
- public Handler getMainThreadHandler() { return null; };
- }
- """
- )
-
- private val looperStub: TestFile = java(
- """
- package android.os;
-
- public class Looper {}
- """
- )
-
- private val handlerStub: TestFile = java(
- """
- package android.os;
-
- public class Handler {}
- """
- )
-
- private val stubs = arrayOf(contextStub, looperStub, handlerStub)
+ private val stubs = androidStubs
}
diff --git a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/NonInjectedServiceDetectorTest.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/NonInjectedServiceDetectorTest.kt
new file mode 100644
index 0000000..6b9f88f
--- /dev/null
+++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/NonInjectedServiceDetectorTest.kt
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.systemui.lint
+
+import com.android.tools.lint.checks.infrastructure.LintDetectorTest
+import com.android.tools.lint.checks.infrastructure.TestFiles
+import com.android.tools.lint.checks.infrastructure.TestLintTask
+import com.android.tools.lint.detector.api.Detector
+import com.android.tools.lint.detector.api.Issue
+import org.junit.Test
+
+@Suppress("UnstableApiUsage")
+class NonInjectedServiceDetectorTest : LintDetectorTest() {
+
+ override fun getDetector(): Detector = NonInjectedServiceDetector()
+ override fun lint(): TestLintTask = super.lint().allowMissingSdk(true)
+ override fun getIssues(): List<Issue> = listOf(NonInjectedServiceDetector.ISSUE)
+
+ @Test
+ fun testGetServiceWithString() {
+ lint()
+ .files(
+ TestFiles.java(
+ """
+ package test.pkg;
+ import android.content.Context;
+
+ public class TestClass1 {
+ public void getSystemServiceWithoutDagger(Context context) {
+ context.getSystemService("user");
+ }
+ }
+ """
+ )
+ .indented(),
+ *stubs
+ )
+ .issues(NonInjectedServiceDetector.ISSUE)
+ .run()
+ .expectWarningCount(1)
+ .expectContains("Use @Inject to get the handle")
+ }
+
+ @Test
+ fun testGetServiceWithClass() {
+ lint()
+ .files(
+ TestFiles.java(
+ """
+ package test.pkg;
+ import android.content.Context;
+ import android.os.UserManager;
+
+ public class TestClass2 {
+ public void getSystemServiceWithoutDagger(Context context) {
+ context.getSystemService(UserManager.class);
+ }
+ }
+ """
+ )
+ .indented(),
+ *stubs
+ )
+ .issues(NonInjectedServiceDetector.ISSUE)
+ .run()
+ .expectWarningCount(1)
+ .expectContains("Use @Inject to get the handle")
+ }
+
+ private val stubs = androidStubs
+}
diff --git a/packages/SystemUI/checks/tests/com/android/systemui/lint/RegisterReceiverViaContextDetectorTest.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/RegisterReceiverViaContextDetectorTest.kt
similarity index 60%
rename from packages/SystemUI/checks/tests/com/android/systemui/lint/RegisterReceiverViaContextDetectorTest.kt
rename to packages/SystemUI/checks/tests/com/android/internal/systemui/lint/RegisterReceiverViaContextDetectorTest.kt
index 76c0519..802ceba 100644
--- a/packages/SystemUI/checks/tests/com/android/systemui/lint/RegisterReceiverViaContextDetectorTest.kt
+++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/RegisterReceiverViaContextDetectorTest.kt
@@ -17,26 +17,26 @@
package com.android.internal.systemui.lint
import com.android.tools.lint.checks.infrastructure.LintDetectorTest
-import com.android.tools.lint.checks.infrastructure.TestFile
import com.android.tools.lint.checks.infrastructure.TestFiles
import com.android.tools.lint.checks.infrastructure.TestLintTask
import com.android.tools.lint.detector.api.Detector
import com.android.tools.lint.detector.api.Issue
import org.junit.Test
+@Suppress("UnstableApiUsage")
class RegisterReceiverViaContextDetectorTest : LintDetectorTest() {
override fun getDetector(): Detector = RegisterReceiverViaContextDetector()
override fun lint(): TestLintTask = super.lint().allowMissingSdk(true)
- override fun getIssues(): List<Issue> = listOf(
- RegisterReceiverViaContextDetector.ISSUE)
+ override fun getIssues(): List<Issue> = listOf(RegisterReceiverViaContextDetector.ISSUE)
private val explanation = "BroadcastReceivers should be registered via BroadcastDispatcher."
@Test
fun testRegisterReceiver() {
- lint().files(
+ lint()
+ .files(
TestFiles.java(
"""
package test.pkg;
@@ -51,17 +51,20 @@
}
}
"""
- ).indented(),
- *stubs)
- .issues(RegisterReceiverViaContextDetector.ISSUE)
- .run()
- .expectWarningCount(1)
- .expectContains(explanation)
+ )
+ .indented(),
+ *stubs
+ )
+ .issues(RegisterReceiverViaContextDetector.ISSUE)
+ .run()
+ .expectWarningCount(1)
+ .expectContains(explanation)
}
@Test
fun testRegisterReceiverAsUser() {
- lint().files(
+ lint()
+ .files(
TestFiles.java(
"""
package test.pkg;
@@ -79,17 +82,20 @@
}
}
"""
- ).indented(),
- *stubs)
- .issues(RegisterReceiverViaContextDetector.ISSUE)
- .run()
- .expectWarningCount(1)
- .expectContains(explanation)
+ )
+ .indented(),
+ *stubs
+ )
+ .issues(RegisterReceiverViaContextDetector.ISSUE)
+ .run()
+ .expectWarningCount(1)
+ .expectContains(explanation)
}
@Test
fun testRegisterReceiverForAllUsers() {
- lint().files(
+ lint()
+ .files(
TestFiles.java(
"""
package test.pkg;
@@ -107,65 +113,15 @@
}
}
"""
- ).indented(),
- *stubs)
- .issues(RegisterReceiverViaContextDetector.ISSUE)
- .run()
- .expectWarningCount(1)
- .expectContains(explanation)
+ )
+ .indented(),
+ *stubs
+ )
+ .issues(RegisterReceiverViaContextDetector.ISSUE)
+ .run()
+ .expectWarningCount(1)
+ .expectContains(explanation)
}
- private val contextStub: TestFile = java(
- """
- package android.content;
- import android.os.Handler;
- import android.os.UserHandle;
-
- public class Context {
- public void registerReceiver(BroadcastReceiver receiver, IntentFilter filter,
- int flags) {};
- public void registerReceiverAsUser(BroadcastReceiver receiver, UserHandle user,
- IntentFilter filter, String broadcastPermission, Handler scheduler) {};
- public void registerReceiverForAllUsers(BroadcastReceiver receiver, IntentFilter filter,
- String broadcastPermission, Handler scheduler) {};
- }
- """
- )
-
- private val broadcastReceiverStub: TestFile = java(
- """
- package android.content;
-
- public class BroadcastReceiver {}
- """
- )
-
- private val intentFilterStub: TestFile = java(
- """
- package android.content;
-
- public class IntentFilter {}
- """
- )
-
- private val handlerStub: TestFile = java(
- """
- package android.os;
-
- public class Handler {}
- """
- )
-
- private val userHandleStub: TestFile = java(
- """
- package android.os;
-
- public enum UserHandle {
- ALL
- }
- """
- )
-
- private val stubs = arrayOf(contextStub, broadcastReceiverStub, intentFilterStub, handlerStub,
- userHandleStub)
+ private val stubs = androidStubs
}
diff --git a/packages/SystemUI/checks/tests/com/android/systemui/lint/SlowUserQueryDetectorTest.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/SlowUserQueryDetectorTest.kt
similarity index 74%
rename from packages/SystemUI/checks/tests/com/android/systemui/lint/SlowUserQueryDetectorTest.kt
rename to packages/SystemUI/checks/tests/com/android/internal/systemui/lint/SlowUserQueryDetectorTest.kt
index 2738f04..e265837 100644
--- a/packages/SystemUI/checks/tests/com/android/systemui/lint/SlowUserQueryDetectorTest.kt
+++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/SlowUserQueryDetectorTest.kt
@@ -1,13 +1,29 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
package com.android.internal.systemui.lint
import com.android.tools.lint.checks.infrastructure.LintDetectorTest
-import com.android.tools.lint.checks.infrastructure.TestFile
import com.android.tools.lint.checks.infrastructure.TestFiles
import com.android.tools.lint.checks.infrastructure.TestLintTask
import com.android.tools.lint.detector.api.Detector
import com.android.tools.lint.detector.api.Issue
import org.junit.Test
+@Suppress("UnstableApiUsage")
class SlowUserQueryDetectorTest : LintDetectorTest() {
override fun getDetector(): Detector = SlowUserQueryDetector()
@@ -134,61 +150,5 @@
.expectClean()
}
- private val activityManagerStub: TestFile =
- java(
- """
- package android.app;
-
- public class ActivityManager {
- public static int getCurrentUser() {};
- }
- """
- )
-
- private val userManagerStub: TestFile =
- java(
- """
- package android.os;
- import android.content.pm.UserInfo;
- import android.annotation.UserIdInt;
-
- public class UserManager {
- public UserInfo getUserInfo(@UserIdInt int userId) {};
- }
- """
- )
-
- private val userIdIntStub: TestFile =
- java(
- """
- package android.annotation;
-
- public @interface UserIdInt {}
- """
- )
-
- private val userInfoStub: TestFile =
- java(
- """
- package android.content.pm;
-
- public class UserInfo {}
- """
- )
-
- private val userTrackerStub: TestFile =
- java(
- """
- package com.android.systemui.settings;
- import android.content.pm.UserInfo;
-
- public interface UserTracker {
- public int getUserId();
- public UserInfo getUserInfo();
- }
- """
- )
-
- private val stubs =
- arrayOf(activityManagerStub, userManagerStub, userIdIntStub, userInfoStub, userTrackerStub)
+ private val stubs = androidStubs
}
diff --git a/packages/SystemUI/checks/tests/com/android/systemui/lint/SoftwareBitmapDetectorTest.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/SoftwareBitmapDetectorTest.kt
similarity index 70%
rename from packages/SystemUI/checks/tests/com/android/systemui/lint/SoftwareBitmapDetectorTest.kt
rename to packages/SystemUI/checks/tests/com/android/internal/systemui/lint/SoftwareBitmapDetectorTest.kt
index 890f2b8..fd6ab09 100644
--- a/packages/SystemUI/checks/tests/com/android/systemui/lint/SoftwareBitmapDetectorTest.kt
+++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/SoftwareBitmapDetectorTest.kt
@@ -17,7 +17,6 @@
package com.android.internal.systemui.lint
import com.android.tools.lint.checks.infrastructure.LintDetectorTest
-import com.android.tools.lint.checks.infrastructure.TestFile
import com.android.tools.lint.checks.infrastructure.TestFiles
import com.android.tools.lint.checks.infrastructure.TestLintTask
import com.android.tools.lint.detector.api.Detector
@@ -36,7 +35,8 @@
@Test
fun testSoftwareBitmap() {
- lint().files(
+ lint()
+ .files(
TestFiles.java(
"""
import android.graphics.Bitmap;
@@ -48,17 +48,20 @@
}
}
"""
- ).indented(),
- *stubs)
- .issues(SoftwareBitmapDetector.ISSUE)
- .run()
- .expectWarningCount(2)
- .expectContains(explanation)
+ )
+ .indented(),
+ *stubs
+ )
+ .issues(SoftwareBitmapDetector.ISSUE)
+ .run()
+ .expectWarningCount(2)
+ .expectContains(explanation)
}
@Test
fun testHardwareBitmap() {
- lint().files(
+ lint()
+ .files(
TestFiles.java(
"""
import android.graphics.Bitmap;
@@ -69,29 +72,14 @@
}
}
"""
- ).indented(),
- *stubs)
- .issues(SoftwareBitmapDetector.ISSUE)
- .run()
- .expectWarningCount(0)
+ )
+ .indented(),
+ *stubs
+ )
+ .issues(SoftwareBitmapDetector.ISSUE)
+ .run()
+ .expectWarningCount(0)
}
- private val bitmapStub: TestFile = java(
- """
- package android.graphics;
-
- public class Bitmap {
- public enum Config {
- ARGB_8888,
- RGB_565,
- HARDWARE
- }
- public static Bitmap createBitmap(int width, int height, Config config) {
- return null;
- }
- }
- """
- )
-
- private val stubs = arrayOf(bitmapStub)
+ private val stubs = androidStubs
}
diff --git a/packages/SystemUI/res-keyguard/layout/footer_actions.xml b/packages/SystemUI/res-keyguard/layout/footer_actions.xml
index 1ce106e..716c4fe 100644
--- a/packages/SystemUI/res-keyguard/layout/footer_actions.xml
+++ b/packages/SystemUI/res-keyguard/layout/footer_actions.xml
@@ -61,7 +61,7 @@
</com.android.systemui.statusbar.phone.MultiUserSwitch>
<com.android.systemui.statusbar.AlphaOptimizedFrameLayout
- android:id="@+id/settings_button_container"
+ android:id="@id/settings_button_container"
android:layout_width="@dimen/qs_footer_action_button_size"
android:layout_height="@dimen/qs_footer_action_button_size"
android:background="@drawable/qs_footer_action_circle"
@@ -85,7 +85,7 @@
</com.android.systemui.statusbar.AlphaOptimizedFrameLayout>
<com.android.systemui.statusbar.AlphaOptimizedImageView
- android:id="@+id/pm_lite"
+ android:id="@id/pm_lite"
android:layout_width="@dimen/qs_footer_action_button_size"
android:layout_height="@dimen/qs_footer_action_button_size"
android:background="@drawable/qs_footer_action_circle_color"
diff --git a/packages/SystemUI/res-keyguard/values/ids.xml b/packages/SystemUI/res-keyguard/values/ids.xml
new file mode 100644
index 0000000..0dff4ff
--- /dev/null
+++ b/packages/SystemUI/res-keyguard/values/ids.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ ~ Copyright (C) 2022 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ ~
+ -->
+
+<resources>
+ <item type="id" name="header_footer_views_added_tag_key" />
+</resources>
diff --git a/packages/SystemUI/res/layout/media_ttt_chip.xml b/packages/SystemUI/res/layout/media_ttt_chip.xml
index d886806..ae8e38e 100644
--- a/packages/SystemUI/res/layout/media_ttt_chip.xml
+++ b/packages/SystemUI/res/layout/media_ttt_chip.xml
@@ -16,7 +16,7 @@
<!-- Wrap in a frame layout so that we can update the margins on the inner layout. (Since this view
is the root view of a window, we cannot change the root view's margins.) -->
<!-- Alphas start as 0 because the view will be animated in. -->
-<FrameLayout
+<com.android.systemui.media.taptotransfer.sender.MediaTttChipRootView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
android:id="@+id/media_ttt_sender_chip"
@@ -97,4 +97,4 @@
/>
</LinearLayout>
-</FrameLayout>
+</com.android.systemui.media.taptotransfer.sender.MediaTttChipRootView>
diff --git a/packages/SystemUI/res/values/ids.xml b/packages/SystemUI/res/values/ids.xml
index f22e797..ba5f67f 100644
--- a/packages/SystemUI/res/values/ids.xml
+++ b/packages/SystemUI/res/values/ids.xml
@@ -188,5 +188,10 @@
<item type="id" name="face_scanning_anim"/>
<item type="id" name="qqs_tile_layout"/>
+
+ <!-- The buttons in the Quick Settings footer actions.-->
+ <item type="id" name="settings_button_container"/>
+ <item type="id" name="pm_lite"/>
+
</resources>
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/ISystemUiProxy.aidl b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/ISystemUiProxy.aidl
index e77c650..2b2b05ce 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/ISystemUiProxy.aidl
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/ISystemUiProxy.aidl
@@ -81,11 +81,6 @@
*/
void stopScreenPinning() = 17;
- /*
- * Notifies that the swipe-to-home (recents animation) is finished.
- */
- void notifySwipeToHomeFinished() = 23;
-
/**
* Notifies that quickstep will switch to a new task
* @param rotation indicates which Surface.Rotation the gesture was started in
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/utilities/PreviewPositionHelper.java b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/utilities/PreviewPositionHelper.java
new file mode 100644
index 0000000..72f8b7b
--- /dev/null
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/utilities/PreviewPositionHelper.java
@@ -0,0 +1,209 @@
+package com.android.systemui.shared.recents.utilities;
+
+import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+
+import android.graphics.Matrix;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.view.Surface;
+
+import com.android.systemui.shared.recents.model.ThumbnailData;
+
+/**
+ * Utility class to position the thumbnail in the TaskView
+ */
+public class PreviewPositionHelper {
+
+ public static final float MAX_PCT_BEFORE_ASPECT_RATIOS_CONSIDERED_DIFFERENT = 0.1f;
+
+ // Contains the portion of the thumbnail that is unclipped when fullscreen progress = 1.
+ private final RectF mClippedInsets = new RectF();
+ private final Matrix mMatrix = new Matrix();
+ private boolean mIsOrientationChanged;
+
+ public Matrix getMatrix() {
+ return mMatrix;
+ }
+
+ public void setOrientationChanged(boolean orientationChanged) {
+ mIsOrientationChanged = orientationChanged;
+ }
+
+ public boolean isOrientationChanged() {
+ return mIsOrientationChanged;
+ }
+
+ /**
+ * Updates the matrix based on the provided parameters
+ */
+ public void updateThumbnailMatrix(Rect thumbnailBounds, ThumbnailData thumbnailData,
+ int canvasWidth, int canvasHeight, int screenWidthPx, int taskbarSize, boolean isTablet,
+ int currentRotation, boolean isRtl) {
+ boolean isRotated = false;
+ boolean isOrientationDifferent;
+
+ int thumbnailRotation = thumbnailData.rotation;
+ int deltaRotate = getRotationDelta(currentRotation, thumbnailRotation);
+ RectF thumbnailClipHint = new RectF();
+ float canvasScreenRatio = canvasWidth / (float) screenWidthPx;
+ float scaledTaskbarSize = taskbarSize * canvasScreenRatio;
+ thumbnailClipHint.bottom = isTablet ? scaledTaskbarSize : 0;
+
+ float scale = thumbnailData.scale;
+ final float thumbnailScale;
+
+ // Landscape vs portrait change.
+ // Note: Disable rotation in grid layout.
+ boolean windowingModeSupportsRotation =
+ thumbnailData.windowingMode == WINDOWING_MODE_FULLSCREEN && !isTablet;
+ isOrientationDifferent = isOrientationChange(deltaRotate)
+ && windowingModeSupportsRotation;
+ if (canvasWidth == 0 || canvasHeight == 0 || scale == 0) {
+ // If we haven't measured , skip the thumbnail drawing and only draw the background
+ // color
+ thumbnailScale = 0f;
+ } else {
+ // Rotate the screenshot if not in multi-window mode
+ isRotated = deltaRotate > 0 && windowingModeSupportsRotation;
+
+ float surfaceWidth = thumbnailBounds.width() / scale;
+ float surfaceHeight = thumbnailBounds.height() / scale;
+ float availableWidth = surfaceWidth
+ - (thumbnailClipHint.left + thumbnailClipHint.right);
+ float availableHeight = surfaceHeight
+ - (thumbnailClipHint.top + thumbnailClipHint.bottom);
+
+ float canvasAspect = canvasWidth / (float) canvasHeight;
+ float availableAspect = isRotated
+ ? availableHeight / availableWidth
+ : availableWidth / availableHeight;
+ boolean isAspectLargelyDifferent =
+ Utilities.isRelativePercentDifferenceGreaterThan(canvasAspect,
+ availableAspect, MAX_PCT_BEFORE_ASPECT_RATIOS_CONSIDERED_DIFFERENT);
+ if (isRotated && isAspectLargelyDifferent) {
+ // Do not rotate thumbnail if it would not improve fit
+ isRotated = false;
+ isOrientationDifferent = false;
+ }
+
+ if (isAspectLargelyDifferent) {
+ // Crop letterbox insets if insets isn't already clipped
+ thumbnailClipHint.left = thumbnailData.letterboxInsets.left;
+ thumbnailClipHint.right = thumbnailData.letterboxInsets.right;
+ thumbnailClipHint.top = thumbnailData.letterboxInsets.top;
+ thumbnailClipHint.bottom = thumbnailData.letterboxInsets.bottom;
+ availableWidth = surfaceWidth
+ - (thumbnailClipHint.left + thumbnailClipHint.right);
+ availableHeight = surfaceHeight
+ - (thumbnailClipHint.top + thumbnailClipHint.bottom);
+ }
+
+ final float targetW, targetH;
+ if (isOrientationDifferent) {
+ targetW = canvasHeight;
+ targetH = canvasWidth;
+ } else {
+ targetW = canvasWidth;
+ targetH = canvasHeight;
+ }
+ float targetAspect = targetW / targetH;
+
+ // Update the clipHint such that
+ // > the final clipped position has same aspect ratio as requested by canvas
+ // > first fit the width and crop the extra height
+ // > if that will leave empty space, fit the height and crop the width instead
+ float croppedWidth = availableWidth;
+ float croppedHeight = croppedWidth / targetAspect;
+ if (croppedHeight > availableHeight) {
+ croppedHeight = availableHeight;
+ if (croppedHeight < targetH) {
+ croppedHeight = Math.min(targetH, surfaceHeight);
+ }
+ croppedWidth = croppedHeight * targetAspect;
+
+ // One last check in case the task aspect radio messed up something
+ if (croppedWidth > surfaceWidth) {
+ croppedWidth = surfaceWidth;
+ croppedHeight = croppedWidth / targetAspect;
+ }
+ }
+
+ // Update the clip hints. Align to 0,0, crop the remaining.
+ if (isRtl) {
+ thumbnailClipHint.left += availableWidth - croppedWidth;
+ if (thumbnailClipHint.right < 0) {
+ thumbnailClipHint.left += thumbnailClipHint.right;
+ thumbnailClipHint.right = 0;
+ }
+ } else {
+ thumbnailClipHint.right += availableWidth - croppedWidth;
+ if (thumbnailClipHint.left < 0) {
+ thumbnailClipHint.right += thumbnailClipHint.left;
+ thumbnailClipHint.left = 0;
+ }
+ }
+ thumbnailClipHint.bottom += availableHeight - croppedHeight;
+ if (thumbnailClipHint.top < 0) {
+ thumbnailClipHint.bottom += thumbnailClipHint.top;
+ thumbnailClipHint.top = 0;
+ } else if (thumbnailClipHint.bottom < 0) {
+ thumbnailClipHint.top += thumbnailClipHint.bottom;
+ thumbnailClipHint.bottom = 0;
+ }
+
+ thumbnailScale = targetW / (croppedWidth * scale);
+ }
+
+ if (!isRotated) {
+ mMatrix.setTranslate(
+ -thumbnailClipHint.left * scale,
+ -thumbnailClipHint.top * scale);
+ } else {
+ setThumbnailRotation(deltaRotate, thumbnailBounds);
+ }
+
+ mClippedInsets.set(0, 0, 0, scaledTaskbarSize);
+
+ mMatrix.postScale(thumbnailScale, thumbnailScale);
+ mIsOrientationChanged = isOrientationDifferent;
+ }
+
+ private int getRotationDelta(int oldRotation, int newRotation) {
+ int delta = newRotation - oldRotation;
+ if (delta < 0) delta += 4;
+ return delta;
+ }
+
+ /**
+ * @param deltaRotation the number of 90 degree turns from the current orientation
+ * @return {@code true} if the change in rotation results in a shift from landscape to
+ * portrait or vice versa, {@code false} otherwise
+ */
+ private boolean isOrientationChange(int deltaRotation) {
+ return deltaRotation == Surface.ROTATION_90 || deltaRotation == Surface.ROTATION_270;
+ }
+
+ private void setThumbnailRotation(int deltaRotate, Rect thumbnailPosition) {
+ float translateX = 0;
+ float translateY = 0;
+
+ mMatrix.setRotate(90 * deltaRotate);
+ switch (deltaRotate) { /* Counter-clockwise */
+ case Surface.ROTATION_90:
+ translateX = thumbnailPosition.height();
+ break;
+ case Surface.ROTATION_270:
+ translateY = thumbnailPosition.width();
+ break;
+ case Surface.ROTATION_180:
+ translateX = thumbnailPosition.width();
+ translateY = thumbnailPosition.height();
+ break;
+ }
+ mMatrix.postTranslate(translateX, translateY);
+ }
+
+ public RectF getClippedInsets() {
+ return mClippedInsets;
+ }
+}
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/utilities/Utilities.java b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/utilities/Utilities.java
index 56326e3..77a13bd 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/utilities/Utilities.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/utilities/Utilities.java
@@ -62,6 +62,16 @@
return false; // Default
}
+ /**
+ * Compares the ratio of two quantities and returns whether that ratio is greater than the
+ * provided bound. Order of quantities does not matter. Bound should be a decimal representation
+ * of a percentage.
+ */
+ public static boolean isRelativePercentDifferenceGreaterThan(float first, float second,
+ float bound) {
+ return (Math.abs(first - second) / Math.abs((first + second) / 2.0f)) > bound;
+ }
+
/** Calculates the constrast between two colors, using the algorithm provided by the WCAG v2. */
public static float computeContrastBetweenColors(int bg, int fg) {
float bgR = Color.red(bg) / 255f;
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
index ca19b78..744e7da 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
@@ -69,8 +69,7 @@
import android.annotation.AnyThread;
import android.annotation.MainThread;
-import android.annotation.NonNull;
-import android.annotation.Nullable;
+import android.annotation.SuppressLint;
import android.app.ActivityManager;
import android.app.ActivityTaskManager;
import android.app.ActivityTaskManager.RootTaskInfo;
@@ -113,7 +112,6 @@
import android.os.UserHandle;
import android.os.UserManager;
import android.provider.Settings;
-import android.service.dreams.DreamService;
import android.service.dreams.IDreamManager;
import android.telephony.CarrierConfigManager;
import android.telephony.ServiceState;
@@ -126,6 +124,9 @@
import android.util.SparseArray;
import android.util.SparseBooleanArray;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.jank.InteractionJankMonitor;
import com.android.internal.logging.InstanceId;
@@ -266,9 +267,9 @@
private final KeyguardUpdateMonitorLogger mLogger;
private final boolean mIsPrimaryUser;
private final AuthController mAuthController;
- private final StatusBarStateController mStatusBarStateController;
private final UiEventLogger mUiEventLogger;
private final Set<Integer> mFaceAcquiredInfoIgnoreList;
+ private final PackageManager mPackageManager;
private int mStatusBarState;
private final StatusBarStateController.StateListener mStatusBarStateControllerListener =
new StatusBarStateController.StateListener() {
@@ -289,7 +290,7 @@
};
HashMap<Integer, SimData> mSimDatas = new HashMap<>();
- HashMap<Integer, ServiceState> mServiceStates = new HashMap<Integer, ServiceState>();
+ HashMap<Integer, ServiceState> mServiceStates = new HashMap<>();
private int mPhoneState;
private boolean mKeyguardIsVisible;
@@ -321,34 +322,41 @@
private final ArrayList<WeakReference<KeyguardUpdateMonitorCallback>>
mCallbacks = Lists.newArrayList();
private ContentObserver mDeviceProvisionedObserver;
- private ContentObserver mTimeFormatChangeObserver;
+ private final ContentObserver mTimeFormatChangeObserver;
private boolean mSwitchingUser;
private boolean mDeviceInteractive;
- private SubscriptionManager mSubscriptionManager;
+ private final SubscriptionManager mSubscriptionManager;
private final TelephonyListenerManager mTelephonyListenerManager;
- private List<SubscriptionInfo> mSubscriptionInfo;
- private TrustManager mTrustManager;
- private UserManager mUserManager;
- private KeyguardBypassController mKeyguardBypassController;
- private int mFingerprintRunningState = BIOMETRIC_STATE_STOPPED;
- private int mFaceRunningState = BIOMETRIC_STATE_STOPPED;
- private LockPatternUtils mLockPatternUtils;
- private final IDreamManager mDreamManager;
- private boolean mIsDreaming;
+ private final TrustManager mTrustManager;
+ private final UserManager mUserManager;
private final DevicePolicyManager mDevicePolicyManager;
private final BroadcastDispatcher mBroadcastDispatcher;
private final InteractionJankMonitor mInteractionJankMonitor;
private final LatencyTracker mLatencyTracker;
+ private final StatusBarStateController mStatusBarStateController;
+ private final Executor mBackgroundExecutor;
+ private final SensorPrivacyManager mSensorPrivacyManager;
+ private final ActiveUnlockConfig mActiveUnlockConfig;
+ private final PowerManager mPowerManager;
+ private final IDreamManager mDreamManager;
+ private final TelephonyManager mTelephonyManager;
+ @Nullable
+ private final FingerprintManager mFpm;
+ @Nullable
+ private final FaceManager mFaceManager;
+ private final LockPatternUtils mLockPatternUtils;
+ private final boolean mWakeOnFingerprintAcquiredStart;
+
+ private KeyguardBypassController mKeyguardBypassController;
+ private List<SubscriptionInfo> mSubscriptionInfo;
+ private int mFingerprintRunningState = BIOMETRIC_STATE_STOPPED;
+ private int mFaceRunningState = BIOMETRIC_STATE_STOPPED;
+ private boolean mIsDreaming;
private boolean mLogoutEnabled;
private boolean mIsFaceEnrolled;
private int mActiveMobileDataSubscription = SubscriptionManager.INVALID_SUBSCRIPTION_ID;
- private final Executor mBackgroundExecutor;
- private SensorPrivacyManager mSensorPrivacyManager;
- private final ActiveUnlockConfig mActiveUnlockConfig;
- private final PowerManager mPowerManager;
- private final boolean mWakeOnFingerprintAcquiredStart;
/**
* Short delay before restarting fingerprint authentication after a successful try. This should
@@ -375,12 +383,10 @@
}
private final Handler mHandler;
- private SparseBooleanArray mBiometricEnabledForUser = new SparseBooleanArray();
- private BiometricManager mBiometricManager;
- private IBiometricEnabledOnKeyguardCallback mBiometricEnabledCallback =
+ private final IBiometricEnabledOnKeyguardCallback mBiometricEnabledCallback =
new IBiometricEnabledOnKeyguardCallback.Stub() {
@Override
- public void onChanged(boolean enabled, int userId) throws RemoteException {
+ public void onChanged(boolean enabled, int userId) {
mHandler.post(() -> {
mBiometricEnabledForUser.put(userId, enabled);
updateBiometricListeningState(BIOMETRIC_ACTION_UPDATE,
@@ -399,7 +405,7 @@
}
};
- private OnSubscriptionsChangedListener mSubscriptionListener =
+ private final OnSubscriptionsChangedListener mSubscriptionListener =
new OnSubscriptionsChangedListener() {
@Override
public void onSubscriptionsChanged() {
@@ -418,11 +424,12 @@
}
}
- private SparseBooleanArray mUserIsUnlocked = new SparseBooleanArray();
- private SparseBooleanArray mUserHasTrust = new SparseBooleanArray();
- private SparseBooleanArray mUserTrustIsManaged = new SparseBooleanArray();
- private SparseBooleanArray mUserTrustIsUsuallyManaged = new SparseBooleanArray();
- private Map<Integer, Intent> mSecondaryLockscreenRequirement = new HashMap<Integer, Intent>();
+ private final SparseBooleanArray mUserIsUnlocked = new SparseBooleanArray();
+ private final SparseBooleanArray mUserHasTrust = new SparseBooleanArray();
+ private final SparseBooleanArray mUserTrustIsManaged = new SparseBooleanArray();
+ private final SparseBooleanArray mUserTrustIsUsuallyManaged = new SparseBooleanArray();
+ private final SparseBooleanArray mBiometricEnabledForUser = new SparseBooleanArray();
+ private final Map<Integer, Intent> mSecondaryLockscreenRequirement = new HashMap<>();
@VisibleForTesting
SparseArray<BiometricAuthenticated> mUserFingerprintAuthenticated = new SparseArray<>();
@@ -583,7 +590,7 @@
}
if (sil == null) {
// getCompleteActiveSubscriptionInfoList was null callers expect an empty list.
- mSubscriptionInfo = new ArrayList<SubscriptionInfo>();
+ mSubscriptionInfo = new ArrayList<>();
} else {
mSubscriptionInfo = sil;
}
@@ -718,7 +725,7 @@
* If the device is dreaming, awakens the device
*/
public void awakenFromDream() {
- if (mIsDreaming && mDreamManager != null) {
+ if (mIsDreaming) {
try {
mDreamManager.awaken();
} catch (RemoteException e) {
@@ -763,12 +770,8 @@
}
private void reportSuccessfulBiometricUnlock(boolean isStrongBiometric, int userId) {
- mBackgroundExecutor.execute(new Runnable() {
- @Override
- public void run() {
- mLockPatternUtils.reportSuccessfulBiometricUnlock(isStrongBiometric, userId);
- }
- });
+ mBackgroundExecutor.execute(
+ () -> mLockPatternUtils.reportSuccessfulBiometricUnlock(isStrongBiometric, userId));
}
private void handleFingerprintAuthFailed() {
@@ -851,7 +854,8 @@
}
}
- private Runnable mRetryFingerprintAuthentication = new Runnable() {
+ private final Runnable mRetryFingerprintAuthentication = new Runnable() {
+ @SuppressLint("MissingPermission")
@Override
public void run() {
mLogger.logRetryAfterFpHwUnavailable(mHardwareFingerprintUnavailableRetryCount);
@@ -895,7 +899,7 @@
boolean lockedOutStateChanged = false;
if (msgId == FingerprintManager.FINGERPRINT_ERROR_LOCKOUT_PERMANENT) {
- lockedOutStateChanged |= !mFingerprintLockedOutPermanent;
+ lockedOutStateChanged = !mFingerprintLockedOutPermanent;
mFingerprintLockedOutPermanent = true;
mLogger.d("Fingerprint locked out - requiring strong auth");
mLockPatternUtils.requireStrongAuth(
@@ -940,9 +944,9 @@
// that the events will arrive in a particular order. Add a delay here in case
// an unlock is in progress. In this is a normal unlock the extra delay won't
// be noticeable.
- mHandler.postDelayed(() -> {
- updateFingerprintListeningState(BIOMETRIC_ACTION_UPDATE);
- }, getBiometricLockoutDelay());
+ mHandler.postDelayed(
+ () -> updateFingerprintListeningState(BIOMETRIC_ACTION_UPDATE),
+ getBiometricLockoutDelay());
} else {
updateFingerprintListeningState(BIOMETRIC_ACTION_UPDATE);
}
@@ -1076,7 +1080,7 @@
}
}
- private Runnable mRetryFaceAuthentication = new Runnable() {
+ private final Runnable mRetryFaceAuthentication = new Runnable() {
@Override
public void run() {
mLogger.logRetryingAfterFaceHwUnavailable(mHardwareFaceUnavailableRetryCount);
@@ -1102,12 +1106,8 @@
// Error is always the end of authentication lifecycle
mFaceCancelSignal = null;
- boolean cameraPrivacyEnabled = false;
- if (mSensorPrivacyManager != null) {
- cameraPrivacyEnabled = mSensorPrivacyManager
- .isSensorPrivacyEnabled(SensorPrivacyManager.TOGGLE_TYPE_SOFTWARE,
- SensorPrivacyManager.Sensors.CAMERA);
- }
+ boolean cameraPrivacyEnabled = mSensorPrivacyManager.isSensorPrivacyEnabled(
+ SensorPrivacyManager.TOGGLE_TYPE_SOFTWARE, SensorPrivacyManager.Sensors.CAMERA);
if (msgId == FaceManager.FACE_ERROR_CANCELED
&& mFaceRunningState == BIOMETRIC_STATE_CANCELLING_RESTARTING) {
@@ -1158,10 +1158,8 @@
mFaceLockedOutPermanent = (mode == BIOMETRIC_LOCKOUT_PERMANENT);
final boolean changed = (mFaceLockedOutPermanent != wasLockoutPermanent);
- mHandler.postDelayed(() -> {
- updateFaceListeningState(BIOMETRIC_ACTION_UPDATE,
- FACE_AUTH_TRIGGERED_FACE_LOCKOUT_RESET);
- }, getBiometricLockoutDelay());
+ mHandler.postDelayed(() -> updateFaceListeningState(BIOMETRIC_ACTION_UPDATE,
+ FACE_AUTH_TRIGGERED_FACE_LOCKOUT_RESET), getBiometricLockoutDelay());
if (changed) {
notifyLockedOutStateChanged(BiometricSourceType.FACE);
@@ -1200,28 +1198,24 @@
return mFaceRunningState == BIOMETRIC_STATE_RUNNING;
}
- private boolean isTrustDisabled(int userId) {
+ private boolean isTrustDisabled() {
// Don't allow trust agent if device is secured with a SIM PIN. This is here
// mainly because there's no other way to prompt the user to enter their SIM PIN
// once they get past the keyguard screen.
- final boolean disabledBySimPin = isSimPinSecure();
- return disabledBySimPin;
+ return isSimPinSecure(); // Disabled by SIM PIN
}
private boolean isFingerprintDisabled(int userId) {
- final DevicePolicyManager dpm =
- (DevicePolicyManager) mContext.getSystemService(Context.DEVICE_POLICY_SERVICE);
- return dpm != null && (dpm.getKeyguardDisabledFeatures(null, userId)
- & DevicePolicyManager.KEYGUARD_DISABLE_FINGERPRINT) != 0
+ return (mDevicePolicyManager.getKeyguardDisabledFeatures(null, userId)
+ & DevicePolicyManager.KEYGUARD_DISABLE_FINGERPRINT) != 0
|| isSimPinSecure();
}
private boolean isFaceDisabled(int userId) {
- final DevicePolicyManager dpm =
- (DevicePolicyManager) mContext.getSystemService(Context.DEVICE_POLICY_SERVICE);
// TODO(b/140035044)
- return whitelistIpcs(() -> dpm != null && (dpm.getKeyguardDisabledFeatures(null, userId)
- & DevicePolicyManager.KEYGUARD_DISABLE_FACE) != 0
+ return whitelistIpcs(() ->
+ (mDevicePolicyManager.getKeyguardDisabledFeatures(null, userId)
+ & DevicePolicyManager.KEYGUARD_DISABLE_FACE) != 0
|| isSimPinSecure());
}
@@ -1243,7 +1237,7 @@
}
public boolean getUserHasTrust(int userId) {
- return !isTrustDisabled(userId) && mUserHasTrust.get(userId);
+ return !isTrustDisabled() && mUserHasTrust.get(userId);
}
/**
@@ -1275,7 +1269,7 @@
}
public boolean getUserTrustIsManaged(int userId) {
- return mUserTrustIsManaged.get(userId) && !isTrustDisabled(userId);
+ return mUserTrustIsManaged.get(userId) && !isTrustDisabled();
}
private void updateSecondaryLockscreenRequirement(int userId) {
@@ -1293,7 +1287,7 @@
Intent intent =
new Intent(DevicePolicyManager.ACTION_BIND_SECONDARY_LOCKSCREEN_SERVICE)
.setPackage(supervisorComponent.getPackageName());
- ResolveInfo resolveInfo = mContext.getPackageManager().resolveService(intent, 0);
+ ResolveInfo resolveInfo = mPackageManager.resolveService(intent, 0);
if (resolveInfo != null && resolveInfo.serviceInfo != null) {
Intent launchIntent =
new Intent().setComponent(resolveInfo.serviceInfo.getComponentName());
@@ -1662,22 +1656,19 @@
CancellationSignal mFingerprintCancelSignal;
@VisibleForTesting
CancellationSignal mFaceCancelSignal;
- private FingerprintManager mFpm;
- private FaceManager mFaceManager;
private List<FingerprintSensorPropertiesInternal> mFingerprintSensorProperties;
private List<FaceSensorPropertiesInternal> mFaceSensorProperties;
private boolean mFingerprintLockedOut;
private boolean mFingerprintLockedOutPermanent;
private boolean mFaceLockedOutPermanent;
- private HashMap<Integer, Boolean> mIsUnlockWithFingerprintPossible = new HashMap<>();
- private TelephonyManager mTelephonyManager;
+ private final HashMap<Integer, Boolean> mIsUnlockWithFingerprintPossible = new HashMap<>();
/**
* When we receive a
* {@link com.android.internal.telephony.TelephonyIntents#ACTION_SIM_STATE_CHANGED} broadcast,
* and then pass a result via our handler to {@link KeyguardUpdateMonitor#handleSimStateChange},
* we need a single object to pass to the handler. This class helps decode
- * the intent and provide a {@link SimCard.State} result.
+ * the intent and provide a {@link SimData} result.
*/
private static class SimData {
public int simState;
@@ -1898,9 +1889,20 @@
UiEventLogger uiEventLogger,
// This has to be a provider because SessionTracker depends on KeyguardUpdateMonitor :(
Provider<SessionTracker> sessionTrackerProvider,
- PowerManager powerManager) {
+ PowerManager powerManager,
+ TrustManager trustManager,
+ SubscriptionManager subscriptionManager,
+ UserManager userManager,
+ IDreamManager dreamManager,
+ DevicePolicyManager devicePolicyManager,
+ SensorPrivacyManager sensorPrivacyManager,
+ TelephonyManager telephonyManager,
+ PackageManager packageManager,
+ @Nullable FaceManager faceManager,
+ @Nullable FingerprintManager fingerprintManager,
+ @Nullable BiometricManager biometricManager) {
mContext = context;
- mSubscriptionManager = SubscriptionManager.from(context);
+ mSubscriptionManager = subscriptionManager;
mTelephonyListenerManager = telephonyListenerManager;
mDeviceProvisioned = isDeviceProvisionedInSettingsDb();
mStrongAuthTracker = new StrongAuthTracker(context, this::notifyStrongAuthStateChanged);
@@ -1914,12 +1916,20 @@
mLockPatternUtils = lockPatternUtils;
mAuthController = authController;
dumpManager.registerDumpable(getClass().getName(), this);
- mSensorPrivacyManager = context.getSystemService(SensorPrivacyManager.class);
+ mSensorPrivacyManager = sensorPrivacyManager;
mActiveUnlockConfig = activeUnlockConfiguration;
mLogger = logger;
mUiEventLogger = uiEventLogger;
mSessionTrackerProvider = sessionTrackerProvider;
mPowerManager = powerManager;
+ mTrustManager = trustManager;
+ mUserManager = userManager;
+ mDreamManager = dreamManager;
+ mTelephonyManager = telephonyManager;
+ mDevicePolicyManager = devicePolicyManager;
+ mPackageManager = packageManager;
+ mFpm = fingerprintManager;
+ mFaceManager = faceManager;
mActiveUnlockConfig.setKeyguardUpdateMonitor(this);
mWakeOnFingerprintAcquiredStart = context.getResources()
.getBoolean(com.android.internal.R.bool.kg_wake_on_acquire_start);
@@ -2064,8 +2074,7 @@
// listener now with the service state from the default sub.
mBackgroundExecutor.execute(() -> {
int subId = SubscriptionManager.getDefaultSubscriptionId();
- ServiceState serviceState = mContext.getSystemService(TelephonyManager.class)
- .getServiceStateForSubscriber(subId);
+ ServiceState serviceState = mTelephonyManager.getServiceStateForSubscriber(subId);
mHandler.sendMessage(
mHandler.obtainMessage(MSG_SERVICE_STATE_CHANGE, subId, 0, serviceState));
});
@@ -2087,26 +2096,21 @@
e.rethrowAsRuntimeException();
}
- mTrustManager = context.getSystemService(TrustManager.class);
mTrustManager.registerTrustListener(this);
setStrongAuthTracker(mStrongAuthTracker);
- mDreamManager = IDreamManager.Stub.asInterface(
- ServiceManager.getService(DreamService.DREAM_SERVICE));
-
- if (mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_FINGERPRINT)) {
- mFpm = (FingerprintManager) context.getSystemService(Context.FINGERPRINT_SERVICE);
+ if (mFpm != null) {
mFingerprintSensorProperties = mFpm.getSensorPropertiesInternal();
+ mFpm.addLockoutResetCallback(mFingerprintLockoutResetCallback);
}
- if (mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_FACE)) {
- mFaceManager = (FaceManager) context.getSystemService(Context.FACE_SERVICE);
+ if (mFaceManager != null) {
mFaceSensorProperties = mFaceManager.getSensorPropertiesInternal();
+ mFaceManager.addLockoutResetCallback(mFaceLockoutResetCallback);
}
- if (mFpm != null || mFaceManager != null) {
- mBiometricManager = context.getSystemService(BiometricManager.class);
- mBiometricManager.registerEnabledOnKeyguardCallback(mBiometricEnabledCallback);
+ if (biometricManager != null) {
+ biometricManager.registerEnabledOnKeyguardCallback(mBiometricEnabledCallback);
}
// in case authenticators aren't registered yet at this point:
@@ -2125,19 +2129,11 @@
}
});
updateBiometricListeningState(BIOMETRIC_ACTION_UPDATE, FACE_AUTH_UPDATED_ON_KEYGUARD_INIT);
- if (mFpm != null) {
- mFpm.addLockoutResetCallback(mFingerprintLockoutResetCallback);
- }
- if (mFaceManager != null) {
- mFaceManager.addLockoutResetCallback(mFaceLockoutResetCallback);
- }
TaskStackChangeListeners.getInstance().registerTaskStackListener(mTaskStackListener);
- mUserManager = context.getSystemService(UserManager.class);
mIsPrimaryUser = mUserManager.isPrimaryUser();
int user = ActivityManager.getCurrentUser();
mUserIsUnlocked.put(user, mUserManager.isUserUnlocked(user));
- mDevicePolicyManager = context.getSystemService(DevicePolicyManager.class);
mLogoutEnabled = mDevicePolicyManager.isLogoutEnabled();
updateSecondaryLockscreenRequirement(user);
List<UserInfo> allUsers = mUserManager.getUsers();
@@ -2147,22 +2143,8 @@
}
updateAirplaneModeState();
- mTelephonyManager =
- (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
- if (mTelephonyManager != null) {
- mTelephonyListenerManager.addActiveDataSubscriptionIdListener(mPhoneStateListener);
- // Set initial sim states values.
- for (int slot = 0; slot < mTelephonyManager.getActiveModemCount(); slot++) {
- int state = mTelephonyManager.getSimState(slot);
- int[] subIds = mSubscriptionManager.getSubscriptionIds(slot);
- if (subIds != null) {
- for (int subId : subIds) {
- mHandler.obtainMessage(MSG_SIM_STATE_CHANGE, subId, slot, state)
- .sendToTarget();
- }
- }
- }
- }
+ mTelephonyListenerManager.addActiveDataSubscriptionIdListener(mPhoneStateListener);
+ initializeSimState();
mTimeFormatChangeObserver = new ContentObserver(mHandler) {
@Override
@@ -2179,11 +2161,25 @@
false, mTimeFormatChangeObserver, UserHandle.USER_ALL);
}
+ private void initializeSimState() {
+ // Set initial sim states values.
+ for (int slot = 0; slot < mTelephonyManager.getActiveModemCount(); slot++) {
+ int state = mTelephonyManager.getSimState(slot);
+ int[] subIds = mSubscriptionManager.getSubscriptionIds(slot);
+ if (subIds != null) {
+ for (int subId : subIds) {
+ mHandler.obtainMessage(MSG_SIM_STATE_CHANGE, subId, slot, state)
+ .sendToTarget();
+ }
+ }
+ }
+ }
+
private void updateFaceEnrolled(int userId) {
mIsFaceEnrolled = whitelistIpcs(
() -> mFaceManager != null && mFaceManager.isHardwareDetected()
- && mBiometricEnabledForUser.get(userId))
- && mAuthController.isFaceAuthEnrolled(userId);
+ && mFaceManager.hasEnrolledTemplates(userId)
+ && mBiometricEnabledForUser.get(userId));
}
public boolean isFaceSupported() {
@@ -2221,7 +2217,7 @@
}
@Override
- public void onUserSwitchComplete(int newUserId) throws RemoteException {
+ public void onUserSwitchComplete(int newUserId) {
mHandler.sendMessage(mHandler.obtainMessage(MSG_USER_SWITCH_COMPLETE,
newUserId, 0));
}
@@ -2780,6 +2776,7 @@
return isUnlockWithFacePossible(userId) || isUnlockWithFingerprintPossible(userId);
}
+ @SuppressLint("MissingPermission")
@VisibleForTesting
boolean isUnlockWithFingerprintPossible(int userId) {
// TODO (b/242022358), make this rely on onEnrollmentChanged event and update it only once.
@@ -2910,6 +2907,7 @@
try {
reply.sendResult(null);
} catch (RemoteException e) {
+ mLogger.logException(e, "Ignored exception while userSwitching");
}
}
@@ -3176,7 +3174,7 @@
return false;
}
Intent homeIntent = new Intent(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_HOME);
- ResolveInfo resolveInfo = mContext.getPackageManager().resolveActivityAsUser(homeIntent,
+ ResolveInfo resolveInfo = mPackageManager.resolveActivityAsUser(homeIntent,
0 /* flags */, getCurrentUser());
if (resolveInfo == null) {
@@ -3306,11 +3304,7 @@
}
// change in battery overheat
- if (current.health != old.health) {
- return true;
- }
-
- return false;
+ return current.health != old.health;
}
/**
@@ -3361,10 +3355,8 @@
public void setSwitchingUser(boolean switching) {
mSwitchingUser = switching;
// Since this comes in on a binder thread, we need to post if first
- mHandler.post(() -> {
- updateBiometricListeningState(BIOMETRIC_ACTION_UPDATE,
- FACE_AUTH_UPDATED_USER_SWITCHING);
- });
+ mHandler.post(() -> updateBiometricListeningState(BIOMETRIC_ACTION_UPDATE,
+ FACE_AUTH_UPDATED_USER_SWITCHING));
}
private void sendUpdates(KeyguardUpdateMonitorCallback callback) {
@@ -3473,7 +3465,7 @@
/**
* If any SIM cards are currently secure.
*
- * @see #isSimPinSecure(State)
+ * @see #isSimPinSecure(int)
*/
public boolean isSimPinSecure() {
// True if any SIM is pin secure
@@ -3520,10 +3512,7 @@
* @return true if and only if the state has changed for the specified {@code slotId}
*/
private boolean refreshSimState(int subId, int slotId) {
- final TelephonyManager tele =
- (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE);
- int state = (tele != null) ?
- tele.getSimState(slotId) : TelephonyManager.SIM_STATE_UNKNOWN;
+ int state = mTelephonyManager.getSimState(slotId);
SimData data = mSimDatas.get(subId);
final boolean changed;
if (data == null) {
@@ -3666,13 +3655,8 @@
* Unregister all listeners.
*/
public void destroy() {
- // TODO: inject these dependencies:
- TelephonyManager telephony =
- (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE);
- if (telephony != null) {
- mTelephonyListenerManager.removeActiveDataSubscriptionIdListener(mPhoneStateListener);
- }
-
+ mStatusBarStateController.removeCallback(mStatusBarStateControllerListener);
+ mTelephonyListenerManager.removeActiveDataSubscriptionIdListener(mPhoneStateListener);
mSubscriptionManager.removeOnSubscriptionsChangedListener(mSubscriptionListener);
if (mDeviceProvisionedObserver != null) {
@@ -3702,8 +3686,9 @@
mHandler.removeCallbacksAndMessages(null);
}
+ @SuppressLint("MissingPermission")
@Override
- public void dump(PrintWriter pw, String[] args) {
+ public void dump(@NonNull PrintWriter pw, @NonNull String[] args) {
pw.println("KeyguardUpdateMonitor state:");
pw.println(" getUserHasTrust()=" + getUserHasTrust(getCurrentUser()));
pw.println(" getUserUnlockedWithBiometric()="
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUserSwitcherPopupMenu.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUserSwitcherPopupMenu.java
index efa5558..b793fd2 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardUserSwitcherPopupMenu.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUserSwitcherPopupMenu.java
@@ -66,10 +66,13 @@
listView.setDividerHeight(mContext.getResources().getDimensionPixelSize(
R.dimen.bouncer_user_switcher_popup_divider_height));
- int height = mContext.getResources().getDimensionPixelSize(
- R.dimen.bouncer_user_switcher_popup_header_height);
- listView.addHeaderView(createSpacer(height), null, false);
- listView.addFooterView(createSpacer(height), null, false);
+ if (listView.getTag(R.id.header_footer_views_added_tag_key) == null) {
+ int height = mContext.getResources().getDimensionPixelSize(
+ R.dimen.bouncer_user_switcher_popup_header_height);
+ listView.addHeaderView(createSpacer(height), null, false);
+ listView.addFooterView(createSpacer(height), null, false);
+ listView.setTag(R.id.header_footer_views_added_tag_key, new Object());
+ }
listView.setOnTouchListener((v, ev) -> {
if (ev.getActionMasked() == MotionEvent.ACTION_DOWN) {
diff --git a/packages/SystemUI/src/com/android/keyguard/logging/KeyguardLogger.kt b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardLogger.kt
new file mode 100644
index 0000000..80b9c4e
--- /dev/null
+++ b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardLogger.kt
@@ -0,0 +1,53 @@
+package com.android.keyguard.logging
+
+import com.android.systemui.log.LogBuffer
+import com.android.systemui.log.LogLevel
+import com.android.systemui.log.LogLevel.DEBUG
+import com.android.systemui.log.LogLevel.ERROR
+import com.android.systemui.log.LogLevel.VERBOSE
+import com.android.systemui.log.LogLevel.WARNING
+import com.android.systemui.log.MessageInitializer
+import com.android.systemui.log.MessagePrinter
+import com.android.systemui.log.dagger.KeyguardLog
+import com.google.errorprone.annotations.CompileTimeConstant
+import javax.inject.Inject
+
+private const val TAG = "KeyguardLog"
+
+class KeyguardLogger @Inject constructor(@KeyguardLog private val buffer: LogBuffer) {
+ fun d(@CompileTimeConstant msg: String) = log(msg, DEBUG)
+
+ fun e(@CompileTimeConstant msg: String) = log(msg, ERROR)
+
+ fun v(@CompileTimeConstant msg: String) = log(msg, VERBOSE)
+
+ fun w(@CompileTimeConstant msg: String) = log(msg, WARNING)
+
+ fun log(msg: String, level: LogLevel) = buffer.log(TAG, level, msg)
+
+ private fun debugLog(messageInitializer: MessageInitializer, messagePrinter: MessagePrinter) {
+ buffer.log(TAG, DEBUG, messageInitializer, messagePrinter)
+ }
+
+ // TODO: remove after b/237743330 is fixed
+ fun logStatusBarCalculatedAlpha(alpha: Float) {
+ debugLog({ double1 = alpha.toDouble() }, { "Calculated new alpha: $double1" })
+ }
+
+ // TODO: remove after b/237743330 is fixed
+ fun logStatusBarExplicitAlpha(alpha: Float) {
+ debugLog({ double1 = alpha.toDouble() }, { "new mExplicitAlpha value: $double1" })
+ }
+
+ // TODO: remove after b/237743330 is fixed
+ fun logStatusBarAlphaVisibility(visibility: Int, alpha: Float, state: String) {
+ debugLog(
+ {
+ int1 = visibility
+ double1 = alpha.toDouble()
+ str1 = state
+ },
+ { "changing visibility to $int1 with alpha $double1 in state: $str1" }
+ )
+ }
+}
diff --git a/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt
index 7a00cd9..bf9f4c8 100644
--- a/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt
+++ b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt
@@ -45,7 +45,7 @@
fun e(@CompileTimeConstant msg: String) = log(msg, ERROR)
- fun v(@CompileTimeConstant msg: String) = log(msg, ERROR)
+ fun v(@CompileTimeConstant msg: String) = log(msg, VERBOSE)
fun w(@CompileTimeConstant msg: String) = log(msg, WARNING)
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
index ef3c34a..1ceb6b3 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
@@ -151,7 +151,6 @@
@Nullable private List<FingerprintSensorPropertiesInternal> mSidefpsProps;
@NonNull private final SparseBooleanArray mUdfpsEnrolledForUser;
- @NonNull private final SparseBooleanArray mFaceEnrolledForUser;
@NonNull private final SensorPrivacyManager mSensorPrivacyManager;
private final WakefulnessLifecycle mWakefulnessLifecycle;
private boolean mAllFingerprintAuthenticatorsRegistered;
@@ -346,15 +345,6 @@
}
}
}
- if (mFaceProps == null) {
- Log.d(TAG, "handleEnrollmentsChanged, mFaceProps is null");
- } else {
- for (FaceSensorPropertiesInternal prop : mFaceProps) {
- if (prop.sensorId == sensorId) {
- mFaceEnrolledForUser.put(userId, hasEnrollments);
- }
- }
- }
for (Callback cb : mCallbacks) {
cb.onEnrollmentsChanged(modality);
}
@@ -719,7 +709,6 @@
mWindowManager = windowManager;
mInteractionJankMonitor = jankMonitor;
mUdfpsEnrolledForUser = new SparseBooleanArray();
- mFaceEnrolledForUser = new SparseBooleanArray();
mVibratorHelper = vibrator;
mOrientationListener = new BiometricDisplayListener(
@@ -1068,7 +1057,7 @@
return false;
}
- return mFaceEnrolledForUser.get(userId);
+ return mFaceManager.hasEnrolledTemplates(userId);
}
/**
diff --git a/packages/SystemUI/src/com/android/systemui/classifier/Classifier.java b/packages/SystemUI/src/com/android/systemui/classifier/Classifier.java
index c292296..701df89 100644
--- a/packages/SystemUI/src/com/android/systemui/classifier/Classifier.java
+++ b/packages/SystemUI/src/com/android/systemui/classifier/Classifier.java
@@ -45,6 +45,7 @@
public static final int QS_SWIPE_SIDE = 15;
public static final int BACK_GESTURE = 16;
public static final int QS_SWIPE_NESTED = 17;
+ public static final int MEDIA_SEEKBAR = 18;
@IntDef({
QUICK_SETTINGS,
@@ -65,7 +66,8 @@
LOCK_ICON,
QS_SWIPE_SIDE,
QS_SWIPE_NESTED,
- BACK_GESTURE
+ BACK_GESTURE,
+ MEDIA_SEEKBAR,
})
@Retention(RetentionPolicy.SOURCE)
public @interface InteractionType {}
diff --git a/packages/SystemUI/src/com/android/systemui/classifier/DistanceClassifier.java b/packages/SystemUI/src/com/android/systemui/classifier/DistanceClassifier.java
index 5e4f149..f8ee49a 100644
--- a/packages/SystemUI/src/com/android/systemui/classifier/DistanceClassifier.java
+++ b/packages/SystemUI/src/com/android/systemui/classifier/DistanceClassifier.java
@@ -23,6 +23,7 @@
import static com.android.internal.config.sysui.SystemUiDeviceConfigFlags.BRIGHTLINE_FALSING_DISTANCE_VERTICAL_FLING_THRESHOLD_IN;
import static com.android.internal.config.sysui.SystemUiDeviceConfigFlags.BRIGHTLINE_FALSING_DISTANCE_VERTICAL_SWIPE_THRESHOLD_IN;
import static com.android.systemui.classifier.Classifier.BRIGHTNESS_SLIDER;
+import static com.android.systemui.classifier.Classifier.MEDIA_SEEKBAR;
import static com.android.systemui.classifier.Classifier.QS_COLLAPSE;
import static com.android.systemui.classifier.Classifier.QS_SWIPE_NESTED;
import static com.android.systemui.classifier.Classifier.SHADE_DRAG;
@@ -153,6 +154,7 @@
@Classifier.InteractionType int interactionType,
double historyBelief, double historyConfidence) {
if (interactionType == BRIGHTNESS_SLIDER
+ || interactionType == MEDIA_SEEKBAR
|| interactionType == SHADE_DRAG
|| interactionType == QS_COLLAPSE
|| interactionType == Classifier.UDFPS_AUTHENTICATION
diff --git a/packages/SystemUI/src/com/android/systemui/classifier/ProximityClassifier.java b/packages/SystemUI/src/com/android/systemui/classifier/ProximityClassifier.java
index 07f94e7..e8c83b1 100644
--- a/packages/SystemUI/src/com/android/systemui/classifier/ProximityClassifier.java
+++ b/packages/SystemUI/src/com/android/systemui/classifier/ProximityClassifier.java
@@ -18,6 +18,7 @@
import static com.android.internal.config.sysui.SystemUiDeviceConfigFlags.BRIGHTLINE_FALSING_PROXIMITY_PERCENT_COVERED_THRESHOLD;
import static com.android.systemui.classifier.Classifier.BRIGHTNESS_SLIDER;
+import static com.android.systemui.classifier.Classifier.MEDIA_SEEKBAR;
import static com.android.systemui.classifier.Classifier.QS_COLLAPSE;
import static com.android.systemui.classifier.Classifier.QS_SWIPE_SIDE;
import static com.android.systemui.classifier.Classifier.QUICK_SETTINGS;
@@ -119,7 +120,8 @@
@Classifier.InteractionType int interactionType,
double historyBelief, double historyConfidence) {
if (interactionType == QUICK_SETTINGS || interactionType == BRIGHTNESS_SLIDER
- || interactionType == QS_COLLAPSE || interactionType == QS_SWIPE_SIDE) {
+ || interactionType == QS_COLLAPSE || interactionType == QS_SWIPE_SIDE
+ || interactionType == MEDIA_SEEKBAR) {
return Result.passed(0);
}
diff --git a/packages/SystemUI/src/com/android/systemui/classifier/TypeClassifier.java b/packages/SystemUI/src/com/android/systemui/classifier/TypeClassifier.java
index 776bc88..f576a5a 100644
--- a/packages/SystemUI/src/com/android/systemui/classifier/TypeClassifier.java
+++ b/packages/SystemUI/src/com/android/systemui/classifier/TypeClassifier.java
@@ -20,6 +20,7 @@
import static com.android.systemui.classifier.Classifier.BOUNCER_UNLOCK;
import static com.android.systemui.classifier.Classifier.BRIGHTNESS_SLIDER;
import static com.android.systemui.classifier.Classifier.LEFT_AFFORDANCE;
+import static com.android.systemui.classifier.Classifier.MEDIA_SEEKBAR;
import static com.android.systemui.classifier.Classifier.NOTIFICATION_DISMISS;
import static com.android.systemui.classifier.Classifier.NOTIFICATION_DRAG_DOWN;
import static com.android.systemui.classifier.Classifier.PULSE_EXPAND;
@@ -93,6 +94,10 @@
case QS_SWIPE_NESTED:
wrongDirection = !vertical;
break;
+ case MEDIA_SEEKBAR:
+ confidence = 0;
+ wrongDirection = vertical;
+ break;
default:
wrongDirection = true;
break;
diff --git a/packages/SystemUI/src/com/android/systemui/classifier/ZigZagClassifier.java b/packages/SystemUI/src/com/android/systemui/classifier/ZigZagClassifier.java
index de2bdf7..840982c 100644
--- a/packages/SystemUI/src/com/android/systemui/classifier/ZigZagClassifier.java
+++ b/packages/SystemUI/src/com/android/systemui/classifier/ZigZagClassifier.java
@@ -22,6 +22,7 @@
import static com.android.internal.config.sysui.SystemUiDeviceConfigFlags.BRIGHTLINE_FALSING_ZIGZAG_Y_SECONDARY_DEVIANCE;
import static com.android.systemui.classifier.Classifier.BRIGHTNESS_SLIDER;
import static com.android.systemui.classifier.Classifier.LOCK_ICON;
+import static com.android.systemui.classifier.Classifier.MEDIA_SEEKBAR;
import static com.android.systemui.classifier.Classifier.SHADE_DRAG;
import android.graphics.Point;
@@ -91,6 +92,7 @@
@Classifier.InteractionType int interactionType,
double historyBelief, double historyConfidence) {
if (interactionType == BRIGHTNESS_SLIDER
+ || interactionType == MEDIA_SEEKBAR
|| interactionType == SHADE_DRAG
|| interactionType == LOCK_ICON) {
return Result.passed(0);
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java b/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java
index 4096ed4..139a8b7 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java
@@ -46,6 +46,7 @@
import android.content.res.Resources;
import android.hardware.SensorManager;
import android.hardware.SensorPrivacyManager;
+import android.hardware.biometrics.BiometricManager;
import android.hardware.camera2.CameraManager;
import android.hardware.devicestate.DeviceStateManager;
import android.hardware.display.AmbientDisplayConfiguration;
@@ -237,22 +238,39 @@
@Singleton
static IDreamManager provideIDreamManager() {
return IDreamManager.Stub.asInterface(
- ServiceManager.checkService(DreamService.DREAM_SERVICE));
+ ServiceManager.getService(DreamService.DREAM_SERVICE));
}
@Provides
@Singleton
@Nullable
static FaceManager provideFaceManager(Context context) {
- return context.getSystemService(FaceManager.class);
-
+ if (context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_FACE)) {
+ return context.getSystemService(FaceManager.class);
+ }
+ return null;
}
@Provides
@Singleton
@Nullable
static FingerprintManager providesFingerprintManager(Context context) {
- return context.getSystemService(FingerprintManager.class);
+ if (context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_FINGERPRINT)) {
+ return context.getSystemService(FingerprintManager.class);
+ }
+ return null;
+ }
+
+ /**
+ * @return null if both faceManager and fingerprintManager are null.
+ */
+ @Provides
+ @Singleton
+ @Nullable
+ static BiometricManager providesBiometricManager(Context context,
+ @Nullable FaceManager faceManager, @Nullable FingerprintManager fingerprintManager) {
+ return faceManager == null && fingerprintManager == null ? null :
+ context.getSystemService(BiometricManager.class);
}
@Provides
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
index 443d277..06dbab9 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
@@ -81,6 +81,7 @@
import com.android.systemui.statusbar.policy.dagger.SmartRepliesInflationModule;
import com.android.systemui.statusbar.policy.dagger.StatusBarPolicyModule;
import com.android.systemui.statusbar.window.StatusBarWindowModule;
+import com.android.systemui.telephony.data.repository.TelephonyRepositoryModule;
import com.android.systemui.tuner.dagger.TunerModule;
import com.android.systemui.unfold.SysUIUnfoldModule;
import com.android.systemui.user.UserModule;
@@ -145,6 +146,7 @@
StatusBarWindowModule.class,
SysUIConcurrencyModule.class,
SysUIUnfoldModule.class,
+ TelephonyRepositoryModule.class,
TunerModule.class,
UserModule.class,
UtilModule.class,
diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeHost.java b/packages/SystemUI/src/com/android/systemui/doze/DozeHost.java
index b598554..4c4aa5c 100644
--- a/packages/SystemUI/src/com/android/systemui/doze/DozeHost.java
+++ b/packages/SystemUI/src/com/android/systemui/doze/DozeHost.java
@@ -33,6 +33,18 @@
boolean isProvisioned();
/**
+ * Whether there's a pulse that's been requested but hasn't started transitioning to pulsing
+ * states yet.
+ */
+ boolean isPulsePending();
+
+ /**
+ * @param isPulsePending whether a pulse has been requested but hasn't started transitioning
+ * to the pulse state yet
+ */
+ void setPulsePending(boolean isPulsePending);
+
+ /**
* Makes a current pulse last for twice as long.
* @param reason why we're extending it.
*/
diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeLog.java b/packages/SystemUI/src/com/android/systemui/doze/DozeLog.java
index 4161cf6..8ae305b 100644
--- a/packages/SystemUI/src/com/android/systemui/doze/DozeLog.java
+++ b/packages/SystemUI/src/com/android/systemui/doze/DozeLog.java
@@ -280,8 +280,8 @@
/**
* Appends pulse dropped event to logs
*/
- public void tracePulseDropped(boolean pulsePending, DozeMachine.State state, boolean blocked) {
- mLogger.logPulseDropped(pulsePending, state, blocked);
+ public void tracePulseDropped(String from, DozeMachine.State state) {
+ mLogger.logPulseDropped(from, state);
}
/**
@@ -292,6 +292,13 @@
}
/**
+ * Appends pulsing event to logs.
+ */
+ public void tracePulseEvent(String pulseEvent, boolean dozing, int pulseReason) {
+ mLogger.logPulseEvent(pulseEvent, dozing, DozeLog.reasonToString(pulseReason));
+ }
+
+ /**
* Appends pulse dropped event to logs
* @param reason why the pulse was dropped
*/
diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeLogger.kt b/packages/SystemUI/src/com/android/systemui/doze/DozeLogger.kt
index 4b279ec..21a2c3b 100644
--- a/packages/SystemUI/src/com/android/systemui/doze/DozeLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/doze/DozeLogger.kt
@@ -224,13 +224,12 @@
})
}
- fun logPulseDropped(pulsePending: Boolean, state: DozeMachine.State, blocked: Boolean) {
+ fun logPulseDropped(from: String, state: DozeMachine.State) {
buffer.log(TAG, INFO, {
- bool1 = pulsePending
- str1 = state.name
- bool2 = blocked
+ str1 = from
+ str2 = state.name
}, {
- "Pulse dropped, pulsePending=$bool1 state=$str1 blocked=$bool2"
+ "Pulse dropped, cannot pulse from=$str1 state=$str2"
})
}
@@ -243,6 +242,16 @@
})
}
+ fun logPulseEvent(pulseEvent: String, dozing: Boolean, pulseReason: String) {
+ buffer.log(TAG, DEBUG, {
+ str1 = pulseEvent
+ bool1 = dozing
+ str2 = pulseReason
+ }, {
+ "Pulse-$str1 dozing=$bool1 pulseReason=$str2"
+ })
+ }
+
fun logPulseDropped(reason: String) {
buffer.log(TAG, INFO, {
str1 = reason
diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java b/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java
index 00ac8bc..ef454ff 100644
--- a/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java
+++ b/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java
@@ -102,7 +102,6 @@
private final UiEventLogger mUiEventLogger;
private long mNotificationPulseTime;
- private boolean mPulsePending;
private Runnable mAodInterruptRunnable;
/** see {@link #onProximityFar} prox for callback */
@@ -303,8 +302,8 @@
null /* onPulseSuppressedListener */);
}
} else {
- proximityCheckThenCall((result) -> {
- if (result != null && result) {
+ proximityCheckThenCall((isNear) -> {
+ if (isNear != null && isNear) {
// In pocket, drop event.
mDozeLog.traceSensorEventDropped(pulseReason, "prox reporting near");
return;
@@ -410,8 +409,8 @@
sWakeDisplaySensorState = wake;
if (wake) {
- proximityCheckThenCall((result) -> {
- if (result != null && result) {
+ proximityCheckThenCall((isNear) -> {
+ if (isNear != null && isNear) {
// In pocket, drop event.
return;
}
@@ -537,24 +536,44 @@
return;
}
- if (mPulsePending || !mAllowPulseTriggers || !canPulse()) {
- if (mAllowPulseTriggers) {
- mDozeLog.tracePulseDropped(mPulsePending, dozeState, mDozeHost.isPulsingBlocked());
+ if (!mAllowPulseTriggers || mDozeHost.isPulsePending() || !canPulse()) {
+ if (!mAllowPulseTriggers) {
+ mDozeLog.tracePulseDropped("requestPulse - !mAllowPulseTriggers");
+ } else if (mDozeHost.isPulsePending()) {
+ mDozeLog.tracePulseDropped("requestPulse - pulsePending");
+ } else if (!canPulse()) {
+ mDozeLog.tracePulseDropped("requestPulse", dozeState);
}
runIfNotNull(onPulseSuppressedListener);
return;
}
- mPulsePending = true;
- proximityCheckThenCall((result) -> {
- if (result != null && result) {
+ mDozeHost.setPulsePending(true);
+ proximityCheckThenCall((isNear) -> {
+ if (isNear != null && isNear) {
// in pocket, abort pulse
- mDozeLog.tracePulseDropped("inPocket");
- mPulsePending = false;
+ mDozeLog.tracePulseDropped("requestPulse - inPocket");
+ mDozeHost.setPulsePending(false);
runIfNotNull(onPulseSuppressedListener);
} else {
// not in pocket, continue pulsing
- continuePulseRequest(reason);
+ final boolean isPulsePending = mDozeHost.isPulsePending();
+ mDozeHost.setPulsePending(false);
+ if (!isPulsePending || mDozeHost.isPulsingBlocked() || !canPulse()) {
+ if (!isPulsePending) {
+ mDozeLog.tracePulseDropped("continuePulseRequest - pulse no longer"
+ + " pending, pulse was cancelled before it could start"
+ + " transitioning to pulsing state.");
+ } else if (mDozeHost.isPulsingBlocked()) {
+ mDozeLog.tracePulseDropped("continuePulseRequest - pulsingBlocked");
+ } else if (!canPulse()) {
+ mDozeLog.tracePulseDropped("continuePulseRequest", mMachine.getState());
+ }
+ runIfNotNull(onPulseSuppressedListener);
+ return;
+ }
+
+ mMachine.requestPulse(reason);
}
}, !mDozeParameters.getProxCheckBeforePulse() || performedProxCheck, reason);
@@ -569,16 +588,6 @@
|| mMachine.getState() == DozeMachine.State.DOZE_AOD_DOCKED;
}
- private void continuePulseRequest(int reason) {
- mPulsePending = false;
- if (mDozeHost.isPulsingBlocked() || !canPulse()) {
- mDozeLog.tracePulseDropped(mPulsePending, mMachine.getState(),
- mDozeHost.isPulsingBlocked());
- return;
- }
- mMachine.requestPulse(reason);
- }
-
@Nullable
private InstanceId getKeyguardSessionId() {
return mSessionTracker.getSessionId(SESSION_KEYGUARD);
@@ -591,7 +600,7 @@
pw.print(" notificationPulseTime=");
pw.println(Formatter.formatShortElapsedTime(mContext, mNotificationPulseTime));
- pw.println(" pulsePending=" + mPulsePending);
+ pw.println(" DozeHost#isPulsePending=" + mDozeHost.isPulsePending());
pw.println("DozeSensors:");
IndentingPrintWriter idpw = new IndentingPrintWriter(pw);
idpw.increaseIndent();
diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.java b/packages/SystemUI/src/com/android/systemui/flags/Flags.java
index 38d9d021..23c96e1 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/Flags.java
+++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.java
@@ -68,6 +68,9 @@
public static final UnreleasedFlag INSTANT_VOICE_REPLY = new UnreleasedFlag(111, true);
+ public static final UnreleasedFlag NOTIFICATION_MEMORY_MONITOR_ENABLED = new UnreleasedFlag(112,
+ false);
+
// next id: 112
/***************************************/
@@ -104,9 +107,26 @@
public static final ReleasedFlag MODERN_USER_SWITCHER_ACTIVITY =
new ReleasedFlag(209, true);
- /** Whether the new implementation of UserSwitcherController should be used. */
- public static final UnreleasedFlag REFACTORED_USER_SWITCHER_CONTROLLER =
- new UnreleasedFlag(210, false);
+ /**
+ * Whether the user interactor and repository should use `UserSwitcherController`.
+ *
+ * <p>If this is {@code false}, the interactor and repo skip the controller and directly access
+ * the framework APIs.
+ */
+ public static final UnreleasedFlag USER_INTERACTOR_AND_REPO_USE_CONTROLLER =
+ new UnreleasedFlag(210, true);
+
+ /**
+ * Whether `UserSwitcherController` should use the user interactor.
+ *
+ * <p>When this is {@code true}, the controller does not directly access framework APIs.
+ * Instead, it goes through the interactor.
+ *
+ * <p>Note: do not set this to true if {@link #USER_INTERACTOR_AND_REPO_USE_CONTROLLER} is
+ * {@code true} as it would created a cycle between controller -> interactor -> controller.
+ */
+ public static final UnreleasedFlag USER_CONTROLLER_USES_INTERACTOR =
+ new UnreleasedFlag(211, false);
/***************************************/
// 300 - power menu
@@ -152,7 +172,7 @@
public static final ResourceBooleanFlag FULL_SCREEN_USER_SWITCHER =
new ResourceBooleanFlag(506, R.bool.config_enableFullscreenUserSwitcher);
- public static final UnreleasedFlag NEW_FOOTER_ACTIONS = new UnreleasedFlag(507, true);
+ public static final ReleasedFlag NEW_FOOTER_ACTIONS = new ReleasedFlag(507);
/***************************************/
// 600- status bar
@@ -247,6 +267,13 @@
public static final SysPropBooleanFlag SHOW_FLOATING_TASKS_AS_BUBBLES =
new SysPropBooleanFlag(1107, "persist.wm.debug.floating_tasks_as_bubbles", false);
+ @Keep
+ public static final SysPropBooleanFlag ENABLE_FLING_TO_DISMISS_BUBBLE =
+ new SysPropBooleanFlag(1108, "persist.wm.debug.fling_to_dismiss_bubble", true);
+ @Keep
+ public static final SysPropBooleanFlag ENABLE_FLING_TO_DISMISS_PIP =
+ new SysPropBooleanFlag(1109, "persist.wm.debug.fling_to_dismiss_pip", true);
+
// 1200 - predictive back
@Keep
public static final SysPropBooleanFlag WM_ENABLE_PREDICTIVE_BACK = new SysPropBooleanFlag(
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt
index 840a4b2..4c4b588 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt
@@ -85,6 +85,15 @@
*/
val dozeAmount: Flow<Float>
+ /**
+ * Returns `true` if the keyguard is showing; `false` otherwise.
+ *
+ * Note: this is also `true` when the lock-screen is occluded with an `Activity` "above" it in
+ * the z-order (which is not really above the system UI window, but rather - the lock-screen
+ * becomes invisible to reveal the "occluding activity").
+ */
+ fun isKeyguardShowing(): Boolean
+
/** Sets whether the bottom area UI should animate the transition out of doze state. */
fun setAnimateDozingTransitions(animate: Boolean)
@@ -103,7 +112,7 @@
@Inject
constructor(
statusBarStateController: StatusBarStateController,
- keyguardStateController: KeyguardStateController,
+ private val keyguardStateController: KeyguardStateController,
dozeHost: DozeHost,
) : KeyguardRepository {
private val _animateBottomAreaDozingTransitions = MutableStateFlow(false)
@@ -168,6 +177,10 @@
awaitClose { statusBarStateController.removeCallback(callback) }
}
+ override fun isKeyguardShowing(): Boolean {
+ return keyguardStateController.isShowing
+ }
+
override fun setAnimateDozingTransitions(animate: Boolean) {
_animateBottomAreaDozingTransitions.value = animate
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt
index dccc941..192919e 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt
@@ -29,7 +29,7 @@
class KeyguardInteractor
@Inject
constructor(
- repository: KeyguardRepository,
+ private val repository: KeyguardRepository,
) {
/**
* The amount of doze the system is in, where `1.0` is fully dozing and `0.0` is not dozing at
@@ -40,4 +40,8 @@
val isDozing: Flow<Boolean> = repository.isDozing
/** Whether the keyguard is showing ot not. */
val isKeyguardShowing: Flow<Boolean> = repository.isKeyguardShowing
+
+ fun isKeyguardShowing(): Boolean {
+ return repository.isKeyguardShowing()
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/KeyguardLog.kt b/packages/SystemUI/src/com/android/systemui/log/dagger/KeyguardLog.kt
new file mode 100644
index 0000000..aef3471
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/KeyguardLog.kt
@@ -0,0 +1,10 @@
+package com.android.systemui.log.dagger
+
+import javax.inject.Qualifier
+
+/**
+ * A [com.android.systemui.log.LogBuffer] for keyguard-related stuff. Should be used mostly for
+ * adding temporary logs or logging from smaller classes when creating new separate log class might
+ * be an overkill.
+ */
+@Qualifier @MustBeDocumented @Retention(AnnotationRetention.RUNTIME) annotation class KeyguardLog
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
index f2f6bad..0c5564b 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
@@ -43,7 +43,7 @@
@SysUISingleton
@DozeLog
public static LogBuffer provideDozeLogBuffer(LogBufferFactory factory) {
- return factory.create("DozeLog", 100);
+ return factory.create("DozeLog", 120);
}
/** Provides a logging buffer for all logs related to the data layer of notifications. */
@@ -344,4 +344,14 @@
public static LogBuffer provideUdfpsLogBuffer(LogBufferFactory factory) {
return factory.create("UdfpsLog", 1000);
}
+
+ /**
+ * Provides a {@link LogBuffer} for general keyguard-related logs.
+ */
+ @Provides
+ @SysUISingleton
+ @KeyguardLog
+ public static LogBuffer provideKeyguardLogBuffer(LogBufferFactory factory) {
+ return factory.create("KeyguardLog", 250);
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/media/SeekBarViewModel.kt b/packages/SystemUI/src/com/android/systemui/media/SeekBarViewModel.kt
index 0359c63..17ebfec 100644
--- a/packages/SystemUI/src/com/android/systemui/media/SeekBarViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/SeekBarViewModel.kt
@@ -30,7 +30,10 @@
import androidx.core.view.GestureDetectorCompat
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
+import com.android.systemui.classifier.Classifier.MEDIA_SEEKBAR
import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.plugins.FalsingManager
+import com.android.systemui.plugins.FalsingManager.LOW_PENALTY
import com.android.systemui.statusbar.NotificationMediaManager
import com.android.systemui.util.concurrency.RepeatableExecutor
import javax.inject.Inject
@@ -72,7 +75,8 @@
/** ViewModel for seek bar in QS media player. */
class SeekBarViewModel @Inject constructor(
- @Background private val bgExecutor: RepeatableExecutor
+ @Background private val bgExecutor: RepeatableExecutor,
+ private val falsingManager: FalsingManager,
) {
private var _data = Progress(false, false, false, false, null, 0)
set(value) {
@@ -275,7 +279,7 @@
/** Gets a listener to attach to the seek bar to handle seeking. */
val seekBarListener: SeekBar.OnSeekBarChangeListener
get() {
- return SeekBarChangeListener(this)
+ return SeekBarChangeListener(this, falsingManager)
}
/** Attach touch handlers to the seek bar view. */
@@ -315,7 +319,8 @@
}
private class SeekBarChangeListener(
- val viewModel: SeekBarViewModel
+ val viewModel: SeekBarViewModel,
+ val falsingManager: FalsingManager,
) : SeekBar.OnSeekBarChangeListener {
override fun onProgressChanged(bar: SeekBar, progress: Int, fromUser: Boolean) {
if (fromUser) {
@@ -328,6 +333,13 @@
}
override fun onStopTrackingTouch(bar: SeekBar) {
+ // in addition to the normal functionality of both functions.
+ // isFalseTouch returns true if there is a real/false tap since it is not a move.
+ // isFalseTap returns true if there is a real/false move since it is not a tap.
+ if (falsingManager.isFalseTouch(MEDIA_SEEKBAR) &&
+ falsingManager.isFalseTap(LOW_PENALTY)) {
+ viewModel.onSeekFalse()
+ }
viewModel.onSeek(bar.progress.toLong())
}
}
@@ -340,7 +352,7 @@
*/
private class SeekBarTouchListener(
private val viewModel: SeekBarViewModel,
- private val bar: SeekBar
+ private val bar: SeekBar,
) : View.OnTouchListener, GestureDetector.OnGestureListener {
// Gesture detector helps decide which touch events to intercept.
diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/ChipStateSender.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/ChipStateSender.kt
index 4379d25..aae973d 100644
--- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/ChipStateSender.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/ChipStateSender.kt
@@ -25,6 +25,7 @@
import com.android.internal.logging.UiEventLogger
import com.android.internal.statusbar.IUndoMediaTransferCallback
import com.android.systemui.R
+import com.android.systemui.plugins.FalsingManager
import com.android.systemui.temporarydisplay.DEFAULT_TIMEOUT_MILLIS
/**
@@ -107,12 +108,15 @@
controllerSender: MediaTttChipControllerSender,
routeInfo: MediaRoute2Info,
undoCallback: IUndoMediaTransferCallback?,
- uiEventLogger: MediaTttSenderUiEventLogger
+ uiEventLogger: MediaTttSenderUiEventLogger,
+ falsingManager: FalsingManager,
): View.OnClickListener? {
if (undoCallback == null) {
return null
}
return View.OnClickListener {
+ if (falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) return@OnClickListener
+
uiEventLogger.logUndoClicked(
MediaTttSenderUiEvents.MEDIA_TTT_SENDER_UNDO_TRANSFER_TO_RECEIVER_CLICKED
)
@@ -143,12 +147,15 @@
controllerSender: MediaTttChipControllerSender,
routeInfo: MediaRoute2Info,
undoCallback: IUndoMediaTransferCallback?,
- uiEventLogger: MediaTttSenderUiEventLogger
+ uiEventLogger: MediaTttSenderUiEventLogger,
+ falsingManager: FalsingManager,
): View.OnClickListener? {
if (undoCallback == null) {
return null
}
return View.OnClickListener {
+ if (falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) return@OnClickListener
+
uiEventLogger.logUndoClicked(
MediaTttSenderUiEvents.MEDIA_TTT_SENDER_UNDO_TRANSFER_TO_THIS_DEVICE_CLICKED
)
@@ -215,7 +222,8 @@
controllerSender: MediaTttChipControllerSender,
routeInfo: MediaRoute2Info,
undoCallback: IUndoMediaTransferCallback?,
- uiEventLogger: MediaTttSenderUiEventLogger
+ uiEventLogger: MediaTttSenderUiEventLogger,
+ falsingManager: FalsingManager,
): View.OnClickListener? = null
companion object {
diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSender.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSender.kt
index e539f3f..007eb8f 100644
--- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSender.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSender.kt
@@ -22,25 +22,30 @@
import android.os.PowerManager
import android.util.Log
import android.view.Gravity
+import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.view.WindowManager
import android.view.accessibility.AccessibilityManager
import android.widget.TextView
import com.android.internal.statusbar.IUndoMediaTransferCallback
+import com.android.systemui.Gefingerpoken
import com.android.systemui.R
import com.android.systemui.animation.Interpolators
import com.android.systemui.animation.ViewHierarchyAnimator
+import com.android.systemui.classifier.FalsingCollector
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.media.taptotransfer.common.MediaTttLogger
import com.android.systemui.media.taptotransfer.common.MediaTttUtils
+import com.android.systemui.plugins.FalsingManager
import com.android.systemui.statusbar.CommandQueue
import com.android.systemui.statusbar.policy.ConfigurationController
import com.android.systemui.temporarydisplay.TemporaryDisplayRemovalReason
import com.android.systemui.temporarydisplay.TemporaryViewDisplayController
import com.android.systemui.temporarydisplay.TemporaryViewInfo
import com.android.systemui.util.concurrency.DelayableExecutor
+import dagger.Lazy
import javax.inject.Inject
/**
@@ -57,7 +62,11 @@
accessibilityManager: AccessibilityManager,
configurationController: ConfigurationController,
powerManager: PowerManager,
- private val uiEventLogger: MediaTttSenderUiEventLogger
+ private val uiEventLogger: MediaTttSenderUiEventLogger,
+ // Added Lazy<> to delay the time we create Falsing instances.
+ // And overcome performance issue, check [b/247817628] for details.
+ private val falsingManager: Lazy<FalsingManager>,
+ private val falsingCollector: Lazy<FalsingCollector>,
) : TemporaryViewDisplayController<ChipSenderInfo, MediaTttLogger>(
context,
logger,
@@ -70,6 +79,9 @@
MediaTttUtils.WINDOW_TITLE,
MediaTttUtils.WAKE_REASON,
) {
+
+ private lateinit var parent: MediaTttChipRootView
+
override val windowLayoutParams = commonWindowLayoutParams.apply {
gravity = Gravity.TOP.or(Gravity.CENTER_HORIZONTAL)
}
@@ -120,6 +132,15 @@
val chipState = newInfo.state
+ // Detect falsing touches on the chip.
+ parent = currentView.requireViewById(R.id.media_ttt_sender_chip)
+ parent.touchHandler = object : Gefingerpoken {
+ override fun onTouchEvent(ev: MotionEvent?): Boolean {
+ falsingCollector.get().onTouchEvent(ev)
+ return false
+ }
+ }
+
// App icon
val iconInfo = MediaTttUtils.getIconInfoFromPackageName(
context, newInfo.routeInfo.clientPackageName, logger
@@ -142,7 +163,11 @@
// Undo
val undoView = currentView.requireViewById<View>(R.id.undo)
val undoClickListener = chipState.undoClickListener(
- this, newInfo.routeInfo, newInfo.undoCallback, uiEventLogger
+ this,
+ newInfo.routeInfo,
+ newInfo.undoCallback,
+ uiEventLogger,
+ falsingManager.get(),
)
undoView.setOnClickListener(undoClickListener)
undoView.visibility = (undoClickListener != null).visibleIfTrue()
diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipRootView.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipRootView.kt
new file mode 100644
index 0000000..3373159
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipRootView.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.media.taptotransfer.sender
+
+import android.content.Context
+import android.util.AttributeSet
+import android.view.MotionEvent
+import android.widget.FrameLayout
+import com.android.systemui.Gefingerpoken
+
+/** A simple subclass that allows for observing touch events on chip. */
+class MediaTttChipRootView(
+ context: Context,
+ attrs: AttributeSet?
+) : FrameLayout(context, attrs) {
+
+ /** Assign this field to observe touch events. */
+ var touchHandler: Gefingerpoken? = null
+
+ override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
+ touchHandler?.onTouchEvent(ev)
+ return super.dispatchTouchEvent(ev)
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java b/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java
index ba3c919..7a44058 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java
@@ -60,6 +60,7 @@
import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel;
import com.android.systemui.statusbar.CommandQueue;
import com.android.systemui.statusbar.StatusBarState;
+import com.android.systemui.statusbar.SysuiStatusBarStateController;
import com.android.systemui.statusbar.notification.stack.StackStateAnimator;
import com.android.systemui.statusbar.phone.KeyguardBypassController;
import com.android.systemui.statusbar.policy.BrightnessMirrorController;
@@ -82,7 +83,7 @@
private static final String EXTRA_VISIBLE = "visible";
private final Rect mQsBounds = new Rect();
- private final StatusBarStateController mStatusBarStateController;
+ private final SysuiStatusBarStateController mStatusBarStateController;
private final FalsingManager mFalsingManager;
private final KeyguardBypassController mBypassController;
private boolean mQsExpanded;
@@ -159,7 +160,7 @@
* Progress of pull down from the center of the lock screen.
* @see com.android.systemui.statusbar.LockscreenShadeTransitionController
*/
- private float mFullShadeProgress;
+ private float mLockscreenToShadeProgress;
private boolean mOverScrolling;
@@ -177,7 +178,7 @@
@Inject
public QSFragment(RemoteInputQuickSettingsDisabler remoteInputQsDisabler,
QSTileHost qsTileHost,
- StatusBarStateController statusBarStateController, CommandQueue commandQueue,
+ SysuiStatusBarStateController statusBarStateController, CommandQueue commandQueue,
@Named(QS_PANEL) MediaHost qsMediaHost,
@Named(QUICK_QS_PANEL) MediaHost qqsMediaHost,
KeyguardBypassController keyguardBypassController,
@@ -585,7 +586,7 @@
mTransitioningToFullShade = isTransitioningToFullShade;
updateShowCollapsedOnKeyguard();
}
- mFullShadeProgress = qsTransitionFraction;
+ mLockscreenToShadeProgress = qsTransitionFraction;
setQsExpansion(mLastQSExpansion, mLastPanelFraction, mLastHeaderTranslation,
isTransitioningToFullShade ? qsSquishinessFraction : mSquishinessFraction);
}
@@ -709,10 +710,13 @@
}
if (mInSplitShade) {
// Large screens in landscape.
- if (mTransitioningToFullShade || isKeyguardState()) {
+ // Need to check upcoming state as for unlocked -> AOD transition current state is
+ // not updated yet, but we're transitioning and UI should already follow KEYGUARD state
+ if (mTransitioningToFullShade || mStatusBarStateController.getCurrentOrUpcomingState()
+ == StatusBarState.KEYGUARD) {
// Always use "mFullShadeProgress" on keyguard, because
// "panelExpansionFractions" is always 1 on keyguard split shade.
- return mFullShadeProgress;
+ return mLockscreenToShadeProgress;
} else {
return panelExpansionFraction;
}
@@ -721,7 +725,7 @@
if (mTransitioningToFullShade) {
// Only use this value during the standard lock screen shade expansion. During the
// "quick" expansion from top, this value is 0.
- return mFullShadeProgress;
+ return mLockscreenToShadeProgress;
} else {
return panelExpansionFraction;
}
@@ -929,7 +933,7 @@
indentingPw.println("mLastHeaderTranslation: " + mLastHeaderTranslation);
indentingPw.println("mInSplitShade: " + mInSplitShade);
indentingPw.println("mTransitioningToFullShade: " + mTransitioningToFullShade);
- indentingPw.println("mFullShadeProgress: " + mFullShadeProgress);
+ indentingPw.println("mLockscreenToShadeProgress: " + mLockscreenToShadeProgress);
indentingPw.println("mOverScrolling: " + mOverScrolling);
indentingPw.println("isCustomizing: " + mQSCustomizerController.isCustomizing());
View view = getView();
diff --git a/packages/SystemUI/src/com/android/systemui/qs/footer/ui/binder/FooterActionsViewBinder.kt b/packages/SystemUI/src/com/android/systemui/qs/footer/ui/binder/FooterActionsViewBinder.kt
index 28ddead..dd1ffcc 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/footer/ui/binder/FooterActionsViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/footer/ui/binder/FooterActionsViewBinder.kt
@@ -222,6 +222,7 @@
private fun bindButton(button: IconButtonViewHolder, model: FooterActionsButtonViewModel?) {
val buttonView = button.view
+ buttonView.id = model?.id ?: View.NO_ID
buttonView.isVisible = model != null
if (model == null) {
return
diff --git a/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsButtonViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsButtonViewModel.kt
index 2ad0513..5a8f684 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsButtonViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsButtonViewModel.kt
@@ -25,6 +25,7 @@
* power buttons.
*/
data class FooterActionsButtonViewModel(
+ val id: Int?,
val icon: Icon,
val iconTint: Int?,
@DrawableRes val background: Int,
diff --git a/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModel.kt
index a935338..8b3f4b4 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModel.kt
@@ -138,6 +138,7 @@
/** The model for the settings button. */
val settings: FooterActionsButtonViewModel =
FooterActionsButtonViewModel(
+ id = R.id.settings_button_container,
Icon.Resource(
R.drawable.ic_settings,
ContentDescription.Resource(R.string.accessibility_quick_settings_settings)
@@ -151,6 +152,7 @@
val power: FooterActionsButtonViewModel? =
if (showPowerButton) {
FooterActionsButtonViewModel(
+ id = R.id.pm_lite,
Icon.Resource(
android.R.drawable.ic_lock_power_off,
ContentDescription.Resource(R.string.accessibility_quick_settings_power_menu)
@@ -256,6 +258,7 @@
}
return FooterActionsButtonViewModel(
+ id = null,
Icon.Loaded(
icon,
ContentDescription.Loaded(userSwitcherContentDescription(status.currentUserName)),
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/UserDetailView.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/UserDetailView.java
index 97476b2..d2d5063 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/UserDetailView.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/UserDetailView.java
@@ -134,7 +134,7 @@
v.bind(name, drawable, item.info.id);
}
v.setActivated(item.isCurrent);
- v.setDisabledByAdmin(mController.isDisabledByAdmin(item));
+ v.setDisabledByAdmin(item.isDisabledByAdmin());
v.setEnabled(item.isSwitchToEnabled);
UserSwitcherController.setSelectableAlpha(v);
@@ -173,16 +173,16 @@
Trace.beginSection("UserDetailView.Adapter#onClick");
UserRecord userRecord =
(UserRecord) view.getTag();
- if (mController.isDisabledByAdmin(userRecord)) {
+ if (userRecord.isDisabledByAdmin()) {
final Intent intent = RestrictedLockUtils.getShowAdminSupportDetailsIntent(
- mContext, mController.getEnforcedAdmin(userRecord));
+ mContext, userRecord.enforcedAdmin);
mController.startActivity(intent);
} else if (userRecord.isSwitchToEnabled) {
MetricsLogger.action(mContext, MetricsEvent.QS_SWITCH_USER);
mUiEventLogger.log(QSUserSwitcherEvent.QS_USER_SWITCH);
if (!userRecord.isAddUser
&& !userRecord.isRestricted
- && !mController.isDisabledByAdmin(userRecord)) {
+ && !userRecord.isDisabledByAdmin()) {
if (mCurrentUserView != null) {
mCurrentUserView.setActivated(false);
}
diff --git a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
index 7e2a5c5..899e57d 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
@@ -342,14 +342,6 @@
}
@Override
- public void notifySwipeToHomeFinished() {
- verifyCallerAndClearCallingIdentity("notifySwipeToHomeFinished", () ->
- mPipOptional.ifPresent(
- pip -> pip.setPinnedStackAnimationType(
- PipAnimationController.ANIM_TYPE_ALPHA)));
- }
-
- @Override
public void notifySwipeUpGestureStarted() {
verifyCallerAndClearCallingIdentityPostMain("notifySwipeUpGestureStarted", () ->
notifySwipeUpGestureStartedInternal());
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ImageExporter.java b/packages/SystemUI/src/com/android/systemui/screenshot/ImageExporter.java
index 55602a9..e3658de 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ImageExporter.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ImageExporter.java
@@ -19,6 +19,7 @@
import static android.os.FileUtils.closeQuietly;
import android.annotation.IntRange;
+import android.content.ContentProvider;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.graphics.Bitmap;
@@ -29,6 +30,7 @@
import android.os.ParcelFileDescriptor;
import android.os.SystemClock;
import android.os.Trace;
+import android.os.UserHandle;
import android.provider.MediaStore;
import android.util.Log;
@@ -142,8 +144,9 @@
*
* @return a listenable future result
*/
- ListenableFuture<Result> export(Executor executor, UUID requestId, Bitmap bitmap) {
- return export(executor, requestId, bitmap, ZonedDateTime.now());
+ ListenableFuture<Result> export(Executor executor, UUID requestId, Bitmap bitmap,
+ UserHandle owner) {
+ return export(executor, requestId, bitmap, ZonedDateTime.now(), owner);
}
/**
@@ -155,10 +158,10 @@
* @return a listenable future result
*/
ListenableFuture<Result> export(Executor executor, UUID requestId, Bitmap bitmap,
- ZonedDateTime captureTime) {
+ ZonedDateTime captureTime, UserHandle owner) {
final Task task = new Task(mResolver, requestId, bitmap, captureTime, mCompressFormat,
- mQuality, /* publish */ true);
+ mQuality, /* publish */ true, owner);
return CallbackToFutureAdapter.getFuture(
(completer) -> {
@@ -174,28 +177,6 @@
);
}
- /**
- * Delete the entry.
- *
- * @param executor the thread for execution
- * @param uri the uri of the image to publish
- *
- * @return a listenable future result
- */
- ListenableFuture<Result> delete(Executor executor, Uri uri) {
- return CallbackToFutureAdapter.getFuture((completer) -> {
- executor.execute(() -> {
- mResolver.delete(uri, null);
-
- Result result = new Result();
- result.uri = uri;
- result.deleted = true;
- completer.set(result);
- });
- return "ContentResolver#delete";
- });
- }
-
static class Result {
Uri uri;
UUID requestId;
@@ -203,7 +184,6 @@
long timestamp;
CompressFormat format;
boolean published;
- boolean deleted;
@Override
public String toString() {
@@ -214,7 +194,6 @@
sb.append(", timestamp=").append(timestamp);
sb.append(", format=").append(format);
sb.append(", published=").append(published);
- sb.append(", deleted=").append(deleted);
sb.append('}');
return sb.toString();
}
@@ -227,17 +206,19 @@
private final ZonedDateTime mCaptureTime;
private final CompressFormat mFormat;
private final int mQuality;
+ private final UserHandle mOwner;
private final String mFileName;
private final boolean mPublish;
Task(ContentResolver resolver, UUID requestId, Bitmap bitmap, ZonedDateTime captureTime,
- CompressFormat format, int quality, boolean publish) {
+ CompressFormat format, int quality, boolean publish, UserHandle owner) {
mResolver = resolver;
mRequestId = requestId;
mBitmap = bitmap;
mCaptureTime = captureTime;
mFormat = format;
mQuality = quality;
+ mOwner = owner;
mFileName = createFilename(mCaptureTime, mFormat);
mPublish = publish;
}
@@ -253,7 +234,7 @@
start = Instant.now();
}
- uri = createEntry(mResolver, mFormat, mCaptureTime, mFileName);
+ uri = createEntry(mResolver, mFormat, mCaptureTime, mFileName, mOwner);
throwIfInterrupted();
writeImage(mResolver, mBitmap, mFormat, mQuality, uri);
@@ -297,15 +278,20 @@
}
private static Uri createEntry(ContentResolver resolver, CompressFormat format,
- ZonedDateTime time, String fileName) throws ImageExportException {
+ ZonedDateTime time, String fileName, UserHandle owner) throws ImageExportException {
Trace.beginSection("ImageExporter_createEntry");
try {
final ContentValues values = createMetadata(time, format, fileName);
- Uri uri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);
+ Uri baseUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
+ if (UserHandle.myUserId() != owner.getIdentifier()) {
+ baseUri = ContentProvider.maybeAddUserId(baseUri, owner.getIdentifier());
+ }
+ Uri uri = resolver.insert(baseUri, values);
if (uri == null) {
throw new ImageExportException(RESOLVER_INSERT_RETURNED_NULL);
}
+ Log.d(TAG, "Inserted new URI: " + uri);
return uri;
} finally {
Trace.endSection();
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/LongScreenshotActivity.java b/packages/SystemUI/src/com/android/systemui/screenshot/LongScreenshotActivity.java
index ba6e98e..8bf956b 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/LongScreenshotActivity.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/LongScreenshotActivity.java
@@ -30,6 +30,7 @@
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Bundle;
+import android.os.Process;
import android.os.UserHandle;
import android.text.TextUtils;
import android.util.Log;
@@ -387,7 +388,9 @@
mOutputBitmap = renderBitmap(drawable, bounds);
ListenableFuture<ImageExporter.Result> exportFuture = mImageExporter.export(
- mBackgroundExecutor, UUID.randomUUID(), mOutputBitmap, ZonedDateTime.now());
+ mBackgroundExecutor, UUID.randomUUID(), mOutputBitmap, ZonedDateTime.now(),
+ // TODO: Owner must match the owner of the captured window.
+ Process.myUserHandle());
exportFuture.addListener(() -> onExportCompleted(action, exportFuture), mUiExecutor);
}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java b/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java
index f248d69..077ad35 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java
@@ -48,6 +48,8 @@
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.config.sysui.SystemUiDeviceConfigFlags;
import com.android.systemui.R;
+import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.flags.Flags;
import com.android.systemui.screenshot.ScreenshotController.SavedImageData.ActionTransition;
import com.google.common.util.concurrent.ListenableFuture;
@@ -71,6 +73,7 @@
private static final String SCREENSHOT_SHARE_SUBJECT_TEMPLATE = "Screenshot (%s)";
private final Context mContext;
+ private FeatureFlags mFlags;
private final ScreenshotSmartActions mScreenshotSmartActions;
private final ScreenshotController.SaveImageInBackgroundData mParams;
private final ScreenshotController.SavedImageData mImageData;
@@ -84,7 +87,10 @@
private final ImageExporter mImageExporter;
private long mImageTime;
- SaveImageInBackgroundTask(Context context, ImageExporter exporter,
+ SaveImageInBackgroundTask(
+ Context context,
+ FeatureFlags flags,
+ ImageExporter exporter,
ScreenshotSmartActions screenshotSmartActions,
ScreenshotController.SaveImageInBackgroundData data,
Supplier<ActionTransition> sharedElementTransition,
@@ -92,6 +98,7 @@
screenshotNotificationSmartActionsProvider
) {
mContext = context;
+ mFlags = flags;
mScreenshotSmartActions = screenshotSmartActions;
mImageData = new ScreenshotController.SavedImageData();
mQuickShareData = new ScreenshotController.QuickShareData();
@@ -117,7 +124,8 @@
}
// TODO: move to constructor / from ScreenshotRequest
final UUID requestId = UUID.randomUUID();
- final UserHandle user = getUserHandleOfForegroundApplication(mContext);
+ final UserHandle user = mFlags.isEnabled(Flags.SCREENSHOT_WORK_PROFILE_POLICY)
+ ? mParams.owner : getUserHandleOfForegroundApplication(mContext);
Thread.currentThread().setPriority(Thread.MAX_PRIORITY);
@@ -133,8 +141,9 @@
// Call synchronously here since already on a background thread.
ListenableFuture<ImageExporter.Result> future =
- mImageExporter.export(Runnable::run, requestId, image);
+ mImageExporter.export(Runnable::run, requestId, image, mParams.owner);
ImageExporter.Result result = future.get();
+ Log.d(TAG, "Saved screenshot: " + result);
final Uri uri = result.uri;
mImageTime = result.timestamp;
@@ -157,6 +166,7 @@
}
mImageData.uri = uri;
+ mImageData.owner = user;
mImageData.smartActions = smartActions;
mImageData.shareTransition = createShareAction(mContext, mContext.getResources(), uri);
mImageData.editTransition = createEditAction(mContext, mContext.getResources(), uri);
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java
index 3fee232..df32d20 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java
@@ -34,6 +34,7 @@
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.annotation.MainThread;
+import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.ActivityManager;
import android.app.ActivityOptions;
@@ -57,7 +58,9 @@
import android.media.MediaPlayer;
import android.net.Uri;
import android.os.Bundle;
+import android.os.Process;
import android.os.RemoteException;
+import android.os.UserHandle;
import android.provider.Settings;
import android.util.DisplayMetrics;
import android.util.Log;
@@ -90,6 +93,7 @@
import com.android.systemui.broadcast.BroadcastSender;
import com.android.systemui.clipboardoverlay.ClipboardOverlayController;
import com.android.systemui.dagger.qualifiers.Main;
+import com.android.systemui.flags.FeatureFlags;
import com.android.systemui.screenshot.ScreenshotController.SavedImageData.ActionTransition;
import com.android.systemui.screenshot.TakeScreenshotService.RequestCallback;
import com.android.systemui.util.Assert;
@@ -151,6 +155,7 @@
public Consumer<Uri> finisher;
public ScreenshotController.ActionsReadyListener mActionsReadyListener;
public ScreenshotController.QuickShareActionReadyListener mQuickShareActionsReadyListener;
+ public UserHandle owner;
void clearImage() {
image = null;
@@ -167,6 +172,8 @@
public Notification.Action deleteAction;
public List<Notification.Action> smartActions;
public Notification.Action quickShareAction;
+ public UserHandle owner;
+
/**
* POD for shared element transition.
@@ -242,6 +249,7 @@
private static final int SCREENSHOT_CORNER_DEFAULT_TIMEOUT_MILLIS = 6000;
private final WindowContext mContext;
+ private final FeatureFlags mFlags;
private final ScreenshotNotificationsController mNotificationsController;
private final ScreenshotSmartActions mScreenshotSmartActions;
private final UiEventLogger mUiEventLogger;
@@ -288,6 +296,7 @@
@Inject
ScreenshotController(
Context context,
+ FeatureFlags flags,
ScreenshotSmartActions screenshotSmartActions,
ScreenshotNotificationsController screenshotNotificationsController,
ScrollCaptureClient scrollCaptureClient,
@@ -331,6 +340,7 @@
final Context displayContext = context.createDisplayContext(getDefaultDisplay());
mContext = (WindowContext) displayContext.createWindowContext(TYPE_SCREENSHOT, null);
mWindowManager = mContext.getSystemService(WindowManager.class);
+ mFlags = flags;
mAccessibilityManager = AccessibilityManager.getInstance(mContext);
@@ -377,7 +387,6 @@
void handleImageAsScreenshot(Bitmap screenshot, Rect screenshotScreenBounds,
Insets visibleInsets, int taskId, int userId, ComponentName topComponent,
Consumer<Uri> finisher, RequestCallback requestCallback) {
- // TODO: use task Id, userId, topComponent for smart handler
Assert.isMainThread();
if (screenshot == null) {
Log.e(TAG, "Got null bitmap from screenshot message");
@@ -395,7 +404,7 @@
}
mCurrentRequestCallback = requestCallback;
saveScreenshot(screenshot, finisher, screenshotScreenBounds, visibleInsets, topComponent,
- showFlash);
+ showFlash, UserHandle.of(userId));
}
/**
@@ -543,14 +552,15 @@
return;
}
- saveScreenshot(screenshot, finisher, screenRect, Insets.NONE, topComponent, true);
+ saveScreenshot(screenshot, finisher, screenRect, Insets.NONE, topComponent, true,
+ Process.myUserHandle());
mBroadcastSender.sendBroadcast(new Intent(ClipboardOverlayController.SCREENSHOT_ACTION),
ClipboardOverlayController.SELF_PERMISSION);
}
private void saveScreenshot(Bitmap screenshot, Consumer<Uri> finisher, Rect screenRect,
- Insets screenInsets, ComponentName topComponent, boolean showFlash) {
+ Insets screenInsets, ComponentName topComponent, boolean showFlash, UserHandle owner) {
withWindowAttached(() ->
mScreenshotView.announceForAccessibility(
mContext.getResources().getString(R.string.screenshot_saving_title)));
@@ -575,11 +585,11 @@
mScreenBitmap = screenshot;
- if (!isUserSetupComplete()) {
+ if (!isUserSetupComplete(owner)) {
Log.w(TAG, "User setup not complete, displaying toast only");
// User setup isn't complete, so we don't want to show any UI beyond a toast, as editing
// and sharing shouldn't be exposed to the user.
- saveScreenshotAndToast(finisher);
+ saveScreenshotAndToast(owner, finisher);
return;
}
@@ -587,7 +597,7 @@
mScreenBitmap.setHasAlpha(false);
mScreenBitmap.prepareToDraw();
- saveScreenshotInWorkerThread(finisher, this::showUiOnActionsReady,
+ saveScreenshotInWorkerThread(owner, finisher, this::showUiOnActionsReady,
this::showUiOnQuickShareActionReady);
// The window is focusable by default
@@ -853,11 +863,12 @@
* Save the bitmap but don't show the normal screenshot UI.. just a toast (or notification on
* failure).
*/
- private void saveScreenshotAndToast(Consumer<Uri> finisher) {
+ private void saveScreenshotAndToast(UserHandle owner, Consumer<Uri> finisher) {
// Play the shutter sound to notify that we've taken a screenshot
playCameraSound();
saveScreenshotInWorkerThread(
+ owner,
/* onComplete */ finisher,
/* actionsReadyListener */ imageData -> {
if (DEBUG_CALLBACK) {
@@ -925,9 +936,11 @@
/**
* Creates a new worker thread and saves the screenshot to the media store.
*/
- private void saveScreenshotInWorkerThread(Consumer<Uri> finisher,
- @Nullable ScreenshotController.ActionsReadyListener actionsReadyListener,
- @Nullable ScreenshotController.QuickShareActionReadyListener
+ private void saveScreenshotInWorkerThread(
+ UserHandle owner,
+ @NonNull Consumer<Uri> finisher,
+ @Nullable ActionsReadyListener actionsReadyListener,
+ @Nullable QuickShareActionReadyListener
quickShareActionsReadyListener) {
ScreenshotController.SaveImageInBackgroundData
data = new ScreenshotController.SaveImageInBackgroundData();
@@ -935,13 +948,14 @@
data.finisher = finisher;
data.mActionsReadyListener = actionsReadyListener;
data.mQuickShareActionsReadyListener = quickShareActionsReadyListener;
+ data.owner = owner;
if (mSaveInBgTask != null) {
// just log success/failure for the pre-existing screenshot
mSaveInBgTask.setActionsReadyListener(this::logSuccessOnActionsReady);
}
- mSaveInBgTask = new SaveImageInBackgroundTask(mContext, mImageExporter,
+ mSaveInBgTask = new SaveImageInBackgroundTask(mContext, mFlags, mImageExporter,
mScreenshotSmartActions, data, getActionTransitionSupplier(),
mScreenshotNotificationSmartActionsProvider);
mSaveInBgTask.execute();
@@ -960,6 +974,15 @@
mScreenshotHandler.resetTimeout();
if (imageData.uri != null) {
+ if (!imageData.owner.equals(Process.myUserHandle())) {
+ // TODO: Handle non-primary user ownership (e.g. Work Profile)
+ // This image is owned by another user. Special treatment will be
+ // required in the UI (badging) as well as sending intents which can
+ // correctly forward those URIs on to be read (actions).
+
+ Log.d(TAG, "*** Screenshot saved to a non-primary user ("
+ + imageData.owner + ") as " + imageData.uri);
+ }
mScreenshotHandler.post(() -> {
if (mScreenshotAnimation != null && mScreenshotAnimation.isRunning()) {
mScreenshotAnimation.addListener(new AnimatorListenerAdapter() {
@@ -1033,9 +1056,9 @@
}
}
- private boolean isUserSetupComplete() {
- return Settings.Secure.getInt(mContext.getContentResolver(),
- SETTINGS_SECURE_USER_SETUP_COMPLETE, 0) == 1;
+ private boolean isUserSetupComplete(UserHandle owner) {
+ return Settings.Secure.getInt(mContext.createContextAsUser(owner, 0)
+ .getContentResolver(), SETTINGS_SECURE_USER_SETUP_COMPLETE, 0) == 1;
}
/**
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotPolicyImpl.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotPolicyImpl.kt
index c2a5060..3a35286 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotPolicyImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotPolicyImpl.kt
@@ -68,7 +68,9 @@
}
override suspend fun isManagedProfile(@UserIdInt userId: Int): Boolean {
- return withContext(bgDispatcher) { userMgr.isManagedProfile(userId) }
+ val managed = withContext(bgDispatcher) { userMgr.isManagedProfile(userId) }
+ Log.d(TAG, "isManagedProfile: $managed")
+ return managed
}
private fun nonPipVisibleTask(info: RootTaskInfo): Boolean {
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScrollCaptureController.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScrollCaptureController.java
index 83b60fb..30a0b8f 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ScrollCaptureController.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScrollCaptureController.java
@@ -78,6 +78,7 @@
static class LongScreenshot {
private final ImageTileSet mImageTileSet;
private final Session mSession;
+ // TODO: Add UserHandle so LongScreenshots can adhere to work profile screenshot policy
LongScreenshot(Session session, ImageTileSet imageTileSet) {
mSession = session;
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
index 7254e09..1110386 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
@@ -425,8 +425,6 @@
new KeyguardClockPositionAlgorithm.Result();
private boolean mIsExpanding;
- private boolean mBlockTouches;
-
/**
* Determines if QS should be already expanded when expanding shade.
* Used for split shade, two finger gesture as well as accessibility shortcut to QS.
@@ -1693,7 +1691,6 @@
public void resetViews(boolean animate) {
mIsLaunchTransitionFinished = false;
- mBlockTouches = false;
mCentralSurfaces.getGutsManager().closeAndSaveGuts(true /* leavebehind */, true /* force */,
true /* controls */, -1 /* x */, -1 /* y */, true /* resetMenu */);
if (animate && !isFullyCollapsed()) {
@@ -4198,7 +4195,7 @@
"NPVC onInterceptTouchEvent (" + event.getId() + "): (" + event.getX()
+ "," + event.getY() + ")");
}
- if (mBlockTouches || mQs.disallowPanelTouches()) {
+ if (mQs.disallowPanelTouches()) {
return false;
}
initDownStates(event);
@@ -4241,8 +4238,7 @@
}
- if (mBlockTouches || (mQsFullyExpanded && mQs != null
- && mQs.disallowPanelTouches())) {
+ if (mQsFullyExpanded && mQs != null && mQs.disallowPanelTouches()) {
return false;
}
@@ -4700,6 +4696,8 @@
if (!animatingUnlockedShadeToKeyguard) {
// Only make the status bar visible if we're not animating the screen off, since
// we only want to be showing the clock/notifications during the animation.
+ mShadeLog.v("Updating keyguard status bar state to "
+ + (keyguardShowing ? "visible" : "invisible"));
mKeyguardStatusBarViewController.updateViewState(
/* alpha= */ 1f,
keyguardShowing ? View.VISIBLE : View.INVISIBLE);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java
index 8699441..c290ce2 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java
@@ -42,7 +42,6 @@
import com.android.internal.statusbar.NotificationVisibility;
import com.android.internal.widget.LockPatternUtils;
-import com.android.systemui.Dependency;
import com.android.systemui.Dumpable;
import com.android.systemui.broadcast.BroadcastDispatcher;
import com.android.systemui.dagger.SysUISingleton;
@@ -97,6 +96,7 @@
private final List<UserChangedListener> mListeners = new ArrayList<>();
private final BroadcastDispatcher mBroadcastDispatcher;
private final NotificationClickNotifier mClickNotifier;
+ private final Lazy<OverviewProxyService> mOverviewProxyServiceLazy;
private boolean mShowLockscreenNotifications;
private boolean mAllowLockscreenRemoteInput;
@@ -157,7 +157,7 @@
break;
case Intent.ACTION_USER_UNLOCKED:
// Start the overview connection to the launcher service
- Dependency.get(OverviewProxyService.class).startConnectionToCurrentUser();
+ mOverviewProxyServiceLazy.get().startConnectionToCurrentUser();
break;
case NOTIFICATION_UNLOCKED_BY_WORK_CHALLENGE_ACTION:
final IntentSender intentSender = intent.getParcelableExtra(
@@ -199,6 +199,7 @@
Lazy<NotificationVisibilityProvider> visibilityProviderLazy,
Lazy<CommonNotifCollection> commonNotifCollectionLazy,
NotificationClickNotifier clickNotifier,
+ Lazy<OverviewProxyService> overviewProxyServiceLazy,
KeyguardManager keyguardManager,
StatusBarStateController statusBarStateController,
@Main Handler mainHandler,
@@ -214,6 +215,7 @@
mVisibilityProviderLazy = visibilityProviderLazy;
mCommonNotifCollectionLazy = commonNotifCollectionLazy;
mClickNotifier = clickNotifier;
+ mOverviewProxyServiceLazy = overviewProxyServiceLazy;
statusBarStateController.addCallback(this);
mLockPatternUtils = new LockPatternUtils(context);
mKeyguardManager = keyguardManager;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java
index c900c5a..4be5a1a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java
@@ -43,7 +43,6 @@
import android.view.View;
import android.widget.ImageView;
-import com.android.systemui.Dependency;
import com.android.systemui.Dumpable;
import com.android.systemui.animation.Interpolators;
import com.android.systemui.colorextraction.SysuiColorExtractor;
@@ -89,11 +88,9 @@
private static final String TAG = "NotificationMediaManager";
public static final boolean DEBUG_MEDIA = false;
- private final StatusBarStateController mStatusBarStateController
- = Dependency.get(StatusBarStateController.class);
- private final SysuiColorExtractor mColorExtractor = Dependency.get(SysuiColorExtractor.class);
- private final KeyguardStateController mKeyguardStateController = Dependency.get(
- KeyguardStateController.class);
+ private final StatusBarStateController mStatusBarStateController;
+ private final SysuiColorExtractor mColorExtractor;
+ private final KeyguardStateController mKeyguardStateController;
private final KeyguardBypassController mKeyguardBypassController;
private static final HashSet<Integer> PAUSED_MEDIA_STATES = new HashSet<>();
private static final HashSet<Integer> CONNECTING_MEDIA_STATES = new HashSet<>();
@@ -179,6 +176,9 @@
NotifCollection notifCollection,
@Main DelayableExecutor mainExecutor,
MediaDataManager mediaDataManager,
+ StatusBarStateController statusBarStateController,
+ SysuiColorExtractor colorExtractor,
+ KeyguardStateController keyguardStateController,
DumpManager dumpManager) {
mContext = context;
mMediaArtworkProcessor = mediaArtworkProcessor;
@@ -192,6 +192,9 @@
mMediaDataManager = mediaDataManager;
mNotifPipeline = notifPipeline;
mNotifCollection = notifCollection;
+ mStatusBarStateController = statusBarStateController;
+ mColorExtractor = colorExtractor;
+ mKeyguardStateController = keyguardStateController;
setupNotifPipeline();
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java
index 7cd79ca..11e3d17 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java
@@ -26,6 +26,7 @@
import com.android.internal.statusbar.IStatusBarService;
import com.android.systemui.animation.ActivityLaunchAnimator;
import com.android.systemui.animation.DialogLaunchAnimator;
+import com.android.systemui.colorextraction.SysuiColorExtractor;
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.dump.DumpManager;
@@ -130,6 +131,9 @@
NotifCollection notifCollection,
@Main DelayableExecutor mainExecutor,
MediaDataManager mediaDataManager,
+ StatusBarStateController statusBarStateController,
+ SysuiColorExtractor colorExtractor,
+ KeyguardStateController keyguardStateController,
DumpManager dumpManager) {
return new NotificationMediaManager(
context,
@@ -142,6 +146,9 @@
notifCollection,
mainExecutor,
mediaDataManager,
+ statusBarStateController,
+ colorExtractor,
+ keyguardStateController,
dumpManager);
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt
index 8278b54..ccf6fec 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt
@@ -393,7 +393,7 @@
val posted = mPostedEntries.compute(entry.key) { _, value ->
value?.also { update ->
update.wasUpdated = true
- update.shouldHeadsUpEver = update.shouldHeadsUpEver || shouldHeadsUpEver
+ update.shouldHeadsUpEver = shouldHeadsUpEver
update.shouldHeadsUpAgain = update.shouldHeadsUpAgain || shouldHeadsUpAgain
update.isAlerting = isAlerting
update.isBinding = isBinding
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerImpl.kt
index 801e544..8eef3f3 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerImpl.kt
@@ -19,6 +19,8 @@
import android.service.notification.StatusBarNotification
import com.android.systemui.ForegroundServiceNotificationListener
import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags
import com.android.systemui.people.widget.PeopleSpaceWidgetManager
import com.android.systemui.plugins.statusbar.NotificationSwipeActionHelper.SnoozeOption
import com.android.systemui.statusbar.NotificationListener
@@ -38,6 +40,7 @@
import com.android.systemui.statusbar.notification.collection.render.NotifStackController
import com.android.systemui.statusbar.notification.interruption.HeadsUpViewBinder
import com.android.systemui.statusbar.notification.logging.NotificationLogger
+import com.android.systemui.statusbar.notification.logging.NotificationMemoryMonitor
import com.android.systemui.statusbar.notification.row.NotifBindPipelineInitializer
import com.android.systemui.statusbar.notification.stack.NotificationListContainer
import com.android.systemui.statusbar.phone.CentralSurfaces
@@ -71,6 +74,8 @@
private val peopleSpaceWidgetManager: PeopleSpaceWidgetManager,
private val bubblesOptional: Optional<Bubbles>,
private val fgsNotifListener: ForegroundServiceNotificationListener,
+ private val memoryMonitor: Lazy<NotificationMemoryMonitor>,
+ private val featureFlags: FeatureFlags
) : NotificationsController {
override fun initialize(
@@ -112,6 +117,9 @@
notificationLogger.setUpWithContainer(listContainer)
peopleSpaceWidgetManager.attach(notificationListener)
fgsNotifListener.init()
+ if (featureFlags.isEnabled(Flags.NOTIFICATION_MEMORY_MONITOR_ENABLED)) {
+ memoryMonitor.get().init()
+ }
}
// TODO: Convert all functions below this line into listeners instead of public methods
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemory.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemory.kt
new file mode 100644
index 0000000..832a739
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemory.kt
@@ -0,0 +1,41 @@
+/*
+ *
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.logging
+
+/** Describes usage of a notification. */
+data class NotificationMemoryUsage(
+ val packageName: String,
+ val notificationId: String,
+ val objectUsage: NotificationObjectUsage,
+)
+
+/**
+ * Describes current memory usage of a [android.app.Notification] object.
+ *
+ * The values are in bytes.
+ */
+data class NotificationObjectUsage(
+ val smallIcon: Int,
+ val largeIcon: Int,
+ val extras: Int,
+ val style: String?,
+ val styleIcon: Int,
+ val bigPicture: Int,
+ val extender: Int,
+ val hasCustomView: Boolean,
+)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryMonitor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryMonitor.kt
new file mode 100644
index 0000000..ef7fa33
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryMonitor.kt
@@ -0,0 +1,239 @@
+/*
+ *
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.logging
+
+import android.app.Notification
+import android.app.Person
+import android.graphics.Bitmap
+import android.graphics.drawable.Icon
+import android.os.Bundle
+import android.os.Parcel
+import android.os.Parcelable
+import android.util.Log
+import androidx.annotation.WorkerThread
+import androidx.core.util.contains
+import com.android.systemui.Dumpable
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.statusbar.notification.NotificationUtils
+import com.android.systemui.statusbar.notification.collection.NotifPipeline
+import com.android.systemui.statusbar.notification.collection.NotificationEntry
+import java.io.PrintWriter
+import javax.inject.Inject
+
+/** This class monitors and logs current Notification memory use. */
+@SysUISingleton
+class NotificationMemoryMonitor
+@Inject
+constructor(
+ val notificationPipeline: NotifPipeline,
+ val dumpManager: DumpManager,
+) : Dumpable {
+
+ companion object {
+ private const val TAG = "NotificationMemMonitor"
+ private const val CAR_EXTENSIONS = "android.car.EXTENSIONS"
+ private const val CAR_EXTENSIONS_LARGE_ICON = "large_icon"
+ private const val TV_EXTENSIONS = "android.tv.EXTENSIONS"
+ private const val WEARABLE_EXTENSIONS = "android.wearable.EXTENSIONS"
+ private const val WEARABLE_EXTENSIONS_BACKGROUND = "background"
+ }
+
+ fun init() {
+ Log.d(TAG, "NotificationMemoryMonitor initialized.")
+ dumpManager.registerDumpable(javaClass.simpleName, this)
+ }
+
+ override fun dump(pw: PrintWriter, args: Array<out String>) {
+ currentNotificationMemoryUse().forEach { use -> pw.println(use.toString()) }
+ }
+
+ @WorkerThread
+ fun currentNotificationMemoryUse(): List<NotificationMemoryUsage> {
+ return notificationMemoryUse(notificationPipeline.allNotifs)
+ }
+
+ /** Returns a list of memory use entries for currently shown notifications. */
+ @WorkerThread
+ fun notificationMemoryUse(
+ notifications: Collection<NotificationEntry>
+ ): List<NotificationMemoryUsage> {
+ return notifications.asSequence().map { entry ->
+ val packageName = entry.sbn.packageName
+ val notificationObjectUsage =
+ computeNotificationObjectUse(entry.sbn.notification, hashSetOf())
+ NotificationMemoryUsage(
+ packageName,
+ NotificationUtils.logKey(entry.sbn.key),
+ notificationObjectUsage)
+ }.toList()
+ }
+
+ /**
+ * Computes the estimated memory usage of a given [Notification] object. It'll attempt to
+ * inspect Bitmaps in the object and provide summary of memory usage.
+ */
+ private fun computeNotificationObjectUse(
+ notification: Notification,
+ seenBitmaps: HashSet<Int>
+ ): NotificationObjectUsage {
+ val extras = notification.extras
+ val smallIconUse = computeIconUse(notification.smallIcon, seenBitmaps)
+ val largeIconUse = computeIconUse(notification.getLargeIcon(), seenBitmaps)
+
+ // Collect memory usage of extra styles
+
+ // Big Picture
+ val bigPictureIconUse =
+ computeParcelableUse(extras, Notification.EXTRA_PICTURE_ICON, seenBitmaps) +
+ computeParcelableUse(extras, Notification.EXTRA_LARGE_ICON_BIG, seenBitmaps)
+ val bigPictureUse =
+ computeParcelableUse(extras, Notification.EXTRA_PICTURE, seenBitmaps) +
+ computeParcelableUse(extras, Notification.EXTRA_PICTURE_ICON, seenBitmaps)
+
+ // People
+ val peopleList = extras.getParcelableArrayList<Person>(Notification.EXTRA_PEOPLE_LIST)
+ val peopleUse =
+ peopleList?.sumOf { person -> computeIconUse(person.icon, seenBitmaps) } ?: 0
+
+ // Calling
+ val callingPersonUse =
+ computeParcelableUse(extras, Notification.EXTRA_CALL_PERSON, seenBitmaps)
+ val verificationIconUse =
+ computeParcelableUse(extras, Notification.EXTRA_VERIFICATION_ICON, seenBitmaps)
+
+ // Messages
+ val messages =
+ Notification.MessagingStyle.Message.getMessagesFromBundleArray(
+ extras.getParcelableArray(Notification.EXTRA_MESSAGES)
+ )
+ val messagesUse =
+ messages.sumOf { msg -> computeIconUse(msg.senderPerson?.icon, seenBitmaps) }
+ val historicMessages =
+ Notification.MessagingStyle.Message.getMessagesFromBundleArray(
+ extras.getParcelableArray(Notification.EXTRA_HISTORIC_MESSAGES)
+ )
+ val historyicMessagesUse =
+ historicMessages.sumOf { msg -> computeIconUse(msg.senderPerson?.icon, seenBitmaps) }
+
+ // Extenders
+ val carExtender = extras.getBundle(CAR_EXTENSIONS)
+ val carExtenderSize = carExtender?.let { computeBundleSize(it) } ?: 0
+ val carExtenderIcon =
+ computeParcelableUse(carExtender, CAR_EXTENSIONS_LARGE_ICON, seenBitmaps)
+
+ val tvExtender = extras.getBundle(TV_EXTENSIONS)
+ val tvExtenderSize = tvExtender?.let { computeBundleSize(it) } ?: 0
+
+ val wearExtender = extras.getBundle(WEARABLE_EXTENSIONS)
+ val wearExtenderSize = wearExtender?.let { computeBundleSize(it) } ?: 0
+ val wearExtenderBackground =
+ computeParcelableUse(wearExtender, WEARABLE_EXTENSIONS_BACKGROUND, seenBitmaps)
+
+ val style = notification.notificationStyle
+ val hasCustomView = notification.contentView != null || notification.bigContentView != null
+ val extrasSize = computeBundleSize(extras)
+
+ return NotificationObjectUsage(
+ smallIconUse,
+ largeIconUse,
+ extrasSize,
+ style?.simpleName,
+ bigPictureIconUse +
+ peopleUse +
+ callingPersonUse +
+ verificationIconUse +
+ messagesUse +
+ historyicMessagesUse,
+ bigPictureUse,
+ carExtenderSize +
+ carExtenderIcon +
+ tvExtenderSize +
+ wearExtenderSize +
+ wearExtenderBackground,
+ hasCustomView
+ )
+ }
+
+ /**
+ * Calculates size of the bundle data (excluding FDs and other shared objects like ashmem
+ * bitmaps). Can be slow.
+ */
+ private fun computeBundleSize(extras: Bundle): Int {
+ val parcel = Parcel.obtain()
+ try {
+ extras.writeToParcel(parcel, 0)
+ return parcel.dataSize()
+ } finally {
+ parcel.recycle()
+ }
+ }
+
+ /**
+ * Deserializes [Icon], [Bitmap] or [Person] from extras and computes its memory use. Returns 0
+ * if the key does not exist in extras.
+ */
+ private fun computeParcelableUse(extras: Bundle?, key: String, seenBitmaps: HashSet<Int>): Int {
+ return when (val parcelable = extras?.getParcelable<Parcelable>(key)) {
+ is Bitmap -> computeBitmapUse(parcelable, seenBitmaps)
+ is Icon -> computeIconUse(parcelable, seenBitmaps)
+ is Person -> computeIconUse(parcelable.icon, seenBitmaps)
+ else -> 0
+ }
+ }
+
+ /**
+ * Calculates the byte size of bitmaps or data in the Icon object. Returns 0 if the icon is
+ * defined via Uri or a resource.
+ *
+ * @return memory usage in bytes or 0 if the icon is Uri/Resource based
+ */
+ private fun computeIconUse(icon: Icon?, seenBitmaps: HashSet<Int>) =
+ when (icon?.type) {
+ Icon.TYPE_BITMAP -> computeBitmapUse(icon.bitmap, seenBitmaps)
+ Icon.TYPE_ADAPTIVE_BITMAP -> computeBitmapUse(icon.bitmap, seenBitmaps)
+ Icon.TYPE_DATA -> computeDataUse(icon, seenBitmaps)
+ else -> 0
+ }
+
+ /**
+ * Returns the amount of memory a given bitmap is using. If the bitmap reference is part of
+ * seenBitmaps set, this method returns 0 to avoid double counting.
+ *
+ * @return memory usage of the bitmap in bytes
+ */
+ private fun computeBitmapUse(bitmap: Bitmap, seenBitmaps: HashSet<Int>? = null): Int {
+ val refId = System.identityHashCode(bitmap)
+ if (seenBitmaps?.contains(refId) == true) {
+ return 0
+ }
+
+ seenBitmaps?.add(refId)
+ return bitmap.allocationByteCount
+ }
+
+ private fun computeDataUse(icon: Icon, seenBitmaps: HashSet<Int>): Int {
+ val refId = System.identityHashCode(icon.dataBytes)
+ if (seenBitmaps.contains(refId)) {
+ return 0
+ }
+
+ seenBitmaps.add(refId)
+ return icon.dataLength
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationPanelLogger.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationPanelLogger.java
index 9faef1b..5ca13c9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationPanelLogger.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationPanelLogger.java
@@ -45,11 +45,21 @@
void logPanelShown(boolean isLockscreen,
@Nullable List<NotificationEntry> visibleNotifications);
+ /**
+ * Log a NOTIFICATION_PANEL_REPORTED statsd event, with
+ * {@link NotificationPanelEvent#NOTIFICATION_DRAG} as the eventID.
+ *
+ * @param draggedNotification the notification that is being dragged
+ */
+ void logNotificationDrag(NotificationEntry draggedNotification);
+
enum NotificationPanelEvent implements UiEventLogger.UiEventEnum {
@UiEvent(doc = "Notification panel shown from status bar.")
NOTIFICATION_PANEL_OPEN_STATUS_BAR(200),
@UiEvent(doc = "Notification panel shown from lockscreen.")
- NOTIFICATION_PANEL_OPEN_LOCKSCREEN(201);
+ NOTIFICATION_PANEL_OPEN_LOCKSCREEN(201),
+ @UiEvent(doc = "Notification was dragged")
+ NOTIFICATION_DRAG(1226);
private final int mId;
NotificationPanelEvent(int id) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationPanelLoggerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationPanelLoggerImpl.java
index 75a6019..9a63228 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationPanelLoggerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationPanelLoggerImpl.java
@@ -16,12 +16,15 @@
package com.android.systemui.statusbar.notification.logging;
+import static com.android.systemui.statusbar.notification.logging.NotificationPanelLogger.NotificationPanelEvent.NOTIFICATION_DRAG;
+
import com.android.systemui.shared.system.SysUiStatsLog;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
import com.android.systemui.statusbar.notification.logging.nano.Notifications;
import com.google.protobuf.nano.MessageNano;
+import java.util.Collections;
import java.util.List;
/**
@@ -38,4 +41,14 @@
/* int num_notifications*/ proto.notifications.length,
/* byte[] notifications*/ MessageNano.toByteArray(proto));
}
+
+ @Override
+ public void logNotificationDrag(NotificationEntry draggedNotification) {
+ final Notifications.NotificationList proto = NotificationPanelLogger.toNotificationProto(
+ Collections.singletonList(draggedNotification));
+ SysUiStatsLog.write(SysUiStatsLog.NOTIFICATION_PANEL_REPORTED,
+ /* int event_id */ NOTIFICATION_DRAG.getId(),
+ /* int num_notifications*/ proto.notifications.length,
+ /* byte[] notifications*/ MessageNano.toByteArray(proto));
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowDragController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowDragController.java
index 4939a9c..64f87ca 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowDragController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowDragController.java
@@ -45,12 +45,17 @@
import androidx.annotation.VisibleForTesting;
+import com.android.internal.logging.InstanceId;
+import com.android.internal.logging.InstanceIdSequence;
import com.android.systemui.R;
import com.android.systemui.animation.Interpolators;
import com.android.systemui.shade.ShadeController;
import com.android.systemui.statusbar.CommandQueue;
+import com.android.systemui.statusbar.notification.logging.NotificationPanelLogger;
import com.android.systemui.statusbar.policy.HeadsUpManager;
+import java.util.Collections;
+
import javax.inject.Inject;
/**
@@ -63,14 +68,17 @@
private final Context mContext;
private final HeadsUpManager mHeadsUpManager;
private final ShadeController mShadeController;
+ private NotificationPanelLogger mNotificationPanelLogger;
@Inject
public ExpandableNotificationRowDragController(Context context,
HeadsUpManager headsUpManager,
- ShadeController shadeController) {
+ ShadeController shadeController,
+ NotificationPanelLogger notificationPanelLogger) {
mContext = context;
mHeadsUpManager = headsUpManager;
mShadeController = shadeController;
+ mNotificationPanelLogger = notificationPanelLogger;
init();
}
@@ -120,12 +128,16 @@
dragIntent.putExtra(ClipDescription.EXTRA_PENDING_INTENT, contentIntent);
dragIntent.putExtra(Intent.EXTRA_USER, android.os.Process.myUserHandle());
ClipData.Item item = new ClipData.Item(dragIntent);
+ InstanceId instanceId = new InstanceIdSequence(Integer.MAX_VALUE).newInstanceId();
+ item.getIntent().putExtra(ClipDescription.EXTRA_LOGGING_INSTANCE_ID, instanceId);
ClipData dragData = new ClipData(clipDescription, item);
View.DragShadowBuilder myShadow = new View.DragShadowBuilder(snapshot);
view.setOnDragListener(getDraggedViewDragListener());
boolean result = view.startDragAndDrop(dragData, myShadow, null, View.DRAG_FLAG_GLOBAL
| View.DRAG_FLAG_REQUEST_SURFACE_FOR_RETURN_ANIMATION);
if (result) {
+ // Log notification drag only if it succeeds
+ mNotificationPanelLogger.logNotificationDrag(enr.getEntry());
view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
if (enr.isPinned()) {
mHeadsUpManager.releaseAllImmediately();
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 f7ce43b..b2c7a82 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
@@ -4434,10 +4434,11 @@
Trace.beginSection("CentralSurfaces#updateDozing");
mDozing = isDozing;
- // Collapse the notification panel if open
boolean dozingAnimated = mDozeServiceHost.getDozingRequested()
&& mDozeParameters.shouldControlScreenOff();
- mNotificationPanelViewController.resetViews(dozingAnimated);
+ // resetting views is already done when going into doze, there's no need to
+ // reset them again when we're waking up
+ mNotificationPanelViewController.resetViews(dozingAnimated && isDozing);
updateQsExpansionEnabled();
mKeyguardViewMediator.setDozing(mDozing);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeScrimController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeScrimController.java
index 7de4668..0067316 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeScrimController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeScrimController.java
@@ -18,7 +18,6 @@
import android.annotation.NonNull;
import android.os.Handler;
-import android.util.Log;
import com.android.internal.annotations.VisibleForTesting;
import com.android.systemui.dagger.SysUISingleton;
@@ -34,9 +33,6 @@
*/
@SysUISingleton
public class DozeScrimController implements StateListener {
- private static final String TAG = "DozeScrimController";
- private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
-
private final DozeLog mDozeLog;
private final DozeParameters mDozeParameters;
private final Handler mHandler = new Handler();
@@ -44,28 +40,26 @@
private boolean mDozing;
private DozeHost.PulseCallback mPulseCallback;
private int mPulseReason;
- private boolean mFullyPulsing;
private final ScrimController.Callback mScrimCallback = new ScrimController.Callback() {
@Override
public void onDisplayBlanked() {
- if (DEBUG) {
- Log.d(TAG, "Pulse in, mDozing=" + mDozing + " mPulseReason="
- + DozeLog.reasonToString(mPulseReason));
- }
if (!mDozing) {
+ mDozeLog.tracePulseDropped("onDisplayBlanked - not dozing");
return;
}
- // Signal that the pulse is ready to turn the screen on and draw.
- pulseStarted();
+ if (mPulseCallback != null) {
+ // Signal that the pulse is ready to turn the screen on and draw.
+ mDozeLog.tracePulseStart(mPulseReason);
+ mPulseCallback.onPulseStarted();
+ }
}
@Override
public void onFinished() {
- if (DEBUG) {
- Log.d(TAG, "Pulse in finished, mDozing=" + mDozing);
- }
+ mDozeLog.tracePulseEvent("scrimCallback-onFinished", mDozing, mPulseReason);
+
if (!mDozing) {
return;
}
@@ -78,7 +72,6 @@
mHandler.postDelayed(mPulseOutExtended,
mDozeParameters.getPulseVisibleDurationExtended());
}
- mFullyPulsing = true;
}
/**
@@ -118,19 +111,14 @@
}
if (!mDozing || mPulseCallback != null) {
- if (DEBUG) {
- Log.d(TAG, "Pulse suppressed. Dozing: " + mDozeParameters + " had callback? "
- + (mPulseCallback != null));
- }
// Pulse suppressed.
callback.onPulseFinished();
if (!mDozing) {
- mDozeLog.tracePulseDropped("device isn't dozing");
+ mDozeLog.tracePulseDropped("pulse - device isn't dozing");
} else {
- mDozeLog.tracePulseDropped("already has pulse callback mPulseCallback="
+ mDozeLog.tracePulseDropped("pulse - already has pulse callback mPulseCallback="
+ mPulseCallback);
}
-
return;
}
@@ -141,9 +129,7 @@
}
public void pulseOutNow() {
- if (mPulseCallback != null && mFullyPulsing) {
- mPulseOut.run();
- }
+ mPulseOut.run();
}
public boolean isPulsing() {
@@ -168,24 +154,16 @@
private void cancelPulsing() {
if (mPulseCallback != null) {
- if (DEBUG) Log.d(TAG, "Cancel pulsing");
- mFullyPulsing = false;
+ mDozeLog.tracePulseEvent("cancel", mDozing, mPulseReason);
mHandler.removeCallbacks(mPulseOut);
mHandler.removeCallbacks(mPulseOutExtended);
pulseFinished();
}
}
- private void pulseStarted() {
- mDozeLog.tracePulseStart(mPulseReason);
- if (mPulseCallback != null) {
- mPulseCallback.onPulseStarted();
- }
- }
-
private void pulseFinished() {
- mDozeLog.tracePulseFinish();
if (mPulseCallback != null) {
+ mDozeLog.tracePulseFinish();
mPulseCallback.onPulseFinished();
mPulseCallback = null;
}
@@ -202,10 +180,9 @@
private final Runnable mPulseOut = new Runnable() {
@Override
public void run() {
- mFullyPulsing = false;
mHandler.removeCallbacks(mPulseOut);
mHandler.removeCallbacks(mPulseOutExtended);
- if (DEBUG) Log.d(TAG, "Pulse out, mDozing=" + mDozing);
+ mDozeLog.tracePulseEvent("out", mDozing, mPulseReason);
if (!mDozing) return;
pulseFinished();
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeServiceHost.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeServiceHost.java
index 24ce5e9..5196e10 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeServiceHost.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeServiceHost.java
@@ -36,7 +36,6 @@
import com.android.systemui.doze.DozeHost;
import com.android.systemui.doze.DozeLog;
import com.android.systemui.doze.DozeReceiver;
-import com.android.systemui.keyguard.KeyguardViewMediator;
import com.android.systemui.keyguard.WakefulnessLifecycle;
import com.android.systemui.shade.NotificationPanelViewController;
import com.android.systemui.shade.NotificationShadeWindowViewController;
@@ -48,6 +47,7 @@
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
import com.android.systemui.statusbar.policy.BatteryController;
import com.android.systemui.statusbar.policy.DeviceProvisionedController;
+import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener;
import com.android.systemui.util.Assert;
import java.util.ArrayList;
@@ -80,7 +80,6 @@
private final BatteryController mBatteryController;
private final ScrimController mScrimController;
private final Lazy<BiometricUnlockController> mBiometricUnlockControllerLazy;
- private final KeyguardViewMediator mKeyguardViewMediator;
private final Lazy<AssistManager> mAssistManagerLazy;
private final DozeScrimController mDozeScrimController;
private final KeyguardUpdateMonitor mKeyguardUpdateMonitor;
@@ -95,6 +94,7 @@
private View mAmbientIndicationContainer;
private CentralSurfaces mCentralSurfaces;
private boolean mAlwaysOnSuppressed;
+ private boolean mPulsePending;
@Inject
public DozeServiceHost(DozeLog dozeLog, PowerManager powerManager,
@@ -104,7 +104,6 @@
HeadsUpManagerPhone headsUpManagerPhone, BatteryController batteryController,
ScrimController scrimController,
Lazy<BiometricUnlockController> biometricUnlockControllerLazy,
- KeyguardViewMediator keyguardViewMediator,
Lazy<AssistManager> assistManagerLazy,
DozeScrimController dozeScrimController, KeyguardUpdateMonitor keyguardUpdateMonitor,
PulseExpansionHandler pulseExpansionHandler,
@@ -122,7 +121,6 @@
mBatteryController = batteryController;
mScrimController = scrimController;
mBiometricUnlockControllerLazy = biometricUnlockControllerLazy;
- mKeyguardViewMediator = keyguardViewMediator;
mAssistManagerLazy = assistManagerLazy;
mDozeScrimController = dozeScrimController;
mKeyguardUpdateMonitor = keyguardUpdateMonitor;
@@ -131,6 +129,7 @@
mNotificationWakeUpCoordinator = notificationWakeUpCoordinator;
mAuthController = authController;
mNotificationIconAreaController = notificationIconAreaController;
+ mHeadsUpManagerPhone.addListener(mOnHeadsUpChangedListener);
}
// TODO: we should try to not pass status bar in here if we can avoid it.
@@ -246,7 +245,7 @@
mDozeScrimController.pulse(new PulseCallback() {
@Override
public void onPulseStarted() {
- callback.onPulseStarted();
+ callback.onPulseStarted(); // requestState(DozeMachine.State.DOZE_PULSING)
mCentralSurfaces.updateNotificationPanelTouchState();
setPulsing(true);
}
@@ -254,7 +253,7 @@
@Override
public void onPulseFinished() {
mPulsing = false;
- callback.onPulseFinished();
+ callback.onPulseFinished(); // requestState(DozeMachine.State.DOZE_PULSE_DONE)
mCentralSurfaces.updateNotificationPanelTouchState();
mScrimController.setWakeLockScreenSensorActive(false);
setPulsing(false);
@@ -338,9 +337,8 @@
@Override
public void stopPulsing() {
- if (mDozeScrimController.isPulsing()) {
- mDozeScrimController.pulseOutNow();
- }
+ setPulsePending(false); // prevent any pending pulses from continuing
+ mDozeScrimController.pulseOutNow();
}
@Override
@@ -451,6 +449,16 @@
}
}
+ @Override
+ public boolean isPulsePending() {
+ return mPulsePending;
+ }
+
+ @Override
+ public void setPulsePending(boolean isPulsePending) {
+ mPulsePending = isPulsePending;
+ }
+
/**
* Whether always-on-display is being suppressed. This does not affect wakeup gestures like
* pickup and tap.
@@ -458,4 +466,22 @@
public boolean isAlwaysOnSuppressed() {
return mAlwaysOnSuppressed;
}
+
+ final OnHeadsUpChangedListener mOnHeadsUpChangedListener = new OnHeadsUpChangedListener() {
+ @Override
+ public void onHeadsUpStateChanged(NotificationEntry entry, boolean isHeadsUp) {
+ if (mStatusBarStateController.isDozing() && isHeadsUp) {
+ entry.setPulseSuppressed(false);
+ fireNotificationPulse(entry);
+ if (isPulsing()) {
+ mDozeScrimController.cancelPendingPulseTimeout();
+ }
+ }
+ if (!isHeadsUp && !mHeadsUpManagerPhone.hasNotifications()) {
+ // There are no longer any notifications to show. We should end the
+ // pulse now.
+ stopPulsing();
+ }
+ }
+ };
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java
index 0026b71..054bd28 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java
@@ -40,6 +40,7 @@
import com.android.keyguard.CarrierTextController;
import com.android.keyguard.KeyguardUpdateMonitor;
import com.android.keyguard.KeyguardUpdateMonitorCallback;
+import com.android.keyguard.logging.KeyguardLogger;
import com.android.systemui.R;
import com.android.systemui.animation.Interpolators;
import com.android.systemui.battery.BatteryMeterViewController;
@@ -116,6 +117,7 @@
private final CommandQueue mCommandQueue;
private final Executor mMainExecutor;
private final Object mLock = new Object();
+ private final KeyguardLogger mLogger;
private final ConfigurationController.ConfigurationListener mConfigurationListener =
new ConfigurationController.ConfigurationListener() {
@@ -279,7 +281,8 @@
StatusBarUserInfoTracker statusBarUserInfoTracker,
SecureSettings secureSettings,
CommandQueue commandQueue,
- @Main Executor mainExecutor
+ @Main Executor mainExecutor,
+ KeyguardLogger logger
) {
super(view);
mCarrierTextController = carrierTextController;
@@ -304,6 +307,7 @@
mSecureSettings = secureSettings;
mCommandQueue = commandQueue;
mMainExecutor = mainExecutor;
+ mLogger = logger;
mFirstBypassAttempt = mKeyguardBypassController.getBypassEnabled();
mKeyguardStateController.addCallback(
@@ -430,6 +434,7 @@
/** Animate the keyguard status bar in. */
public void animateKeyguardStatusBarIn() {
+ mLogger.d("animating status bar in");
if (mDisableStateTracker.isDisabled()) {
// If our view is disabled, don't allow us to animate in.
return;
@@ -445,6 +450,7 @@
/** Animate the keyguard status bar out. */
public void animateKeyguardStatusBarOut(long startDelay, long duration) {
+ mLogger.d("animating status bar out");
ValueAnimator anim = ValueAnimator.ofFloat(mView.getAlpha(), 0f);
anim.addUpdateListener(mAnimatorUpdateListener);
anim.setStartDelay(startDelay);
@@ -481,6 +487,9 @@
newAlpha = Math.min(getKeyguardContentsAlpha(), alphaQsExpansion)
* mKeyguardStatusBarAnimateAlpha
* (1.0f - mKeyguardHeadsUpShowingAmount);
+ if (newAlpha != mView.getAlpha() && (newAlpha == 0 || newAlpha == 1)) {
+ mLogger.logStatusBarCalculatedAlpha(newAlpha);
+ }
}
boolean hideForBypass =
@@ -503,6 +512,10 @@
if (mDisableStateTracker.isDisabled()) {
visibility = View.INVISIBLE;
}
+ if (visibility != mView.getVisibility()) {
+ mLogger.logStatusBarAlphaVisibility(visibility, alpha,
+ StatusBarState.toString(mStatusBarState));
+ }
mView.setAlpha(alpha);
mView.setVisibility(visibility);
}
@@ -596,6 +609,8 @@
pw.println("KeyguardStatusBarView:");
pw.println(" mBatteryListening: " + mBatteryListening);
pw.println(" mExplicitAlpha: " + mExplicitAlpha);
+ pw.println(" alpha: " + mView.getAlpha());
+ pw.println(" visibility: " + mView.getVisibility());
mView.dump(pw, args);
}
@@ -605,6 +620,10 @@
* @param alpha a value between 0 and 1. -1 if the value is to be reset/ignored.
*/
public void setAlpha(float alpha) {
+ if (mExplicitAlpha != alpha && (mExplicitAlpha == -1 || alpha == -1)) {
+ // logged if value changed to ignored or from ignored
+ mLogger.logStatusBarExplicitAlpha(alpha);
+ }
mExplicitAlpha = alpha;
updateViewState();
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarHeadsUpChangeListener.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarHeadsUpChangeListener.java
index ae201e3..5512bed 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarHeadsUpChangeListener.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarHeadsUpChangeListener.java
@@ -21,8 +21,6 @@
import com.android.systemui.statusbar.NotificationRemoteInputManager;
import com.android.systemui.statusbar.NotificationShadeWindowController;
import com.android.systemui.statusbar.StatusBarState;
-import com.android.systemui.statusbar.notification.collection.NotificationEntry;
-import com.android.systemui.statusbar.notification.init.NotificationsController;
import com.android.systemui.statusbar.phone.dagger.CentralSurfacesComponent;
import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener;
import com.android.systemui.statusbar.window.StatusBarWindowController;
@@ -41,9 +39,6 @@
private final HeadsUpManagerPhone mHeadsUpManager;
private final StatusBarStateController mStatusBarStateController;
private final NotificationRemoteInputManager mNotificationRemoteInputManager;
- private final NotificationsController mNotificationsController;
- private final DozeServiceHost mDozeServiceHost;
- private final DozeScrimController mDozeScrimController;
@Inject
StatusBarHeadsUpChangeListener(
@@ -53,10 +48,7 @@
KeyguardBypassController keyguardBypassController,
HeadsUpManagerPhone headsUpManager,
StatusBarStateController statusBarStateController,
- NotificationRemoteInputManager notificationRemoteInputManager,
- NotificationsController notificationsController,
- DozeServiceHost dozeServiceHost,
- DozeScrimController dozeScrimController) {
+ NotificationRemoteInputManager notificationRemoteInputManager) {
mNotificationShadeWindowController = notificationShadeWindowController;
mStatusBarWindowController = statusBarWindowController;
@@ -65,9 +57,6 @@
mHeadsUpManager = headsUpManager;
mStatusBarStateController = statusBarStateController;
mNotificationRemoteInputManager = notificationRemoteInputManager;
- mNotificationsController = notificationsController;
- mDozeServiceHost = dozeServiceHost;
- mDozeScrimController = dozeScrimController;
}
@Override
@@ -117,20 +106,4 @@
}
}
}
-
- @Override
- public void onHeadsUpStateChanged(NotificationEntry entry, boolean isHeadsUp) {
- if (mStatusBarStateController.isDozing() && isHeadsUp) {
- entry.setPulseSuppressed(false);
- mDozeServiceHost.fireNotificationPulse(entry);
- if (mDozeServiceHost.isPulsing()) {
- mDozeScrimController.cancelPendingPulseTimeout();
- }
- }
- if (!isHeadsUp && !mHeadsUpManager.hasNotifications()) {
- // There are no longer any notifications to show. We should end the
- //pulse now.
- mDozeScrimController.pulseOutNow();
- }
- }
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardUserSwitcherController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardUserSwitcherController.java
index 0995a00..712953e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardUserSwitcherController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardUserSwitcherController.java
@@ -505,7 +505,7 @@
v.bind(name, drawable, item.info.id);
}
v.setActivated(item.isCurrent);
- v.setDisabledByAdmin(getController().isDisabledByAdmin(item));
+ v.setDisabledByAdmin(item.isDisabledByAdmin());
v.setEnabled(item.isSwitchToEnabled);
UserSwitcherController.setSelectableAlpha(v);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.kt
index 843c232..146b222 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.kt
@@ -19,7 +19,6 @@
import android.annotation.UserIdInt
import android.content.Intent
import android.view.View
-import com.android.settingslib.RestrictedLockUtils.EnforcedAdmin
import com.android.systemui.Dumpable
import com.android.systemui.qs.user.UserSwitchDialogController.DialogShower
import com.android.systemui.user.data.source.UserRecord
@@ -130,12 +129,6 @@
/** Whether keyguard is showing. */
val isKeyguardShowing: Boolean
- /** Returns the [EnforcedAdmin] for the given record, or `null` if there isn't one. */
- fun getEnforcedAdmin(record: UserRecord): EnforcedAdmin?
-
- /** Returns `true` if the given record is disabled by the admin; `false` otherwise. */
- fun isDisabledByAdmin(record: UserRecord): Boolean
-
/** Starts an activity with the given [Intent]. */
fun startActivity(intent: Intent)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherControllerImpl.kt
index 12834f6..1692656 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherControllerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherControllerImpl.kt
@@ -17,13 +17,21 @@
package com.android.systemui.statusbar.policy
+import android.content.Context
import android.content.Intent
import android.view.View
-import com.android.settingslib.RestrictedLockUtils
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.flags.FeatureFlags
import com.android.systemui.flags.Flags
+import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
+import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.qs.user.UserSwitchDialogController
import com.android.systemui.user.data.source.UserRecord
+import com.android.systemui.user.domain.interactor.GuestUserInteractor
+import com.android.systemui.user.domain.interactor.UserInteractor
+import com.android.systemui.user.legacyhelper.data.LegacyUserDataHelper
+import com.android.systemui.user.legacyhelper.ui.LegacyUserUiHelper
import dagger.Lazy
import java.io.PrintWriter
import java.lang.ref.WeakReference
@@ -31,58 +39,76 @@
import kotlinx.coroutines.flow.Flow
/** Implementation of [UserSwitcherController]. */
+@SysUISingleton
class UserSwitcherControllerImpl
@Inject
constructor(
- private val flags: FeatureFlags,
+ @Application private val applicationContext: Context,
+ flags: FeatureFlags,
@Suppress("DEPRECATION") private val oldImpl: Lazy<UserSwitcherControllerOldImpl>,
+ private val userInteractorLazy: Lazy<UserInteractor>,
+ private val guestUserInteractorLazy: Lazy<GuestUserInteractor>,
+ private val keyguardInteractorLazy: Lazy<KeyguardInteractor>,
+ private val activityStarter: ActivityStarter,
) : UserSwitcherController {
- private val isNewImpl: Boolean
- get() = flags.isEnabled(Flags.REFACTORED_USER_SWITCHER_CONTROLLER)
+ private val useInteractor: Boolean =
+ flags.isEnabled(Flags.USER_CONTROLLER_USES_INTERACTOR) &&
+ !flags.isEnabled(Flags.USER_INTERACTOR_AND_REPO_USE_CONTROLLER)
private val _oldImpl: UserSwitcherControllerOldImpl
get() = oldImpl.get()
+ private val userInteractor: UserInteractor by lazy { userInteractorLazy.get() }
+ private val guestUserInteractor: GuestUserInteractor by lazy { guestUserInteractorLazy.get() }
+ private val keyguardInteractor: KeyguardInteractor by lazy { keyguardInteractorLazy.get() }
- private fun notYetImplemented(): Nothing {
- error("Not yet implemented!")
+ private val callbackCompatMap =
+ mutableMapOf<UserSwitcherController.UserSwitchCallback, UserInteractor.UserCallback>()
+
+ private fun notSupported(): Nothing {
+ error("Not supported in the new implementation!")
}
override val users: ArrayList<UserRecord>
get() =
- if (isNewImpl) {
- notYetImplemented()
+ if (useInteractor) {
+ userInteractor.userRecords.value
} else {
_oldImpl.users
}
override val isSimpleUserSwitcher: Boolean
get() =
- if (isNewImpl) {
- notYetImplemented()
+ if (useInteractor) {
+ userInteractor.isSimpleUserSwitcher
} else {
_oldImpl.isSimpleUserSwitcher
}
override fun init(view: View) {
- if (isNewImpl) {
- notYetImplemented()
- } else {
+ if (!useInteractor) {
_oldImpl.init(view)
}
}
override val currentUserRecord: UserRecord?
get() =
- if (isNewImpl) {
- notYetImplemented()
+ if (useInteractor) {
+ userInteractor.selectedUserRecord.value
} else {
_oldImpl.currentUserRecord
}
override val currentUserName: String?
get() =
- if (isNewImpl) {
- notYetImplemented()
+ if (useInteractor) {
+ currentUserRecord?.let {
+ LegacyUserUiHelper.getUserRecordName(
+ context = applicationContext,
+ record = it,
+ isGuestUserAutoCreated = userInteractor.isGuestUserAutoCreated,
+ isGuestUserResetting = userInteractor.isGuestUserResetting,
+ )
+ }
} else {
_oldImpl.currentUserName
}
@@ -91,8 +117,8 @@
userId: Int,
dialogShower: UserSwitchDialogController.DialogShower?
) {
- if (isNewImpl) {
- notYetImplemented()
+ if (useInteractor) {
+ userInteractor.selectUser(userId)
} else {
_oldImpl.onUserSelected(userId, dialogShower)
}
@@ -100,24 +126,24 @@
override val isAddUsersFromLockScreenEnabled: Flow<Boolean>
get() =
- if (isNewImpl) {
- notYetImplemented()
+ if (useInteractor) {
+ notSupported()
} else {
_oldImpl.isAddUsersFromLockScreenEnabled
}
override val isGuestUserAutoCreated: Boolean
get() =
- if (isNewImpl) {
- notYetImplemented()
+ if (useInteractor) {
+ userInteractor.isGuestUserAutoCreated
} else {
_oldImpl.isGuestUserAutoCreated
}
override val isGuestUserResetting: Boolean
get() =
- if (isNewImpl) {
- notYetImplemented()
+ if (useInteractor) {
+ userInteractor.isGuestUserResetting
} else {
_oldImpl.isGuestUserResetting
}
@@ -125,40 +151,48 @@
override fun createAndSwitchToGuestUser(
dialogShower: UserSwitchDialogController.DialogShower?,
) {
- if (isNewImpl) {
- notYetImplemented()
+ if (useInteractor) {
+ notSupported()
} else {
_oldImpl.createAndSwitchToGuestUser(dialogShower)
}
}
override fun showAddUserDialog(dialogShower: UserSwitchDialogController.DialogShower?) {
- if (isNewImpl) {
- notYetImplemented()
+ if (useInteractor) {
+ notSupported()
} else {
_oldImpl.showAddUserDialog(dialogShower)
}
}
override fun startSupervisedUserActivity() {
- if (isNewImpl) {
- notYetImplemented()
+ if (useInteractor) {
+ notSupported()
} else {
_oldImpl.startSupervisedUserActivity()
}
}
override fun onDensityOrFontScaleChanged() {
- if (isNewImpl) {
- notYetImplemented()
- } else {
+ if (!useInteractor) {
_oldImpl.onDensityOrFontScaleChanged()
}
}
override fun addAdapter(adapter: WeakReference<BaseUserSwitcherAdapter>) {
- if (isNewImpl) {
- notYetImplemented()
+ if (useInteractor) {
+ userInteractor.addCallback(
+ object : UserInteractor.UserCallback {
+ override fun isEvictable(): Boolean {
+ return adapter.get() == null
+ }
+
+ override fun onUserStateChanged() {
+ adapter.get()?.notifyDataSetChanged()
+ }
+ }
+ )
} else {
_oldImpl.addAdapter(adapter)
}
@@ -168,16 +202,23 @@
record: UserRecord,
dialogShower: UserSwitchDialogController.DialogShower?,
) {
- if (isNewImpl) {
- notYetImplemented()
+ if (useInteractor) {
+ if (LegacyUserDataHelper.isUser(record)) {
+ userInteractor.selectUser(record.resolveId())
+ } else {
+ userInteractor.executeAction(LegacyUserDataHelper.toUserActionModel(record))
+ }
} else {
_oldImpl.onUserListItemClicked(record, dialogShower)
}
}
override fun removeGuestUser(guestUserId: Int, targetUserId: Int) {
- if (isNewImpl) {
- notYetImplemented()
+ if (useInteractor) {
+ userInteractor.removeGuestUser(
+ guestUserId = guestUserId,
+ targetUserId = targetUserId,
+ )
} else {
_oldImpl.removeGuestUser(guestUserId, targetUserId)
}
@@ -188,16 +229,16 @@
targetUserId: Int,
forceRemoveGuestOnExit: Boolean
) {
- if (isNewImpl) {
- notYetImplemented()
+ if (useInteractor) {
+ userInteractor.exitGuestUser(guestUserId, targetUserId, forceRemoveGuestOnExit)
} else {
_oldImpl.exitGuestUser(guestUserId, targetUserId, forceRemoveGuestOnExit)
}
}
override fun schedulePostBootGuestCreation() {
- if (isNewImpl) {
- notYetImplemented()
+ if (useInteractor) {
+ guestUserInteractor.onDeviceBootCompleted()
} else {
_oldImpl.schedulePostBootGuestCreation()
}
@@ -205,63 +246,57 @@
override val isKeyguardShowing: Boolean
get() =
- if (isNewImpl) {
- notYetImplemented()
+ if (useInteractor) {
+ keyguardInteractor.isKeyguardShowing()
} else {
_oldImpl.isKeyguardShowing
}
- override fun getEnforcedAdmin(record: UserRecord): RestrictedLockUtils.EnforcedAdmin? {
- return if (isNewImpl) {
- notYetImplemented()
- } else {
- _oldImpl.getEnforcedAdmin(record)
- }
- }
-
- override fun isDisabledByAdmin(record: UserRecord): Boolean {
- return if (isNewImpl) {
- notYetImplemented()
- } else {
- _oldImpl.isDisabledByAdmin(record)
- }
- }
-
override fun startActivity(intent: Intent) {
- if (isNewImpl) {
- notYetImplemented()
+ if (useInteractor) {
+ activityStarter.startActivity(intent, /* dismissShade= */ false)
} else {
_oldImpl.startActivity(intent)
}
}
override fun refreshUsers(forcePictureLoadForId: Int) {
- if (isNewImpl) {
- notYetImplemented()
+ if (useInteractor) {
+ userInteractor.refreshUsers()
} else {
_oldImpl.refreshUsers(forcePictureLoadForId)
}
}
override fun addUserSwitchCallback(callback: UserSwitcherController.UserSwitchCallback) {
- if (isNewImpl) {
- notYetImplemented()
+ if (useInteractor) {
+ val interactorCallback =
+ object : UserInteractor.UserCallback {
+ override fun onUserStateChanged() {
+ callback.onUserSwitched()
+ }
+ }
+ callbackCompatMap[callback] = interactorCallback
+ userInteractor.addCallback(interactorCallback)
} else {
_oldImpl.addUserSwitchCallback(callback)
}
}
override fun removeUserSwitchCallback(callback: UserSwitcherController.UserSwitchCallback) {
- if (isNewImpl) {
- notYetImplemented()
+ if (useInteractor) {
+ val interactorCallback = callbackCompatMap.remove(callback)
+ if (interactorCallback != null) {
+ userInteractor.removeCallback(interactorCallback)
+ }
} else {
_oldImpl.removeUserSwitchCallback(callback)
}
}
override fun dump(pw: PrintWriter, args: Array<out String>) {
- if (isNewImpl) {
- notYetImplemented()
+ if (useInteractor) {
+ userInteractor.dump(pw)
} else {
_oldImpl.dump(pw, args)
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherControllerOldImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherControllerOldImpl.java
index d365aa6..46d2f3a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherControllerOldImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherControllerOldImpl.java
@@ -17,17 +17,13 @@
import static android.os.UserManager.SWITCHABILITY_STATUS_OK;
-import static com.android.settingslib.RestrictedLockUtils.EnforcedAdmin;
-
import android.annotation.UserIdInt;
-import android.app.ActivityManager;
import android.app.AlertDialog;
import android.app.Dialog;
import android.app.IActivityManager;
import android.app.admin.DevicePolicyManager;
import android.content.BroadcastReceiver;
import android.content.Context;
-import android.content.DialogInterface;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.UserInfo;
@@ -40,7 +36,6 @@
import android.provider.Settings;
import android.telephony.TelephonyCallback;
import android.text.TextUtils;
-import android.util.ArraySet;
import android.util.Log;
import android.util.SparseArray;
import android.util.SparseBooleanArray;
@@ -49,17 +44,14 @@
import android.widget.Toast;
import androidx.annotation.Nullable;
-import androidx.collection.SimpleArrayMap;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.jank.InteractionJankMonitor;
import com.android.internal.logging.UiEventLogger;
import com.android.internal.util.LatencyTracker;
-import com.android.settingslib.RestrictedLockUtilsInternal;
import com.android.settingslib.users.UserCreatingDialog;
import com.android.systemui.GuestResetOrExitSessionReceiver;
import com.android.systemui.GuestResumeSessionReceiver;
-import com.android.systemui.R;
import com.android.systemui.SystemUISecondaryUserService;
import com.android.systemui.animation.DialogCuj;
import com.android.systemui.animation.DialogLaunchAnimator;
@@ -75,10 +67,12 @@
import com.android.systemui.qs.QSUserSwitcherEvent;
import com.android.systemui.qs.user.UserSwitchDialogController.DialogShower;
import com.android.systemui.settings.UserTracker;
-import com.android.systemui.statusbar.phone.SystemUIDialog;
import com.android.systemui.telephony.TelephonyListenerManager;
-import com.android.systemui.user.CreateUserActivity;
import com.android.systemui.user.data.source.UserRecord;
+import com.android.systemui.user.legacyhelper.data.LegacyUserDataHelper;
+import com.android.systemui.user.shared.model.UserActionModel;
+import com.android.systemui.user.ui.dialog.AddUserDialog;
+import com.android.systemui.user.ui.dialog.ExitGuestDialog;
import com.android.systemui.util.settings.GlobalSettings;
import com.android.systemui.util.settings.SecureSettings;
@@ -139,9 +133,6 @@
private final InteractionJankMonitor mInteractionJankMonitor;
private final LatencyTracker mLatencyTracker;
private final DialogLaunchAnimator mDialogLaunchAnimator;
- private final SimpleArrayMap<UserRecord, EnforcedAdmin> mEnforcedAdminByUserRecord =
- new SimpleArrayMap<>();
- private final ArraySet<UserRecord> mDisabledByAdmin = new ArraySet<>();
private ArrayList<UserRecord> mUsers = new ArrayList<>();
@VisibleForTesting
@@ -334,7 +325,6 @@
for (UserInfo info : infos) {
boolean isCurrent = currentId == info.id;
- boolean switchToEnabled = canSwitchUsers || isCurrent;
if (!mUserSwitcherEnabled && !info.isPrimary()) {
continue;
}
@@ -343,25 +333,22 @@
if (info.isGuest()) {
// Tapping guest icon triggers remove and a user switch therefore
// the icon shouldn't be enabled even if the user is current
- guestRecord = new UserRecord(info, null /* picture */,
- true /* isGuest */, isCurrent, false /* isAddUser */,
- false /* isRestricted */, canSwitchUsers,
- false /* isAddSupervisedUser */);
+ guestRecord = LegacyUserDataHelper.createRecord(
+ mContext,
+ mUserManager,
+ null /* picture */,
+ info,
+ isCurrent,
+ canSwitchUsers);
} else if (info.supportsSwitchToByUser()) {
- Bitmap picture = bitmaps.get(info.id);
- if (picture == null) {
- picture = mUserManager.getUserIcon(info.id);
-
- if (picture != null) {
- int avatarSize = mContext.getResources()
- .getDimensionPixelSize(R.dimen.max_avatar_size);
- picture = Bitmap.createScaledBitmap(
- picture, avatarSize, avatarSize, true);
- }
- }
- records.add(new UserRecord(info, picture, false /* isGuest */,
- isCurrent, false /* isAddUser */, false /* isRestricted */,
- switchToEnabled, false /* isAddSupervisedUser */));
+ records.add(
+ LegacyUserDataHelper.createRecord(
+ mContext,
+ mUserManager,
+ bitmaps.get(info.id),
+ info,
+ isCurrent,
+ canSwitchUsers));
}
}
}
@@ -372,18 +359,20 @@
// we will just use it as an indicator for "Resetting guest...".
// Otherwise, default to canSwitchUsers.
boolean isSwitchToGuestEnabled = !mGuestIsResetting.get() && canSwitchUsers;
- guestRecord = new UserRecord(null /* info */, null /* picture */,
- true /* isGuest */, false /* isCurrent */,
- false /* isAddUser */, false /* isRestricted */,
- isSwitchToGuestEnabled, false /* isAddSupervisedUser */);
- checkIfAddUserDisallowedByAdminOnly(guestRecord);
+ guestRecord = LegacyUserDataHelper.createRecord(
+ mContext,
+ currentId,
+ UserActionModel.ENTER_GUEST_MODE,
+ false /* isRestricted */,
+ isSwitchToGuestEnabled);
records.add(guestRecord);
} else if (canCreateGuest(guestRecord != null)) {
- guestRecord = new UserRecord(null /* info */, null /* picture */,
- true /* isGuest */, false /* isCurrent */,
- false /* isAddUser */, createIsRestricted(), canSwitchUsers,
- false /* isAddSupervisedUser */);
- checkIfAddUserDisallowedByAdminOnly(guestRecord);
+ guestRecord = LegacyUserDataHelper.createRecord(
+ mContext,
+ currentId,
+ UserActionModel.ENTER_GUEST_MODE,
+ false /* isRestricted */,
+ canSwitchUsers);
records.add(guestRecord);
}
} else {
@@ -391,20 +380,23 @@
}
if (canCreateUser()) {
- UserRecord addUserRecord = new UserRecord(null /* info */, null /* picture */,
- false /* isGuest */, false /* isCurrent */, true /* isAddUser */,
- createIsRestricted(), canSwitchUsers,
- false /* isAddSupervisedUser */);
- checkIfAddUserDisallowedByAdminOnly(addUserRecord);
- records.add(addUserRecord);
+ final UserRecord userRecord = LegacyUserDataHelper.createRecord(
+ mContext,
+ currentId,
+ UserActionModel.ADD_USER,
+ createIsRestricted(),
+ canSwitchUsers);
+ records.add(userRecord);
}
if (canCreateSupervisedUser()) {
- UserRecord addUserRecord = new UserRecord(null /* info */, null /* picture */,
- false /* isGuest */, false /* isCurrent */, false /* isAddUser */,
- createIsRestricted(), canSwitchUsers, true /* isAddSupervisedUser */);
- checkIfAddUserDisallowedByAdminOnly(addUserRecord);
- records.add(addUserRecord);
+ final UserRecord userRecord = LegacyUserDataHelper.createRecord(
+ mContext,
+ currentId,
+ UserActionModel.ADD_SUPERVISED_USER,
+ createIsRestricted(),
+ canSwitchUsers);
+ records.add(userRecord);
}
mUiExecutor.execute(() -> {
@@ -591,12 +583,23 @@
showExitGuestDialog(id, isGuestEphemeral, newId, dialogShower);
}
- private void showExitGuestDialog(int id, boolean isGuestEphemeral,
- int targetId, DialogShower dialogShower) {
+ private void showExitGuestDialog(
+ int id,
+ boolean isGuestEphemeral,
+ int targetId,
+ DialogShower dialogShower) {
if (mExitGuestDialog != null && mExitGuestDialog.isShowing()) {
mExitGuestDialog.cancel();
}
- mExitGuestDialog = new ExitGuestDialog(mContext, id, isGuestEphemeral, targetId);
+ mExitGuestDialog = new ExitGuestDialog(
+ mContext,
+ id,
+ isGuestEphemeral,
+ targetId,
+ mKeyguardStateController.isShowing(),
+ mFalsingManager,
+ mDialogLaunchAnimator,
+ this::exitGuestUser);
if (dialogShower != null) {
dialogShower.showDialog(mExitGuestDialog, new DialogCuj(
InteractionJankMonitor.CUJ_USER_DIALOG_OPEN,
@@ -622,7 +625,15 @@
if (mAddUserDialog != null && mAddUserDialog.isShowing()) {
mAddUserDialog.cancel();
}
- mAddUserDialog = new AddUserDialog(mContext);
+ final UserInfo currentUser = mUserTracker.getUserInfo();
+ mAddUserDialog = new AddUserDialog(
+ mContext,
+ currentUser.getUserHandle(),
+ mKeyguardStateController.isShowing(),
+ /* showEphemeralMessage= */currentUser.isGuest() && currentUser.isEphemeral(),
+ mFalsingManager,
+ mBroadcastSender,
+ mDialogLaunchAnimator);
if (dialogShower != null) {
dialogShower.showDialog(mAddUserDialog,
new DialogCuj(
@@ -964,30 +975,6 @@
return mKeyguardStateController.isShowing();
}
- @Override
- @Nullable
- public EnforcedAdmin getEnforcedAdmin(UserRecord record) {
- return mEnforcedAdminByUserRecord.get(record);
- }
-
- @Override
- public boolean isDisabledByAdmin(UserRecord record) {
- return mDisabledByAdmin.contains(record);
- }
-
- private void checkIfAddUserDisallowedByAdminOnly(UserRecord record) {
- EnforcedAdmin admin = RestrictedLockUtilsInternal.checkIfRestrictionEnforced(mContext,
- UserManager.DISALLOW_ADD_USER, mUserTracker.getUserId());
- if (admin != null && !RestrictedLockUtilsInternal.hasBaseUserRestriction(mContext,
- UserManager.DISALLOW_ADD_USER, mUserTracker.getUserId())) {
- mDisabledByAdmin.add(record);
- mEnforcedAdminByUserRecord.put(record, admin);
- } else {
- mDisabledByAdmin.remove(record);
- mEnforcedAdminByUserRecord.put(record, null);
- }
- }
-
private boolean shouldUseSimpleUserSwitcher() {
int defaultSimpleUserSwitcher = mContext.getResources().getBoolean(
com.android.internal.R.bool.config_expandLockScreenUserSwitcher) ? 1 : 0;
@@ -1052,133 +1039,4 @@
}
}
};
-
-
- private final class ExitGuestDialog extends SystemUIDialog implements
- DialogInterface.OnClickListener {
-
- private final int mGuestId;
- private final int mTargetId;
- private final boolean mIsGuestEphemeral;
-
- ExitGuestDialog(Context context, int guestId, boolean isGuestEphemeral,
- int targetId) {
- super(context);
- if (isGuestEphemeral) {
- setTitle(context.getString(
- com.android.settingslib.R.string.guest_exit_dialog_title));
- setMessage(context.getString(
- com.android.settingslib.R.string.guest_exit_dialog_message));
- setButton(DialogInterface.BUTTON_NEUTRAL,
- context.getString(android.R.string.cancel), this);
- setButton(DialogInterface.BUTTON_POSITIVE,
- context.getString(
- com.android.settingslib.R.string.guest_exit_dialog_button), this);
- } else {
- setTitle(context.getString(
- com.android.settingslib
- .R.string.guest_exit_dialog_title_non_ephemeral));
- setMessage(context.getString(
- com.android.settingslib
- .R.string.guest_exit_dialog_message_non_ephemeral));
- setButton(DialogInterface.BUTTON_NEUTRAL,
- context.getString(android.R.string.cancel), this);
- setButton(DialogInterface.BUTTON_NEGATIVE,
- context.getString(
- com.android.settingslib.R.string.guest_exit_clear_data_button),
- this);
- setButton(DialogInterface.BUTTON_POSITIVE,
- context.getString(
- com.android.settingslib.R.string.guest_exit_save_data_button),
- this);
- }
- SystemUIDialog.setWindowOnTop(this, mKeyguardStateController.isShowing());
- setCanceledOnTouchOutside(false);
- mGuestId = guestId;
- mTargetId = targetId;
- mIsGuestEphemeral = isGuestEphemeral;
- }
-
- @Override
- public void onClick(DialogInterface dialog, int which) {
- int penalty = which == BUTTON_NEGATIVE ? FalsingManager.NO_PENALTY
- : FalsingManager.HIGH_PENALTY;
- if (mFalsingManager.isFalseTap(penalty)) {
- return;
- }
- if (mIsGuestEphemeral) {
- if (which == DialogInterface.BUTTON_POSITIVE) {
- mDialogLaunchAnimator.dismissStack(this);
- // Ephemeral guest: exit guest, guest is removed by the system
- // on exit, since its marked ephemeral
- exitGuestUser(mGuestId, mTargetId, false);
- } else if (which == DialogInterface.BUTTON_NEGATIVE) {
- // Cancel clicked, do nothing
- cancel();
- }
- } else {
- if (which == DialogInterface.BUTTON_POSITIVE) {
- mDialogLaunchAnimator.dismissStack(this);
- // Non-ephemeral guest: exit guest, guest is not removed by the system
- // on exit, since its marked non-ephemeral
- exitGuestUser(mGuestId, mTargetId, false);
- } else if (which == DialogInterface.BUTTON_NEGATIVE) {
- mDialogLaunchAnimator.dismissStack(this);
- // Non-ephemeral guest: remove guest and then exit
- exitGuestUser(mGuestId, mTargetId, true);
- } else if (which == DialogInterface.BUTTON_NEUTRAL) {
- // Cancel clicked, do nothing
- cancel();
- }
- }
- }
- }
-
- @VisibleForTesting
- final class AddUserDialog extends SystemUIDialog implements
- DialogInterface.OnClickListener {
-
- AddUserDialog(Context context) {
- super(context);
-
- setTitle(com.android.settingslib.R.string.user_add_user_title);
- String message = context.getString(
- com.android.settingslib.R.string.user_add_user_message_short);
- UserInfo currentUser = mUserTracker.getUserInfo();
- if (currentUser != null && currentUser.isGuest() && currentUser.isEphemeral()) {
- message += context.getString(R.string.user_add_user_message_guest_remove);
- }
- setMessage(message);
- setButton(DialogInterface.BUTTON_NEUTRAL,
- context.getString(android.R.string.cancel), this);
- setButton(DialogInterface.BUTTON_POSITIVE,
- context.getString(android.R.string.ok), this);
- SystemUIDialog.setWindowOnTop(this, mKeyguardStateController.isShowing());
- }
-
- @Override
- public void onClick(DialogInterface dialog, int which) {
- int penalty = which == BUTTON_NEGATIVE ? FalsingManager.NO_PENALTY
- : FalsingManager.MODERATE_PENALTY;
- if (mFalsingManager.isFalseTap(penalty)) {
- return;
- }
- if (which == BUTTON_NEUTRAL) {
- cancel();
- } else {
- mDialogLaunchAnimator.dismissStack(this);
- if (ActivityManager.isUserAMonkey()) {
- return;
- }
- // Use broadcast instead of ShadeController, as this dialog may have started in
- // another process and normal dagger bindings are not available
- mBroadcastSender.sendBroadcastAsUser(
- new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS), UserHandle.CURRENT);
- getContext().startActivityAsUser(
- CreateUserActivity.createIntentForStart(getContext()),
- mUserTracker.getUserHandle());
- }
- }
- }
-
}
diff --git a/packages/SystemUI/src/com/android/systemui/telephony/data/repository/TelephonyRepository.kt b/packages/SystemUI/src/com/android/systemui/telephony/data/repository/TelephonyRepository.kt
new file mode 100644
index 0000000..9c38dc0f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/telephony/data/repository/TelephonyRepository.kt
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package com.android.systemui.telephony.data.repository
+
+import android.telephony.Annotation
+import android.telephony.TelephonyCallback
+import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.telephony.TelephonyListenerManager
+import javax.inject.Inject
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+
+/** Defines interface for classes that encapsulate _some_ telephony-related state. */
+interface TelephonyRepository {
+ /** The state of the current call. */
+ @Annotation.CallState val callState: Flow<Int>
+}
+
+/**
+ * NOTE: This repository tracks only telephony-related state regarding the default mobile
+ * subscription. `TelephonyListenerManager` does not create new instances of `TelephonyManager` on a
+ * per-subscription basis and thus will always be tracking telephony information regarding
+ * `SubscriptionManager.getDefaultSubscriptionId`. See `TelephonyManager` and `SubscriptionManager`
+ * for more documentation.
+ */
+@SysUISingleton
+class TelephonyRepositoryImpl
+@Inject
+constructor(
+ private val manager: TelephonyListenerManager,
+) : TelephonyRepository {
+ @Annotation.CallState
+ override val callState: Flow<Int> = conflatedCallbackFlow {
+ val listener = TelephonyCallback.CallStateListener { state -> trySend(state) }
+
+ manager.addCallStateListener(listener)
+
+ awaitClose { manager.removeCallStateListener(listener) }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/telephony/data/repository/TelephonyRepositoryModule.kt b/packages/SystemUI/src/com/android/systemui/telephony/data/repository/TelephonyRepositoryModule.kt
new file mode 100644
index 0000000..630fbf2
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/telephony/data/repository/TelephonyRepositoryModule.kt
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package com.android.systemui.telephony.data.repository
+
+import dagger.Binds
+import dagger.Module
+
+@Module
+interface TelephonyRepositoryModule {
+ @Binds fun repository(impl: TelephonyRepositoryImpl): TelephonyRepository
+}
diff --git a/packages/SystemUI/src/com/android/systemui/telephony/domain/interactor/TelephonyInteractor.kt b/packages/SystemUI/src/com/android/systemui/telephony/domain/interactor/TelephonyInteractor.kt
new file mode 100644
index 0000000..86ca33d
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/telephony/domain/interactor/TelephonyInteractor.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package com.android.systemui.telephony.domain.interactor
+
+import android.telephony.Annotation
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.telephony.data.repository.TelephonyRepository
+import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+
+/** Hosts business logic related to telephony. */
+@SysUISingleton
+class TelephonyInteractor
+@Inject
+constructor(
+ repository: TelephonyRepository,
+) {
+ @Annotation.CallState val callState: Flow<Int> = repository.callState
+}
diff --git a/packages/SystemUI/src/com/android/systemui/user/UserModule.java b/packages/SystemUI/src/com/android/systemui/user/UserModule.java
index 5b522dc..0c72b78 100644
--- a/packages/SystemUI/src/com/android/systemui/user/UserModule.java
+++ b/packages/SystemUI/src/com/android/systemui/user/UserModule.java
@@ -20,6 +20,7 @@
import com.android.settingslib.users.EditUserInfoController;
import com.android.systemui.user.data.repository.UserRepositoryModule;
+import com.android.systemui.user.ui.dialog.UserDialogModule;
import dagger.Binds;
import dagger.Module;
@@ -32,6 +33,7 @@
*/
@Module(
includes = {
+ UserDialogModule.class,
UserRepositoryModule.class,
}
)
diff --git a/packages/SystemUI/src/com/android/systemui/user/data/model/UserSwitcherSettingsModel.kt b/packages/SystemUI/src/com/android/systemui/user/data/model/UserSwitcherSettingsModel.kt
new file mode 100644
index 0000000..4fd55c0
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/user/data/model/UserSwitcherSettingsModel.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package com.android.systemui.user.data.model
+
+/** Encapsulates the state of settings related to user switching. */
+data class UserSwitcherSettingsModel(
+ val isSimpleUserSwitcher: Boolean = false,
+ val isAddUsersFromLockscreen: Boolean = false,
+ val isUserSwitcherEnabled: Boolean = false,
+)
diff --git a/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt b/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt
index 0356388..3014f39 100644
--- a/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt
@@ -18,9 +18,13 @@
package com.android.systemui.user.data.repository
import android.content.Context
+import android.content.pm.UserInfo
import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.Drawable
+import android.os.UserHandle
import android.os.UserManager
+import android.provider.Settings
+import androidx.annotation.VisibleForTesting
import androidx.appcompat.content.res.AppCompatResources
import com.android.internal.util.UserIcons
import com.android.systemui.R
@@ -29,15 +33,36 @@
import com.android.systemui.common.shared.model.Text
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.flags.FeatureFlags
+import com.android.systemui.flags.Flags
+import com.android.systemui.settings.UserTracker
import com.android.systemui.statusbar.policy.UserSwitcherController
+import com.android.systemui.user.data.model.UserSwitcherSettingsModel
import com.android.systemui.user.data.source.UserRecord
import com.android.systemui.user.legacyhelper.ui.LegacyUserUiHelper
import com.android.systemui.user.shared.model.UserActionModel
import com.android.systemui.user.shared.model.UserModel
+import com.android.systemui.util.settings.GlobalSettings
+import com.android.systemui.util.settings.SettingsProxyExt.observerFlow
+import java.util.concurrent.atomic.AtomicBoolean
import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.asExecutor
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.emptyFlow
+import kotlinx.coroutines.flow.filterNotNull
+import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.flow.onStart
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
/**
* Acts as source of truth for user related data.
@@ -55,6 +80,18 @@
/** List of available user-related actions. */
val actions: Flow<List<UserActionModel>>
+ /** User switcher related settings. */
+ val userSwitcherSettings: Flow<UserSwitcherSettingsModel>
+
+ /** List of all users on the device. */
+ val userInfos: Flow<List<UserInfo>>
+
+ /** [UserInfo] of the currently-selected user. */
+ val selectedUserInfo: Flow<UserInfo>
+
+ /** User ID of the last non-guest selected user. */
+ val lastSelectedNonGuestUserId: Int
+
/** Whether actions are available even when locked. */
val isActionableWhenLocked: Flow<Boolean>
@@ -62,7 +99,23 @@
val isGuestUserAutoCreated: Boolean
/** Whether the guest user is currently being reset. */
- val isGuestUserResetting: Boolean
+ var isGuestUserResetting: Boolean
+
+ /** Whether we've scheduled the creation of a guest user. */
+ val isGuestUserCreationScheduled: AtomicBoolean
+
+ /** The user of the secondary service. */
+ var secondaryUserId: Int
+
+ /** Whether refresh users should be paused. */
+ var isRefreshUsersPaused: Boolean
+
+ /** Asynchronously refresh the list of users. This will cause [userInfos] to be updated. */
+ fun refreshUsers()
+
+ fun getSelectedUserInfo(): UserInfo
+
+ fun isSimpleUserSwitcher(): Boolean
}
@SysUISingleton
@@ -71,9 +124,31 @@
constructor(
@Application private val appContext: Context,
private val manager: UserManager,
- controller: UserSwitcherController,
+ private val controller: UserSwitcherController,
+ @Application private val applicationScope: CoroutineScope,
+ @Main private val mainDispatcher: CoroutineDispatcher,
+ @Background private val backgroundDispatcher: CoroutineDispatcher,
+ private val globalSettings: GlobalSettings,
+ private val tracker: UserTracker,
+ private val featureFlags: FeatureFlags,
) : UserRepository {
+ private val isNewImpl: Boolean
+ get() = !featureFlags.isEnabled(Flags.USER_INTERACTOR_AND_REPO_USE_CONTROLLER)
+
+ private val _userSwitcherSettings = MutableStateFlow<UserSwitcherSettingsModel?>(null)
+ override val userSwitcherSettings: Flow<UserSwitcherSettingsModel> =
+ _userSwitcherSettings.asStateFlow().filterNotNull()
+
+ private val _userInfos = MutableStateFlow<List<UserInfo>?>(null)
+ override val userInfos: Flow<List<UserInfo>> = _userInfos.filterNotNull()
+
+ private val _selectedUserInfo = MutableStateFlow<UserInfo?>(null)
+ override val selectedUserInfo: Flow<UserInfo> = _selectedUserInfo.filterNotNull()
+
+ override var lastSelectedNonGuestUserId: Int = UserHandle.USER_SYSTEM
+ private set
+
private val userRecords: Flow<List<UserRecord>> = conflatedCallbackFlow {
fun send() {
trySendWithFailureLogging(
@@ -99,11 +174,148 @@
override val actions: Flow<List<UserActionModel>> =
userRecords.map { records -> records.filter { it.isNotUser() }.map { it.toActionModel() } }
- override val isActionableWhenLocked: Flow<Boolean> = controller.isAddUsersFromLockScreenEnabled
+ override val isActionableWhenLocked: Flow<Boolean> =
+ if (isNewImpl) {
+ emptyFlow()
+ } else {
+ controller.isAddUsersFromLockScreenEnabled
+ }
- override val isGuestUserAutoCreated: Boolean = controller.isGuestUserAutoCreated
+ override val isGuestUserAutoCreated: Boolean =
+ if (isNewImpl) {
+ appContext.resources.getBoolean(com.android.internal.R.bool.config_guestUserAutoCreated)
+ } else {
+ controller.isGuestUserAutoCreated
+ }
- override val isGuestUserResetting: Boolean = controller.isGuestUserResetting
+ private var _isGuestUserResetting: Boolean = false
+ override var isGuestUserResetting: Boolean =
+ if (isNewImpl) {
+ _isGuestUserResetting
+ } else {
+ controller.isGuestUserResetting
+ }
+ set(value) =
+ if (isNewImpl) {
+ _isGuestUserResetting = value
+ } else {
+ error("Not supported in the old implementation!")
+ }
+
+ override val isGuestUserCreationScheduled = AtomicBoolean()
+
+ override var secondaryUserId: Int = UserHandle.USER_NULL
+
+ override var isRefreshUsersPaused: Boolean = false
+
+ init {
+ if (isNewImpl) {
+ observeSelectedUser()
+ observeUserSettings()
+ }
+ }
+
+ override fun refreshUsers() {
+ applicationScope.launch {
+ val result = withContext(backgroundDispatcher) { manager.aliveUsers }
+
+ if (result != null) {
+ _userInfos.value = result
+ }
+ }
+ }
+
+ override fun getSelectedUserInfo(): UserInfo {
+ return checkNotNull(_selectedUserInfo.value)
+ }
+
+ override fun isSimpleUserSwitcher(): Boolean {
+ return checkNotNull(_userSwitcherSettings.value?.isSimpleUserSwitcher)
+ }
+
+ private fun observeSelectedUser() {
+ conflatedCallbackFlow {
+ fun send() {
+ trySendWithFailureLogging(tracker.userInfo, TAG)
+ }
+
+ val callback =
+ object : UserTracker.Callback {
+ override fun onUserChanged(newUser: Int, userContext: Context) {
+ send()
+ }
+ }
+
+ tracker.addCallback(callback, mainDispatcher.asExecutor())
+ send()
+
+ awaitClose { tracker.removeCallback(callback) }
+ }
+ .onEach {
+ if (!it.isGuest) {
+ lastSelectedNonGuestUserId = it.id
+ }
+
+ _selectedUserInfo.value = it
+ }
+ .launchIn(applicationScope)
+ }
+
+ private fun observeUserSettings() {
+ globalSettings
+ .observerFlow(
+ names =
+ arrayOf(
+ SETTING_SIMPLE_USER_SWITCHER,
+ Settings.Global.ADD_USERS_WHEN_LOCKED,
+ Settings.Global.USER_SWITCHER_ENABLED,
+ ),
+ userId = UserHandle.USER_SYSTEM,
+ )
+ .onStart { emit(Unit) } // Forces an initial update.
+ .map { getSettings() }
+ .onEach { _userSwitcherSettings.value = it }
+ .launchIn(applicationScope)
+ }
+
+ private suspend fun getSettings(): UserSwitcherSettingsModel {
+ return withContext(backgroundDispatcher) {
+ val isSimpleUserSwitcher =
+ globalSettings.getIntForUser(
+ SETTING_SIMPLE_USER_SWITCHER,
+ if (
+ appContext.resources.getBoolean(
+ com.android.internal.R.bool.config_expandLockScreenUserSwitcher
+ )
+ ) {
+ 1
+ } else {
+ 0
+ },
+ UserHandle.USER_SYSTEM,
+ ) != 0
+
+ val isAddUsersFromLockscreen =
+ globalSettings.getIntForUser(
+ Settings.Global.ADD_USERS_WHEN_LOCKED,
+ 0,
+ UserHandle.USER_SYSTEM,
+ ) != 0
+
+ val isUserSwitcherEnabled =
+ globalSettings.getIntForUser(
+ Settings.Global.USER_SWITCHER_ENABLED,
+ 0,
+ UserHandle.USER_SYSTEM,
+ ) != 0
+
+ UserSwitcherSettingsModel(
+ isSimpleUserSwitcher = isSimpleUserSwitcher,
+ isAddUsersFromLockscreen = isAddUsersFromLockscreen,
+ isUserSwitcherEnabled = isUserSwitcherEnabled,
+ )
+ }
+ }
private fun UserRecord.isUser(): Boolean {
return when {
@@ -125,6 +337,7 @@
image = getUserImage(this),
isSelected = isCurrent,
isSelectable = isSwitchToEnabled || isGuest,
+ isGuest = isGuest,
)
}
@@ -162,5 +375,6 @@
companion object {
private const val TAG = "UserRepository"
+ @VisibleForTesting const val SETTING_SIMPLE_USER_SWITCHER = "lockscreenSimpleUserSwitcher"
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/user/data/source/UserRecord.kt b/packages/SystemUI/src/com/android/systemui/user/data/source/UserRecord.kt
index cf6da9a..9370286 100644
--- a/packages/SystemUI/src/com/android/systemui/user/data/source/UserRecord.kt
+++ b/packages/SystemUI/src/com/android/systemui/user/data/source/UserRecord.kt
@@ -19,6 +19,7 @@
import android.content.pm.UserInfo
import android.graphics.Bitmap
import android.os.UserHandle
+import com.android.settingslib.RestrictedLockUtils
/** Encapsulates raw data for a user or an option item related to managing users on the device. */
data class UserRecord(
@@ -41,6 +42,11 @@
@JvmField val isSwitchToEnabled: Boolean = false,
/** Whether this record represents an option to add another supervised user to the device. */
@JvmField val isAddSupervisedUser: Boolean = false,
+ /**
+ * An enforcing admin, if the user action represented by this record is disabled by the admin.
+ * If not disabled, this is `null`.
+ */
+ @JvmField val enforcedAdmin: RestrictedLockUtils.EnforcedAdmin? = null,
) {
/** Returns a new instance of [UserRecord] with its [isCurrent] set to the given value. */
fun copyWithIsCurrent(isCurrent: Boolean): UserRecord {
@@ -59,6 +65,14 @@
}
}
+ /**
+ * Returns `true` if the user action represented by this record has been disabled by an admin;
+ * `false` otherwise.
+ */
+ fun isDisabledByAdmin(): Boolean {
+ return enforcedAdmin != null
+ }
+
companion object {
@JvmStatic
fun createForGuest(): UserRecord {
diff --git a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/GuestUserInteractor.kt b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/GuestUserInteractor.kt
new file mode 100644
index 0000000..07e5cf9
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/GuestUserInteractor.kt
@@ -0,0 +1,322 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package com.android.systemui.user.domain.interactor
+
+import android.annotation.UserIdInt
+import android.app.admin.DevicePolicyManager
+import android.content.Context
+import android.content.pm.UserInfo
+import android.os.RemoteException
+import android.os.UserHandle
+import android.os.UserManager
+import android.util.Log
+import android.view.WindowManagerGlobal
+import android.widget.Toast
+import com.android.internal.logging.UiEventLogger
+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.qs.QSUserSwitcherEvent
+import com.android.systemui.statusbar.policy.DeviceProvisionedController
+import com.android.systemui.user.data.repository.UserRepository
+import com.android.systemui.user.domain.model.ShowDialogRequestModel
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.suspendCancellableCoroutine
+import kotlinx.coroutines.withContext
+
+/** Encapsulates business logic to interact with guest user data and systems. */
+@SysUISingleton
+class GuestUserInteractor
+@Inject
+constructor(
+ @Application private val applicationContext: Context,
+ @Application private val applicationScope: CoroutineScope,
+ @Main private val mainDispatcher: CoroutineDispatcher,
+ @Background private val backgroundDispatcher: CoroutineDispatcher,
+ private val manager: UserManager,
+ private val repository: UserRepository,
+ private val deviceProvisionedController: DeviceProvisionedController,
+ private val devicePolicyManager: DevicePolicyManager,
+ private val refreshUsersScheduler: RefreshUsersScheduler,
+ private val uiEventLogger: UiEventLogger,
+) {
+ /** Whether the device is configured to always have a guest user available. */
+ val isGuestUserAutoCreated: Boolean = repository.isGuestUserAutoCreated
+
+ /** Whether the guest user is currently being reset. */
+ val isGuestUserResetting: Boolean = repository.isGuestUserResetting
+
+ /** Notifies that the device has finished booting. */
+ fun onDeviceBootCompleted() {
+ applicationScope.launch {
+ if (isDeviceAllowedToAddGuest()) {
+ guaranteePresent()
+ return@launch
+ }
+
+ suspendCancellableCoroutine<Unit> { continuation ->
+ val callback =
+ object : DeviceProvisionedController.DeviceProvisionedListener {
+ override fun onDeviceProvisionedChanged() {
+ continuation.resumeWith(Result.success(Unit))
+ deviceProvisionedController.removeCallback(this)
+ }
+ }
+
+ deviceProvisionedController.addCallback(callback)
+ }
+
+ if (isDeviceAllowedToAddGuest()) {
+ guaranteePresent()
+ }
+ }
+ }
+
+ /** Creates a guest user and switches to it. */
+ fun createAndSwitchTo(
+ showDialog: (ShowDialogRequestModel) -> Unit,
+ dismissDialog: () -> Unit,
+ selectUser: (userId: Int) -> Unit,
+ ) {
+ applicationScope.launch {
+ val newGuestUserId = create(showDialog, dismissDialog)
+ if (newGuestUserId != UserHandle.USER_NULL) {
+ selectUser(newGuestUserId)
+ }
+ }
+ }
+
+ /** Exits the guest user, switching back to the last non-guest user or to the default user. */
+ fun exit(
+ @UserIdInt guestUserId: Int,
+ @UserIdInt targetUserId: Int,
+ forceRemoveGuestOnExit: Boolean,
+ showDialog: (ShowDialogRequestModel) -> Unit,
+ dismissDialog: () -> Unit,
+ switchUser: (userId: Int) -> Unit,
+ ) {
+ val currentUserInfo = repository.getSelectedUserInfo()
+ if (currentUserInfo.id != guestUserId) {
+ Log.w(
+ TAG,
+ "User requesting to start a new session ($guestUserId) is not current user" +
+ " (${currentUserInfo.id})"
+ )
+ return
+ }
+
+ if (!currentUserInfo.isGuest) {
+ Log.w(TAG, "User requesting to start a new session ($guestUserId) is not a guest")
+ return
+ }
+
+ applicationScope.launch {
+ var newUserId = UserHandle.USER_SYSTEM
+ if (targetUserId == UserHandle.USER_NULL) {
+ // When a target user is not specified switch to last non guest user:
+ val lastSelectedNonGuestUserHandle = repository.lastSelectedNonGuestUserId
+ if (lastSelectedNonGuestUserHandle != UserHandle.USER_SYSTEM) {
+ val info =
+ withContext(backgroundDispatcher) {
+ manager.getUserInfo(lastSelectedNonGuestUserHandle)
+ }
+ if (info != null && info.isEnabled && info.supportsSwitchToByUser()) {
+ newUserId = info.id
+ }
+ }
+ } else {
+ newUserId = targetUserId
+ }
+
+ if (currentUserInfo.isEphemeral || forceRemoveGuestOnExit) {
+ uiEventLogger.log(QSUserSwitcherEvent.QS_USER_GUEST_REMOVE)
+ remove(currentUserInfo.id, newUserId, showDialog, dismissDialog, switchUser)
+ } else {
+ uiEventLogger.log(QSUserSwitcherEvent.QS_USER_SWITCH)
+ switchUser(newUserId)
+ }
+ }
+ }
+
+ /**
+ * Guarantees that the guest user is present on the device, creating it if needed and if allowed
+ * to.
+ */
+ suspend fun guaranteePresent() {
+ if (!isDeviceAllowedToAddGuest()) {
+ return
+ }
+
+ val guestUser = withContext(backgroundDispatcher) { manager.findCurrentGuestUser() }
+ if (guestUser == null) {
+ scheduleCreation()
+ }
+ }
+
+ /** Removes the guest user from the device. */
+ suspend fun remove(
+ @UserIdInt guestUserId: Int,
+ @UserIdInt targetUserId: Int,
+ showDialog: (ShowDialogRequestModel) -> Unit,
+ dismissDialog: () -> Unit,
+ switchUser: (userId: Int) -> Unit,
+ ) {
+ val currentUser: UserInfo = repository.getSelectedUserInfo()
+ if (currentUser.id != guestUserId) {
+ Log.w(
+ TAG,
+ "User requesting to start a new session ($guestUserId) is not current user" +
+ " ($currentUser.id)"
+ )
+ return
+ }
+
+ if (!currentUser.isGuest) {
+ Log.w(TAG, "User requesting to start a new session ($guestUserId) is not a guest")
+ return
+ }
+
+ val marked =
+ withContext(backgroundDispatcher) { manager.markGuestForDeletion(currentUser.id) }
+ if (!marked) {
+ Log.w(TAG, "Couldn't mark the guest for deletion for user $guestUserId")
+ return
+ }
+
+ if (targetUserId == UserHandle.USER_NULL) {
+ // Create a new guest in the foreground, and then immediately switch to it
+ val newGuestId = create(showDialog, dismissDialog)
+ if (newGuestId == UserHandle.USER_NULL) {
+ Log.e(TAG, "Could not create new guest, switching back to system user")
+ switchUser(UserHandle.USER_SYSTEM)
+ withContext(backgroundDispatcher) { manager.removeUser(currentUser.id) }
+ try {
+ WindowManagerGlobal.getWindowManagerService().lockNow(/* options= */ null)
+ } catch (e: RemoteException) {
+ Log.e(
+ TAG,
+ "Couldn't remove guest because ActivityManager or WindowManager is dead"
+ )
+ }
+ return
+ }
+
+ switchUser(newGuestId)
+
+ withContext(backgroundDispatcher) { manager.removeUser(currentUser.id) }
+ } else {
+ if (repository.isGuestUserAutoCreated) {
+ repository.isGuestUserResetting = true
+ }
+ switchUser(targetUserId)
+ manager.removeUser(currentUser.id)
+ }
+ }
+
+ /**
+ * Creates the guest user and adds it to the device.
+ *
+ * @param showDialog A function to invoke to show a dialog.
+ * @param dismissDialog A function to invoke to dismiss a dialog.
+ * @return The user ID of the newly-created guest user.
+ */
+ private suspend fun create(
+ showDialog: (ShowDialogRequestModel) -> Unit,
+ dismissDialog: () -> Unit,
+ ): Int {
+ return withContext(mainDispatcher) {
+ showDialog(ShowDialogRequestModel.ShowUserCreationDialog(isGuest = true))
+ val guestUserId = createInBackground()
+ dismissDialog()
+ if (guestUserId != UserHandle.USER_NULL) {
+ uiEventLogger.log(QSUserSwitcherEvent.QS_USER_GUEST_ADD)
+ } else {
+ Toast.makeText(
+ applicationContext,
+ com.android.settingslib.R.string.add_guest_failed,
+ Toast.LENGTH_SHORT,
+ )
+ .show()
+ }
+
+ guestUserId
+ }
+ }
+
+ /** Schedules the creation of the guest user. */
+ private suspend fun scheduleCreation() {
+ if (!repository.isGuestUserCreationScheduled.compareAndSet(false, true)) {
+ return
+ }
+
+ withContext(backgroundDispatcher) {
+ val newGuestUserId = createInBackground()
+ repository.isGuestUserCreationScheduled.set(false)
+ repository.isGuestUserResetting = false
+ if (newGuestUserId == UserHandle.USER_NULL) {
+ Log.w(TAG, "Could not create new guest while exiting existing guest")
+ // Refresh users so that we still display "Guest" if
+ // config_guestUserAutoCreated=true
+ refreshUsersScheduler.refreshIfNotPaused()
+ }
+ }
+ }
+
+ /**
+ * Creates a guest user and return its multi-user user ID.
+ *
+ * This method does not check if a guest already exists before it makes a call to [UserManager]
+ * to create a new one.
+ *
+ * @return The multi-user user ID of the newly created guest user, or [UserHandle.USER_NULL] if
+ * the guest couldn't be created.
+ */
+ @UserIdInt
+ private suspend fun createInBackground(): Int {
+ return withContext(backgroundDispatcher) {
+ try {
+ val guestUser = manager.createGuest(applicationContext)
+ if (guestUser != null) {
+ guestUser.id
+ } else {
+ Log.e(
+ TAG,
+ "Couldn't create guest, most likely because there already exists one!"
+ )
+ UserHandle.USER_NULL
+ }
+ } catch (e: UserManager.UserOperationException) {
+ Log.e(TAG, "Couldn't create guest user!", e)
+ UserHandle.USER_NULL
+ }
+ }
+ }
+
+ private fun isDeviceAllowedToAddGuest(): Boolean {
+ return deviceProvisionedController.isDeviceProvisioned &&
+ !devicePolicyManager.isDeviceManaged
+ }
+
+ companion object {
+ private const val TAG = "GuestUserInteractor"
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/RefreshUsersScheduler.kt b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/RefreshUsersScheduler.kt
new file mode 100644
index 0000000..8f36821
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/RefreshUsersScheduler.kt
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package com.android.systemui.user.domain.interactor
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.user.data.repository.UserRepository
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+
+/** Encapsulates logic for pausing, unpausing, and scheduling a delayed job. */
+@SysUISingleton
+class RefreshUsersScheduler
+@Inject
+constructor(
+ @Application private val applicationScope: CoroutineScope,
+ @Main private val mainDispatcher: CoroutineDispatcher,
+ private val repository: UserRepository,
+) {
+ private var scheduledUnpauseJob: Job? = null
+ private var isPaused = false
+
+ fun pause() {
+ applicationScope.launch(mainDispatcher) {
+ isPaused = true
+ scheduledUnpauseJob?.cancel()
+ scheduledUnpauseJob =
+ applicationScope.launch {
+ delay(PAUSE_REFRESH_USERS_TIMEOUT_MS)
+ unpauseAndRefresh()
+ }
+ }
+ }
+
+ fun unpauseAndRefresh() {
+ applicationScope.launch(mainDispatcher) {
+ isPaused = false
+ refreshIfNotPaused()
+ }
+ }
+
+ fun refreshIfNotPaused() {
+ applicationScope.launch(mainDispatcher) {
+ if (isPaused) {
+ return@launch
+ }
+
+ repository.refreshUsers()
+ }
+ }
+
+ companion object {
+ private const val PAUSE_REFRESH_USERS_TIMEOUT_MS = 3000L
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserActionsUtil.kt b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserActionsUtil.kt
new file mode 100644
index 0000000..1b4746a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserActionsUtil.kt
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package com.android.systemui.user.domain.interactor
+
+import android.os.UserHandle
+import android.os.UserManager
+import com.android.systemui.user.data.repository.UserRepository
+
+/** Utilities related to user management actions. */
+object UserActionsUtil {
+
+ /** Returns `true` if it's possible to add a guest user to the device; `false` otherwise. */
+ fun canCreateGuest(
+ manager: UserManager,
+ repository: UserRepository,
+ isUserSwitcherEnabled: Boolean,
+ isAddUsersFromLockScreenEnabled: Boolean,
+ ): Boolean {
+ if (!isUserSwitcherEnabled) {
+ return false
+ }
+
+ return currentUserCanCreateUsers(manager, repository) ||
+ anyoneCanCreateUsers(manager, isAddUsersFromLockScreenEnabled)
+ }
+
+ /** Returns `true` if it's possible to add a user to the device; `false` otherwise. */
+ fun canCreateUser(
+ manager: UserManager,
+ repository: UserRepository,
+ isUserSwitcherEnabled: Boolean,
+ isAddUsersFromLockScreenEnabled: Boolean,
+ ): Boolean {
+ if (!isUserSwitcherEnabled) {
+ return false
+ }
+
+ if (
+ !currentUserCanCreateUsers(manager, repository) &&
+ !anyoneCanCreateUsers(manager, isAddUsersFromLockScreenEnabled)
+ ) {
+ return false
+ }
+
+ return manager.canAddMoreUsers(UserManager.USER_TYPE_FULL_SECONDARY)
+ }
+
+ /**
+ * Returns `true` if it's possible to add a supervised user to the device; `false` otherwise.
+ */
+ fun canCreateSupervisedUser(
+ manager: UserManager,
+ repository: UserRepository,
+ isUserSwitcherEnabled: Boolean,
+ isAddUsersFromLockScreenEnabled: Boolean,
+ supervisedUserPackageName: String?
+ ): Boolean {
+ if (supervisedUserPackageName.isNullOrEmpty()) {
+ return false
+ }
+
+ return canCreateUser(
+ manager,
+ repository,
+ isUserSwitcherEnabled,
+ isAddUsersFromLockScreenEnabled
+ )
+ }
+
+ /**
+ * Returns `true` if the current user is allowed to add users to the device; `false` otherwise.
+ */
+ private fun currentUserCanCreateUsers(
+ manager: UserManager,
+ repository: UserRepository,
+ ): Boolean {
+ val currentUser = repository.getSelectedUserInfo()
+ if (!currentUser.isAdmin && currentUser.id != UserHandle.USER_SYSTEM) {
+ return false
+ }
+
+ return systemCanCreateUsers(manager)
+ }
+
+ /** Returns `true` if the system can add users to the device; `false` otherwise. */
+ private fun systemCanCreateUsers(
+ manager: UserManager,
+ ): Boolean {
+ return !manager.hasBaseUserRestriction(UserManager.DISALLOW_ADD_USER, UserHandle.SYSTEM)
+ }
+
+ /** Returns `true` if it's allowed to add users to the device at all; `false` otherwise. */
+ private fun anyoneCanCreateUsers(
+ manager: UserManager,
+ isAddUsersFromLockScreenEnabled: Boolean,
+ ): Boolean {
+ return systemCanCreateUsers(manager) && isAddUsersFromLockScreenEnabled
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserInteractor.kt b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserInteractor.kt
index 3c5b969..a84238c 100644
--- a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserInteractor.kt
@@ -17,94 +17,725 @@
package com.android.systemui.user.domain.interactor
+import android.annotation.SuppressLint
+import android.annotation.UserIdInt
+import android.app.ActivityManager
+import android.content.Context
import android.content.Intent
+import android.content.IntentFilter
+import android.content.pm.UserInfo
+import android.graphics.drawable.BitmapDrawable
+import android.graphics.drawable.Drawable
+import android.os.RemoteException
+import android.os.UserHandle
+import android.os.UserManager
import android.provider.Settings
+import android.util.Log
+import com.android.internal.util.UserIcons
+import com.android.systemui.R
+import com.android.systemui.SystemUISecondaryUserService
+import com.android.systemui.broadcast.BroadcastDispatcher
+import com.android.systemui.common.shared.model.Text
import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.statusbar.policy.UserSwitcherController
+import com.android.systemui.telephony.domain.interactor.TelephonyInteractor
import com.android.systemui.user.data.repository.UserRepository
+import com.android.systemui.user.data.source.UserRecord
+import com.android.systemui.user.domain.model.ShowDialogRequestModel
+import com.android.systemui.user.legacyhelper.data.LegacyUserDataHelper
import com.android.systemui.user.shared.model.UserActionModel
import com.android.systemui.user.shared.model.UserModel
+import com.android.systemui.util.kotlin.pairwise
+import java.io.PrintWriter
import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.sync.Mutex
+import kotlinx.coroutines.sync.withLock
+import kotlinx.coroutines.withContext
/** Encapsulates business logic to interact with user data and systems. */
@SysUISingleton
class UserInteractor
@Inject
constructor(
- repository: UserRepository,
+ @Application private val applicationContext: Context,
+ private val repository: UserRepository,
private val controller: UserSwitcherController,
private val activityStarter: ActivityStarter,
- keyguardInteractor: KeyguardInteractor,
+ private val keyguardInteractor: KeyguardInteractor,
+ private val featureFlags: FeatureFlags,
+ private val manager: UserManager,
+ @Application private val applicationScope: CoroutineScope,
+ telephonyInteractor: TelephonyInteractor,
+ broadcastDispatcher: BroadcastDispatcher,
+ @Background private val backgroundDispatcher: CoroutineDispatcher,
+ private val activityManager: ActivityManager,
+ private val refreshUsersScheduler: RefreshUsersScheduler,
+ private val guestUserInteractor: GuestUserInteractor,
) {
+ /**
+ * Defines interface for classes that can be notified when the state of users on the device is
+ * changed.
+ */
+ interface UserCallback {
+ /** Returns `true` if this callback can be cleaned-up. */
+ fun isEvictable(): Boolean = false
+ /** Notifies that the state of users on the device has changed. */
+ fun onUserStateChanged()
+ }
+
+ private val isNewImpl: Boolean
+ get() = !featureFlags.isEnabled(Flags.USER_INTERACTOR_AND_REPO_USE_CONTROLLER)
+
+ private val supervisedUserPackageName: String?
+ get() =
+ applicationContext.getString(
+ com.android.internal.R.string.config_supervisedUserCreationPackage
+ )
+
+ private val callbackMutex = Mutex()
+ private val callbacks = mutableSetOf<UserCallback>()
+
/** List of current on-device users to select from. */
- val users: Flow<List<UserModel>> = repository.users
+ val users: Flow<List<UserModel>>
+ get() =
+ if (isNewImpl) {
+ combine(
+ repository.userInfos,
+ repository.selectedUserInfo,
+ repository.userSwitcherSettings,
+ ) { userInfos, selectedUserInfo, settings ->
+ toUserModels(
+ userInfos = userInfos,
+ selectedUserId = selectedUserInfo.id,
+ isUserSwitcherEnabled = settings.isUserSwitcherEnabled,
+ )
+ }
+ } else {
+ repository.users
+ }
/** The currently-selected user. */
- val selectedUser: Flow<UserModel> = repository.selectedUser
+ val selectedUser: Flow<UserModel>
+ get() =
+ if (isNewImpl) {
+ combine(
+ repository.selectedUserInfo,
+ repository.userSwitcherSettings,
+ ) { selectedUserInfo, settings ->
+ val selectedUserId = selectedUserInfo.id
+ checkNotNull(
+ toUserModel(
+ userInfo = selectedUserInfo,
+ selectedUserId = selectedUserId,
+ canSwitchUsers = canSwitchUsers(selectedUserId),
+ isUserSwitcherEnabled = settings.isUserSwitcherEnabled,
+ )
+ )
+ }
+ } else {
+ repository.selectedUser
+ }
/** List of user-switcher related actions that are available. */
- val actions: Flow<List<UserActionModel>> =
- combine(
- repository.isActionableWhenLocked,
- keyguardInteractor.isKeyguardShowing,
- ) { isActionableWhenLocked, isLocked ->
- isActionableWhenLocked || !isLocked
- }
- .flatMapLatest { isActionable ->
- if (isActionable) {
- repository.actions.map { actions ->
- actions +
- if (actions.isNotEmpty()) {
- // If we have actions, we add NAVIGATE_TO_USER_MANAGEMENT because
- // that's a user
- // switcher specific action that is not known to the our data source
- // or other
- // features.
- listOf(UserActionModel.NAVIGATE_TO_USER_MANAGEMENT)
- } else {
- // If no actions, don't add the navigate action.
- emptyList()
- }
+ val actions: Flow<List<UserActionModel>>
+ get() =
+ if (isNewImpl) {
+ combine(
+ repository.userInfos,
+ repository.userSwitcherSettings,
+ keyguardInteractor.isKeyguardShowing,
+ ) { userInfos, settings, isDeviceLocked ->
+ buildList {
+ val hasGuestUser = userInfos.any { it.isGuest }
+ if (
+ !hasGuestUser &&
+ (guestUserInteractor.isGuestUserAutoCreated ||
+ UserActionsUtil.canCreateGuest(
+ manager,
+ repository,
+ settings.isUserSwitcherEnabled,
+ settings.isAddUsersFromLockscreen,
+ ))
+ ) {
+ add(UserActionModel.ENTER_GUEST_MODE)
+ }
+
+ if (isDeviceLocked && !settings.isAddUsersFromLockscreen) {
+ // The device is locked and our setting to allow actions that add users
+ // from the lock-screen is not enabled. The guest action from above is
+ // always allowed, even when the device is locked, but the various "add
+ // user" actions below are not. We can finish building the list here.
+ return@buildList
+ }
+
+ if (
+ UserActionsUtil.canCreateUser(
+ manager,
+ repository,
+ settings.isUserSwitcherEnabled,
+ settings.isAddUsersFromLockscreen,
+ )
+ ) {
+ add(UserActionModel.ADD_USER)
+ }
+
+ if (
+ UserActionsUtil.canCreateSupervisedUser(
+ manager,
+ repository,
+ settings.isUserSwitcherEnabled,
+ settings.isAddUsersFromLockscreen,
+ supervisedUserPackageName,
+ )
+ ) {
+ add(UserActionModel.ADD_SUPERVISED_USER)
+ }
}
- } else {
- // If not actionable it means that we're not allowed to show actions when locked
- // and we
- // are locked. Therefore, we should show no actions.
- flowOf(emptyList())
}
+ } else {
+ combine(
+ repository.isActionableWhenLocked,
+ keyguardInteractor.isKeyguardShowing,
+ ) { isActionableWhenLocked, isLocked ->
+ isActionableWhenLocked || !isLocked
+ }
+ .flatMapLatest { isActionable ->
+ if (isActionable) {
+ repository.actions.map { actions ->
+ actions +
+ if (actions.isNotEmpty()) {
+ // If we have actions, we add NAVIGATE_TO_USER_MANAGEMENT
+ // because that's a user switcher specific action that is
+ // not known to the our data source or other features.
+ listOf(UserActionModel.NAVIGATE_TO_USER_MANAGEMENT)
+ } else {
+ // If no actions, don't add the navigate action.
+ emptyList()
+ }
+ }
+ } else {
+ // If not actionable it means that we're not allowed to show actions
+ // when
+ // locked and we are locked. Therefore, we should show no actions.
+ flowOf(emptyList())
+ }
+ }
}
+ val userRecords: StateFlow<ArrayList<UserRecord>> =
+ if (isNewImpl) {
+ combine(
+ repository.userInfos,
+ repository.selectedUserInfo,
+ actions,
+ repository.userSwitcherSettings,
+ ) { userInfos, selectedUserInfo, actionModels, settings ->
+ ArrayList(
+ userInfos.map {
+ toRecord(
+ userInfo = it,
+ selectedUserId = selectedUserInfo.id,
+ )
+ } +
+ actionModels.map {
+ toRecord(
+ action = it,
+ selectedUserId = selectedUserInfo.id,
+ isAddFromLockscreenEnabled = settings.isAddUsersFromLockscreen,
+ )
+ }
+ )
+ }
+ .onEach { notifyCallbacks() }
+ .stateIn(
+ scope = applicationScope,
+ started = SharingStarted.Eagerly,
+ initialValue = ArrayList(),
+ )
+ } else {
+ MutableStateFlow(ArrayList())
+ }
+
+ val selectedUserRecord: StateFlow<UserRecord?> =
+ if (isNewImpl) {
+ repository.selectedUserInfo
+ .map { selectedUserInfo ->
+ toRecord(userInfo = selectedUserInfo, selectedUserId = selectedUserInfo.id)
+ }
+ .stateIn(
+ scope = applicationScope,
+ started = SharingStarted.Eagerly,
+ initialValue = null,
+ )
+ } else {
+ MutableStateFlow(null)
+ }
+
/** Whether the device is configured to always have a guest user available. */
- val isGuestUserAutoCreated: Boolean = repository.isGuestUserAutoCreated
+ val isGuestUserAutoCreated: Boolean = guestUserInteractor.isGuestUserAutoCreated
/** Whether the guest user is currently being reset. */
- val isGuestUserResetting: Boolean = repository.isGuestUserResetting
+ val isGuestUserResetting: Boolean = guestUserInteractor.isGuestUserResetting
+
+ private val _dialogShowRequests = MutableStateFlow<ShowDialogRequestModel?>(null)
+ val dialogShowRequests: Flow<ShowDialogRequestModel?> = _dialogShowRequests.asStateFlow()
+
+ private val _dialogDismissRequests = MutableStateFlow<Unit?>(null)
+ val dialogDismissRequests: Flow<Unit?> = _dialogDismissRequests.asStateFlow()
+
+ val isSimpleUserSwitcher: Boolean
+ get() =
+ if (isNewImpl) {
+ repository.isSimpleUserSwitcher()
+ } else {
+ error("Not supported in the old implementation!")
+ }
+
+ init {
+ if (isNewImpl) {
+ refreshUsersScheduler.refreshIfNotPaused()
+ telephonyInteractor.callState
+ .distinctUntilChanged()
+ .onEach { refreshUsersScheduler.refreshIfNotPaused() }
+ .launchIn(applicationScope)
+
+ combine(
+ broadcastDispatcher.broadcastFlow(
+ filter =
+ IntentFilter().apply {
+ addAction(Intent.ACTION_USER_ADDED)
+ addAction(Intent.ACTION_USER_REMOVED)
+ addAction(Intent.ACTION_USER_INFO_CHANGED)
+ addAction(Intent.ACTION_USER_SWITCHED)
+ addAction(Intent.ACTION_USER_STOPPED)
+ addAction(Intent.ACTION_USER_UNLOCKED)
+ },
+ user = UserHandle.SYSTEM,
+ map = { intent, _ -> intent },
+ ),
+ repository.selectedUserInfo.pairwise(null),
+ ) { intent, selectedUserChange ->
+ Pair(intent, selectedUserChange.previousValue)
+ }
+ .onEach { (intent, previousSelectedUser) ->
+ onBroadcastReceived(intent, previousSelectedUser)
+ }
+ .launchIn(applicationScope)
+ }
+ }
+
+ fun addCallback(callback: UserCallback) {
+ applicationScope.launch { callbackMutex.withLock { callbacks.add(callback) } }
+ }
+
+ fun removeCallback(callback: UserCallback) {
+ applicationScope.launch { callbackMutex.withLock { callbacks.remove(callback) } }
+ }
+
+ fun refreshUsers() {
+ refreshUsersScheduler.refreshIfNotPaused()
+ }
+
+ fun onDialogShown() {
+ _dialogShowRequests.value = null
+ }
+
+ fun onDialogDismissed() {
+ _dialogDismissRequests.value = null
+ }
+
+ fun dump(pw: PrintWriter) {
+ pw.println("UserInteractor state:")
+ pw.println(" lastSelectedNonGuestUserId=${repository.lastSelectedNonGuestUserId}")
+
+ val users = userRecords.value.filter { it.info != null }
+ pw.println(" userCount=${userRecords.value.count { LegacyUserDataHelper.isUser(it) }}")
+ for (i in users.indices) {
+ pw.println(" ${users[i]}")
+ }
+
+ val actions = userRecords.value.filter { it.info == null }
+ pw.println(" actionCount=${userRecords.value.count { !LegacyUserDataHelper.isUser(it) }}")
+ for (i in actions.indices) {
+ pw.println(" ${actions[i]}")
+ }
+
+ pw.println("isSimpleUserSwitcher=$isSimpleUserSwitcher")
+ pw.println("isGuestUserAutoCreated=$isGuestUserAutoCreated")
+ }
+
+ fun onDeviceBootCompleted() {
+ guestUserInteractor.onDeviceBootCompleted()
+ }
/** Switches to the user with the given user ID. */
fun selectUser(
- userId: Int,
+ newlySelectedUserId: Int,
) {
- controller.onUserSelected(userId, /* dialogShower= */ null)
+ if (isNewImpl) {
+ val currentlySelectedUserInfo = repository.getSelectedUserInfo()
+ if (
+ newlySelectedUserId == currentlySelectedUserInfo.id &&
+ currentlySelectedUserInfo.isGuest
+ ) {
+ // Here when clicking on the currently-selected guest user to leave guest mode
+ // and return to the previously-selected non-guest user.
+ showDialog(
+ ShowDialogRequestModel.ShowExitGuestDialog(
+ guestUserId = currentlySelectedUserInfo.id,
+ targetUserId = repository.lastSelectedNonGuestUserId,
+ isGuestEphemeral = currentlySelectedUserInfo.isEphemeral,
+ isKeyguardShowing = keyguardInteractor.isKeyguardShowing(),
+ onExitGuestUser = this::exitGuestUser,
+ )
+ )
+ return
+ }
+
+ if (currentlySelectedUserInfo.isGuest) {
+ // Here when switching from guest to a non-guest user.
+ showDialog(
+ ShowDialogRequestModel.ShowExitGuestDialog(
+ guestUserId = currentlySelectedUserInfo.id,
+ targetUserId = newlySelectedUserId,
+ isGuestEphemeral = currentlySelectedUserInfo.isEphemeral,
+ isKeyguardShowing = keyguardInteractor.isKeyguardShowing(),
+ onExitGuestUser = this::exitGuestUser,
+ )
+ )
+ return
+ }
+
+ switchUser(newlySelectedUserId)
+ } else {
+ controller.onUserSelected(newlySelectedUserId, /* dialogShower= */ null)
+ }
}
/** Executes the given action. */
fun executeAction(action: UserActionModel) {
- when (action) {
- UserActionModel.ENTER_GUEST_MODE -> controller.createAndSwitchToGuestUser(null)
- UserActionModel.ADD_USER -> controller.showAddUserDialog(null)
- UserActionModel.ADD_SUPERVISED_USER -> controller.startSupervisedUserActivity()
- UserActionModel.NAVIGATE_TO_USER_MANAGEMENT ->
- activityStarter.startActivity(
- Intent(Settings.ACTION_USER_SETTINGS),
- /* dismissShade= */ false,
- )
+ if (isNewImpl) {
+ when (action) {
+ UserActionModel.ENTER_GUEST_MODE ->
+ guestUserInteractor.createAndSwitchTo(
+ this::showDialog,
+ this::dismissDialog,
+ this::selectUser,
+ )
+ UserActionModel.ADD_USER -> {
+ val currentUser = repository.getSelectedUserInfo()
+ showDialog(
+ ShowDialogRequestModel.ShowAddUserDialog(
+ userHandle = currentUser.userHandle,
+ isKeyguardShowing = keyguardInteractor.isKeyguardShowing(),
+ showEphemeralMessage = currentUser.isGuest && currentUser.isEphemeral,
+ )
+ )
+ }
+ UserActionModel.ADD_SUPERVISED_USER ->
+ activityStarter.startActivity(
+ Intent()
+ .setAction(UserManager.ACTION_CREATE_SUPERVISED_USER)
+ .setPackage(supervisedUserPackageName)
+ .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK),
+ /* dismissShade= */ false,
+ )
+ UserActionModel.NAVIGATE_TO_USER_MANAGEMENT ->
+ activityStarter.startActivity(
+ Intent(Settings.ACTION_USER_SETTINGS),
+ /* dismissShade= */ false,
+ )
+ }
+ } else {
+ when (action) {
+ UserActionModel.ENTER_GUEST_MODE -> controller.createAndSwitchToGuestUser(null)
+ UserActionModel.ADD_USER -> controller.showAddUserDialog(null)
+ UserActionModel.ADD_SUPERVISED_USER -> controller.startSupervisedUserActivity()
+ UserActionModel.NAVIGATE_TO_USER_MANAGEMENT ->
+ activityStarter.startActivity(
+ Intent(Settings.ACTION_USER_SETTINGS),
+ /* dismissShade= */ false,
+ )
+ }
}
}
+
+ fun exitGuestUser(
+ @UserIdInt guestUserId: Int,
+ @UserIdInt targetUserId: Int,
+ forceRemoveGuestOnExit: Boolean,
+ ) {
+ guestUserInteractor.exit(
+ guestUserId = guestUserId,
+ targetUserId = targetUserId,
+ forceRemoveGuestOnExit = forceRemoveGuestOnExit,
+ showDialog = this::showDialog,
+ dismissDialog = this::dismissDialog,
+ switchUser = this::switchUser,
+ )
+ }
+
+ fun removeGuestUser(
+ @UserIdInt guestUserId: Int,
+ @UserIdInt targetUserId: Int,
+ ) {
+ applicationScope.launch {
+ guestUserInteractor.remove(
+ guestUserId = guestUserId,
+ targetUserId = targetUserId,
+ ::showDialog,
+ ::dismissDialog,
+ ::selectUser,
+ )
+ }
+ }
+
+ private fun showDialog(request: ShowDialogRequestModel) {
+ _dialogShowRequests.value = request
+ }
+
+ private fun dismissDialog() {
+ _dialogDismissRequests.value = Unit
+ }
+
+ private fun notifyCallbacks() {
+ applicationScope.launch {
+ callbackMutex.withLock {
+ val iterator = callbacks.iterator()
+ while (iterator.hasNext()) {
+ val callback = iterator.next()
+ if (!callback.isEvictable()) {
+ callback.onUserStateChanged()
+ } else {
+ iterator.remove()
+ }
+ }
+ }
+ }
+ }
+
+ private suspend fun toRecord(
+ userInfo: UserInfo,
+ selectedUserId: Int,
+ ): UserRecord {
+ return LegacyUserDataHelper.createRecord(
+ context = applicationContext,
+ manager = manager,
+ userInfo = userInfo,
+ picture = null,
+ isCurrent = userInfo.id == selectedUserId,
+ canSwitchUsers = canSwitchUsers(selectedUserId),
+ )
+ }
+
+ private suspend fun toRecord(
+ action: UserActionModel,
+ selectedUserId: Int,
+ isAddFromLockscreenEnabled: Boolean,
+ ): UserRecord {
+ return LegacyUserDataHelper.createRecord(
+ context = applicationContext,
+ selectedUserId = selectedUserId,
+ actionType = action,
+ isRestricted =
+ if (action == UserActionModel.ENTER_GUEST_MODE) {
+ // Entering guest mode is never restricted, so it's allowed to happen from the
+ // lockscreen even if the "add from lockscreen" system setting is off.
+ false
+ } else {
+ !isAddFromLockscreenEnabled
+ },
+ isSwitchToEnabled =
+ canSwitchUsers(selectedUserId) &&
+ // If the user is auto-created is must not be currently resetting.
+ !(isGuestUserAutoCreated && isGuestUserResetting),
+ )
+ }
+
+ private fun switchUser(userId: Int) {
+ // TODO(b/246631653): track jank and lantecy like in the old impl.
+ refreshUsersScheduler.pause()
+ try {
+ activityManager.switchUser(userId)
+ } catch (e: RemoteException) {
+ Log.e(TAG, "Couldn't switch user.", e)
+ }
+ }
+
+ private suspend fun onBroadcastReceived(
+ intent: Intent,
+ previousUserInfo: UserInfo?,
+ ) {
+ val shouldRefreshAllUsers =
+ when (intent.action) {
+ Intent.ACTION_USER_SWITCHED -> {
+ dismissDialog()
+ val selectedUserId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, -1)
+ if (previousUserInfo?.id != selectedUserId) {
+ notifyCallbacks()
+ restartSecondaryService(selectedUserId)
+ }
+ if (guestUserInteractor.isGuestUserAutoCreated) {
+ guestUserInteractor.guaranteePresent()
+ }
+ true
+ }
+ Intent.ACTION_USER_INFO_CHANGED -> true
+ Intent.ACTION_USER_UNLOCKED -> {
+ // If we unlocked the system user, we should refresh all users.
+ intent.getIntExtra(
+ Intent.EXTRA_USER_HANDLE,
+ UserHandle.USER_NULL,
+ ) == UserHandle.USER_SYSTEM
+ }
+ else -> true
+ }
+
+ if (shouldRefreshAllUsers) {
+ refreshUsersScheduler.unpauseAndRefresh()
+ }
+ }
+
+ private fun restartSecondaryService(@UserIdInt userId: Int) {
+ val intent = Intent(applicationContext, SystemUISecondaryUserService::class.java)
+ // Disconnect from the old secondary user's service
+ val secondaryUserId = repository.secondaryUserId
+ if (secondaryUserId != UserHandle.USER_NULL) {
+ applicationContext.stopServiceAsUser(
+ intent,
+ UserHandle.of(secondaryUserId),
+ )
+ repository.secondaryUserId = UserHandle.USER_NULL
+ }
+
+ // Connect to the new secondary user's service (purely to ensure that a persistent
+ // SystemUI application is created for that user)
+ if (userId != UserHandle.USER_SYSTEM) {
+ applicationContext.startServiceAsUser(
+ intent,
+ UserHandle.of(userId),
+ )
+ repository.secondaryUserId = userId
+ }
+ }
+
+ private suspend fun toUserModels(
+ userInfos: List<UserInfo>,
+ selectedUserId: Int,
+ isUserSwitcherEnabled: Boolean,
+ ): List<UserModel> {
+ val canSwitchUsers = canSwitchUsers(selectedUserId)
+
+ return userInfos
+ // The guest user should go in the last position.
+ .sortedBy { it.isGuest }
+ .mapNotNull { userInfo ->
+ toUserModel(
+ userInfo = userInfo,
+ selectedUserId = selectedUserId,
+ canSwitchUsers = canSwitchUsers,
+ isUserSwitcherEnabled = isUserSwitcherEnabled,
+ )
+ }
+ }
+
+ private suspend fun toUserModel(
+ userInfo: UserInfo,
+ selectedUserId: Int,
+ canSwitchUsers: Boolean,
+ isUserSwitcherEnabled: Boolean,
+ ): UserModel? {
+ val userId = userInfo.id
+ val isSelected = userId == selectedUserId
+
+ return when {
+ // When the user switcher is not enabled in settings, we only show the primary user.
+ !isUserSwitcherEnabled && !userInfo.isPrimary -> null
+
+ // We avoid showing disabled users.
+ !userInfo.isEnabled -> null
+ userInfo.isGuest ->
+ UserModel(
+ id = userId,
+ name = Text.Loaded(userInfo.name),
+ image =
+ getUserImage(
+ isGuest = true,
+ userId = userId,
+ ),
+ isSelected = isSelected,
+ isSelectable = canSwitchUsers,
+ isGuest = true,
+ )
+ userInfo.supportsSwitchToByUser() ->
+ UserModel(
+ id = userId,
+ name = Text.Loaded(userInfo.name),
+ image =
+ getUserImage(
+ isGuest = false,
+ userId = userId,
+ ),
+ isSelected = isSelected,
+ isSelectable = canSwitchUsers || isSelected,
+ isGuest = false,
+ )
+ else -> null
+ }
+ }
+
+ private suspend fun canSwitchUsers(selectedUserId: Int): Boolean {
+ return withContext(backgroundDispatcher) {
+ manager.getUserSwitchability(UserHandle.of(selectedUserId))
+ } == UserManager.SWITCHABILITY_STATUS_OK
+ }
+
+ @SuppressLint("UseCompatLoadingForDrawables")
+ private suspend fun getUserImage(
+ isGuest: Boolean,
+ userId: Int,
+ ): Drawable {
+ if (isGuest) {
+ return checkNotNull(applicationContext.getDrawable(R.drawable.ic_account_circle))
+ }
+
+ // TODO(b/246631653): cache the bitmaps to avoid the background work to fetch them.
+ // TODO(b/246631653): downscale the bitmaps to R.dimen.max_avatar_size if requested.
+ val userIcon = withContext(backgroundDispatcher) { manager.getUserIcon(userId) }
+ if (userIcon != null) {
+ return BitmapDrawable(userIcon)
+ }
+
+ return UserIcons.getDefaultUserIcon(
+ applicationContext.resources,
+ userId,
+ /* light= */ false
+ )
+ }
+
+ companion object {
+ private const val TAG = "UserInteractor"
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/user/domain/model/ShowDialogRequestModel.kt b/packages/SystemUI/src/com/android/systemui/user/domain/model/ShowDialogRequestModel.kt
new file mode 100644
index 0000000..08d7c5a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/user/domain/model/ShowDialogRequestModel.kt
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package com.android.systemui.user.domain.model
+
+import android.os.UserHandle
+
+/** Encapsulates a request to show a dialog. */
+sealed class ShowDialogRequestModel {
+ data class ShowAddUserDialog(
+ val userHandle: UserHandle,
+ val isKeyguardShowing: Boolean,
+ val showEphemeralMessage: Boolean,
+ ) : ShowDialogRequestModel()
+
+ data class ShowUserCreationDialog(
+ val isGuest: Boolean,
+ ) : ShowDialogRequestModel()
+
+ data class ShowExitGuestDialog(
+ val guestUserId: Int,
+ val targetUserId: Int,
+ val isGuestEphemeral: Boolean,
+ val isKeyguardShowing: Boolean,
+ val onExitGuestUser: (guestId: Int, targetId: Int, forceRemoveGuest: Boolean) -> Unit,
+ ) : ShowDialogRequestModel()
+}
diff --git a/packages/SystemUI/src/com/android/systemui/user/legacyhelper/data/LegacyUserDataHelper.kt b/packages/SystemUI/src/com/android/systemui/user/legacyhelper/data/LegacyUserDataHelper.kt
new file mode 100644
index 0000000..137de15
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/user/legacyhelper/data/LegacyUserDataHelper.kt
@@ -0,0 +1,150 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package com.android.systemui.user.legacyhelper.data
+
+import android.content.Context
+import android.content.pm.UserInfo
+import android.graphics.Bitmap
+import android.os.UserManager
+import com.android.settingslib.RestrictedLockUtils.EnforcedAdmin
+import com.android.settingslib.RestrictedLockUtilsInternal
+import com.android.systemui.R
+import com.android.systemui.user.data.source.UserRecord
+import com.android.systemui.user.shared.model.UserActionModel
+
+/**
+ * Defines utility functions for helping with legacy data code for users.
+ *
+ * We need these to avoid code duplication between logic inside the UserSwitcherController and in
+ * modern architecture classes such as repositories, interactors, and view-models. If we ever
+ * simplify UserSwitcherController (or delete it), the code here could be moved into its call-sites.
+ */
+object LegacyUserDataHelper {
+
+ @JvmStatic
+ fun createRecord(
+ context: Context,
+ manager: UserManager,
+ picture: Bitmap?,
+ userInfo: UserInfo,
+ isCurrent: Boolean,
+ canSwitchUsers: Boolean,
+ ): UserRecord {
+ val isGuest = userInfo.isGuest
+ return UserRecord(
+ info = userInfo,
+ picture =
+ getPicture(
+ manager = manager,
+ context = context,
+ userInfo = userInfo,
+ picture = picture,
+ ),
+ isGuest = isGuest,
+ isCurrent = isCurrent,
+ isSwitchToEnabled = canSwitchUsers || (isCurrent && !isGuest),
+ )
+ }
+
+ @JvmStatic
+ fun createRecord(
+ context: Context,
+ selectedUserId: Int,
+ actionType: UserActionModel,
+ isRestricted: Boolean,
+ isSwitchToEnabled: Boolean,
+ ): UserRecord {
+ return UserRecord(
+ isGuest = actionType == UserActionModel.ENTER_GUEST_MODE,
+ isAddUser = actionType == UserActionModel.ADD_USER,
+ isAddSupervisedUser = actionType == UserActionModel.ADD_SUPERVISED_USER,
+ isRestricted = isRestricted,
+ isSwitchToEnabled = isSwitchToEnabled,
+ enforcedAdmin =
+ getEnforcedAdmin(
+ context = context,
+ selectedUserId = selectedUserId,
+ ),
+ )
+ }
+
+ fun toUserActionModel(record: UserRecord): UserActionModel {
+ check(!isUser(record))
+
+ return when {
+ record.isAddUser -> UserActionModel.ADD_USER
+ record.isAddSupervisedUser -> UserActionModel.ADD_SUPERVISED_USER
+ record.isGuest -> UserActionModel.ENTER_GUEST_MODE
+ else -> error("Not a known action: $record")
+ }
+ }
+
+ fun isUser(record: UserRecord): Boolean {
+ return record.info != null
+ }
+
+ private fun getEnforcedAdmin(
+ context: Context,
+ selectedUserId: Int,
+ ): EnforcedAdmin? {
+ val admin =
+ RestrictedLockUtilsInternal.checkIfRestrictionEnforced(
+ context,
+ UserManager.DISALLOW_ADD_USER,
+ selectedUserId,
+ )
+ ?: return null
+
+ return if (
+ !RestrictedLockUtilsInternal.hasBaseUserRestriction(
+ context,
+ UserManager.DISALLOW_ADD_USER,
+ selectedUserId,
+ )
+ ) {
+ admin
+ } else {
+ null
+ }
+ }
+
+ private fun getPicture(
+ context: Context,
+ manager: UserManager,
+ userInfo: UserInfo,
+ picture: Bitmap?,
+ ): Bitmap? {
+ if (userInfo.isGuest) {
+ return null
+ }
+
+ if (picture != null) {
+ return picture
+ }
+
+ val unscaledOrNull = manager.getUserIcon(userInfo.id) ?: return null
+
+ val avatarSize = context.resources.getDimensionPixelSize(R.dimen.max_avatar_size)
+ return Bitmap.createScaledBitmap(
+ unscaledOrNull,
+ avatarSize,
+ avatarSize,
+ /* filter= */ true,
+ )
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/user/shared/model/UserModel.kt b/packages/SystemUI/src/com/android/systemui/user/shared/model/UserModel.kt
index bf7977a..2095683 100644
--- a/packages/SystemUI/src/com/android/systemui/user/shared/model/UserModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/user/shared/model/UserModel.kt
@@ -32,4 +32,6 @@
val isSelected: Boolean,
/** Whether this use is selectable. A non-selectable user cannot be switched to. */
val isSelectable: Boolean,
+ /** Whether this model represents the guest user. */
+ val isGuest: Boolean,
)
diff --git a/packages/SystemUI/src/com/android/systemui/user/ui/dialog/AddUserDialog.kt b/packages/SystemUI/src/com/android/systemui/user/ui/dialog/AddUserDialog.kt
new file mode 100644
index 0000000..a9d66de
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/user/ui/dialog/AddUserDialog.kt
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+package com.android.systemui.user.ui.dialog
+
+import android.app.ActivityManager
+import android.content.Context
+import android.content.DialogInterface
+import android.content.Intent
+import android.os.UserHandle
+import com.android.settingslib.R
+import com.android.systemui.animation.DialogLaunchAnimator
+import com.android.systemui.broadcast.BroadcastSender
+import com.android.systemui.plugins.FalsingManager
+import com.android.systemui.statusbar.phone.SystemUIDialog
+import com.android.systemui.user.CreateUserActivity
+
+/** Dialog for adding a new user to the device. */
+class AddUserDialog(
+ context: Context,
+ userHandle: UserHandle,
+ isKeyguardShowing: Boolean,
+ showEphemeralMessage: Boolean,
+ private val falsingManager: FalsingManager,
+ private val broadcastSender: BroadcastSender,
+ private val dialogLaunchAnimator: DialogLaunchAnimator
+) : SystemUIDialog(context) {
+
+ private val onClickListener =
+ object : DialogInterface.OnClickListener {
+ override fun onClick(dialog: DialogInterface, which: Int) {
+ val penalty =
+ if (which == BUTTON_NEGATIVE) {
+ FalsingManager.NO_PENALTY
+ } else {
+ FalsingManager.MODERATE_PENALTY
+ }
+ if (falsingManager.isFalseTap(penalty)) {
+ return
+ }
+
+ if (which == BUTTON_NEUTRAL) {
+ cancel()
+ return
+ }
+
+ dialogLaunchAnimator.dismissStack(this@AddUserDialog)
+ if (ActivityManager.isUserAMonkey()) {
+ return
+ }
+
+ // Use broadcast instead of ShadeController, as this dialog may have started in
+ // another
+ // process where normal dagger bindings are not available.
+ broadcastSender.sendBroadcastAsUser(
+ Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS),
+ UserHandle.CURRENT
+ )
+
+ context.startActivityAsUser(
+ CreateUserActivity.createIntentForStart(context),
+ userHandle,
+ )
+ }
+ }
+
+ init {
+ setTitle(R.string.user_add_user_title)
+ val message =
+ context.getString(R.string.user_add_user_message_short) +
+ if (showEphemeralMessage) {
+ context.getString(
+ com.android.systemui.R.string.user_add_user_message_guest_remove
+ )
+ } else {
+ ""
+ }
+ setMessage(message)
+
+ setButton(
+ BUTTON_NEUTRAL,
+ context.getString(android.R.string.cancel),
+ onClickListener,
+ )
+
+ setButton(
+ BUTTON_POSITIVE,
+ context.getString(android.R.string.ok),
+ onClickListener,
+ )
+
+ setWindowOnTop(this, isKeyguardShowing)
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/user/ui/dialog/ExitGuestDialog.kt b/packages/SystemUI/src/com/android/systemui/user/ui/dialog/ExitGuestDialog.kt
new file mode 100644
index 0000000..19ad44d
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/user/ui/dialog/ExitGuestDialog.kt
@@ -0,0 +1,132 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+package com.android.systemui.user.ui.dialog
+
+import android.annotation.UserIdInt
+import android.content.Context
+import android.content.DialogInterface
+import com.android.settingslib.R
+import com.android.systemui.animation.DialogLaunchAnimator
+import com.android.systemui.plugins.FalsingManager
+import com.android.systemui.statusbar.phone.SystemUIDialog
+
+/** Dialog for exiting the guest user. */
+class ExitGuestDialog(
+ context: Context,
+ private val guestUserId: Int,
+ private val isGuestEphemeral: Boolean,
+ private val targetUserId: Int,
+ isKeyguardShowing: Boolean,
+ private val falsingManager: FalsingManager,
+ private val dialogLaunchAnimator: DialogLaunchAnimator,
+ private val onExitGuestUserListener: OnExitGuestUserListener,
+) : SystemUIDialog(context) {
+
+ fun interface OnExitGuestUserListener {
+ fun onExitGuestUser(
+ @UserIdInt guestId: Int,
+ @UserIdInt targetId: Int,
+ forceRemoveGuest: Boolean,
+ )
+ }
+
+ private val onClickListener =
+ object : DialogInterface.OnClickListener {
+ override fun onClick(dialog: DialogInterface, which: Int) {
+ val penalty =
+ if (which == BUTTON_NEGATIVE) {
+ FalsingManager.NO_PENALTY
+ } else {
+ FalsingManager.MODERATE_PENALTY
+ }
+ if (falsingManager.isFalseTap(penalty)) {
+ return
+ }
+
+ if (isGuestEphemeral) {
+ if (which == BUTTON_POSITIVE) {
+ dialogLaunchAnimator.dismissStack(this@ExitGuestDialog)
+ // Ephemeral guest: exit guest, guest is removed by the system
+ // on exit, since its marked ephemeral
+ onExitGuestUserListener.onExitGuestUser(guestUserId, targetUserId, false)
+ } else if (which == BUTTON_NEGATIVE) {
+ // Cancel clicked, do nothing
+ cancel()
+ }
+ } else {
+ when (which) {
+ BUTTON_POSITIVE -> {
+ dialogLaunchAnimator.dismissStack(this@ExitGuestDialog)
+ // Non-ephemeral guest: exit guest, guest is not removed by the system
+ // on exit, since its marked non-ephemeral
+ onExitGuestUserListener.onExitGuestUser(
+ guestUserId,
+ targetUserId,
+ false
+ )
+ }
+ BUTTON_NEGATIVE -> {
+ dialogLaunchAnimator.dismissStack(this@ExitGuestDialog)
+ // Non-ephemeral guest: remove guest and then exit
+ onExitGuestUserListener.onExitGuestUser(guestUserId, targetUserId, true)
+ }
+ BUTTON_NEUTRAL -> {
+ // Cancel clicked, do nothing
+ cancel()
+ }
+ }
+ }
+ }
+ }
+
+ init {
+ if (isGuestEphemeral) {
+ setTitle(context.getString(R.string.guest_exit_dialog_title))
+ setMessage(context.getString(R.string.guest_exit_dialog_message))
+ setButton(
+ BUTTON_NEUTRAL,
+ context.getString(android.R.string.cancel),
+ onClickListener,
+ )
+ setButton(
+ BUTTON_POSITIVE,
+ context.getString(R.string.guest_exit_dialog_button),
+ onClickListener,
+ )
+ } else {
+ setTitle(context.getString(R.string.guest_exit_dialog_title_non_ephemeral))
+ setMessage(context.getString(R.string.guest_exit_dialog_message_non_ephemeral))
+ setButton(
+ BUTTON_NEUTRAL,
+ context.getString(android.R.string.cancel),
+ onClickListener,
+ )
+ setButton(
+ BUTTON_NEGATIVE,
+ context.getString(R.string.guest_exit_clear_data_button),
+ onClickListener,
+ )
+ setButton(
+ BUTTON_POSITIVE,
+ context.getString(R.string.guest_exit_save_data_button),
+ onClickListener,
+ )
+ }
+ setWindowOnTop(this, isKeyguardShowing)
+ setCanceledOnTouchOutside(false)
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserDialogModule.kt b/packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserDialogModule.kt
new file mode 100644
index 0000000..c1d2f47
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserDialogModule.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package com.android.systemui.user.ui.dialog
+
+import com.android.systemui.CoreStartable
+import dagger.Binds
+import dagger.Module
+import dagger.multibindings.ClassKey
+import dagger.multibindings.IntoMap
+
+@Module
+interface UserDialogModule {
+
+ @Binds
+ @IntoMap
+ @ClassKey(UserSwitcherDialogCoordinator::class)
+ fun bindFeature(impl: UserSwitcherDialogCoordinator): CoreStartable
+}
diff --git a/packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserSwitcherDialogCoordinator.kt b/packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserSwitcherDialogCoordinator.kt
new file mode 100644
index 0000000..6e7b523
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserSwitcherDialogCoordinator.kt
@@ -0,0 +1,122 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package com.android.systemui.user.ui.dialog
+
+import android.app.Dialog
+import android.content.Context
+import com.android.settingslib.users.UserCreatingDialog
+import com.android.systemui.CoreStartable
+import com.android.systemui.animation.DialogLaunchAnimator
+import com.android.systemui.broadcast.BroadcastSender
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags
+import com.android.systemui.plugins.FalsingManager
+import com.android.systemui.user.domain.interactor.UserInteractor
+import com.android.systemui.user.domain.model.ShowDialogRequestModel
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.flow.filterNotNull
+import kotlinx.coroutines.launch
+
+/** Coordinates dialogs for user switcher logic. */
+@SysUISingleton
+class UserSwitcherDialogCoordinator
+@Inject
+constructor(
+ @Application private val context: Context,
+ @Application private val applicationScope: CoroutineScope,
+ private val falsingManager: FalsingManager,
+ private val broadcastSender: BroadcastSender,
+ private val dialogLaunchAnimator: DialogLaunchAnimator,
+ private val interactor: UserInteractor,
+ private val featureFlags: FeatureFlags,
+) : CoreStartable(context) {
+
+ private var currentDialog: Dialog? = null
+
+ override fun start() {
+ if (featureFlags.isEnabled(Flags.USER_INTERACTOR_AND_REPO_USE_CONTROLLER)) {
+ return
+ }
+
+ startHandlingDialogShowRequests()
+ startHandlingDialogDismissRequests()
+ }
+
+ private fun startHandlingDialogShowRequests() {
+ applicationScope.launch {
+ interactor.dialogShowRequests.filterNotNull().collect { request ->
+ currentDialog?.let {
+ if (it.isShowing) {
+ it.cancel()
+ }
+ }
+
+ currentDialog =
+ when (request) {
+ is ShowDialogRequestModel.ShowAddUserDialog ->
+ AddUserDialog(
+ context = context,
+ userHandle = request.userHandle,
+ isKeyguardShowing = request.isKeyguardShowing,
+ showEphemeralMessage = request.showEphemeralMessage,
+ falsingManager = falsingManager,
+ broadcastSender = broadcastSender,
+ dialogLaunchAnimator = dialogLaunchAnimator,
+ )
+ is ShowDialogRequestModel.ShowUserCreationDialog ->
+ UserCreatingDialog(
+ context,
+ request.isGuest,
+ )
+ is ShowDialogRequestModel.ShowExitGuestDialog ->
+ ExitGuestDialog(
+ context = context,
+ guestUserId = request.guestUserId,
+ isGuestEphemeral = request.isGuestEphemeral,
+ targetUserId = request.targetUserId,
+ isKeyguardShowing = request.isKeyguardShowing,
+ falsingManager = falsingManager,
+ dialogLaunchAnimator = dialogLaunchAnimator,
+ onExitGuestUserListener = request.onExitGuestUser,
+ )
+ }
+
+ currentDialog?.show()
+ interactor.onDialogShown()
+ }
+ }
+ }
+
+ private fun startHandlingDialogDismissRequests() {
+ applicationScope.launch {
+ interactor.dialogDismissRequests.filterNotNull().collect {
+ currentDialog?.let {
+ if (it.isShowing) {
+ it.cancel()
+ }
+ }
+
+ interactor.onDialogDismissed()
+ }
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModel.kt b/packages/SystemUI/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModel.kt
index 398341d..5b83df7 100644
--- a/packages/SystemUI/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModel.kt
@@ -21,7 +21,10 @@
import androidx.lifecycle.ViewModelProvider
import com.android.systemui.R
import com.android.systemui.common.ui.drawable.CircularDrawable
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags
import com.android.systemui.power.domain.interactor.PowerInteractor
+import com.android.systemui.user.domain.interactor.GuestUserInteractor
import com.android.systemui.user.domain.interactor.UserInteractor
import com.android.systemui.user.legacyhelper.ui.LegacyUserUiHelper
import com.android.systemui.user.shared.model.UserActionModel
@@ -36,9 +39,14 @@
class UserSwitcherViewModel
private constructor(
private val userInteractor: UserInteractor,
+ private val guestUserInteractor: GuestUserInteractor,
private val powerInteractor: PowerInteractor,
+ private val featureFlags: FeatureFlags,
) : ViewModel() {
+ private val isNewImpl: Boolean
+ get() = !featureFlags.isEnabled(Flags.USER_INTERACTOR_AND_REPO_USE_CONTROLLER)
+
/** On-device users. */
val users: Flow<List<UserViewModel>> =
userInteractor.users.map { models -> models.map { user -> toViewModel(user) } }
@@ -47,9 +55,6 @@
val maximumUserColumns: Flow<Int> =
users.map { LegacyUserUiHelper.getMaxUserSwitcherItemColumns(it.size) }
- /** Whether the button to open the user action menu is visible. */
- val isOpenMenuButtonVisible: Flow<Boolean> = userInteractor.actions.map { it.isNotEmpty() }
-
private val _isMenuVisible = MutableStateFlow(false)
/**
* Whether the user action menu should be shown. Once the action menu is dismissed/closed, the
@@ -58,9 +63,23 @@
val isMenuVisible: Flow<Boolean> = _isMenuVisible
/** The user action menu. */
val menu: Flow<List<UserActionViewModel>> =
- userInteractor.actions.map { actions -> actions.map { action -> toViewModel(action) } }
+ userInteractor.actions.map { actions ->
+ if (isNewImpl && actions.isNotEmpty()) {
+ // If we have actions, we add NAVIGATE_TO_USER_MANAGEMENT because that's a user
+ // switcher specific action that is not known to the our data source or other
+ // features.
+ actions + listOf(UserActionModel.NAVIGATE_TO_USER_MANAGEMENT)
+ } else {
+ actions
+ }
+ .map { action -> toViewModel(action) }
+ }
+
+ /** Whether the button to open the user action menu is visible. */
+ val isOpenMenuButtonVisible: Flow<Boolean> = menu.map { it.isNotEmpty() }
private val hasCancelButtonBeenClicked = MutableStateFlow(false)
+ private val isFinishRequiredDueToExecutedAction = MutableStateFlow(false)
/**
* Whether the observer should finish the experience. Once consumed, [onFinished] must be called
@@ -81,6 +100,7 @@
*/
fun onFinished() {
hasCancelButtonBeenClicked.value = false
+ isFinishRequiredDueToExecutedAction.value = false
}
/** Notifies that the user has clicked the "open menu" button. */
@@ -120,8 +140,10 @@
},
// When the cancel button is clicked, we should finish.
hasCancelButtonBeenClicked,
- ) { selectedUserChanged, screenTurnedOff, cancelButtonClicked ->
- selectedUserChanged || screenTurnedOff || cancelButtonClicked
+ // If an executed action told us to finish, we should finish,
+ isFinishRequiredDueToExecutedAction,
+ ) { selectedUserChanged, screenTurnedOff, cancelButtonClicked, executedActionFinish ->
+ selectedUserChanged || screenTurnedOff || cancelButtonClicked || executedActionFinish
}
}
@@ -164,13 +186,25 @@
} else {
LegacyUserUiHelper.getUserSwitcherActionTextResourceId(
isGuest = model == UserActionModel.ENTER_GUEST_MODE,
- isGuestUserAutoCreated = userInteractor.isGuestUserAutoCreated,
- isGuestUserResetting = userInteractor.isGuestUserResetting,
+ isGuestUserAutoCreated = guestUserInteractor.isGuestUserAutoCreated,
+ isGuestUserResetting = guestUserInteractor.isGuestUserResetting,
isAddSupervisedUser = model == UserActionModel.ADD_SUPERVISED_USER,
isAddUser = model == UserActionModel.ADD_USER,
)
},
- onClicked = { userInteractor.executeAction(action = model) },
+ onClicked = {
+ userInteractor.executeAction(action = model)
+ // We don't finish because we want to show a dialog over the full-screen UI and
+ // that dialog can be dismissed in case the user changes their mind and decides not
+ // to add a user.
+ //
+ // We finish for all other actions because they navigate us away from the
+ // full-screen experience or are destructive (like changing to the guest user).
+ val shouldFinish = model != UserActionModel.ADD_USER
+ if (shouldFinish) {
+ isFinishRequiredDueToExecutedAction.value = true
+ }
+ },
)
}
@@ -186,13 +220,17 @@
@Inject
constructor(
private val userInteractor: UserInteractor,
+ private val guestUserInteractor: GuestUserInteractor,
private val powerInteractor: PowerInteractor,
+ private val featureFlags: FeatureFlags,
) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
@Suppress("UNCHECKED_CAST")
return UserSwitcherViewModel(
userInteractor = userInteractor,
+ guestUserInteractor = guestUserInteractor,
powerInteractor = powerInteractor,
+ featureFlags = featureFlags,
)
as T
}
diff --git a/packages/SystemUI/src/com/android/systemui/util/settings/SettingsProxyExt.kt b/packages/SystemUI/src/com/android/systemui/util/settings/SettingsProxyExt.kt
new file mode 100644
index 0000000..0b8257d
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/util/settings/SettingsProxyExt.kt
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package com.android.systemui.util.settings
+
+import android.annotation.UserIdInt
+import android.database.ContentObserver
+import android.os.UserHandle
+import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+
+/** Kotlin extension functions for [SettingsProxy]. */
+object SettingsProxyExt {
+
+ /** Returns a flow of [Unit] that is invoked each time that content is updated. */
+ fun SettingsProxy.observerFlow(
+ vararg names: String,
+ @UserIdInt userId: Int = UserHandle.USER_CURRENT,
+ ): Flow<Unit> {
+ return conflatedCallbackFlow {
+ val observer =
+ object : ContentObserver(null) {
+ override fun onChange(selfChange: Boolean) {
+ trySend(Unit)
+ }
+ }
+
+ names.forEach { name -> registerContentObserverForUser(name, observer, userId) }
+
+ awaitClose { unregisterContentObserver(observer) }
+ }
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerTest.java
index 43f6f1a..c1036e3 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerTest.java
@@ -411,7 +411,7 @@
0 /* flags */);
users.add(new UserRecord(info, null, false /* isGuest */, false /* isCurrent */,
false /* isAddUser */, false /* isRestricted */, true /* isSwitchToEnabled */,
- false /* isAddSupervisedUser */));
+ false /* isAddSupervisedUser */, null /* enforcedAdmin */));
}
return users;
}
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java
index 12d3d42..14b637c 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java
@@ -60,6 +60,7 @@
import android.content.pm.ResolveInfo;
import android.content.pm.ServiceInfo;
import android.content.pm.UserInfo;
+import android.hardware.SensorPrivacyManager;
import android.hardware.biometrics.BiometricConstants;
import android.hardware.biometrics.BiometricManager;
import android.hardware.biometrics.BiometricSourceType;
@@ -80,13 +81,13 @@
import android.os.RemoteException;
import android.os.UserHandle;
import android.os.UserManager;
+import android.service.dreams.IDreamManager;
import android.telephony.ServiceState;
import android.telephony.SubscriptionInfo;
import android.telephony.SubscriptionManager;
import android.telephony.TelephonyManager;
import android.test.suitebuilder.annotation.SmallTest;
import android.testing.AndroidTestingRunner;
-import android.testing.TestableContext;
import android.testing.TestableLooper;
import com.android.dx.mockito.inline.extended.ExtendedMockito;
@@ -171,6 +172,8 @@
@Mock
private DevicePolicyManager mDevicePolicyManager;
@Mock
+ private IDreamManager mDreamManager;
+ @Mock
private KeyguardBypassController mKeyguardBypassController;
@Mock
private SubscriptionManager mSubscriptionManager;
@@ -179,6 +182,8 @@
@Mock
private TelephonyManager mTelephonyManager;
@Mock
+ private SensorPrivacyManager mSensorPrivacyManager;
+ @Mock
private StatusBarStateController mStatusBarStateController;
@Mock
private AuthController mAuthController;
@@ -220,7 +225,6 @@
private TestableLooper mTestableLooper;
private Handler mHandler;
private TestableKeyguardUpdateMonitor mKeyguardUpdateMonitor;
- private TestableContext mSpiedContext;
private MockitoSession mMockitoSession;
private StatusBarStateController.StateListener mStatusBarStateListener;
private IBiometricEnabledOnKeyguardCallback mBiometricEnabledOnKeyguardCallback;
@@ -229,9 +233,6 @@
@Before
public void setup() throws RemoteException {
MockitoAnnotations.initMocks(this);
- mSpiedContext = spy(mContext);
- when(mPackageManager.hasSystemFeature(anyString())).thenReturn(true);
- when(mSpiedContext.getPackageManager()).thenReturn(mPackageManager);
when(mActivityService.getCurrentUser()).thenReturn(mCurrentUserInfo);
when(mActivityService.getCurrentUserId()).thenReturn(mCurrentUserId);
when(mFaceManager.isHardwareDetected()).thenReturn(true);
@@ -280,14 +281,6 @@
.thenReturn(new ServiceState());
when(mLockPatternUtils.getLockSettings()).thenReturn(mLockSettings);
when(mAuthController.isUdfpsEnrolled(anyInt())).thenReturn(false);
- mSpiedContext.addMockSystemService(TrustManager.class, mTrustManager);
- mSpiedContext.addMockSystemService(FingerprintManager.class, mFingerprintManager);
- mSpiedContext.addMockSystemService(BiometricManager.class, mBiometricManager);
- mSpiedContext.addMockSystemService(FaceManager.class, mFaceManager);
- mSpiedContext.addMockSystemService(UserManager.class, mUserManager);
- mSpiedContext.addMockSystemService(DevicePolicyManager.class, mDevicePolicyManager);
- mSpiedContext.addMockSystemService(SubscriptionManager.class, mSubscriptionManager);
- mSpiedContext.addMockSystemService(TelephonyManager.class, mTelephonyManager);
mMockitoSession = ExtendedMockito.mockitoSession()
.spyStatic(SubscriptionManager.class)
@@ -302,7 +295,7 @@
mTestableLooper = TestableLooper.get(this);
allowTestableLooperAsMainThread();
- mKeyguardUpdateMonitor = new TestableKeyguardUpdateMonitor(mSpiedContext);
+ mKeyguardUpdateMonitor = new TestableKeyguardUpdateMonitor(mContext);
verify(mBiometricManager)
.registerEnabledOnKeyguardCallback(mBiometricEnabledCallbackArgCaptor.capture());
@@ -357,7 +350,7 @@
when(mTelephonyManager.getSimState(anyInt())).thenReturn(state);
when(mSubscriptionManager.getSubscriptionIds(anyInt())).thenReturn(new int[]{subId});
- KeyguardUpdateMonitor testKUM = new TestableKeyguardUpdateMonitor(mSpiedContext);
+ KeyguardUpdateMonitor testKUM = new TestableKeyguardUpdateMonitor(mContext);
mTestableLooper.processAllMessages();
@@ -1203,9 +1196,9 @@
@Test
public void testShouldListenForFace_whenFaceManagerNotAvailable_returnsFalse() {
cleanupKeyguardUpdateMonitor();
- mSpiedContext.addMockSystemService(FaceManager.class, null);
- when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_FACE)).thenReturn(false);
- mKeyguardUpdateMonitor = new TestableKeyguardUpdateMonitor(mSpiedContext);
+ mFaceManager = null;
+
+ mKeyguardUpdateMonitor = new TestableKeyguardUpdateMonitor(mContext);
assertThat(mKeyguardUpdateMonitor.shouldListenForFace()).isFalse();
}
@@ -1259,7 +1252,7 @@
// This disables face auth
when(mUserManager.isPrimaryUser()).thenReturn(false);
mKeyguardUpdateMonitor =
- new TestableKeyguardUpdateMonitor(mSpiedContext);
+ new TestableKeyguardUpdateMonitor(mContext);
// Face auth should run when the following is true.
keyguardNotGoingAway();
@@ -1528,15 +1521,16 @@
verify(mHandler, times(1)).removeCallbacks(mKeyguardUpdateMonitor.mFpCancelNotReceived);
mKeyguardUpdateMonitor.dispatchStartedGoingToSleep(0 /* why */);
mTestableLooper.processAllMessages();
- assertThat(mKeyguardUpdateMonitor.shouldListenForFingerprint(anyBoolean())).isEqualTo(true);
+ assertThat(mKeyguardUpdateMonitor.shouldListenForFingerprint(false)).isEqualTo(true);
+ assertThat(mKeyguardUpdateMonitor.shouldListenForFingerprint(true)).isEqualTo(true);
}
@Test
public void testFingerAcquired_wakesUpPowerManager() {
cleanupKeyguardUpdateMonitor();
- mSpiedContext.getOrCreateTestableResources().addOverride(
+ mContext.getOrCreateTestableResources().addOverride(
com.android.internal.R.bool.kg_wake_on_acquire_start, true);
- mKeyguardUpdateMonitor = new TestableKeyguardUpdateMonitor(mSpiedContext);
+ mKeyguardUpdateMonitor = new TestableKeyguardUpdateMonitor(mContext);
fingerprintAcquireStart();
verify(mPowerManager).wakeUp(anyLong(), anyInt(), anyString());
@@ -1545,9 +1539,9 @@
@Test
public void testFingerAcquired_doesNotWakeUpPowerManager() {
cleanupKeyguardUpdateMonitor();
- mSpiedContext.getOrCreateTestableResources().addOverride(
+ mContext.getOrCreateTestableResources().addOverride(
com.android.internal.R.bool.kg_wake_on_acquire_start, false);
- mKeyguardUpdateMonitor = new TestableKeyguardUpdateMonitor(mSpiedContext);
+ mKeyguardUpdateMonitor = new TestableKeyguardUpdateMonitor(mContext);
fingerprintAcquireStart();
verify(mPowerManager, never()).wakeUp(anyLong(), anyInt(), anyString());
@@ -1717,7 +1711,9 @@
mAuthController, mTelephonyListenerManager,
mInteractionJankMonitor, mLatencyTracker, mActiveUnlockConfig,
mKeyguardUpdateMonitorLogger, mUiEventLogger, () -> mSessionTracker,
- mPowerManager);
+ mPowerManager, mTrustManager, mSubscriptionManager, mUserManager,
+ mDreamManager, mDevicePolicyManager, mSensorPrivacyManager, mTelephonyManager,
+ mPackageManager, mFaceManager, mFingerprintManager, mBiometricManager);
setStrongAuthTracker(KeyguardUpdateMonitorTest.this.mStrongAuthTracker);
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeTriggersTest.java b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeTriggersTest.java
index 6436981..781dc15 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeTriggersTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeTriggersTest.java
@@ -144,6 +144,12 @@
mTriggers.transitionTo(DozeMachine.State.INITIALIZED, DozeMachine.State.DOZE);
clearInvocations(mMachine);
+ ArgumentCaptor<Boolean> boolCaptor = ArgumentCaptor.forClass(Boolean.class);
+ doAnswer(invocation ->
+ when(mHost.isPulsePending()).thenReturn(boolCaptor.getValue())
+ ).when(mHost).setPulsePending(boolCaptor.capture());
+
+ when(mHost.isPulsingBlocked()).thenReturn(false);
mProximitySensor.setLastEvent(new ThresholdSensorEvent(true, 1));
captor.getValue().onNotificationAlerted(null /* pulseSuppressedListener */);
mProximitySensor.alertListeners();
@@ -160,6 +166,29 @@
}
@Test
+ public void testOnNotification_noPulseIfPulseIsNotPendingAnymore() {
+ when(mMachine.getState()).thenReturn(DozeMachine.State.DOZE);
+ ArgumentCaptor<DozeHost.Callback> captor = ArgumentCaptor.forClass(DozeHost.Callback.class);
+ doAnswer(invocation -> null).when(mHost).addCallback(captor.capture());
+
+ mTriggers.transitionTo(UNINITIALIZED, DozeMachine.State.INITIALIZED);
+ mTriggers.transitionTo(DozeMachine.State.INITIALIZED, DozeMachine.State.DOZE);
+ clearInvocations(mMachine);
+ when(mHost.isPulsingBlocked()).thenReturn(false);
+
+ // GIVEN pulsePending = false
+ when(mHost.isPulsePending()).thenReturn(false);
+
+ // WHEN prox check returns FAR
+ mProximitySensor.setLastEvent(new ThresholdSensorEvent(false, 2));
+ captor.getValue().onNotificationAlerted(null /* pulseSuppressedListener */);
+ mProximitySensor.alertListeners();
+
+ // THEN don't request pulse because the pending pulse was abandoned early
+ verify(mMachine, never()).requestPulse(anyInt());
+ }
+
+ @Test
public void testTransitionTo_disablesAndEnablesTouchSensors() {
when(mMachine.getState()).thenReturn(DozeMachine.State.DOZE);
@@ -237,6 +266,11 @@
when(mSessionTracker.getSessionId(StatusBarManager.SESSION_KEYGUARD))
.thenReturn(keyguardSessionId);
+ ArgumentCaptor<Boolean> boolCaptor = ArgumentCaptor.forClass(Boolean.class);
+ doAnswer(invocation ->
+ when(mHost.isPulsePending()).thenReturn(boolCaptor.getValue())
+ ).when(mHost).setPulsePending(boolCaptor.capture());
+
// WHEN quick pick up is triggered
mTriggers.onSensor(DozeLog.REASON_SENSOR_QUICK_PICKUP, 100, 100, null);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt
index ba1e168..eea2e95 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt
@@ -116,6 +116,7 @@
val job = underTest.isKeyguardShowing.onEach { latest = it }.launchIn(this)
assertThat(latest).isFalse()
+ assertThat(underTest.isKeyguardShowing()).isFalse()
val captor = argumentCaptor<KeyguardStateController.Callback>()
verify(keyguardStateController).addCallback(captor.capture())
@@ -123,10 +124,12 @@
whenever(keyguardStateController.isShowing).thenReturn(true)
captor.value.onKeyguardShowingChanged()
assertThat(latest).isTrue()
+ assertThat(underTest.isKeyguardShowing()).isTrue()
whenever(keyguardStateController.isShowing).thenReturn(false)
captor.value.onKeyguardShowingChanged()
assertThat(latest).isFalse()
+ assertThat(underTest.isKeyguardShowing()).isFalse()
job.cancel()
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/SeekBarViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/SeekBarViewModelTest.kt
index 82aa612..5973340 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/SeekBarViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/SeekBarViewModelTest.kt
@@ -26,13 +26,13 @@
import androidx.arch.core.executor.ArchTaskExecutor
import androidx.arch.core.executor.TaskExecutor
import androidx.test.filters.SmallTest
-
import com.android.systemui.SysuiTestCase
+import com.android.systemui.classifier.Classifier
+import com.android.systemui.plugins.FalsingManager
import com.android.systemui.util.concurrency.FakeExecutor
import com.android.systemui.util.concurrency.FakeRepeatableExecutor
import com.android.systemui.util.time.FakeSystemClock
import com.google.common.truth.Truth.assertThat
-
import org.junit.After
import org.junit.Before
import org.junit.Ignore
@@ -47,8 +47,8 @@
import org.mockito.Mockito.never
import org.mockito.Mockito.times
import org.mockito.Mockito.verify
-import org.mockito.junit.MockitoJUnit
import org.mockito.Mockito.`when` as whenever
+import org.mockito.junit.MockitoJUnit
@SmallTest
@RunWith(AndroidTestingRunner::class)
@@ -70,6 +70,8 @@
}
@Mock private lateinit var mockController: MediaController
@Mock private lateinit var mockTransport: MediaController.TransportControls
+ @Mock private lateinit var falsingManager: FalsingManager
+ @Mock private lateinit var mockBar: SeekBar
private val token1 = MediaSession.Token(1, null)
private val token2 = MediaSession.Token(2, null)
@@ -78,9 +80,10 @@
@Before
fun setUp() {
fakeExecutor = FakeExecutor(FakeSystemClock())
- viewModel = SeekBarViewModel(FakeRepeatableExecutor(fakeExecutor))
+ viewModel = SeekBarViewModel(FakeRepeatableExecutor(fakeExecutor), falsingManager)
viewModel.logSeek = { }
whenever(mockController.sessionToken).thenReturn(token1)
+ whenever(mockBar.context).thenReturn(context)
// LiveData to run synchronously
ArchTaskExecutor.getInstance().setDelegate(taskExecutor)
@@ -454,6 +457,25 @@
}
@Test
+ fun onFalseTapOrTouch() {
+ whenever(mockController.getTransportControls()).thenReturn(mockTransport)
+ whenever(falsingManager.isFalseTouch(Classifier.MEDIA_SEEKBAR)).thenReturn(true)
+ whenever(falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)).thenReturn(true)
+ viewModel.updateController(mockController)
+ val pos = 169
+
+ viewModel.attachTouchHandlers(mockBar)
+ with(viewModel.seekBarListener) {
+ onStartTrackingTouch(mockBar)
+ onProgressChanged(mockBar, pos, true)
+ onStopTrackingTouch(mockBar)
+ }
+
+ // THEN transport controls should not be used
+ verify(mockTransport, never()).seekTo(pos.toLong())
+ }
+
+ @Test
fun queuePollTaskWhenPlaying() {
// GIVEN that the track is playing
val state = PlaybackState.Builder().run {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSenderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSenderTest.kt
index ff0faf9..098086a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSenderTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSenderTest.kt
@@ -35,7 +35,9 @@
import com.android.internal.statusbar.IUndoMediaTransferCallback
import com.android.systemui.R
import com.android.systemui.SysuiTestCase
+import com.android.systemui.classifier.FalsingCollector
import com.android.systemui.media.taptotransfer.common.MediaTttLogger
+import com.android.systemui.plugins.FalsingManager
import com.android.systemui.statusbar.CommandQueue
import com.android.systemui.statusbar.policy.ConfigurationController
import com.android.systemui.util.concurrency.FakeExecutor
@@ -43,10 +45,12 @@
import com.android.systemui.util.mockito.eq
import com.android.systemui.util.time.FakeSystemClock
import com.google.common.truth.Truth.assertThat
+import dagger.Lazy
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentCaptor
+import org.mockito.ArgumentMatchers.anyInt
import org.mockito.Mock
import org.mockito.Mockito.never
import org.mockito.Mockito.verify
@@ -75,6 +79,14 @@
private lateinit var windowManager: WindowManager
@Mock
private lateinit var commandQueue: CommandQueue
+ @Mock
+ private lateinit var lazyFalsingManager: Lazy<FalsingManager>
+ @Mock
+ private lateinit var falsingManager: FalsingManager
+ @Mock
+ private lateinit var lazyFalsingCollector: Lazy<FalsingCollector>
+ @Mock
+ private lateinit var falsingCollector: FalsingCollector
private lateinit var commandQueueCallback: CommandQueue.Callbacks
private lateinit var fakeAppIconDrawable: Drawable
private lateinit var fakeClock: FakeSystemClock
@@ -101,6 +113,8 @@
senderUiEventLogger = MediaTttSenderUiEventLogger(uiEventLoggerFake)
whenever(accessibilityManager.getRecommendedTimeoutMillis(any(), any())).thenReturn(TIMEOUT)
+ whenever(lazyFalsingManager.get()).thenReturn(falsingManager)
+ whenever(lazyFalsingCollector.get()).thenReturn(falsingCollector)
controllerSender = MediaTttChipControllerSender(
commandQueue,
@@ -111,7 +125,9 @@
accessibilityManager,
configurationController,
powerManager,
- senderUiEventLogger
+ senderUiEventLogger,
+ lazyFalsingManager,
+ lazyFalsingCollector
)
val callbackCaptor = ArgumentCaptor.forClass(CommandQueue.Callbacks::class.java)
@@ -417,6 +433,38 @@
}
@Test
+ fun transferToReceiverSucceeded_withUndoRunnable_falseTap_callbackNotRun() {
+ whenever(lazyFalsingManager.get().isFalseTap(anyInt())).thenReturn(true)
+ var undoCallbackCalled = false
+ val undoCallback = object : IUndoMediaTransferCallback.Stub() {
+ override fun onUndoTriggered() {
+ undoCallbackCalled = true
+ }
+ }
+
+ controllerSender.displayView(transferToReceiverSucceeded(undoCallback))
+ getChipView().getUndoButton().performClick()
+
+ assertThat(undoCallbackCalled).isFalse()
+ }
+
+ @Test
+ fun transferToReceiverSucceeded_withUndoRunnable_realTap_callbackRun() {
+ whenever(lazyFalsingManager.get().isFalseTap(anyInt())).thenReturn(false)
+ var undoCallbackCalled = false
+ val undoCallback = object : IUndoMediaTransferCallback.Stub() {
+ override fun onUndoTriggered() {
+ undoCallbackCalled = true
+ }
+ }
+
+ controllerSender.displayView(transferToReceiverSucceeded(undoCallback))
+ getChipView().getUndoButton().performClick()
+
+ assertThat(undoCallbackCalled).isTrue()
+ }
+
+ @Test
fun transferToReceiverSucceeded_undoButtonClick_switchesToTransferToThisDeviceTriggered() {
val undoCallback = object : IUndoMediaTransferCallback.Stub() {
override fun onUndoTriggered() {}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentTest.java
index 2a66773..d2c2d58 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentTest.java
@@ -14,6 +14,9 @@
package com.android.systemui.qs;
+import static com.android.systemui.statusbar.StatusBarState.KEYGUARD;
+import static com.android.systemui.statusbar.StatusBarState.SHADE;
+
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertTrue;
@@ -49,13 +52,13 @@
import com.android.systemui.flags.Flags;
import com.android.systemui.media.MediaHost;
import com.android.systemui.plugins.FalsingManager;
-import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.qs.customize.QSCustomizerController;
import com.android.systemui.qs.dagger.QSFragmentComponent;
import com.android.systemui.qs.external.TileServiceRequestController;
import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel;
import com.android.systemui.statusbar.CommandQueue;
import com.android.systemui.statusbar.StatusBarState;
+import com.android.systemui.statusbar.SysuiStatusBarStateController;
import com.android.systemui.statusbar.phone.KeyguardBypassController;
import com.android.systemui.statusbar.policy.ConfigurationController;
import com.android.systemui.statusbar.policy.RemoteInputQuickSettingsDisabler;
@@ -93,7 +96,7 @@
@Mock private QSPanel.QSTileLayout mQsTileLayout;
@Mock private QSPanel.QSTileLayout mQQsTileLayout;
@Mock private QSAnimator mQSAnimator;
- @Mock private StatusBarStateController mStatusBarStateController;
+ @Mock private SysuiStatusBarStateController mStatusBarStateController;
@Mock private QSSquishinessController mSquishinessController;
private View mQsFragmentView;
@@ -158,7 +161,7 @@
public void
transitionToFullShade_onKeyguard_noBouncer_setsAlphaUsingLinearInterpolator() {
QSFragment fragment = resumeAndGetFragment();
- setStatusBarState(StatusBarState.KEYGUARD);
+ setStatusBarState(KEYGUARD);
when(mQSPanelController.isBouncerInTransit()).thenReturn(false);
boolean isTransitioningToFullShade = true;
float transitionProgress = 0.5f;
@@ -174,7 +177,7 @@
public void
transitionToFullShade_onKeyguard_bouncerActive_setsAlphaUsingBouncerInterpolator() {
QSFragment fragment = resumeAndGetFragment();
- setStatusBarState(StatusBarState.KEYGUARD);
+ setStatusBarState(KEYGUARD);
when(mQSPanelController.isBouncerInTransit()).thenReturn(true);
boolean isTransitioningToFullShade = true;
float transitionProgress = 0.5f;
@@ -262,6 +265,27 @@
}
@Test
+ public void setQsExpansion_inSplitShade_whenTransitioningToKeyguard_setsAlphaBasedOnShadeTransitionProgress() {
+ QSFragment fragment = resumeAndGetFragment();
+ enableSplitShade();
+ when(mStatusBarStateController.getState()).thenReturn(SHADE);
+ when(mStatusBarStateController.getCurrentOrUpcomingState()).thenReturn(KEYGUARD);
+ boolean isTransitioningToFullShade = false;
+ float transitionProgress = 0;
+ float squishinessFraction = 0f;
+
+ fragment.setTransitionToFullShadeProgress(isTransitioningToFullShade, transitionProgress,
+ squishinessFraction);
+
+ // trigger alpha refresh with non-zero expansion and fraction values
+ fragment.setQsExpansion(/* expansion= */ 1, /* panelExpansionFraction= */1,
+ /* proposedTranslation= */ 0, /* squishinessFraction= */ 1);
+
+ // alpha should follow lockscreen to shade progress, not panel expansion fraction
+ assertThat(mQsFragmentView.getAlpha()).isEqualTo(transitionProgress);
+ }
+
+ @Test
public void getQsMinExpansionHeight_notInSplitShade_returnsHeaderHeight() {
QSFragment fragment = resumeAndGetFragment();
disableSplitShade();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/ImageExporterTest.java b/packages/SystemUI/tests/src/com/android/systemui/screenshot/ImageExporterTest.java
index 7d56339..4c44dac 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/ImageExporterTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/ImageExporterTest.java
@@ -33,6 +33,7 @@
import android.graphics.Paint;
import android.os.Build;
import android.os.ParcelFileDescriptor;
+import android.os.Process;
import android.provider.MediaStore;
import android.testing.AndroidTestingRunner;
@@ -97,7 +98,8 @@
Bitmap original = createCheckerBitmap(10, 10, 10);
ListenableFuture<ImageExporter.Result> direct =
- exporter.export(DIRECT_EXECUTOR, requestId, original, CAPTURE_TIME);
+ exporter.export(DIRECT_EXECUTOR, requestId, original, CAPTURE_TIME,
+ Process.myUserHandle());
assertTrue("future should be done", direct.isDone());
assertFalse("future should not be canceled", direct.isCancelled());
ImageExporter.Result result = direct.get();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/ScreenshotNotificationSmartActionsTest.java b/packages/SystemUI/tests/src/com/android/systemui/screenshot/ScreenshotNotificationSmartActionsTest.java
index 69b7b88..8c9404e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/ScreenshotNotificationSmartActionsTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/ScreenshotNotificationSmartActionsTest.java
@@ -180,7 +180,7 @@
data.finisher = null;
data.mActionsReadyListener = null;
SaveImageInBackgroundTask task =
- new SaveImageInBackgroundTask(mContext, null, mScreenshotSmartActions, data,
+ new SaveImageInBackgroundTask(mContext, null, null, mScreenshotSmartActions, data,
ActionTransition::new, mSmartActionsProvider);
Notification.Action shareAction = task.createShareAction(mContext, mContext.getResources(),
@@ -208,7 +208,7 @@
data.finisher = null;
data.mActionsReadyListener = null;
SaveImageInBackgroundTask task =
- new SaveImageInBackgroundTask(mContext, null, mScreenshotSmartActions, data,
+ new SaveImageInBackgroundTask(mContext, null, null, mScreenshotSmartActions, data,
ActionTransition::new, mSmartActionsProvider);
Notification.Action editAction = task.createEditAction(mContext, mContext.getResources(),
@@ -236,7 +236,7 @@
data.finisher = null;
data.mActionsReadyListener = null;
SaveImageInBackgroundTask task =
- new SaveImageInBackgroundTask(mContext, null, mScreenshotSmartActions, data,
+ new SaveImageInBackgroundTask(mContext, null, null, mScreenshotSmartActions, data,
ActionTransition::new, mSmartActionsProvider);
Notification.Action deleteAction = task.createDeleteAction(mContext,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NonPhoneDependencyTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NonPhoneDependencyTest.java
deleted file mode 100644
index eb4cca8..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NonPhoneDependencyTest.java
+++ /dev/null
@@ -1,86 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF 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;
-
-import static org.junit.Assert.assertFalse;
-
-import android.os.Handler;
-import android.testing.AndroidTestingRunner;
-import android.testing.TestableLooper;
-
-import androidx.test.filters.SmallTest;
-
-import com.android.keyguard.KeyguardUpdateMonitor;
-import com.android.systemui.Dependency;
-import com.android.systemui.SysuiTestCase;
-import com.android.systemui.statusbar.notification.logging.NotificationLogger;
-import com.android.systemui.statusbar.notification.row.NotificationGutsManager;
-import com.android.systemui.statusbar.notification.row.NotificationGutsManager.OnSettingsClickListener;
-import com.android.systemui.statusbar.notification.stack.NotificationListContainer;
-
-import org.junit.Before;
-import org.junit.Ignore;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-
-/**
- * Verifies that particular sets of dependencies don't have dependencies on others. For example,
- * code managing notifications shouldn't directly depend on CentralSurfaces, since there are
- * platforms which want to manage notifications, but don't use CentralSurfaces.
- */
-@SmallTest
-@RunWith(AndroidTestingRunner.class)
-@TestableLooper.RunWithLooper(setAsMainLooper = true)
-public class NonPhoneDependencyTest extends SysuiTestCase {
- @Mock private NotificationPresenter mPresenter;
- @Mock private NotificationListContainer mListContainer;
- @Mock private RemoteInputController.Delegate mDelegate;
- @Mock private NotificationRemoteInputManager.Callback mRemoteInputManagerCallback;
- @Mock private OnSettingsClickListener mOnSettingsClickListener;
-
- @Before
- public void setUp() {
- MockitoAnnotations.initMocks(this);
- mDependency.injectMockDependency(KeyguardUpdateMonitor.class);
- mDependency.injectTestDependency(Dependency.MAIN_HANDLER,
- new Handler(TestableLooper.get(this).getLooper()));
- }
-
- @Ignore("Causes binder calls which fail")
- @Test
- public void testNotificationManagementCodeHasNoDependencyOnStatusBarWindowManager() {
- NotificationGutsManager gutsManager = Dependency.get(NotificationGutsManager.class);
- NotificationLogger notificationLogger = Dependency.get(NotificationLogger.class);
- NotificationMediaManager mediaManager = Dependency.get(NotificationMediaManager.class);
- NotificationRemoteInputManager remoteInputManager =
- Dependency.get(NotificationRemoteInputManager.class);
- NotificationLockscreenUserManager lockscreenUserManager =
- Dependency.get(NotificationLockscreenUserManager.class);
- gutsManager.setUpWithPresenter(mPresenter, mListContainer,
- mOnSettingsClickListener);
- notificationLogger.setUpWithContainer(mListContainer);
- mediaManager.setUpWithPresenter(mPresenter);
- remoteInputManager.setUpWithCallback(mRemoteInputManagerCallback,
- mDelegate);
- lockscreenUserManager.setUpWithPresenter(mPresenter);
-
- TestableLooper.get(this).processAllMessages();
- assertFalse(mDependency.hasInstantiatedDependency(NotificationShadeWindowController.class));
- }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java
index 853d1df..bdafa48 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java
@@ -52,6 +52,7 @@
import com.android.systemui.broadcast.BroadcastDispatcher;
import com.android.systemui.dump.DumpManager;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
+import com.android.systemui.recents.OverviewProxyService;
import com.android.systemui.statusbar.NotificationLockscreenUserManager.NotificationStateChangedListener;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder;
@@ -88,6 +89,8 @@
@Mock
private NotificationClickNotifier mClickNotifier;
@Mock
+ private OverviewProxyService mOverviewProxyService;
+ @Mock
private KeyguardManager mKeyguardManager;
@Mock
private DeviceProvisionedController mDeviceProvisionedController;
@@ -344,6 +347,7 @@
(() -> mVisibilityProvider),
(() -> mNotifCollection),
mClickNotifier,
+ (() -> mOverviewProxyService),
NotificationLockscreenUserManagerTest.this.mKeyguardManager,
mStatusBarStateController,
Handler.createAsync(Looper.myLooper()),
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt
index 2ee3126..2970807 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt
@@ -337,6 +337,40 @@
}
@Test
+ fun testOnEntryUpdated_toAlert() {
+ // GIVEN that an entry is posted that should not heads up
+ setShouldHeadsUp(mEntry, false)
+ mCollectionListener.onEntryAdded(mEntry)
+
+ // WHEN it's updated to heads up
+ setShouldHeadsUp(mEntry)
+ mCollectionListener.onEntryUpdated(mEntry)
+ mBeforeTransformGroupsListener.onBeforeTransformGroups(listOf(mEntry))
+ mBeforeFinalizeFilterListener.onBeforeFinalizeFilter(listOf(mEntry))
+
+ // THEN the notification alerts
+ finishBind(mEntry)
+ verify(mHeadsUpManager).showNotification(mEntry)
+ }
+
+ @Test
+ fun testOnEntryUpdated_toNotAlert() {
+ // GIVEN that an entry is posted that should heads up
+ setShouldHeadsUp(mEntry)
+ mCollectionListener.onEntryAdded(mEntry)
+
+ // WHEN it's updated to not heads up
+ setShouldHeadsUp(mEntry, false)
+ mCollectionListener.onEntryUpdated(mEntry)
+ mBeforeTransformGroupsListener.onBeforeTransformGroups(listOf(mEntry))
+ mBeforeFinalizeFilterListener.onBeforeFinalizeFilter(listOf(mEntry))
+
+ // THEN the notification is never bound or shown
+ verify(mHeadsUpViewBinder, never()).bindHeadsUpView(any(), any())
+ verify(mHeadsUpManager, never()).showNotification(any())
+ }
+
+ @Test
fun testOnEntryRemovedRemovesHeadsUpNotification() {
// GIVEN the current HUN is mEntry
addHUN(mEntry)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryMonitorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryMonitorTest.kt
new file mode 100644
index 0000000..f4d61c8
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryMonitorTest.kt
@@ -0,0 +1,323 @@
+/*
+ *
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.logging
+
+import android.app.Notification
+import android.app.PendingIntent
+import android.app.Person
+import android.content.Intent
+import android.graphics.Bitmap
+import android.graphics.drawable.Icon
+import android.testing.AndroidTestingRunner
+import android.widget.RemoteViews
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.statusbar.notification.NotificationUtils
+import com.android.systemui.statusbar.notification.collection.NotifPipeline
+import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class NotificationMemoryMonitorTest : SysuiTestCase() {
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+ }
+
+ @Test
+ fun currentNotificationMemoryUse_plainNotification() {
+ val notification = createBasicNotification().build()
+ val nmm = createNMMWithNotifications(listOf(notification))
+ val memoryUse = getUseObject(nmm.currentNotificationMemoryUse())
+ assertNotificationObjectSizes(
+ memoryUse,
+ smallIcon = notification.smallIcon.bitmap.allocationByteCount,
+ largeIcon = notification.getLargeIcon().bitmap.allocationByteCount,
+ extras = 3316,
+ bigPicture = 0,
+ extender = 0,
+ style = null,
+ styleIcon = 0,
+ hasCustomView = false,
+ )
+ }
+
+ @Test
+ fun currentNotificationMemoryUse_plainNotification_dontDoubleCountSameBitmap() {
+ val icon = Icon.createWithBitmap(Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888))
+ val notification = createBasicNotification().setLargeIcon(icon).setSmallIcon(icon).build()
+ val nmm = createNMMWithNotifications(listOf(notification))
+ val memoryUse = getUseObject(nmm.currentNotificationMemoryUse())
+ assertNotificationObjectSizes(
+ memoryUse = memoryUse,
+ smallIcon = notification.smallIcon.bitmap.allocationByteCount,
+ largeIcon = 0,
+ extras = 3316,
+ bigPicture = 0,
+ extender = 0,
+ style = null,
+ styleIcon = 0,
+ hasCustomView = false,
+ )
+ }
+
+ @Test
+ fun currentNotificationMemoryUse_customViewNotification_marksTrue() {
+ val notification =
+ createBasicNotification()
+ .setCustomContentView(
+ RemoteViews(context.packageName, android.R.layout.list_content)
+ )
+ .build()
+ val nmm = createNMMWithNotifications(listOf(notification))
+ val memoryUse = getUseObject(nmm.currentNotificationMemoryUse())
+ assertNotificationObjectSizes(
+ memoryUse = memoryUse,
+ smallIcon = notification.smallIcon.bitmap.allocationByteCount,
+ largeIcon = notification.getLargeIcon().bitmap.allocationByteCount,
+ extras = 3384,
+ bigPicture = 0,
+ extender = 0,
+ style = null,
+ styleIcon = 0,
+ hasCustomView = true,
+ )
+ }
+
+ @Test
+ fun currentNotificationMemoryUse_notificationWithDataIcon_calculatesCorrectly() {
+ val dataIcon = Icon.createWithData(ByteArray(444444), 0, 444444)
+ val notification =
+ createBasicNotification().setLargeIcon(dataIcon).setSmallIcon(dataIcon).build()
+ val nmm = createNMMWithNotifications(listOf(notification))
+ val memoryUse = getUseObject(nmm.currentNotificationMemoryUse())
+ assertNotificationObjectSizes(
+ memoryUse = memoryUse,
+ smallIcon = 444444,
+ largeIcon = 0,
+ extras = 3212,
+ bigPicture = 0,
+ extender = 0,
+ style = null,
+ styleIcon = 0,
+ hasCustomView = false,
+ )
+ }
+
+ @Test
+ fun currentNotificationMemoryUse_bigPictureStyle() {
+ val bigPicture =
+ Icon.createWithBitmap(Bitmap.createBitmap(600, 400, Bitmap.Config.ARGB_8888))
+ val bigPictureIcon =
+ Icon.createWithAdaptiveBitmap(Bitmap.createBitmap(386, 432, Bitmap.Config.ARGB_8888))
+ val notification =
+ createBasicNotification()
+ .setStyle(
+ Notification.BigPictureStyle()
+ .bigPicture(bigPicture)
+ .bigLargeIcon(bigPictureIcon)
+ )
+ .build()
+ val nmm = createNMMWithNotifications(listOf(notification))
+ val memoryUse = getUseObject(nmm.currentNotificationMemoryUse())
+ assertNotificationObjectSizes(
+ memoryUse = memoryUse,
+ smallIcon = notification.smallIcon.bitmap.allocationByteCount,
+ largeIcon = notification.getLargeIcon().bitmap.allocationByteCount,
+ extras = 4092,
+ bigPicture = 960000,
+ extender = 0,
+ style = "BigPictureStyle",
+ styleIcon = bigPictureIcon.bitmap.allocationByteCount,
+ hasCustomView = false,
+ )
+ }
+
+ @Test
+ fun currentNotificationMemoryUse_callingStyle() {
+ val personIcon =
+ Icon.createWithBitmap(Bitmap.createBitmap(386, 432, Bitmap.Config.ARGB_8888))
+ val person = Person.Builder().setIcon(personIcon).setName("Person").build()
+ val fakeIntent =
+ PendingIntent.getActivity(context, 0, Intent(), PendingIntent.FLAG_IMMUTABLE)
+ val notification =
+ createBasicNotification()
+ .setStyle(Notification.CallStyle.forIncomingCall(person, fakeIntent, fakeIntent))
+ .build()
+ val nmm = createNMMWithNotifications(listOf(notification))
+ val memoryUse = getUseObject(nmm.currentNotificationMemoryUse())
+ assertNotificationObjectSizes(
+ memoryUse = memoryUse,
+ smallIcon = notification.smallIcon.bitmap.allocationByteCount,
+ largeIcon = notification.getLargeIcon().bitmap.allocationByteCount,
+ extras = 4084,
+ bigPicture = 0,
+ extender = 0,
+ style = "CallStyle",
+ styleIcon = personIcon.bitmap.allocationByteCount,
+ hasCustomView = false,
+ )
+ }
+
+ @Test
+ fun currentNotificationMemoryUse_messagingStyle() {
+ val personIcon =
+ Icon.createWithBitmap(Bitmap.createBitmap(386, 432, Bitmap.Config.ARGB_8888))
+ val person = Person.Builder().setIcon(personIcon).setName("Person").build()
+ val message = Notification.MessagingStyle.Message("Message!", 4323, person)
+ val historicPersonIcon =
+ Icon.createWithBitmap(Bitmap.createBitmap(348, 382, Bitmap.Config.ARGB_8888))
+ val historicPerson =
+ Person.Builder().setIcon(historicPersonIcon).setName("Historic person").build()
+ val historicMessage =
+ Notification.MessagingStyle.Message("Historic message!", 5848, historicPerson)
+
+ val notification =
+ createBasicNotification()
+ .setStyle(
+ Notification.MessagingStyle(person)
+ .addMessage(message)
+ .addHistoricMessage(historicMessage)
+ )
+ .build()
+ val nmm = createNMMWithNotifications(listOf(notification))
+ val memoryUse = getUseObject(nmm.currentNotificationMemoryUse())
+ assertNotificationObjectSizes(
+ memoryUse = memoryUse,
+ smallIcon = notification.smallIcon.bitmap.allocationByteCount,
+ largeIcon = notification.getLargeIcon().bitmap.allocationByteCount,
+ extras = 5024,
+ bigPicture = 0,
+ extender = 0,
+ style = "MessagingStyle",
+ styleIcon =
+ personIcon.bitmap.allocationByteCount +
+ historicPersonIcon.bitmap.allocationByteCount,
+ hasCustomView = false,
+ )
+ }
+
+ @Test
+ fun currentNotificationMemoryUse_carExtender() {
+ val carIcon = Bitmap.createBitmap(432, 322, Bitmap.Config.ARGB_8888)
+ val extender = Notification.CarExtender().setLargeIcon(carIcon)
+ val notification = createBasicNotification().extend(extender).build()
+ val nmm = createNMMWithNotifications(listOf(notification))
+ val memoryUse = getUseObject(nmm.currentNotificationMemoryUse())
+ assertNotificationObjectSizes(
+ memoryUse = memoryUse,
+ smallIcon = notification.smallIcon.bitmap.allocationByteCount,
+ largeIcon = notification.getLargeIcon().bitmap.allocationByteCount,
+ extras = 3612,
+ bigPicture = 0,
+ extender = 556656,
+ style = null,
+ styleIcon = 0,
+ hasCustomView = false,
+ )
+ }
+
+ @Test
+ fun currentNotificationMemoryUse_tvWearExtender() {
+ val tvExtender = Notification.TvExtender().setChannel("channel2")
+ val wearBackground = Bitmap.createBitmap(443, 433, Bitmap.Config.ARGB_8888)
+ val wearExtender = Notification.WearableExtender().setBackground(wearBackground)
+ val notification = createBasicNotification().extend(tvExtender).extend(wearExtender).build()
+ val nmm = createNMMWithNotifications(listOf(notification))
+ val memoryUse = getUseObject(nmm.currentNotificationMemoryUse())
+ assertNotificationObjectSizes(
+ memoryUse = memoryUse,
+ smallIcon = notification.smallIcon.bitmap.allocationByteCount,
+ largeIcon = notification.getLargeIcon().bitmap.allocationByteCount,
+ extras = 3820,
+ bigPicture = 0,
+ extender = 388 + wearBackground.allocationByteCount,
+ style = null,
+ styleIcon = 0,
+ hasCustomView = false,
+ )
+ }
+
+ private fun createBasicNotification(): Notification.Builder {
+ val smallIcon =
+ Icon.createWithBitmap(Bitmap.createBitmap(250, 250, Bitmap.Config.ARGB_8888))
+ val largeIcon =
+ Icon.createWithBitmap(Bitmap.createBitmap(400, 400, Bitmap.Config.ARGB_8888))
+ return Notification.Builder(context)
+ .setSmallIcon(smallIcon)
+ .setLargeIcon(largeIcon)
+ .setContentTitle("This is a title")
+ .setContentText("This is content text.")
+ }
+
+ /** This will generate a nicer error message than comparing objects */
+ private fun assertNotificationObjectSizes(
+ memoryUse: NotificationMemoryUsage,
+ smallIcon: Int,
+ largeIcon: Int,
+ extras: Int,
+ bigPicture: Int,
+ extender: Int,
+ style: String?,
+ styleIcon: Int,
+ hasCustomView: Boolean
+ ) {
+ assertThat(memoryUse.packageName).isEqualTo("test_pkg")
+ assertThat(memoryUse.notificationId)
+ .isEqualTo(NotificationUtils.logKey("0|test_pkg|0|test|0"))
+ assertThat(memoryUse.objectUsage.smallIcon).isEqualTo(smallIcon)
+ assertThat(memoryUse.objectUsage.largeIcon).isEqualTo(largeIcon)
+ assertThat(memoryUse.objectUsage.extras).isEqualTo(extras)
+ assertThat(memoryUse.objectUsage.bigPicture).isEqualTo(bigPicture)
+ assertThat(memoryUse.objectUsage.extender).isEqualTo(extender)
+ if (style == null) {
+ assertThat(memoryUse.objectUsage.style).isNull()
+ } else {
+ assertThat(memoryUse.objectUsage.style).isEqualTo(style)
+ }
+ assertThat(memoryUse.objectUsage.styleIcon).isEqualTo(styleIcon)
+ assertThat(memoryUse.objectUsage.hasCustomView).isEqualTo(hasCustomView)
+ }
+
+ private fun getUseObject(
+ singleItemUseList: List<NotificationMemoryUsage>
+ ): NotificationMemoryUsage {
+ assertThat(singleItemUseList).hasSize(1)
+ return singleItemUseList[0]
+ }
+
+ private fun createNMMWithNotifications(
+ notifications: List<Notification>
+ ): NotificationMemoryMonitor {
+ val notifPipeline: NotifPipeline = mock()
+ val notificationEntries =
+ notifications.map { n ->
+ NotificationEntryBuilder().setTag("test").setNotification(n).build()
+ }
+ whenever(notifPipeline.allNotifs).thenReturn(notificationEntries)
+ return NotificationMemoryMonitor(notifPipeline, mock())
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationPanelLoggerFake.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationPanelLoggerFake.java
index 7e97629..dae0aa2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationPanelLoggerFake.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationPanelLoggerFake.java
@@ -40,6 +40,10 @@
NotificationPanelLogger.toNotificationProto(visibleNotifications)));
}
+ @Override
+ public void logNotificationDrag(NotificationEntry draggedNotification) {
+ }
+
public static class CallRecord {
public boolean isLockscreen;
public Notifications.NotificationList list;
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowDragControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowDragControllerTest.java
index 922e93d..ed2afe7 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowDragControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowDragControllerTest.java
@@ -40,6 +40,8 @@
import com.android.systemui.SysuiTestCase;
import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin;
import com.android.systemui.shade.ShadeController;
+import com.android.systemui.statusbar.notification.logging.NotificationPanelLogger;
+import com.android.systemui.statusbar.notification.logging.NotificationPanelLoggerFake;
import com.android.systemui.statusbar.policy.HeadsUpManager;
import org.junit.Before;
@@ -63,6 +65,7 @@
private NotificationMenuRowPlugin.MenuItem mMenuItem =
mock(NotificationMenuRowPlugin.MenuItem.class);
private ShadeController mShadeController = mock(ShadeController.class);
+ private NotificationPanelLogger mNotificationPanelLogger = mock(NotificationPanelLogger.class);
@Before
public void setUp() throws Exception {
@@ -82,7 +85,7 @@
when(mMenuRow.getLongpressMenuItem(any(Context.class))).thenReturn(mMenuItem);
mController = new ExpandableNotificationRowDragController(mContext, mHeadsUpManager,
- mShadeController);
+ mShadeController, mNotificationPanelLogger);
}
@Test
@@ -96,6 +99,7 @@
mRow.doDragCallback(0, 0);
verify(controller).startDragAndDrop(mRow);
verify(mHeadsUpManager, times(1)).releaseAllImmediately();
+ verify(mNotificationPanelLogger, times(1)).logNotificationDrag(any());
}
@Test
@@ -107,6 +111,7 @@
verify(controller).startDragAndDrop(mRow);
verify(mShadeController).animateCollapsePanels(eq(0), eq(true),
eq(false), anyFloat());
+ verify(mNotificationPanelLogger, times(1)).logNotificationDrag(any());
}
@Test
@@ -124,6 +129,7 @@
// Verify that we never start the actual drag since there is no content
verify(mRow, never()).startDragAndDrop(any(), any(), any(), anyInt());
+ verify(mNotificationPanelLogger, never()).logNotificationDrag(any());
}
private ExpandableNotificationRowDragController createSpyController() {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/DozeServiceHostTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/DozeServiceHostTest.java
index 9de9db1..996851e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/DozeServiceHostTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/DozeServiceHostTest.java
@@ -40,7 +40,6 @@
import com.android.systemui.biometrics.AuthController;
import com.android.systemui.doze.DozeHost;
import com.android.systemui.doze.DozeLog;
-import com.android.systemui.keyguard.KeyguardViewMediator;
import com.android.systemui.keyguard.WakefulnessLifecycle;
import com.android.systemui.shade.NotificationPanelViewController;
import com.android.systemui.shade.NotificationShadeWindowViewController;
@@ -73,7 +72,6 @@
@Mock private HeadsUpManagerPhone mHeadsUpManager;
@Mock private ScrimController mScrimController;
@Mock private DozeScrimController mDozeScrimController;
- @Mock private KeyguardViewMediator mKeyguardViewMediator;
@Mock private StatusBarStateControllerImpl mStatusBarStateController;
@Mock private BatteryController mBatteryController;
@Mock private DeviceProvisionedController mDeviceProvisionedController;
@@ -101,7 +99,7 @@
mDozeServiceHost = new DozeServiceHost(mDozeLog, mPowerManager, mWakefullnessLifecycle,
mStatusBarStateController, mDeviceProvisionedController, mHeadsUpManager,
mBatteryController, mScrimController, () -> mBiometricUnlockController,
- mKeyguardViewMediator, () -> mAssistManager, mDozeScrimController,
+ () -> mAssistManager, mDozeScrimController,
mKeyguardUpdateMonitor, mPulseExpansionHandler,
mNotificationShadeWindowController, mNotificationWakeUpCoordinator,
mAuthController, mNotificationIconAreaController);
@@ -132,19 +130,11 @@
verify(mStatusBarStateController).setIsDozing(eq(false));
}
-
@Test
public void testPulseWhileDozing_updatesScrimController() {
mCentralSurfaces.setBarStateForTest(StatusBarState.KEYGUARD);
mCentralSurfaces.showKeyguardImpl();
- // Keep track of callback to be able to stop the pulse
-// DozeHost.PulseCallback[] pulseCallback = new DozeHost.PulseCallback[1];
-// doAnswer(invocation -> {
-// pulseCallback[0] = invocation.getArgument(0);
-// return null;
-// }).when(mDozeScrimController).pulse(any(), anyInt());
-
// Starting a pulse should change the scrim controller to the pulsing state
mDozeServiceHost.pulseWhileDozing(new DozeHost.PulseCallback() {
@Override
@@ -210,4 +200,17 @@
}
}
}
+
+ @Test
+ public void testStopPulsing_setPendingPulseToFalse() {
+ // GIVEN a pending pulse
+ mDozeServiceHost.setPulsePending(true);
+
+ // WHEN pulsing is stopped
+ mDozeServiceHost.stopPulsing();
+
+ // THEN isPendingPulse=false, pulseOutNow is called
+ assertFalse(mDozeServiceHost.isPulsePending());
+ verify(mDozeScrimController).pulseOutNow();
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.java
index cfaa470..6ec5cf8 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.java
@@ -47,6 +47,7 @@
import com.android.keyguard.CarrierTextController;
import com.android.keyguard.KeyguardUpdateMonitor;
import com.android.keyguard.KeyguardUpdateMonitorCallback;
+import com.android.keyguard.logging.KeyguardLogger;
import com.android.systemui.R;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.battery.BatteryMeterViewController;
@@ -123,6 +124,7 @@
private StatusBarUserInfoTracker mStatusBarUserInfoTracker;
@Mock private SecureSettings mSecureSettings;
@Mock private CommandQueue mCommandQueue;
+ @Mock private KeyguardLogger mLogger;
private TestNotificationPanelViewStateProvider mNotificationPanelViewStateProvider;
private KeyguardStatusBarView mKeyguardStatusBarView;
@@ -172,7 +174,8 @@
mStatusBarUserInfoTracker,
mSecureSettings,
mCommandQueue,
- mFakeExecutor
+ mFakeExecutor,
+ mLogger
);
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/telephony/data/repository/TelephonyRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/telephony/data/repository/TelephonyRepositoryImplTest.kt
new file mode 100644
index 0000000..773a0d8
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/telephony/data/repository/TelephonyRepositoryImplTest.kt
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package com.android.systemui.telephony.data.repository
+
+import android.telephony.TelephonyCallback
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.telephony.TelephonyListenerManager
+import com.android.systemui.util.mockito.kotlinArgumentCaptor
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.runBlocking
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.mockito.Mock
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(JUnit4::class)
+class TelephonyRepositoryImplTest : SysuiTestCase() {
+
+ @Mock private lateinit var manager: TelephonyListenerManager
+
+ private lateinit var underTest: TelephonyRepositoryImpl
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+
+ underTest =
+ TelephonyRepositoryImpl(
+ manager = manager,
+ )
+ }
+
+ @Test
+ fun callState() =
+ runBlocking(IMMEDIATE) {
+ var callState: Int? = null
+ val job = underTest.callState.onEach { callState = it }.launchIn(this)
+ val listenerCaptor = kotlinArgumentCaptor<TelephonyCallback.CallStateListener>()
+ verify(manager).addCallStateListener(listenerCaptor.capture())
+ val listener = listenerCaptor.value
+
+ listener.onCallStateChanged(0)
+ assertThat(callState).isEqualTo(0)
+
+ listener.onCallStateChanged(1)
+ assertThat(callState).isEqualTo(1)
+
+ listener.onCallStateChanged(2)
+ assertThat(callState).isEqualTo(2)
+
+ job.cancel()
+
+ verify(manager).removeCallStateListener(listener)
+ }
+
+ companion object {
+ private val IMMEDIATE = Dispatchers.Main.immediate
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplRefactoredTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplRefactoredTest.kt
new file mode 100644
index 0000000..4a8e055
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplRefactoredTest.kt
@@ -0,0 +1,204 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package com.android.systemui.user.data.repository
+
+import android.content.pm.UserInfo
+import android.os.UserHandle
+import android.os.UserManager
+import android.provider.Settings
+import androidx.test.filters.SmallTest
+import com.android.systemui.user.data.model.UserSwitcherSettingsModel
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.runBlocking
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.mockito.Mockito.`when` as whenever
+
+@SmallTest
+@RunWith(JUnit4::class)
+class UserRepositoryImplRefactoredTest : UserRepositoryImplTest() {
+
+ @Before
+ fun setUp() {
+ super.setUp(isRefactored = true)
+ }
+
+ @Test
+ fun userSwitcherSettings() = runSelfCancelingTest {
+ setUpGlobalSettings(
+ isSimpleUserSwitcher = true,
+ isAddUsersFromLockscreen = true,
+ isUserSwitcherEnabled = true,
+ )
+ underTest = create(this)
+
+ var value: UserSwitcherSettingsModel? = null
+ underTest.userSwitcherSettings.onEach { value = it }.launchIn(this)
+
+ assertUserSwitcherSettings(
+ model = value,
+ expectedSimpleUserSwitcher = true,
+ expectedAddUsersFromLockscreen = true,
+ expectedUserSwitcherEnabled = true,
+ )
+
+ setUpGlobalSettings(
+ isSimpleUserSwitcher = false,
+ isAddUsersFromLockscreen = true,
+ isUserSwitcherEnabled = true,
+ )
+ assertUserSwitcherSettings(
+ model = value,
+ expectedSimpleUserSwitcher = false,
+ expectedAddUsersFromLockscreen = true,
+ expectedUserSwitcherEnabled = true,
+ )
+ }
+
+ @Test
+ fun refreshUsers() = runSelfCancelingTest {
+ underTest = create(this)
+ val initialExpectedValue =
+ setUpUsers(
+ count = 3,
+ selectedIndex = 0,
+ )
+ var userInfos: List<UserInfo>? = null
+ var selectedUserInfo: UserInfo? = null
+ underTest.userInfos.onEach { userInfos = it }.launchIn(this)
+ underTest.selectedUserInfo.onEach { selectedUserInfo = it }.launchIn(this)
+
+ underTest.refreshUsers()
+ assertThat(userInfos).isEqualTo(initialExpectedValue)
+ assertThat(selectedUserInfo).isEqualTo(initialExpectedValue[0])
+ assertThat(underTest.lastSelectedNonGuestUserId).isEqualTo(selectedUserInfo?.id)
+
+ val secondExpectedValue =
+ setUpUsers(
+ count = 4,
+ selectedIndex = 1,
+ )
+ underTest.refreshUsers()
+ assertThat(userInfos).isEqualTo(secondExpectedValue)
+ assertThat(selectedUserInfo).isEqualTo(secondExpectedValue[1])
+ assertThat(underTest.lastSelectedNonGuestUserId).isEqualTo(selectedUserInfo?.id)
+
+ val selectedNonGuestUserId = selectedUserInfo?.id
+ val thirdExpectedValue =
+ setUpUsers(
+ count = 2,
+ hasGuest = true,
+ selectedIndex = 1,
+ )
+ underTest.refreshUsers()
+ assertThat(userInfos).isEqualTo(thirdExpectedValue)
+ assertThat(selectedUserInfo).isEqualTo(thirdExpectedValue[1])
+ assertThat(selectedUserInfo?.isGuest).isTrue()
+ assertThat(underTest.lastSelectedNonGuestUserId).isEqualTo(selectedNonGuestUserId)
+ }
+
+ private fun setUpUsers(
+ count: Int,
+ hasGuest: Boolean = false,
+ selectedIndex: Int = 0,
+ ): List<UserInfo> {
+ val userInfos =
+ (0 until count).map { index ->
+ createUserInfo(
+ index,
+ isGuest = hasGuest && index == count - 1,
+ )
+ }
+ whenever(manager.aliveUsers).thenReturn(userInfos)
+ tracker.set(userInfos, selectedIndex)
+ return userInfos
+ }
+
+ private fun createUserInfo(
+ id: Int,
+ isGuest: Boolean,
+ ): UserInfo {
+ val flags = 0
+ return UserInfo(
+ id,
+ "user_$id",
+ /* iconPath= */ "",
+ flags,
+ if (isGuest) UserManager.USER_TYPE_FULL_GUEST else UserInfo.getDefaultUserType(flags),
+ )
+ }
+
+ private fun setUpGlobalSettings(
+ isSimpleUserSwitcher: Boolean = false,
+ isAddUsersFromLockscreen: Boolean = false,
+ isUserSwitcherEnabled: Boolean = true,
+ ) {
+ context.orCreateTestableResources.addOverride(
+ com.android.internal.R.bool.config_expandLockScreenUserSwitcher,
+ true,
+ )
+ globalSettings.putIntForUser(
+ UserRepositoryImpl.SETTING_SIMPLE_USER_SWITCHER,
+ if (isSimpleUserSwitcher) 1 else 0,
+ UserHandle.USER_SYSTEM,
+ )
+ globalSettings.putIntForUser(
+ Settings.Global.ADD_USERS_WHEN_LOCKED,
+ if (isAddUsersFromLockscreen) 1 else 0,
+ UserHandle.USER_SYSTEM,
+ )
+ globalSettings.putIntForUser(
+ Settings.Global.USER_SWITCHER_ENABLED,
+ if (isUserSwitcherEnabled) 1 else 0,
+ UserHandle.USER_SYSTEM,
+ )
+ }
+
+ private fun assertUserSwitcherSettings(
+ model: UserSwitcherSettingsModel?,
+ expectedSimpleUserSwitcher: Boolean,
+ expectedAddUsersFromLockscreen: Boolean,
+ expectedUserSwitcherEnabled: Boolean,
+ ) {
+ checkNotNull(model)
+ assertThat(model.isSimpleUserSwitcher).isEqualTo(expectedSimpleUserSwitcher)
+ assertThat(model.isAddUsersFromLockscreen).isEqualTo(expectedAddUsersFromLockscreen)
+ assertThat(model.isUserSwitcherEnabled).isEqualTo(expectedUserSwitcherEnabled)
+ }
+
+ /**
+ * Executes the given block of execution within the scope of a dedicated [CoroutineScope] which
+ * is then automatically canceled and cleaned-up.
+ */
+ private fun runSelfCancelingTest(
+ block: suspend CoroutineScope.() -> Unit,
+ ) =
+ runBlocking(Dispatchers.Main.immediate) {
+ val scope = CoroutineScope(coroutineContext + Job())
+ block(scope)
+ scope.cancel()
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplTest.kt
index 6fec343..dcea83a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplTest.kt
@@ -17,201 +17,54 @@
package com.android.systemui.user.data.repository
-import android.content.pm.UserInfo
import android.os.UserManager
-import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
+import com.android.systemui.flags.FakeFeatureFlags
+import com.android.systemui.flags.Flags
+import com.android.systemui.settings.FakeUserTracker
import com.android.systemui.statusbar.policy.UserSwitcherController
-import com.android.systemui.user.data.source.UserRecord
-import com.android.systemui.user.shared.model.UserActionModel
-import com.android.systemui.user.shared.model.UserModel
-import com.android.systemui.util.mockito.any
-import com.android.systemui.util.mockito.capture
-import com.google.common.truth.Truth.assertThat
+import com.android.systemui.util.settings.FakeSettings
+import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.launchIn
-import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.onEach
-import kotlinx.coroutines.runBlocking
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
-import org.mockito.ArgumentCaptor
-import org.mockito.Captor
+import kotlinx.coroutines.test.TestCoroutineScope
import org.mockito.Mock
-import org.mockito.Mockito.verify
-import org.mockito.Mockito.`when` as whenever
import org.mockito.MockitoAnnotations
-@SmallTest
-@RunWith(JUnit4::class)
-class UserRepositoryImplTest : SysuiTestCase() {
+abstract class UserRepositoryImplTest : SysuiTestCase() {
- @Mock private lateinit var manager: UserManager
- @Mock private lateinit var controller: UserSwitcherController
- @Captor
- private lateinit var userSwitchCallbackCaptor:
- ArgumentCaptor<UserSwitcherController.UserSwitchCallback>
+ @Mock protected lateinit var manager: UserManager
+ @Mock protected lateinit var controller: UserSwitcherController
- private lateinit var underTest: UserRepositoryImpl
+ protected lateinit var underTest: UserRepositoryImpl
- @Before
- fun setUp() {
+ protected lateinit var globalSettings: FakeSettings
+ protected lateinit var tracker: FakeUserTracker
+ protected lateinit var featureFlags: FakeFeatureFlags
+
+ protected fun setUp(isRefactored: Boolean) {
MockitoAnnotations.initMocks(this)
- whenever(controller.isAddUsersFromLockScreenEnabled).thenReturn(MutableStateFlow(false))
- whenever(controller.isGuestUserAutoCreated).thenReturn(false)
- whenever(controller.isGuestUserResetting).thenReturn(false)
- underTest =
- UserRepositoryImpl(
- appContext = context,
- manager = manager,
- controller = controller,
- )
+ globalSettings = FakeSettings()
+ tracker = FakeUserTracker()
+ featureFlags = FakeFeatureFlags()
+ featureFlags.set(Flags.USER_INTERACTOR_AND_REPO_USE_CONTROLLER, !isRefactored)
}
- @Test
- fun `users - registers for updates`() =
- runBlocking(IMMEDIATE) {
- val job = underTest.users.onEach {}.launchIn(this)
-
- verify(controller).addUserSwitchCallback(any())
-
- job.cancel()
- }
-
- @Test
- fun `users - unregisters from updates`() =
- runBlocking(IMMEDIATE) {
- val job = underTest.users.onEach {}.launchIn(this)
- verify(controller).addUserSwitchCallback(capture(userSwitchCallbackCaptor))
-
- job.cancel()
-
- verify(controller).removeUserSwitchCallback(userSwitchCallbackCaptor.value)
- }
-
- @Test
- fun `users - does not include actions`() =
- runBlocking(IMMEDIATE) {
- whenever(controller.users)
- .thenReturn(
- arrayListOf(
- createUserRecord(0, isSelected = true),
- createActionRecord(UserActionModel.ADD_USER),
- createUserRecord(1),
- createUserRecord(2),
- createActionRecord(UserActionModel.ADD_SUPERVISED_USER),
- createActionRecord(UserActionModel.ENTER_GUEST_MODE),
- )
- )
- var models: List<UserModel>? = null
- val job = underTest.users.onEach { models = it }.launchIn(this)
-
- assertThat(models).hasSize(3)
- assertThat(models?.get(0)?.id).isEqualTo(0)
- assertThat(models?.get(0)?.isSelected).isTrue()
- assertThat(models?.get(1)?.id).isEqualTo(1)
- assertThat(models?.get(1)?.isSelected).isFalse()
- assertThat(models?.get(2)?.id).isEqualTo(2)
- assertThat(models?.get(2)?.isSelected).isFalse()
- job.cancel()
- }
-
- @Test
- fun selectedUser() =
- runBlocking(IMMEDIATE) {
- whenever(controller.users)
- .thenReturn(
- arrayListOf(
- createUserRecord(0, isSelected = true),
- createUserRecord(1),
- createUserRecord(2),
- )
- )
- var id: Int? = null
- val job = underTest.selectedUser.map { it.id }.onEach { id = it }.launchIn(this)
-
- assertThat(id).isEqualTo(0)
-
- whenever(controller.users)
- .thenReturn(
- arrayListOf(
- createUserRecord(0),
- createUserRecord(1),
- createUserRecord(2, isSelected = true),
- )
- )
- verify(controller).addUserSwitchCallback(capture(userSwitchCallbackCaptor))
- userSwitchCallbackCaptor.value.onUserSwitched()
- assertThat(id).isEqualTo(2)
-
- job.cancel()
- }
-
- @Test
- fun `actions - unregisters from updates`() =
- runBlocking(IMMEDIATE) {
- val job = underTest.actions.onEach {}.launchIn(this)
- verify(controller).addUserSwitchCallback(capture(userSwitchCallbackCaptor))
-
- job.cancel()
-
- verify(controller).removeUserSwitchCallback(userSwitchCallbackCaptor.value)
- }
-
- @Test
- fun `actions - registers for updates`() =
- runBlocking(IMMEDIATE) {
- val job = underTest.actions.onEach {}.launchIn(this)
-
- verify(controller).addUserSwitchCallback(any())
-
- job.cancel()
- }
-
- @Test
- fun `actopms - does not include users`() =
- runBlocking(IMMEDIATE) {
- whenever(controller.users)
- .thenReturn(
- arrayListOf(
- createUserRecord(0, isSelected = true),
- createActionRecord(UserActionModel.ADD_USER),
- createUserRecord(1),
- createUserRecord(2),
- createActionRecord(UserActionModel.ADD_SUPERVISED_USER),
- createActionRecord(UserActionModel.ENTER_GUEST_MODE),
- )
- )
- var models: List<UserActionModel>? = null
- val job = underTest.actions.onEach { models = it }.launchIn(this)
-
- assertThat(models).hasSize(3)
- assertThat(models?.get(0)).isEqualTo(UserActionModel.ADD_USER)
- assertThat(models?.get(1)).isEqualTo(UserActionModel.ADD_SUPERVISED_USER)
- assertThat(models?.get(2)).isEqualTo(UserActionModel.ENTER_GUEST_MODE)
- job.cancel()
- }
-
- private fun createUserRecord(id: Int, isSelected: Boolean = false): UserRecord {
- return UserRecord(
- info = UserInfo(id, "name$id", 0),
- isCurrent = isSelected,
- )
- }
-
- private fun createActionRecord(action: UserActionModel): UserRecord {
- return UserRecord(
- isAddUser = action == UserActionModel.ADD_USER,
- isAddSupervisedUser = action == UserActionModel.ADD_SUPERVISED_USER,
- isGuest = action == UserActionModel.ENTER_GUEST_MODE,
+ protected fun create(scope: CoroutineScope = TestCoroutineScope()): UserRepositoryImpl {
+ return UserRepositoryImpl(
+ appContext = context,
+ manager = manager,
+ controller = controller,
+ applicationScope = scope,
+ mainDispatcher = IMMEDIATE,
+ backgroundDispatcher = IMMEDIATE,
+ globalSettings = globalSettings,
+ tracker = tracker,
+ featureFlags = featureFlags,
)
}
companion object {
- private val IMMEDIATE = Dispatchers.Main.immediate
+ @JvmStatic protected val IMMEDIATE = Dispatchers.Main.immediate
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplUnrefactoredTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplUnrefactoredTest.kt
new file mode 100644
index 0000000..d4b41c1
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplUnrefactoredTest.kt
@@ -0,0 +1,205 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package com.android.systemui.user.data.repository
+
+import android.content.pm.UserInfo
+import androidx.test.filters.SmallTest
+import com.android.systemui.statusbar.policy.UserSwitcherController
+import com.android.systemui.user.data.source.UserRecord
+import com.android.systemui.user.shared.model.UserActionModel
+import com.android.systemui.user.shared.model.UserModel
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.capture
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.runBlocking
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.mockito.ArgumentCaptor
+import org.mockito.Captor
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when` as whenever
+
+@SmallTest
+@RunWith(JUnit4::class)
+class UserRepositoryImplUnrefactoredTest : UserRepositoryImplTest() {
+
+ companion object {
+ private val IMMEDIATE = Dispatchers.Main.immediate
+ }
+
+ @Captor
+ private lateinit var userSwitchCallbackCaptor:
+ ArgumentCaptor<UserSwitcherController.UserSwitchCallback>
+
+ @Before
+ fun setUp() {
+ super.setUp(isRefactored = false)
+
+ whenever(controller.isAddUsersFromLockScreenEnabled).thenReturn(MutableStateFlow(false))
+ whenever(controller.isGuestUserAutoCreated).thenReturn(false)
+ whenever(controller.isGuestUserResetting).thenReturn(false)
+
+ underTest = create()
+ }
+
+ @Test
+ fun `users - registers for updates`() =
+ runBlocking(IMMEDIATE) {
+ val job = underTest.users.onEach {}.launchIn(this)
+
+ verify(controller).addUserSwitchCallback(any())
+
+ job.cancel()
+ }
+
+ @Test
+ fun `users - unregisters from updates`() =
+ runBlocking(IMMEDIATE) {
+ val job = underTest.users.onEach {}.launchIn(this)
+ verify(controller).addUserSwitchCallback(capture(userSwitchCallbackCaptor))
+
+ job.cancel()
+
+ verify(controller).removeUserSwitchCallback(userSwitchCallbackCaptor.value)
+ }
+
+ @Test
+ fun `users - does not include actions`() =
+ runBlocking(IMMEDIATE) {
+ whenever(controller.users)
+ .thenReturn(
+ arrayListOf(
+ createUserRecord(0, isSelected = true),
+ createActionRecord(UserActionModel.ADD_USER),
+ createUserRecord(1),
+ createUserRecord(2),
+ createActionRecord(UserActionModel.ADD_SUPERVISED_USER),
+ createActionRecord(UserActionModel.ENTER_GUEST_MODE),
+ )
+ )
+ var models: List<UserModel>? = null
+ val job = underTest.users.onEach { models = it }.launchIn(this)
+
+ assertThat(models).hasSize(3)
+ assertThat(models?.get(0)?.id).isEqualTo(0)
+ assertThat(models?.get(0)?.isSelected).isTrue()
+ assertThat(models?.get(1)?.id).isEqualTo(1)
+ assertThat(models?.get(1)?.isSelected).isFalse()
+ assertThat(models?.get(2)?.id).isEqualTo(2)
+ assertThat(models?.get(2)?.isSelected).isFalse()
+ job.cancel()
+ }
+
+ @Test
+ fun selectedUser() =
+ runBlocking(IMMEDIATE) {
+ whenever(controller.users)
+ .thenReturn(
+ arrayListOf(
+ createUserRecord(0, isSelected = true),
+ createUserRecord(1),
+ createUserRecord(2),
+ )
+ )
+ var id: Int? = null
+ val job = underTest.selectedUser.map { it.id }.onEach { id = it }.launchIn(this)
+
+ assertThat(id).isEqualTo(0)
+
+ whenever(controller.users)
+ .thenReturn(
+ arrayListOf(
+ createUserRecord(0),
+ createUserRecord(1),
+ createUserRecord(2, isSelected = true),
+ )
+ )
+ verify(controller).addUserSwitchCallback(capture(userSwitchCallbackCaptor))
+ userSwitchCallbackCaptor.value.onUserSwitched()
+ assertThat(id).isEqualTo(2)
+
+ job.cancel()
+ }
+
+ @Test
+ fun `actions - unregisters from updates`() =
+ runBlocking(IMMEDIATE) {
+ val job = underTest.actions.onEach {}.launchIn(this)
+ verify(controller).addUserSwitchCallback(capture(userSwitchCallbackCaptor))
+
+ job.cancel()
+
+ verify(controller).removeUserSwitchCallback(userSwitchCallbackCaptor.value)
+ }
+
+ @Test
+ fun `actions - registers for updates`() =
+ runBlocking(IMMEDIATE) {
+ val job = underTest.actions.onEach {}.launchIn(this)
+
+ verify(controller).addUserSwitchCallback(any())
+
+ job.cancel()
+ }
+
+ @Test
+ fun `actions - does not include users`() =
+ runBlocking(IMMEDIATE) {
+ whenever(controller.users)
+ .thenReturn(
+ arrayListOf(
+ createUserRecord(0, isSelected = true),
+ createActionRecord(UserActionModel.ADD_USER),
+ createUserRecord(1),
+ createUserRecord(2),
+ createActionRecord(UserActionModel.ADD_SUPERVISED_USER),
+ createActionRecord(UserActionModel.ENTER_GUEST_MODE),
+ )
+ )
+ var models: List<UserActionModel>? = null
+ val job = underTest.actions.onEach { models = it }.launchIn(this)
+
+ assertThat(models).hasSize(3)
+ assertThat(models?.get(0)).isEqualTo(UserActionModel.ADD_USER)
+ assertThat(models?.get(1)).isEqualTo(UserActionModel.ADD_SUPERVISED_USER)
+ assertThat(models?.get(2)).isEqualTo(UserActionModel.ENTER_GUEST_MODE)
+ job.cancel()
+ }
+
+ private fun createUserRecord(id: Int, isSelected: Boolean = false): UserRecord {
+ return UserRecord(
+ info = UserInfo(id, "name$id", 0),
+ isCurrent = isSelected,
+ )
+ }
+
+ private fun createActionRecord(action: UserActionModel): UserRecord {
+ return UserRecord(
+ isAddUser = action == UserActionModel.ADD_USER,
+ isAddSupervisedUser = action == UserActionModel.ADD_SUPERVISED_USER,
+ isGuest = action == UserActionModel.ENTER_GUEST_MODE,
+ )
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/GuestUserInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/GuestUserInteractorTest.kt
new file mode 100644
index 0000000..120bf79
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/GuestUserInteractorTest.kt
@@ -0,0 +1,394 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package com.android.systemui.user.domain.interactor
+
+import android.app.admin.DevicePolicyManager
+import android.content.pm.UserInfo
+import android.os.UserHandle
+import android.os.UserManager
+import androidx.test.filters.SmallTest
+import com.android.internal.logging.UiEventLogger
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.statusbar.policy.DeviceProvisionedController
+import com.android.systemui.user.data.repository.FakeUserRepository
+import com.android.systemui.user.domain.model.ShowDialogRequestModel
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.kotlinArgumentCaptor
+import com.android.systemui.util.mockito.whenever
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.test.TestCoroutineScope
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.Mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(JUnit4::class)
+class GuestUserInteractorTest : SysuiTestCase() {
+
+ @Mock private lateinit var manager: UserManager
+ @Mock private lateinit var deviceProvisionedController: DeviceProvisionedController
+ @Mock private lateinit var devicePolicyManager: DevicePolicyManager
+ @Mock private lateinit var uiEventLogger: UiEventLogger
+ @Mock private lateinit var showDialog: (ShowDialogRequestModel) -> Unit
+ @Mock private lateinit var dismissDialog: () -> Unit
+ @Mock private lateinit var selectUser: (Int) -> Unit
+ @Mock private lateinit var switchUser: (Int) -> Unit
+
+ private lateinit var underTest: GuestUserInteractor
+
+ private lateinit var scope: TestCoroutineScope
+ private lateinit var repository: FakeUserRepository
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+ whenever(manager.createGuest(any())).thenReturn(GUEST_USER_INFO)
+
+ scope = TestCoroutineScope()
+ repository = FakeUserRepository()
+ repository.setUserInfos(ALL_USERS)
+
+ underTest =
+ GuestUserInteractor(
+ applicationContext = context,
+ applicationScope = scope,
+ mainDispatcher = IMMEDIATE,
+ backgroundDispatcher = IMMEDIATE,
+ manager = manager,
+ repository = repository,
+ deviceProvisionedController = deviceProvisionedController,
+ devicePolicyManager = devicePolicyManager,
+ refreshUsersScheduler =
+ RefreshUsersScheduler(
+ applicationScope = scope,
+ mainDispatcher = IMMEDIATE,
+ repository = repository,
+ ),
+ uiEventLogger = uiEventLogger,
+ )
+ }
+
+ @Test
+ fun `onDeviceBootCompleted - allowed to add - create guest`() =
+ runBlocking(IMMEDIATE) {
+ setAllowedToAdd()
+
+ underTest.onDeviceBootCompleted()
+
+ verify(manager).createGuest(any())
+ verify(deviceProvisionedController, never()).addCallback(any())
+ }
+
+ @Test
+ fun `onDeviceBootCompleted - await provisioning - and create guest`() =
+ runBlocking(IMMEDIATE) {
+ setAllowedToAdd(isAllowed = false)
+ underTest.onDeviceBootCompleted()
+ val captor =
+ kotlinArgumentCaptor<DeviceProvisionedController.DeviceProvisionedListener>()
+ verify(deviceProvisionedController).addCallback(captor.capture())
+
+ setAllowedToAdd(isAllowed = true)
+ captor.value.onDeviceProvisionedChanged()
+
+ verify(manager).createGuest(any())
+ verify(deviceProvisionedController).removeCallback(captor.value)
+ }
+
+ @Test
+ fun createAndSwitchTo() =
+ runBlocking(IMMEDIATE) {
+ underTest.createAndSwitchTo(
+ showDialog = showDialog,
+ dismissDialog = dismissDialog,
+ selectUser = selectUser,
+ )
+
+ verify(showDialog).invoke(ShowDialogRequestModel.ShowUserCreationDialog(isGuest = true))
+ verify(manager).createGuest(any())
+ verify(dismissDialog).invoke()
+ verify(selectUser).invoke(GUEST_USER_INFO.id)
+ }
+
+ @Test
+ fun `createAndSwitchTo - fails to create - does not switch to`() =
+ runBlocking(IMMEDIATE) {
+ whenever(manager.createGuest(any())).thenReturn(null)
+
+ underTest.createAndSwitchTo(
+ showDialog = showDialog,
+ dismissDialog = dismissDialog,
+ selectUser = selectUser,
+ )
+
+ verify(showDialog).invoke(ShowDialogRequestModel.ShowUserCreationDialog(isGuest = true))
+ verify(manager).createGuest(any())
+ verify(dismissDialog).invoke()
+ verify(selectUser, never()).invoke(anyInt())
+ }
+
+ @Test
+ fun `exit - returns to target user`() =
+ runBlocking(IMMEDIATE) {
+ repository.setSelectedUserInfo(GUEST_USER_INFO)
+
+ val targetUserId = NON_GUEST_USER_INFO.id
+ underTest.exit(
+ guestUserId = GUEST_USER_INFO.id,
+ targetUserId = targetUserId,
+ forceRemoveGuestOnExit = false,
+ showDialog = showDialog,
+ dismissDialog = dismissDialog,
+ switchUser = switchUser,
+ )
+
+ verify(manager, never()).markGuestForDeletion(anyInt())
+ verify(manager, never()).removeUser(anyInt())
+ verify(switchUser).invoke(targetUserId)
+ }
+
+ @Test
+ fun `exit - returns to last non-guest`() =
+ runBlocking(IMMEDIATE) {
+ val expectedUserId = NON_GUEST_USER_INFO.id
+ whenever(manager.getUserInfo(expectedUserId)).thenReturn(NON_GUEST_USER_INFO)
+ repository.lastSelectedNonGuestUserId = expectedUserId
+ repository.setSelectedUserInfo(GUEST_USER_INFO)
+
+ underTest.exit(
+ guestUserId = GUEST_USER_INFO.id,
+ targetUserId = UserHandle.USER_NULL,
+ forceRemoveGuestOnExit = false,
+ showDialog = showDialog,
+ dismissDialog = dismissDialog,
+ switchUser = switchUser,
+ )
+
+ verify(manager, never()).markGuestForDeletion(anyInt())
+ verify(manager, never()).removeUser(anyInt())
+ verify(switchUser).invoke(expectedUserId)
+ }
+
+ @Test
+ fun `exit - last non-guest was removed - returns to system`() =
+ runBlocking(IMMEDIATE) {
+ val removedUserId = 310
+ repository.lastSelectedNonGuestUserId = removedUserId
+ repository.setSelectedUserInfo(GUEST_USER_INFO)
+
+ underTest.exit(
+ guestUserId = GUEST_USER_INFO.id,
+ targetUserId = UserHandle.USER_NULL,
+ forceRemoveGuestOnExit = false,
+ showDialog = showDialog,
+ dismissDialog = dismissDialog,
+ switchUser = switchUser,
+ )
+
+ verify(manager, never()).markGuestForDeletion(anyInt())
+ verify(manager, never()).removeUser(anyInt())
+ verify(switchUser).invoke(UserHandle.USER_SYSTEM)
+ }
+
+ @Test
+ fun `exit - guest was ephemeral - it is removed`() =
+ runBlocking(IMMEDIATE) {
+ whenever(manager.markGuestForDeletion(anyInt())).thenReturn(true)
+ repository.setUserInfos(listOf(NON_GUEST_USER_INFO, EPHEMERAL_GUEST_USER_INFO))
+ repository.setSelectedUserInfo(EPHEMERAL_GUEST_USER_INFO)
+ val targetUserId = NON_GUEST_USER_INFO.id
+
+ underTest.exit(
+ guestUserId = GUEST_USER_INFO.id,
+ targetUserId = targetUserId,
+ forceRemoveGuestOnExit = false,
+ showDialog = showDialog,
+ dismissDialog = dismissDialog,
+ switchUser = switchUser,
+ )
+
+ verify(manager).markGuestForDeletion(EPHEMERAL_GUEST_USER_INFO.id)
+ verify(manager).removeUser(EPHEMERAL_GUEST_USER_INFO.id)
+ verify(switchUser).invoke(targetUserId)
+ }
+
+ @Test
+ fun `exit - force remove guest - it is removed`() =
+ runBlocking(IMMEDIATE) {
+ whenever(manager.markGuestForDeletion(anyInt())).thenReturn(true)
+ repository.setSelectedUserInfo(GUEST_USER_INFO)
+ val targetUserId = NON_GUEST_USER_INFO.id
+
+ underTest.exit(
+ guestUserId = GUEST_USER_INFO.id,
+ targetUserId = targetUserId,
+ forceRemoveGuestOnExit = true,
+ showDialog = showDialog,
+ dismissDialog = dismissDialog,
+ switchUser = switchUser,
+ )
+
+ verify(manager).markGuestForDeletion(GUEST_USER_INFO.id)
+ verify(manager).removeUser(GUEST_USER_INFO.id)
+ verify(switchUser).invoke(targetUserId)
+ }
+
+ @Test
+ fun `exit - selected different from guest user - do nothing`() =
+ runBlocking(IMMEDIATE) {
+ repository.setSelectedUserInfo(NON_GUEST_USER_INFO)
+
+ underTest.exit(
+ guestUserId = GUEST_USER_INFO.id,
+ targetUserId = 123,
+ forceRemoveGuestOnExit = false,
+ showDialog = showDialog,
+ dismissDialog = dismissDialog,
+ switchUser = switchUser,
+ )
+
+ verifyDidNotExit()
+ }
+
+ @Test
+ fun `exit - selected is actually not a guest user - do nothing`() =
+ runBlocking(IMMEDIATE) {
+ repository.setSelectedUserInfo(NON_GUEST_USER_INFO)
+
+ underTest.exit(
+ guestUserId = NON_GUEST_USER_INFO.id,
+ targetUserId = 123,
+ forceRemoveGuestOnExit = false,
+ showDialog = showDialog,
+ dismissDialog = dismissDialog,
+ switchUser = switchUser,
+ )
+
+ verifyDidNotExit()
+ }
+
+ @Test
+ fun `remove - returns to target user`() =
+ runBlocking(IMMEDIATE) {
+ whenever(manager.markGuestForDeletion(anyInt())).thenReturn(true)
+ repository.setSelectedUserInfo(GUEST_USER_INFO)
+
+ val targetUserId = NON_GUEST_USER_INFO.id
+ underTest.remove(
+ guestUserId = GUEST_USER_INFO.id,
+ targetUserId = targetUserId,
+ showDialog = showDialog,
+ dismissDialog = dismissDialog,
+ switchUser = switchUser,
+ )
+
+ verify(manager).markGuestForDeletion(GUEST_USER_INFO.id)
+ verify(manager).removeUser(GUEST_USER_INFO.id)
+ verify(switchUser).invoke(targetUserId)
+ }
+
+ @Test
+ fun `remove - selected different from guest user - do nothing`() =
+ runBlocking(IMMEDIATE) {
+ whenever(manager.markGuestForDeletion(anyInt())).thenReturn(true)
+ repository.setSelectedUserInfo(NON_GUEST_USER_INFO)
+
+ underTest.remove(
+ guestUserId = GUEST_USER_INFO.id,
+ targetUserId = 123,
+ showDialog = showDialog,
+ dismissDialog = dismissDialog,
+ switchUser = switchUser,
+ )
+
+ verifyDidNotRemove()
+ }
+
+ @Test
+ fun `remove - selected is actually not a guest user - do nothing`() =
+ runBlocking(IMMEDIATE) {
+ whenever(manager.markGuestForDeletion(anyInt())).thenReturn(true)
+ repository.setSelectedUserInfo(NON_GUEST_USER_INFO)
+
+ underTest.remove(
+ guestUserId = NON_GUEST_USER_INFO.id,
+ targetUserId = 123,
+ showDialog = showDialog,
+ dismissDialog = dismissDialog,
+ switchUser = switchUser,
+ )
+
+ verifyDidNotRemove()
+ }
+
+ private fun setAllowedToAdd(isAllowed: Boolean = true) {
+ whenever(deviceProvisionedController.isDeviceProvisioned).thenReturn(isAllowed)
+ whenever(devicePolicyManager.isDeviceManaged).thenReturn(!isAllowed)
+ }
+
+ private fun verifyDidNotExit() {
+ verifyDidNotRemove()
+ verify(manager, never()).getUserInfo(anyInt())
+ verify(uiEventLogger, never()).log(any())
+ }
+
+ private fun verifyDidNotRemove() {
+ verify(manager, never()).markGuestForDeletion(anyInt())
+ verify(showDialog, never()).invoke(any())
+ verify(dismissDialog, never()).invoke()
+ verify(switchUser, never()).invoke(anyInt())
+ }
+
+ companion object {
+ private val IMMEDIATE = Dispatchers.Main.immediate
+ private val NON_GUEST_USER_INFO =
+ UserInfo(
+ /* id= */ 818,
+ /* name= */ "non_guest",
+ /* flags= */ 0,
+ )
+ private val GUEST_USER_INFO =
+ UserInfo(
+ /* id= */ 669,
+ /* name= */ "guest",
+ /* iconPath= */ "",
+ /* flags= */ 0,
+ UserManager.USER_TYPE_FULL_GUEST,
+ )
+ private val EPHEMERAL_GUEST_USER_INFO =
+ UserInfo(
+ /* id= */ 669,
+ /* name= */ "guest",
+ /* iconPath= */ "",
+ /* flags= */ UserInfo.FLAG_EPHEMERAL,
+ UserManager.USER_TYPE_FULL_GUEST,
+ )
+ private val ALL_USERS =
+ listOf(
+ NON_GUEST_USER_INFO,
+ GUEST_USER_INFO,
+ )
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/RefreshUsersSchedulerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/RefreshUsersSchedulerTest.kt
new file mode 100644
index 0000000..593ce1f
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/RefreshUsersSchedulerTest.kt
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package com.android.systemui.user.domain.interactor
+
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.user.data.repository.FakeUserRepository
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.runBlocking
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(JUnit4::class)
+class RefreshUsersSchedulerTest : SysuiTestCase() {
+
+ private lateinit var underTest: RefreshUsersScheduler
+
+ private lateinit var repository: FakeUserRepository
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+
+ repository = FakeUserRepository()
+ }
+
+ @Test
+ fun `pause - prevents the next refresh from happening`() =
+ runBlocking(IMMEDIATE) {
+ underTest =
+ RefreshUsersScheduler(
+ applicationScope = this,
+ mainDispatcher = IMMEDIATE,
+ repository = repository,
+ )
+ underTest.pause()
+
+ underTest.refreshIfNotPaused()
+ assertThat(repository.refreshUsersCallCount).isEqualTo(0)
+ }
+
+ @Test
+ fun `unpauseAndRefresh - forces the refresh even when paused`() =
+ runBlocking(IMMEDIATE) {
+ underTest =
+ RefreshUsersScheduler(
+ applicationScope = this,
+ mainDispatcher = IMMEDIATE,
+ repository = repository,
+ )
+ underTest.pause()
+
+ underTest.unpauseAndRefresh()
+
+ assertThat(repository.refreshUsersCallCount).isEqualTo(1)
+ }
+
+ @Test
+ fun `refreshIfNotPaused - refreshes when not paused`() =
+ runBlocking(IMMEDIATE) {
+ underTest =
+ RefreshUsersScheduler(
+ applicationScope = this,
+ mainDispatcher = IMMEDIATE,
+ repository = repository,
+ )
+ underTest.refreshIfNotPaused()
+
+ assertThat(repository.refreshUsersCallCount).isEqualTo(1)
+ }
+
+ companion object {
+ private val IMMEDIATE = Dispatchers.Main.immediate
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorRefactoredTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorRefactoredTest.kt
new file mode 100644
index 0000000..3d5695a
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorRefactoredTest.kt
@@ -0,0 +1,658 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package com.android.systemui.user.domain.interactor
+
+import android.content.Intent
+import android.content.pm.UserInfo
+import android.graphics.Bitmap
+import android.graphics.drawable.Drawable
+import android.os.UserHandle
+import android.os.UserManager
+import android.provider.Settings
+import androidx.test.filters.SmallTest
+import com.android.internal.R.drawable.ic_account_circle
+import com.android.systemui.R
+import com.android.systemui.common.shared.model.Text
+import com.android.systemui.user.data.model.UserSwitcherSettingsModel
+import com.android.systemui.user.data.source.UserRecord
+import com.android.systemui.user.domain.model.ShowDialogRequestModel
+import com.android.systemui.user.shared.model.UserActionModel
+import com.android.systemui.user.shared.model.UserModel
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.eq
+import com.android.systemui.util.mockito.kotlinArgumentCaptor
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.runBlocking
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.Mockito.verify
+
+@SmallTest
+@RunWith(JUnit4::class)
+class UserInteractorRefactoredTest : UserInteractorTest() {
+
+ override fun isRefactored(): Boolean {
+ return true
+ }
+
+ @Before
+ override fun setUp() {
+ super.setUp()
+
+ overrideResource(R.drawable.ic_account_circle, GUEST_ICON)
+ overrideResource(R.dimen.max_avatar_size, 10)
+ overrideResource(
+ com.android.internal.R.string.config_supervisedUserCreationPackage,
+ SUPERVISED_USER_CREATION_APP_PACKAGE,
+ )
+ whenever(manager.getUserIcon(anyInt())).thenReturn(ICON)
+ whenever(manager.canAddMoreUsers(any())).thenReturn(true)
+ }
+
+ @Test
+ fun `users - switcher enabled`() =
+ runBlocking(IMMEDIATE) {
+ val userInfos = createUserInfos(count = 3, includeGuest = true)
+ userRepository.setUserInfos(userInfos)
+ userRepository.setSelectedUserInfo(userInfos[0])
+ userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
+
+ var value: List<UserModel>? = null
+ val job = underTest.users.onEach { value = it }.launchIn(this)
+ assertUsers(models = value, count = 3, includeGuest = true)
+
+ job.cancel()
+ }
+
+ @Test
+ fun `users - switches to second user`() =
+ runBlocking(IMMEDIATE) {
+ val userInfos = createUserInfos(count = 2, includeGuest = false)
+ userRepository.setUserInfos(userInfos)
+ userRepository.setSelectedUserInfo(userInfos[0])
+ userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
+
+ var value: List<UserModel>? = null
+ val job = underTest.users.onEach { value = it }.launchIn(this)
+ userRepository.setSelectedUserInfo(userInfos[1])
+
+ assertUsers(models = value, count = 2, selectedIndex = 1)
+ job.cancel()
+ }
+
+ @Test
+ fun `users - switcher not enabled`() =
+ runBlocking(IMMEDIATE) {
+ val userInfos = createUserInfos(count = 2, includeGuest = false)
+ userRepository.setUserInfos(userInfos)
+ userRepository.setSelectedUserInfo(userInfos[0])
+ userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = false))
+
+ var value: List<UserModel>? = null
+ val job = underTest.users.onEach { value = it }.launchIn(this)
+ assertUsers(models = value, count = 1)
+
+ job.cancel()
+ }
+
+ @Test
+ fun selectedUser() =
+ runBlocking(IMMEDIATE) {
+ val userInfos = createUserInfos(count = 2, includeGuest = false)
+ userRepository.setUserInfos(userInfos)
+ userRepository.setSelectedUserInfo(userInfos[0])
+ userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
+
+ var value: UserModel? = null
+ val job = underTest.selectedUser.onEach { value = it }.launchIn(this)
+ assertUser(value, id = 0, isSelected = true)
+
+ userRepository.setSelectedUserInfo(userInfos[1])
+ assertUser(value, id = 1, isSelected = true)
+
+ job.cancel()
+ }
+
+ @Test
+ fun `actions - device unlocked`() =
+ runBlocking(IMMEDIATE) {
+ val userInfos = createUserInfos(count = 2, includeGuest = false)
+ userRepository.setUserInfos(userInfos)
+ userRepository.setSelectedUserInfo(userInfos[0])
+ userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
+ keyguardRepository.setKeyguardShowing(false)
+ var value: List<UserActionModel>? = null
+ val job = underTest.actions.onEach { value = it }.launchIn(this)
+
+ assertThat(value)
+ .isEqualTo(
+ listOf(
+ UserActionModel.ENTER_GUEST_MODE,
+ UserActionModel.ADD_USER,
+ UserActionModel.ADD_SUPERVISED_USER,
+ )
+ )
+
+ job.cancel()
+ }
+
+ @Test
+ fun `actions - device unlocked user not primary - empty list`() =
+ runBlocking(IMMEDIATE) {
+ val userInfos = createUserInfos(count = 2, includeGuest = false)
+ userRepository.setUserInfos(userInfos)
+ userRepository.setSelectedUserInfo(userInfos[1])
+ userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
+ keyguardRepository.setKeyguardShowing(false)
+ var value: List<UserActionModel>? = null
+ val job = underTest.actions.onEach { value = it }.launchIn(this)
+
+ assertThat(value).isEqualTo(emptyList<UserActionModel>())
+
+ job.cancel()
+ }
+
+ @Test
+ fun `actions - device unlocked user is guest - empty list`() =
+ runBlocking(IMMEDIATE) {
+ val userInfos = createUserInfos(count = 2, includeGuest = true)
+ assertThat(userInfos[1].isGuest).isTrue()
+ userRepository.setUserInfos(userInfos)
+ userRepository.setSelectedUserInfo(userInfos[1])
+ userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
+ keyguardRepository.setKeyguardShowing(false)
+ var value: List<UserActionModel>? = null
+ val job = underTest.actions.onEach { value = it }.launchIn(this)
+
+ assertThat(value).isEqualTo(emptyList<UserActionModel>())
+
+ job.cancel()
+ }
+
+ @Test
+ fun `actions - device locked add from lockscreen set - full list`() =
+ runBlocking(IMMEDIATE) {
+ val userInfos = createUserInfos(count = 2, includeGuest = false)
+ userRepository.setUserInfos(userInfos)
+ userRepository.setSelectedUserInfo(userInfos[0])
+ userRepository.setSettings(
+ UserSwitcherSettingsModel(
+ isUserSwitcherEnabled = true,
+ isAddUsersFromLockscreen = true,
+ )
+ )
+ keyguardRepository.setKeyguardShowing(false)
+ var value: List<UserActionModel>? = null
+ val job = underTest.actions.onEach { value = it }.launchIn(this)
+
+ assertThat(value)
+ .isEqualTo(
+ listOf(
+ UserActionModel.ENTER_GUEST_MODE,
+ UserActionModel.ADD_USER,
+ UserActionModel.ADD_SUPERVISED_USER,
+ )
+ )
+
+ job.cancel()
+ }
+
+ @Test
+ fun `actions - device locked - only guest action is shown`() =
+ runBlocking(IMMEDIATE) {
+ val userInfos = createUserInfos(count = 2, includeGuest = false)
+ userRepository.setUserInfos(userInfos)
+ userRepository.setSelectedUserInfo(userInfos[0])
+ userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
+ keyguardRepository.setKeyguardShowing(true)
+ var value: List<UserActionModel>? = null
+ val job = underTest.actions.onEach { value = it }.launchIn(this)
+
+ assertThat(value).isEqualTo(listOf(UserActionModel.ENTER_GUEST_MODE))
+
+ job.cancel()
+ }
+
+ @Test
+ fun `executeAction - add user - dialog shown`() =
+ runBlocking(IMMEDIATE) {
+ val userInfos = createUserInfos(count = 2, includeGuest = false)
+ userRepository.setUserInfos(userInfos)
+ userRepository.setSelectedUserInfo(userInfos[0])
+ keyguardRepository.setKeyguardShowing(false)
+ var dialogRequest: ShowDialogRequestModel? = null
+ val job = underTest.dialogShowRequests.onEach { dialogRequest = it }.launchIn(this)
+
+ underTest.executeAction(UserActionModel.ADD_USER)
+ assertThat(dialogRequest)
+ .isEqualTo(
+ ShowDialogRequestModel.ShowAddUserDialog(
+ userHandle = userInfos[0].userHandle,
+ isKeyguardShowing = false,
+ showEphemeralMessage = false,
+ )
+ )
+
+ underTest.onDialogShown()
+ assertThat(dialogRequest).isNull()
+
+ job.cancel()
+ }
+
+ @Test
+ fun `executeAction - add supervised user - starts activity`() =
+ runBlocking(IMMEDIATE) {
+ underTest.executeAction(UserActionModel.ADD_SUPERVISED_USER)
+
+ val intentCaptor = kotlinArgumentCaptor<Intent>()
+ verify(activityStarter).startActivity(intentCaptor.capture(), eq(false))
+ assertThat(intentCaptor.value.action)
+ .isEqualTo(UserManager.ACTION_CREATE_SUPERVISED_USER)
+ assertThat(intentCaptor.value.`package`).isEqualTo(SUPERVISED_USER_CREATION_APP_PACKAGE)
+ }
+
+ @Test
+ fun `executeAction - navigate to manage users`() =
+ runBlocking(IMMEDIATE) {
+ underTest.executeAction(UserActionModel.NAVIGATE_TO_USER_MANAGEMENT)
+
+ val intentCaptor = kotlinArgumentCaptor<Intent>()
+ verify(activityStarter).startActivity(intentCaptor.capture(), eq(false))
+ assertThat(intentCaptor.value.action).isEqualTo(Settings.ACTION_USER_SETTINGS)
+ }
+
+ @Test
+ fun `executeAction - guest mode`() =
+ runBlocking(IMMEDIATE) {
+ val userInfos = createUserInfos(count = 2, includeGuest = false)
+ userRepository.setUserInfos(userInfos)
+ userRepository.setSelectedUserInfo(userInfos[0])
+ userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
+ val guestUserInfo = createUserInfo(id = 1337, name = "guest", isGuest = true)
+ whenever(manager.createGuest(any())).thenReturn(guestUserInfo)
+ val dialogRequests = mutableListOf<ShowDialogRequestModel?>()
+ val showDialogsJob =
+ underTest.dialogShowRequests
+ .onEach {
+ dialogRequests.add(it)
+ if (it != null) {
+ underTest.onDialogShown()
+ }
+ }
+ .launchIn(this)
+ val dismissDialogsJob =
+ underTest.dialogDismissRequests
+ .onEach {
+ if (it != null) {
+ underTest.onDialogDismissed()
+ }
+ }
+ .launchIn(this)
+
+ underTest.executeAction(UserActionModel.ENTER_GUEST_MODE)
+
+ assertThat(dialogRequests)
+ .contains(
+ ShowDialogRequestModel.ShowUserCreationDialog(isGuest = true),
+ )
+ verify(activityManager).switchUser(guestUserInfo.id)
+
+ showDialogsJob.cancel()
+ dismissDialogsJob.cancel()
+ }
+
+ @Test
+ fun `selectUser - already selected guest re-selected - exit guest dialog`() =
+ runBlocking(IMMEDIATE) {
+ val userInfos = createUserInfos(count = 2, includeGuest = true)
+ val guestUserInfo = userInfos[1]
+ assertThat(guestUserInfo.isGuest).isTrue()
+ userRepository.setUserInfos(userInfos)
+ userRepository.setSelectedUserInfo(guestUserInfo)
+ userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
+ var dialogRequest: ShowDialogRequestModel? = null
+ val job = underTest.dialogShowRequests.onEach { dialogRequest = it }.launchIn(this)
+
+ underTest.selectUser(newlySelectedUserId = guestUserInfo.id)
+
+ assertThat(dialogRequest)
+ .isInstanceOf(ShowDialogRequestModel.ShowExitGuestDialog::class.java)
+ job.cancel()
+ }
+
+ @Test
+ fun `selectUser - currently guest non-guest selected - exit guest dialog`() =
+ runBlocking(IMMEDIATE) {
+ val userInfos = createUserInfos(count = 2, includeGuest = true)
+ val guestUserInfo = userInfos[1]
+ assertThat(guestUserInfo.isGuest).isTrue()
+ userRepository.setUserInfos(userInfos)
+ userRepository.setSelectedUserInfo(guestUserInfo)
+ userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
+ var dialogRequest: ShowDialogRequestModel? = null
+ val job = underTest.dialogShowRequests.onEach { dialogRequest = it }.launchIn(this)
+
+ underTest.selectUser(newlySelectedUserId = userInfos[0].id)
+
+ assertThat(dialogRequest)
+ .isInstanceOf(ShowDialogRequestModel.ShowExitGuestDialog::class.java)
+ job.cancel()
+ }
+
+ @Test
+ fun `selectUser - not currently guest - switches users`() =
+ runBlocking(IMMEDIATE) {
+ val userInfos = createUserInfos(count = 2, includeGuest = false)
+ userRepository.setUserInfos(userInfos)
+ userRepository.setSelectedUserInfo(userInfos[0])
+ userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
+ var dialogRequest: ShowDialogRequestModel? = null
+ val job = underTest.dialogShowRequests.onEach { dialogRequest = it }.launchIn(this)
+
+ underTest.selectUser(newlySelectedUserId = userInfos[1].id)
+
+ assertThat(dialogRequest).isNull()
+ verify(activityManager).switchUser(userInfos[1].id)
+ job.cancel()
+ }
+
+ @Test
+ fun `Telephony call state changes - refreshes users`() =
+ runBlocking(IMMEDIATE) {
+ val refreshUsersCallCount = userRepository.refreshUsersCallCount
+
+ telephonyRepository.setCallState(1)
+
+ assertThat(userRepository.refreshUsersCallCount).isEqualTo(refreshUsersCallCount + 1)
+ }
+
+ @Test
+ fun `User switched broadcast`() =
+ runBlocking(IMMEDIATE) {
+ val userInfos = createUserInfos(count = 2, includeGuest = false)
+ userRepository.setUserInfos(userInfos)
+ userRepository.setSelectedUserInfo(userInfos[0])
+ userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
+ val callback1: UserInteractor.UserCallback = mock()
+ val callback2: UserInteractor.UserCallback = mock()
+ underTest.addCallback(callback1)
+ underTest.addCallback(callback2)
+ val refreshUsersCallCount = userRepository.refreshUsersCallCount
+
+ userRepository.setSelectedUserInfo(userInfos[1])
+ fakeBroadcastDispatcher.registeredReceivers.forEach {
+ it.onReceive(
+ context,
+ Intent(Intent.ACTION_USER_SWITCHED)
+ .putExtra(Intent.EXTRA_USER_HANDLE, userInfos[1].id),
+ )
+ }
+
+ verify(callback1).onUserStateChanged()
+ verify(callback2).onUserStateChanged()
+ assertThat(userRepository.secondaryUserId).isEqualTo(userInfos[1].id)
+ assertThat(userRepository.refreshUsersCallCount).isEqualTo(refreshUsersCallCount + 1)
+ }
+
+ @Test
+ fun `User info changed broadcast`() =
+ runBlocking(IMMEDIATE) {
+ val userInfos = createUserInfos(count = 2, includeGuest = false)
+ userRepository.setUserInfos(userInfos)
+ userRepository.setSelectedUserInfo(userInfos[0])
+ val refreshUsersCallCount = userRepository.refreshUsersCallCount
+
+ fakeBroadcastDispatcher.registeredReceivers.forEach {
+ it.onReceive(
+ context,
+ Intent(Intent.ACTION_USER_INFO_CHANGED),
+ )
+ }
+
+ assertThat(userRepository.refreshUsersCallCount).isEqualTo(refreshUsersCallCount + 1)
+ }
+
+ @Test
+ fun `System user unlocked broadcast - refresh users`() =
+ runBlocking(IMMEDIATE) {
+ val userInfos = createUserInfos(count = 2, includeGuest = false)
+ userRepository.setUserInfos(userInfos)
+ userRepository.setSelectedUserInfo(userInfos[0])
+ val refreshUsersCallCount = userRepository.refreshUsersCallCount
+
+ fakeBroadcastDispatcher.registeredReceivers.forEach {
+ it.onReceive(
+ context,
+ Intent(Intent.ACTION_USER_UNLOCKED)
+ .putExtra(Intent.EXTRA_USER_HANDLE, UserHandle.USER_SYSTEM),
+ )
+ }
+
+ assertThat(userRepository.refreshUsersCallCount).isEqualTo(refreshUsersCallCount + 1)
+ }
+
+ @Test
+ fun `Non-system user unlocked broadcast - do not refresh users`() =
+ runBlocking(IMMEDIATE) {
+ val userInfos = createUserInfos(count = 2, includeGuest = false)
+ userRepository.setUserInfos(userInfos)
+ userRepository.setSelectedUserInfo(userInfos[0])
+ val refreshUsersCallCount = userRepository.refreshUsersCallCount
+
+ fakeBroadcastDispatcher.registeredReceivers.forEach {
+ it.onReceive(
+ context,
+ Intent(Intent.ACTION_USER_UNLOCKED).putExtra(Intent.EXTRA_USER_HANDLE, 1337),
+ )
+ }
+
+ assertThat(userRepository.refreshUsersCallCount).isEqualTo(refreshUsersCallCount)
+ }
+
+ @Test
+ fun userRecords() =
+ runBlocking(IMMEDIATE) {
+ val userInfos = createUserInfos(count = 3, includeGuest = false)
+ userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
+ userRepository.setUserInfos(userInfos)
+ userRepository.setSelectedUserInfo(userInfos[0])
+ keyguardRepository.setKeyguardShowing(false)
+
+ testCoroutineScope.advanceUntilIdle()
+
+ assertRecords(
+ records = underTest.userRecords.value,
+ userIds = listOf(0, 1, 2),
+ selectedUserIndex = 0,
+ includeGuest = false,
+ expectedActions =
+ listOf(
+ UserActionModel.ENTER_GUEST_MODE,
+ UserActionModel.ADD_USER,
+ UserActionModel.ADD_SUPERVISED_USER,
+ ),
+ )
+ }
+
+ @Test
+ fun selectedUserRecord() =
+ runBlocking(IMMEDIATE) {
+ val userInfos = createUserInfos(count = 3, includeGuest = true)
+ userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
+ userRepository.setUserInfos(userInfos)
+ userRepository.setSelectedUserInfo(userInfos[0])
+ keyguardRepository.setKeyguardShowing(false)
+
+ assertRecordForUser(
+ record = underTest.selectedUserRecord.value,
+ id = 0,
+ hasPicture = true,
+ isCurrent = true,
+ isSwitchToEnabled = true,
+ )
+ }
+
+ private fun assertUsers(
+ models: List<UserModel>?,
+ count: Int,
+ selectedIndex: Int = 0,
+ includeGuest: Boolean = false,
+ ) {
+ checkNotNull(models)
+ assertThat(models.size).isEqualTo(count)
+ models.forEachIndexed { index, model ->
+ assertUser(
+ model = model,
+ id = index,
+ isSelected = index == selectedIndex,
+ isGuest = includeGuest && index == count - 1
+ )
+ }
+ }
+
+ private fun assertUser(
+ model: UserModel?,
+ id: Int,
+ isSelected: Boolean = false,
+ isGuest: Boolean = false,
+ ) {
+ checkNotNull(model)
+ assertThat(model.id).isEqualTo(id)
+ assertThat(model.name).isEqualTo(Text.Loaded(if (isGuest) "guest" else "user_$id"))
+ assertThat(model.isSelected).isEqualTo(isSelected)
+ assertThat(model.isSelectable).isTrue()
+ assertThat(model.isGuest).isEqualTo(isGuest)
+ }
+
+ private fun assertRecords(
+ records: List<UserRecord>,
+ userIds: List<Int>,
+ selectedUserIndex: Int = 0,
+ includeGuest: Boolean = false,
+ expectedActions: List<UserActionModel> = emptyList(),
+ ) {
+ assertThat(records.size >= userIds.size).isTrue()
+ userIds.indices.forEach { userIndex ->
+ val record = records[userIndex]
+ assertThat(record.info).isNotNull()
+ val isGuest = includeGuest && userIndex == userIds.size - 1
+ assertRecordForUser(
+ record = record,
+ id = userIds[userIndex],
+ hasPicture = !isGuest,
+ isCurrent = userIndex == selectedUserIndex,
+ isGuest = isGuest,
+ isSwitchToEnabled = true,
+ )
+ }
+
+ assertThat(records.size - userIds.size).isEqualTo(expectedActions.size)
+ (userIds.size until userIds.size + expectedActions.size).forEach { actionIndex ->
+ val record = records[actionIndex]
+ assertThat(record.info).isNull()
+ assertRecordForAction(
+ record = record,
+ type = expectedActions[actionIndex - userIds.size],
+ )
+ }
+ }
+
+ private fun assertRecordForUser(
+ record: UserRecord?,
+ id: Int? = null,
+ hasPicture: Boolean = false,
+ isCurrent: Boolean = false,
+ isGuest: Boolean = false,
+ isSwitchToEnabled: Boolean = false,
+ ) {
+ checkNotNull(record)
+ assertThat(record.info?.id).isEqualTo(id)
+ assertThat(record.picture != null).isEqualTo(hasPicture)
+ assertThat(record.isCurrent).isEqualTo(isCurrent)
+ assertThat(record.isGuest).isEqualTo(isGuest)
+ assertThat(record.isSwitchToEnabled).isEqualTo(isSwitchToEnabled)
+ }
+
+ private fun assertRecordForAction(
+ record: UserRecord,
+ type: UserActionModel,
+ ) {
+ assertThat(record.isGuest).isEqualTo(type == UserActionModel.ENTER_GUEST_MODE)
+ assertThat(record.isAddUser).isEqualTo(type == UserActionModel.ADD_USER)
+ assertThat(record.isAddSupervisedUser)
+ .isEqualTo(type == UserActionModel.ADD_SUPERVISED_USER)
+ }
+
+ private fun createUserInfos(
+ count: Int,
+ includeGuest: Boolean,
+ ): List<UserInfo> {
+ return (0 until count).map { index ->
+ val isGuest = includeGuest && index == count - 1
+ createUserInfo(
+ id = index,
+ name =
+ if (isGuest) {
+ "guest"
+ } else {
+ "user_$index"
+ },
+ isPrimary = !isGuest && index == 0,
+ isGuest = isGuest,
+ )
+ }
+ }
+
+ private fun createUserInfo(
+ id: Int,
+ name: String,
+ isPrimary: Boolean = false,
+ isGuest: Boolean = false,
+ ): UserInfo {
+ return UserInfo(
+ id,
+ name,
+ /* iconPath= */ "",
+ /* flags= */ if (isPrimary) {
+ UserInfo.FLAG_PRIMARY
+ } else {
+ 0
+ },
+ if (isGuest) {
+ UserManager.USER_TYPE_FULL_GUEST
+ } else {
+ UserManager.USER_TYPE_FULL_SYSTEM
+ },
+ )
+ }
+
+ companion object {
+ private val IMMEDIATE = Dispatchers.Main.immediate
+ private val ICON = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888)
+ private val GUEST_ICON: Drawable = mock()
+ private const val SUPERVISED_USER_CREATION_APP_PACKAGE = "supervisedUserCreation"
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorTest.kt
index e914e2e..8465f4f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorTest.kt
@@ -17,51 +17,61 @@
package com.android.systemui.user.domain.interactor
-import androidx.test.filters.SmallTest
+import android.app.ActivityManager
+import android.app.admin.DevicePolicyManager
+import android.os.UserManager
+import com.android.internal.logging.UiEventLogger
import com.android.systemui.SysuiTestCase
+import com.android.systemui.flags.FakeFeatureFlags
+import com.android.systemui.flags.Flags
import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
import com.android.systemui.plugins.ActivityStarter
+import com.android.systemui.statusbar.policy.DeviceProvisionedController
import com.android.systemui.statusbar.policy.UserSwitcherController
+import com.android.systemui.telephony.data.repository.FakeTelephonyRepository
+import com.android.systemui.telephony.domain.interactor.TelephonyInteractor
import com.android.systemui.user.data.repository.FakeUserRepository
-import com.android.systemui.user.shared.model.UserActionModel
-import com.android.systemui.util.mockito.any
-import com.android.systemui.util.mockito.eq
-import com.android.systemui.util.mockito.nullable
-import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.flow.launchIn
-import kotlinx.coroutines.flow.onEach
-import kotlinx.coroutines.runBlocking
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
+import kotlinx.coroutines.test.TestCoroutineScope
import org.mockito.Mock
-import org.mockito.Mockito.anyBoolean
-import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
-@SmallTest
-@RunWith(JUnit4::class)
-class UserInteractorTest : SysuiTestCase() {
+abstract class UserInteractorTest : SysuiTestCase() {
- @Mock private lateinit var controller: UserSwitcherController
- @Mock private lateinit var activityStarter: ActivityStarter
+ @Mock protected lateinit var controller: UserSwitcherController
+ @Mock protected lateinit var activityStarter: ActivityStarter
+ @Mock protected lateinit var manager: UserManager
+ @Mock protected lateinit var activityManager: ActivityManager
+ @Mock protected lateinit var deviceProvisionedController: DeviceProvisionedController
+ @Mock protected lateinit var devicePolicyManager: DevicePolicyManager
+ @Mock protected lateinit var uiEventLogger: UiEventLogger
- private lateinit var underTest: UserInteractor
+ protected lateinit var underTest: UserInteractor
- private lateinit var userRepository: FakeUserRepository
- private lateinit var keyguardRepository: FakeKeyguardRepository
+ protected lateinit var testCoroutineScope: TestCoroutineScope
+ protected lateinit var userRepository: FakeUserRepository
+ protected lateinit var keyguardRepository: FakeKeyguardRepository
+ protected lateinit var telephonyRepository: FakeTelephonyRepository
- @Before
- fun setUp() {
+ abstract fun isRefactored(): Boolean
+
+ open fun setUp() {
MockitoAnnotations.initMocks(this)
userRepository = FakeUserRepository()
keyguardRepository = FakeKeyguardRepository()
+ telephonyRepository = FakeTelephonyRepository()
+ testCoroutineScope = TestCoroutineScope()
+ val refreshUsersScheduler =
+ RefreshUsersScheduler(
+ applicationScope = testCoroutineScope,
+ mainDispatcher = IMMEDIATE,
+ repository = userRepository,
+ )
underTest =
UserInteractor(
+ applicationContext = context,
repository = userRepository,
controller = controller,
activityStarter = activityStarter,
@@ -69,142 +79,34 @@
KeyguardInteractor(
repository = keyguardRepository,
),
- )
- }
-
- @Test
- fun `actions - not actionable when locked and locked - no actions`() =
- runBlocking(IMMEDIATE) {
- userRepository.setActions(UserActionModel.values().toList())
- userRepository.setActionableWhenLocked(false)
- keyguardRepository.setKeyguardShowing(true)
-
- var actions: List<UserActionModel>? = null
- val job = underTest.actions.onEach { actions = it }.launchIn(this)
-
- assertThat(actions).isEmpty()
- job.cancel()
- }
-
- @Test
- fun `actions - not actionable when locked and not locked`() =
- runBlocking(IMMEDIATE) {
- userRepository.setActions(
- listOf(
- UserActionModel.ENTER_GUEST_MODE,
- UserActionModel.ADD_USER,
- UserActionModel.ADD_SUPERVISED_USER,
- )
- )
- userRepository.setActionableWhenLocked(false)
- keyguardRepository.setKeyguardShowing(false)
-
- var actions: List<UserActionModel>? = null
- val job = underTest.actions.onEach { actions = it }.launchIn(this)
-
- assertThat(actions)
- .isEqualTo(
- listOf(
- UserActionModel.ENTER_GUEST_MODE,
- UserActionModel.ADD_USER,
- UserActionModel.ADD_SUPERVISED_USER,
- UserActionModel.NAVIGATE_TO_USER_MANAGEMENT,
+ featureFlags =
+ FakeFeatureFlags().apply {
+ set(Flags.USER_INTERACTOR_AND_REPO_USE_CONTROLLER, !isRefactored())
+ },
+ manager = manager,
+ applicationScope = testCoroutineScope,
+ telephonyInteractor =
+ TelephonyInteractor(
+ repository = telephonyRepository,
+ ),
+ broadcastDispatcher = fakeBroadcastDispatcher,
+ backgroundDispatcher = IMMEDIATE,
+ activityManager = activityManager,
+ refreshUsersScheduler = refreshUsersScheduler,
+ guestUserInteractor =
+ GuestUserInteractor(
+ applicationContext = context,
+ applicationScope = testCoroutineScope,
+ mainDispatcher = IMMEDIATE,
+ backgroundDispatcher = IMMEDIATE,
+ manager = manager,
+ repository = userRepository,
+ deviceProvisionedController = deviceProvisionedController,
+ devicePolicyManager = devicePolicyManager,
+ refreshUsersScheduler = refreshUsersScheduler,
+ uiEventLogger = uiEventLogger,
)
- )
- job.cancel()
- }
-
- @Test
- fun `actions - actionable when locked and not locked`() =
- runBlocking(IMMEDIATE) {
- userRepository.setActions(
- listOf(
- UserActionModel.ENTER_GUEST_MODE,
- UserActionModel.ADD_USER,
- UserActionModel.ADD_SUPERVISED_USER,
- )
)
- userRepository.setActionableWhenLocked(true)
- keyguardRepository.setKeyguardShowing(false)
-
- var actions: List<UserActionModel>? = null
- val job = underTest.actions.onEach { actions = it }.launchIn(this)
-
- assertThat(actions)
- .isEqualTo(
- listOf(
- UserActionModel.ENTER_GUEST_MODE,
- UserActionModel.ADD_USER,
- UserActionModel.ADD_SUPERVISED_USER,
- UserActionModel.NAVIGATE_TO_USER_MANAGEMENT,
- )
- )
- job.cancel()
- }
-
- @Test
- fun `actions - actionable when locked and locked`() =
- runBlocking(IMMEDIATE) {
- userRepository.setActions(
- listOf(
- UserActionModel.ENTER_GUEST_MODE,
- UserActionModel.ADD_USER,
- UserActionModel.ADD_SUPERVISED_USER,
- )
- )
- userRepository.setActionableWhenLocked(true)
- keyguardRepository.setKeyguardShowing(true)
-
- var actions: List<UserActionModel>? = null
- val job = underTest.actions.onEach { actions = it }.launchIn(this)
-
- assertThat(actions)
- .isEqualTo(
- listOf(
- UserActionModel.ENTER_GUEST_MODE,
- UserActionModel.ADD_USER,
- UserActionModel.ADD_SUPERVISED_USER,
- UserActionModel.NAVIGATE_TO_USER_MANAGEMENT,
- )
- )
- job.cancel()
- }
-
- @Test
- fun selectUser() {
- val userId = 3
-
- underTest.selectUser(userId)
-
- verify(controller).onUserSelected(eq(userId), nullable())
- }
-
- @Test
- fun `executeAction - guest`() {
- underTest.executeAction(UserActionModel.ENTER_GUEST_MODE)
-
- verify(controller).createAndSwitchToGuestUser(nullable())
- }
-
- @Test
- fun `executeAction - add user`() {
- underTest.executeAction(UserActionModel.ADD_USER)
-
- verify(controller).showAddUserDialog(nullable())
- }
-
- @Test
- fun `executeAction - add supervised user`() {
- underTest.executeAction(UserActionModel.ADD_SUPERVISED_USER)
-
- verify(controller).startSupervisedUserActivity()
- }
-
- @Test
- fun `executeAction - manage users`() {
- underTest.executeAction(UserActionModel.NAVIGATE_TO_USER_MANAGEMENT)
-
- verify(activityStarter).startActivity(any(), anyBoolean())
}
companion object {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorUnrefactoredTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorUnrefactoredTest.kt
new file mode 100644
index 0000000..c3a9705
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorUnrefactoredTest.kt
@@ -0,0 +1,188 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package com.android.systemui.user.domain.interactor
+
+import androidx.test.filters.SmallTest
+import com.android.systemui.user.shared.model.UserActionModel
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.eq
+import com.android.systemui.util.mockito.nullable
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.runBlocking
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.mockito.Mockito.anyBoolean
+import org.mockito.Mockito.verify
+
+@SmallTest
+@RunWith(JUnit4::class)
+open class UserInteractorUnrefactoredTest : UserInteractorTest() {
+
+ override fun isRefactored(): Boolean {
+ return false
+ }
+
+ @Before
+ override fun setUp() {
+ super.setUp()
+ }
+
+ @Test
+ fun `actions - not actionable when locked and locked - no actions`() =
+ runBlocking(IMMEDIATE) {
+ userRepository.setActions(UserActionModel.values().toList())
+ userRepository.setActionableWhenLocked(false)
+ keyguardRepository.setKeyguardShowing(true)
+
+ var actions: List<UserActionModel>? = null
+ val job = underTest.actions.onEach { actions = it }.launchIn(this)
+
+ assertThat(actions).isEmpty()
+ job.cancel()
+ }
+
+ @Test
+ fun `actions - not actionable when locked and not locked`() =
+ runBlocking(IMMEDIATE) {
+ userRepository.setActions(
+ listOf(
+ UserActionModel.ENTER_GUEST_MODE,
+ UserActionModel.ADD_USER,
+ UserActionModel.ADD_SUPERVISED_USER,
+ )
+ )
+ userRepository.setActionableWhenLocked(false)
+ keyguardRepository.setKeyguardShowing(false)
+
+ var actions: List<UserActionModel>? = null
+ val job = underTest.actions.onEach { actions = it }.launchIn(this)
+
+ assertThat(actions)
+ .isEqualTo(
+ listOf(
+ UserActionModel.ENTER_GUEST_MODE,
+ UserActionModel.ADD_USER,
+ UserActionModel.ADD_SUPERVISED_USER,
+ UserActionModel.NAVIGATE_TO_USER_MANAGEMENT,
+ )
+ )
+ job.cancel()
+ }
+
+ @Test
+ fun `actions - actionable when locked and not locked`() =
+ runBlocking(IMMEDIATE) {
+ userRepository.setActions(
+ listOf(
+ UserActionModel.ENTER_GUEST_MODE,
+ UserActionModel.ADD_USER,
+ UserActionModel.ADD_SUPERVISED_USER,
+ )
+ )
+ userRepository.setActionableWhenLocked(true)
+ keyguardRepository.setKeyguardShowing(false)
+
+ var actions: List<UserActionModel>? = null
+ val job = underTest.actions.onEach { actions = it }.launchIn(this)
+
+ assertThat(actions)
+ .isEqualTo(
+ listOf(
+ UserActionModel.ENTER_GUEST_MODE,
+ UserActionModel.ADD_USER,
+ UserActionModel.ADD_SUPERVISED_USER,
+ UserActionModel.NAVIGATE_TO_USER_MANAGEMENT,
+ )
+ )
+ job.cancel()
+ }
+
+ @Test
+ fun `actions - actionable when locked and locked`() =
+ runBlocking(IMMEDIATE) {
+ userRepository.setActions(
+ listOf(
+ UserActionModel.ENTER_GUEST_MODE,
+ UserActionModel.ADD_USER,
+ UserActionModel.ADD_SUPERVISED_USER,
+ )
+ )
+ userRepository.setActionableWhenLocked(true)
+ keyguardRepository.setKeyguardShowing(true)
+
+ var actions: List<UserActionModel>? = null
+ val job = underTest.actions.onEach { actions = it }.launchIn(this)
+
+ assertThat(actions)
+ .isEqualTo(
+ listOf(
+ UserActionModel.ENTER_GUEST_MODE,
+ UserActionModel.ADD_USER,
+ UserActionModel.ADD_SUPERVISED_USER,
+ UserActionModel.NAVIGATE_TO_USER_MANAGEMENT,
+ )
+ )
+ job.cancel()
+ }
+
+ @Test
+ fun selectUser() {
+ val userId = 3
+
+ underTest.selectUser(userId)
+
+ verify(controller).onUserSelected(eq(userId), nullable())
+ }
+
+ @Test
+ fun `executeAction - guest`() {
+ underTest.executeAction(UserActionModel.ENTER_GUEST_MODE)
+
+ verify(controller).createAndSwitchToGuestUser(nullable())
+ }
+
+ @Test
+ fun `executeAction - add user`() {
+ underTest.executeAction(UserActionModel.ADD_USER)
+
+ verify(controller).showAddUserDialog(nullable())
+ }
+
+ @Test
+ fun `executeAction - add supervised user`() {
+ underTest.executeAction(UserActionModel.ADD_SUPERVISED_USER)
+
+ verify(controller).startSupervisedUserActivity()
+ }
+
+ @Test
+ fun `executeAction - manage users`() {
+ underTest.executeAction(UserActionModel.NAVIGATE_TO_USER_MANAGEMENT)
+
+ verify(activityStarter).startActivity(any(), anyBoolean())
+ }
+
+ companion object {
+ private val IMMEDIATE = Dispatchers.Main.immediate
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt
index ef4500d..0344e3f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt
@@ -17,17 +17,28 @@
package com.android.systemui.user.ui.viewmodel
+import android.app.ActivityManager
+import android.app.admin.DevicePolicyManager
import android.graphics.drawable.Drawable
+import android.os.UserManager
import androidx.test.filters.SmallTest
+import com.android.internal.logging.UiEventLogger
import com.android.systemui.SysuiTestCase
import com.android.systemui.common.shared.model.Text
+import com.android.systemui.flags.FakeFeatureFlags
+import com.android.systemui.flags.Flags
import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.power.data.repository.FakePowerRepository
import com.android.systemui.power.domain.interactor.PowerInteractor
+import com.android.systemui.statusbar.policy.DeviceProvisionedController
import com.android.systemui.statusbar.policy.UserSwitcherController
+import com.android.systemui.telephony.data.repository.FakeTelephonyRepository
+import com.android.systemui.telephony.domain.interactor.TelephonyInteractor
import com.android.systemui.user.data.repository.FakeUserRepository
+import com.android.systemui.user.domain.interactor.GuestUserInteractor
+import com.android.systemui.user.domain.interactor.RefreshUsersScheduler
import com.android.systemui.user.domain.interactor.UserInteractor
import com.android.systemui.user.legacyhelper.ui.LegacyUserUiHelper
import com.android.systemui.user.shared.model.UserActionModel
@@ -38,6 +49,7 @@
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.test.TestCoroutineScope
import kotlinx.coroutines.yield
import org.junit.Before
import org.junit.Test
@@ -52,6 +64,11 @@
@Mock private lateinit var controller: UserSwitcherController
@Mock private lateinit var activityStarter: ActivityStarter
+ @Mock private lateinit var activityManager: ActivityManager
+ @Mock private lateinit var manager: UserManager
+ @Mock private lateinit var deviceProvisionedController: DeviceProvisionedController
+ @Mock private lateinit var devicePolicyManager: DevicePolicyManager
+ @Mock private lateinit var uiEventLogger: UiEventLogger
private lateinit var underTest: UserSwitcherViewModel
@@ -66,22 +83,60 @@
userRepository = FakeUserRepository()
keyguardRepository = FakeKeyguardRepository()
powerRepository = FakePowerRepository()
+ val featureFlags = FakeFeatureFlags()
+ featureFlags.set(Flags.USER_INTERACTOR_AND_REPO_USE_CONTROLLER, true)
+ val scope = TestCoroutineScope()
+ val refreshUsersScheduler =
+ RefreshUsersScheduler(
+ applicationScope = scope,
+ mainDispatcher = IMMEDIATE,
+ repository = userRepository,
+ )
+ val guestUserInteractor =
+ GuestUserInteractor(
+ applicationContext = context,
+ applicationScope = scope,
+ mainDispatcher = IMMEDIATE,
+ backgroundDispatcher = IMMEDIATE,
+ manager = manager,
+ repository = userRepository,
+ deviceProvisionedController = deviceProvisionedController,
+ devicePolicyManager = devicePolicyManager,
+ refreshUsersScheduler = refreshUsersScheduler,
+ uiEventLogger = uiEventLogger,
+ )
+
underTest =
UserSwitcherViewModel.Factory(
userInteractor =
UserInteractor(
+ applicationContext = context,
repository = userRepository,
controller = controller,
activityStarter = activityStarter,
keyguardInteractor =
KeyguardInteractor(
repository = keyguardRepository,
- )
+ ),
+ featureFlags = featureFlags,
+ manager = manager,
+ applicationScope = scope,
+ telephonyInteractor =
+ TelephonyInteractor(
+ repository = FakeTelephonyRepository(),
+ ),
+ broadcastDispatcher = fakeBroadcastDispatcher,
+ backgroundDispatcher = IMMEDIATE,
+ activityManager = activityManager,
+ refreshUsersScheduler = refreshUsersScheduler,
+ guestUserInteractor = guestUserInteractor,
),
powerInteractor =
PowerInteractor(
repository = powerRepository,
),
+ featureFlags = featureFlags,
+ guestUserInteractor = guestUserInteractor,
)
.create(UserSwitcherViewModel::class.java)
}
@@ -97,6 +152,7 @@
image = USER_IMAGE,
isSelected = true,
isSelectable = true,
+ isGuest = false,
),
UserModel(
id = 1,
@@ -104,6 +160,7 @@
image = USER_IMAGE,
isSelected = false,
isSelectable = true,
+ isGuest = false,
),
UserModel(
id = 2,
@@ -111,6 +168,7 @@
image = USER_IMAGE,
isSelected = false,
isSelectable = false,
+ isGuest = false,
),
)
)
@@ -260,7 +318,7 @@
job.cancel()
}
- private fun setUsers(count: Int) {
+ private suspend fun setUsers(count: Int) {
userRepository.setUsers(
(0 until count).map { index ->
UserModel(
@@ -269,6 +327,7 @@
image = USER_IMAGE,
isSelected = index == 0,
isSelectable = true,
+ isGuest = false,
)
}
)
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/broadcast/FakeBroadcastDispatcher.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/broadcast/FakeBroadcastDispatcher.kt
index 53dcc8d..bb646f0 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/broadcast/FakeBroadcastDispatcher.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/broadcast/FakeBroadcastDispatcher.kt
@@ -37,10 +37,18 @@
dumpManager: DumpManager,
logger: BroadcastDispatcherLogger,
userTracker: UserTracker
-) : BroadcastDispatcher(
- context, looper, executor, dumpManager, logger, userTracker, PendingRemovalStore(logger)) {
+) :
+ BroadcastDispatcher(
+ context,
+ looper,
+ executor,
+ dumpManager,
+ logger,
+ userTracker,
+ PendingRemovalStore(logger)
+ ) {
- private val registeredReceivers = ArraySet<BroadcastReceiver>()
+ val registeredReceivers = ArraySet<BroadcastReceiver>()
override fun registerReceiverWithHandler(
receiver: BroadcastReceiver,
@@ -78,4 +86,4 @@
}
registeredReceivers.clear()
}
-}
\ No newline at end of file
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt
index 42b434a..725b1f4 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt
@@ -44,6 +44,10 @@
private val _dozeAmount = MutableStateFlow(0f)
override val dozeAmount: Flow<Float> = _dozeAmount
+ override fun isKeyguardShowing(): Boolean {
+ return _isKeyguardShowing.value
+ }
+
override fun setAnimateDozingTransitions(animate: Boolean) {
_animateBottomAreaDozingTransitions.tryEmit(animate)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/settings/FakeUserTracker.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/settings/FakeUserTracker.kt
index b2b1764..9726bf8 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/settings/FakeUserTracker.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/settings/FakeUserTracker.kt
@@ -26,20 +26,24 @@
/** A fake [UserTracker] to be used in tests. */
class FakeUserTracker(
- userId: Int = 0,
- userHandle: UserHandle = UserHandle.of(userId),
- userInfo: UserInfo = mock(),
- userProfiles: List<UserInfo> = emptyList(),
+ private var _userId: Int = 0,
+ private var _userHandle: UserHandle = UserHandle.of(_userId),
+ private var _userInfo: UserInfo = mock(),
+ private var _userProfiles: List<UserInfo> = emptyList(),
userContentResolver: ContentResolver = MockContentResolver(),
userContext: Context = mock(),
private val onCreateCurrentUserContext: (Context) -> Context = { mock() },
) : UserTracker {
val callbacks = mutableListOf<UserTracker.Callback>()
- override val userId: Int = userId
- override val userHandle: UserHandle = userHandle
- override val userInfo: UserInfo = userInfo
- override val userProfiles: List<UserInfo> = userProfiles
+ override val userId: Int
+ get() = _userId
+ override val userHandle: UserHandle
+ get() = _userHandle
+ override val userInfo: UserInfo
+ get() = _userInfo
+ override val userProfiles: List<UserInfo>
+ get() = _userProfiles
override val userContentResolver: ContentResolver = userContentResolver
override val userContext: Context = userContext
@@ -55,4 +59,13 @@
override fun createCurrentUserContext(context: Context): Context {
return onCreateCurrentUserContext(context)
}
+
+ fun set(userInfos: List<UserInfo>, selectedUserIndex: Int) {
+ _userProfiles = userInfos
+ _userInfo = userInfos[selectedUserIndex]
+ _userId = _userInfo.id
+ _userHandle = UserHandle.of(_userId)
+
+ callbacks.forEach { it.onUserChanged(_userId, userContext) }
+ }
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/telephony/data/repository/FakeTelephonyRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/telephony/data/repository/FakeTelephonyRepository.kt
new file mode 100644
index 0000000..59f24ef
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/telephony/data/repository/FakeTelephonyRepository.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package com.android.systemui.telephony.data.repository
+
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+
+class FakeTelephonyRepository : TelephonyRepository {
+
+ private val _callState = MutableStateFlow(0)
+ override val callState: Flow<Int> = _callState.asStateFlow()
+
+ fun setCallState(value: Int) {
+ _callState.value = value
+ }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/user/data/repository/FakeUserRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/user/data/repository/FakeUserRepository.kt
index 20f1e36..4df8aa4 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/user/data/repository/FakeUserRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/user/data/repository/FakeUserRepository.kt
@@ -17,12 +17,18 @@
package com.android.systemui.user.data.repository
+import android.content.pm.UserInfo
+import android.os.UserHandle
+import com.android.systemui.user.data.model.UserSwitcherSettingsModel
import com.android.systemui.user.shared.model.UserActionModel
import com.android.systemui.user.shared.model.UserModel
+import java.util.concurrent.atomic.AtomicBoolean
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.yield
class FakeUserRepository : UserRepository {
@@ -34,21 +40,71 @@
private val _actions = MutableStateFlow<List<UserActionModel>>(emptyList())
override val actions: Flow<List<UserActionModel>> = _actions.asStateFlow()
+ private val _userSwitcherSettings = MutableStateFlow(UserSwitcherSettingsModel())
+ override val userSwitcherSettings: Flow<UserSwitcherSettingsModel> =
+ _userSwitcherSettings.asStateFlow()
+
+ private val _userInfos = MutableStateFlow<List<UserInfo>>(emptyList())
+ override val userInfos: Flow<List<UserInfo>> = _userInfos.asStateFlow()
+
+ private val _selectedUserInfo = MutableStateFlow<UserInfo?>(null)
+ override val selectedUserInfo: Flow<UserInfo> = _selectedUserInfo.filterNotNull()
+
+ override var lastSelectedNonGuestUserId: Int = UserHandle.USER_SYSTEM
+
private val _isActionableWhenLocked = MutableStateFlow(false)
override val isActionableWhenLocked: Flow<Boolean> = _isActionableWhenLocked.asStateFlow()
private var _isGuestUserAutoCreated: Boolean = false
override val isGuestUserAutoCreated: Boolean
get() = _isGuestUserAutoCreated
- private var _isGuestUserResetting: Boolean = false
- override val isGuestUserResetting: Boolean
- get() = _isGuestUserResetting
+
+ override var isGuestUserResetting: Boolean = false
+
+ override val isGuestUserCreationScheduled = AtomicBoolean()
+
+ override var secondaryUserId: Int = UserHandle.USER_NULL
+
+ override var isRefreshUsersPaused: Boolean = false
+
+ var refreshUsersCallCount: Int = 0
+ private set
+
+ override fun refreshUsers() {
+ refreshUsersCallCount++
+ }
+
+ override fun getSelectedUserInfo(): UserInfo {
+ return checkNotNull(_selectedUserInfo.value)
+ }
+
+ override fun isSimpleUserSwitcher(): Boolean {
+ return _userSwitcherSettings.value.isSimpleUserSwitcher
+ }
+
+ fun setUserInfos(infos: List<UserInfo>) {
+ _userInfos.value = infos
+ }
+
+ suspend fun setSelectedUserInfo(userInfo: UserInfo) {
+ check(_userInfos.value.contains(userInfo)) {
+ "Cannot select the following user, it is not in the list of user infos: $userInfo!"
+ }
+
+ _selectedUserInfo.value = userInfo
+ yield()
+ }
+
+ suspend fun setSettings(settings: UserSwitcherSettingsModel) {
+ _userSwitcherSettings.value = settings
+ yield()
+ }
fun setUsers(models: List<UserModel>) {
_users.value = models
}
- fun setSelectedUser(userId: Int) {
+ suspend fun setSelectedUser(userId: Int) {
check(_users.value.find { it.id == userId } != null) {
"Cannot select a user with ID $userId - no user with that ID found!"
}
@@ -62,6 +118,7 @@
}
}
)
+ yield()
}
fun setActions(models: List<UserActionModel>) {
@@ -75,8 +132,4 @@
fun setGuestUserAutoCreated(value: Boolean) {
_isGuestUserAutoCreated = value
}
-
- fun setGuestUserResetting(value: Boolean) {
- _isGuestUserResetting = value
- }
}
diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java
index 56a5a8f..0426c79 100644
--- a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java
+++ b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java
@@ -1175,6 +1175,9 @@
}
private void updateSpecialAccessPermissionAsSystem(PackageInfo packageInfo) {
+ if (packageInfo == null) {
+ return;
+ }
if (containsEither(packageInfo.requestedPermissions,
android.Manifest.permission.RUN_IN_BACKGROUND,
android.Manifest.permission.REQUEST_COMPANION_RUN_IN_BACKGROUND)) {
diff --git a/services/companion/java/com/android/server/companion/PackageUtils.java b/services/companion/java/com/android/server/companion/PackageUtils.java
index 9bad45b..3ab4aa8 100644
--- a/services/companion/java/com/android/server/companion/PackageUtils.java
+++ b/services/companion/java/com/android/server/companion/PackageUtils.java
@@ -54,12 +54,19 @@
private static final String PROPERTY_PRIMARY_TAG =
"android.companion.PROPERTY_PRIMARY_COMPANION_DEVICE_SERVICE";
- static @Nullable PackageInfo getPackageInfo(@NonNull Context context,
+ @Nullable
+ static PackageInfo getPackageInfo(@NonNull Context context,
@UserIdInt int userId, @NonNull String packageName) {
final PackageManager pm = context.getPackageManager();
final PackageInfoFlags flags = PackageInfoFlags.of(GET_PERMISSIONS | GET_CONFIGURATIONS);
- return Binder.withCleanCallingIdentity(() ->
- pm.getPackageInfoAsUser(packageName, flags , userId));
+ return Binder.withCleanCallingIdentity(() -> {
+ try {
+ return pm.getPackageInfoAsUser(packageName, flags, userId);
+ } catch (PackageManager.NameNotFoundException e) {
+ Slog.e(TAG, "Package [" + packageName + "] is not found.");
+ return null;
+ }
+ });
}
static void enforceUsesCompanionDeviceFeature(@NonNull Context context,
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 4204162..5f27f59 100644
--- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
+++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
@@ -95,6 +95,7 @@
private final AssociationInfo mAssociationInfo;
private final PendingTrampolineCallback mPendingTrampolineCallback;
private final int mOwnerUid;
+ private final int mDeviceId;
private final InputController mInputController;
private VirtualAudioController mVirtualAudioController;
@VisibleForTesting
@@ -140,19 +141,40 @@
private final SparseArray<GenericWindowPolicyController> mWindowPolicyControllers =
new SparseArray<>();
- VirtualDeviceImpl(Context context, AssociationInfo associationInfo,
- IBinder token, int ownerUid, OnDeviceCloseListener listener,
+ VirtualDeviceImpl(
+ Context context,
+ AssociationInfo associationInfo,
+ IBinder token,
+ int ownerUid,
+ int deviceId,
+ OnDeviceCloseListener listener,
PendingTrampolineCallback pendingTrampolineCallback,
IVirtualDeviceActivityListener activityListener,
Consumer<ArraySet<Integer>> runningAppsChangedCallback,
VirtualDeviceParams params) {
- this(context, associationInfo, token, ownerUid, /* inputController= */ null, listener,
- pendingTrampolineCallback, activityListener, runningAppsChangedCallback, params);
+ this(
+ context,
+ associationInfo,
+ token,
+ ownerUid,
+ deviceId,
+ /* inputController= */ null,
+ listener,
+ pendingTrampolineCallback,
+ activityListener,
+ runningAppsChangedCallback,
+ params);
}
@VisibleForTesting
- VirtualDeviceImpl(Context context, AssociationInfo associationInfo, IBinder token,
- int ownerUid, InputController inputController, OnDeviceCloseListener listener,
+ VirtualDeviceImpl(
+ Context context,
+ AssociationInfo associationInfo,
+ IBinder token,
+ int ownerUid,
+ int deviceId,
+ InputController inputController,
+ OnDeviceCloseListener listener,
PendingTrampolineCallback pendingTrampolineCallback,
IVirtualDeviceActivityListener activityListener,
Consumer<ArraySet<Integer>> runningAppsChangedCallback,
@@ -164,6 +186,7 @@
mActivityListener = activityListener;
mRunningAppsChangedCallback = runningAppsChangedCallback;
mOwnerUid = ownerUid;
+ mDeviceId = deviceId;
mAppToken = token;
mParams = params;
if (inputController == null) {
@@ -199,6 +222,12 @@
return mAssociationInfo.getDisplayName();
}
+ /** Returns the unique device ID of this device. */
+ @Override // Binder call
+ public int getDeviceId() {
+ return mDeviceId;
+ }
+
@Override // Binder call
public int getAssociationId() {
return mAssociationInfo.getId();
diff --git a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java
index 2b644fe..06dfeab 100644
--- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java
+++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java
@@ -60,12 +60,12 @@
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicInteger;
@SuppressLint("LongLogTag")
public class VirtualDeviceManagerService extends SystemService {
- private static final boolean DEBUG = false;
private static final String TAG = "VirtualDeviceManagerService";
private final Object mVirtualDeviceManagerLock = new Object();
@@ -73,6 +73,10 @@
private final VirtualDeviceManagerInternal mLocalService;
private final Handler mHandler = new Handler(Looper.getMainLooper());
private final PendingTrampolineMap mPendingTrampolines = new PendingTrampolineMap(mHandler);
+
+ private static AtomicInteger sNextUniqueIndex = new AtomicInteger(
+ VirtualDeviceManager.DEFAULT_DEVICE_ID + 1);
+
/**
* Mapping from user IDs to CameraAccessControllers.
*/
@@ -260,8 +264,10 @@
final int userId = UserHandle.getUserId(callingUid);
final CameraAccessController cameraAccessController =
mCameraAccessControllers.get(userId);
+ final int uniqueId = sNextUniqueIndex.getAndIncrement();
+
VirtualDeviceImpl virtualDevice = new VirtualDeviceImpl(getContext(),
- associationInfo, token, callingUid,
+ associationInfo, token, callingUid, uniqueId,
new VirtualDeviceImpl.OnDeviceCloseListener() {
@Override
public void onClose(int associationId) {
diff --git a/services/core/java/com/android/server/StorageManagerService.java b/services/core/java/com/android/server/StorageManagerService.java
index 5b1f740..83d527e 100644
--- a/services/core/java/com/android/server/StorageManagerService.java
+++ b/services/core/java/com/android/server/StorageManagerService.java
@@ -147,7 +147,6 @@
import com.android.internal.util.HexDump;
import com.android.internal.util.IndentingPrintWriter;
import com.android.internal.util.Preconditions;
-import com.android.internal.widget.LockPatternUtils;
import com.android.server.pm.Installer;
import com.android.server.pm.UserManagerInternal;
import com.android.server.storage.AppFuseBridge;
@@ -557,7 +556,6 @@
private IAppOpsService mIAppOpsService;
private final Callbacks mCallbacks;
- private final LockPatternUtils mLockPatternUtils;
private static final String ANR_DELAY_MILLIS_DEVICE_CONFIG_KEY =
"anr_delay_millis";
@@ -1808,7 +1806,6 @@
ANDROID_VOLD_APP_DATA_ISOLATION_ENABLED_PROPERTY, false);
mContext = context;
mCallbacks = new Callbacks(FgThread.get().getLooper());
- mLockPatternUtils = new LockPatternUtils(mContext);
HandlerThread hthread = new HandlerThread(TAG);
hthread.start();
@@ -3069,96 +3066,21 @@
}
}
- private String encodeBytes(byte[] bytes) {
- if (ArrayUtils.isEmpty(bytes)) {
- return "!";
- } else {
- return HexDump.toHexString(bytes);
- }
- }
-
+ /* Only for use by LockSettingsService */
@android.annotation.EnforcePermission(android.Manifest.permission.STORAGE_INTERNAL)
- /*
- * Add this secret to the set of ways we can recover a user's disk
- * encryption key. Changing the secret for a disk encryption key is done in
- * two phases. First, this method is called to add the new secret binding.
- * Second, fixateNewestUserKeyAuth is called to delete all other bindings.
- * This allows other places where a credential is used, such as Gatekeeper,
- * to be updated between the two calls.
- */
@Override
- public void addUserKeyAuth(int userId, int serialNumber, byte[] secret) {
-
- try {
- mVold.addUserKeyAuth(userId, serialNumber, encodeBytes(secret));
- } catch (Exception e) {
- Slog.wtf(TAG, e);
- }
+ public void setUserKeyProtection(@UserIdInt int userId, byte[] secret) throws RemoteException {
+ mVold.setUserKeyProtection(userId, HexDump.toHexString(secret));
}
+ /* Only for use by LockSettingsService */
@android.annotation.EnforcePermission(android.Manifest.permission.STORAGE_INTERNAL)
- /*
- * Store a user's disk encryption key without secret binding. Removing the
- * secret for a disk encryption key is done in two phases. First, this
- * method is called to retrieve the key using the provided secret and store
- * it encrypted with a keystore key not bound to the user. Second,
- * fixateNewestUserKeyAuth is called to delete the key's other bindings.
- */
@Override
- public void clearUserKeyAuth(int userId, int serialNumber, byte[] secret) {
-
- try {
- mVold.clearUserKeyAuth(userId, serialNumber, encodeBytes(secret));
- } catch (Exception e) {
- Slog.wtf(TAG, e);
+ public void unlockUserKey(@UserIdInt int userId, int serialNumber, byte[] secret)
+ throws RemoteException {
+ if (StorageManager.isFileEncrypted()) {
+ mVold.unlockUserKey(userId, serialNumber, HexDump.toHexString(secret));
}
- }
-
- @android.annotation.EnforcePermission(android.Manifest.permission.STORAGE_INTERNAL)
- /*
- * Delete all bindings of a user's disk encryption key except the most
- * recently added one.
- */
- @Override
- public void fixateNewestUserKeyAuth(int userId) {
-
- try {
- mVold.fixateNewestUserKeyAuth(userId);
- } catch (Exception e) {
- Slog.wtf(TAG, e);
- }
- }
-
- @Override
- public void unlockUserKey(int userId, int serialNumber, byte[] secret) {
- boolean isFileEncrypted = StorageManager.isFileEncrypted();
- Slog.d(TAG, "unlockUserKey: " + userId
- + " isFileEncrypted: " + isFileEncrypted
- + " hasSecret: " + (secret != null));
- enforcePermission(android.Manifest.permission.STORAGE_INTERNAL);
-
- if (isUserKeyUnlocked(userId)) {
- Slog.d(TAG, "User " + userId + "'s CE storage is already unlocked");
- return;
- }
-
- if (isFileEncrypted) {
- // When a user has a secure lock screen, a secret is required to
- // unlock the key, so don't bother trying to unlock it without one.
- // This prevents misleading error messages from being logged.
- if (mLockPatternUtils.isSecure(userId) && ArrayUtils.isEmpty(secret)) {
- Slog.d(TAG, "Not unlocking user " + userId
- + "'s CE storage yet because a secret is needed");
- return;
- }
- try {
- mVold.unlockUserKey(userId, serialNumber, encodeBytes(secret));
- } catch (Exception e) {
- Slog.wtf(TAG, e);
- return;
- }
- }
-
synchronized (mLock) {
mLocalUnlockedUsers.append(userId);
}
diff --git a/services/core/java/com/android/server/UiModeManagerService.java b/services/core/java/com/android/server/UiModeManagerService.java
index e81bab1..202f4775 100644
--- a/services/core/java/com/android/server/UiModeManagerService.java
+++ b/services/core/java/com/android/server/UiModeManagerService.java
@@ -1831,7 +1831,7 @@
if (category != null && !dockAppStarted && (mStartDreamImmediatelyOnDock
|| mWindowManager.isKeyguardShowingAndNotOccluded()
|| !mPowerManager.isInteractive())) {
- Sandman.startDreamWhenDockedIfAppropriate(getContext());
+ mInjector.startDreamWhenDockedIfAppropriate(getContext());
}
}
@@ -2148,5 +2148,9 @@
public int getCallingUid() {
return Binder.getCallingUid();
}
+
+ public void startDreamWhenDockedIfAppropriate(Context context) {
+ Sandman.startDreamWhenDockedIfAppropriate(context);
+ }
}
}
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index 3130d2e..34e3eec 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -3528,7 +3528,7 @@
if (subject != null || criticalEventSection != null) {
appendtoANRFile(tracesFile.getAbsolutePath(),
(subject != null ? "Subject: " + subject + "\n\n" : "")
- + criticalEventSection != null ? criticalEventSection : "");
+ + (criticalEventSection != null ? criticalEventSection : ""));
}
Pair<Long, Long> offsets = dumpStackTraces(
@@ -6036,6 +6036,7 @@
* This can be called with or without the global lock held.
*/
@Override
+ @PackageManager.PermissionResult
@PermissionMethod
public int checkPermission(@PermissionName String permission, int pid, int uid) {
if (permission == null) {
@@ -6048,6 +6049,7 @@
* Binder IPC calls go through the public entry point.
* This can be called with or without the global lock held.
*/
+ @PackageManager.PermissionResult
@PermissionMethod
int checkCallingPermission(@PermissionName String permission) {
return checkPermission(permission,
@@ -16401,23 +16403,17 @@
return mInjector.getSecondaryDisplayIdsForStartingBackgroundUsers();
}
- /**
- * Unlocks the given user.
- *
- * @param userId The ID of the user to unlock.
- * @param token No longer used. (This parameter cannot be removed because
- * this method is marked with UnsupportedAppUsage, so its
- * signature might not be safe to change.)
- * @param secret The secret needed to unlock the user's credential-encrypted
- * storage, or null if no secret is needed.
- * @param listener An optional progress listener.
- *
- * @return true if the user was successfully unlocked, otherwise false.
- */
+ /** @deprecated see the AIDL documentation {@inheritDoc} */
@Override
- public boolean unlockUser(int userId, @Nullable byte[] token, @Nullable byte[] secret,
- @Nullable IProgressListener listener) {
- return mUserController.unlockUser(userId, secret, listener);
+ @Deprecated
+ public boolean unlockUser(@UserIdInt int userId, @Nullable byte[] token,
+ @Nullable byte[] secret, @Nullable IProgressListener listener) {
+ return mUserController.unlockUser(userId, listener);
+ }
+
+ @Override
+ public boolean unlockUser2(@UserIdInt int userId, @Nullable IProgressListener listener) {
+ return mUserController.unlockUser(userId, listener);
}
@Override
diff --git a/services/core/java/com/android/server/am/ActivityManagerShellCommand.java b/services/core/java/com/android/server/am/ActivityManagerShellCommand.java
index 10e2aae..e4f947d 100644
--- a/services/core/java/com/android/server/am/ActivityManagerShellCommand.java
+++ b/services/core/java/com/android/server/am/ActivityManagerShellCommand.java
@@ -2119,7 +2119,7 @@
return -1;
}
- boolean success = mInterface.unlockUser(userId, null, null, null);
+ boolean success = mInterface.unlockUser2(userId, null);
if (success) {
pw.println("Success: user unlocked");
} else {
diff --git a/services/core/java/com/android/server/am/CachedAppOptimizer.java b/services/core/java/com/android/server/am/CachedAppOptimizer.java
index 4574302..cbf0aae 100644
--- a/services/core/java/com/android/server/am/CachedAppOptimizer.java
+++ b/services/core/java/com/android/server/am/CachedAppOptimizer.java
@@ -112,6 +112,8 @@
private static final String ATRACE_COMPACTION_TRACK = "Compaction";
private static final String ATRACE_FREEZER_TRACK = "Freezer";
+ private static final int FREEZE_BINDER_TIMEOUT_MS = 100;
+
// Defaults for phenotype flags.
@VisibleForTesting static final Boolean DEFAULT_USE_COMPACTION = false;
@VisibleForTesting static final Boolean DEFAULT_USE_FREEZER = true;
@@ -929,11 +931,13 @@
* @param pid the target pid for which binder transactions are to be frozen
* @param freeze specifies whether to flush transactions and then freeze (true) or unfreeze
* binder for the specificed pid.
+ * @param timeoutMs the timeout in milliseconds to wait for the binder interface to freeze
+ * before giving up.
*
* @throws RuntimeException in case a flush/freeze operation could not complete successfully.
* @return 0 if success, or -EAGAIN indicating there's pending transaction.
*/
- private static native int freezeBinder(int pid, boolean freeze);
+ public static native int freezeBinder(int pid, boolean freeze, int timeoutMs);
/**
* Retrieves binder freeze info about a process.
@@ -1300,7 +1304,7 @@
long freezeTime = opt.getFreezeUnfreezeTime();
try {
- freezeBinder(pid, false);
+ freezeBinder(pid, false, FREEZE_BINDER_TIMEOUT_MS);
} catch (RuntimeException e) {
Slog.e(TAG_AM, "Unable to unfreeze binder for " + pid + " " + app.processName
+ ". Killing it");
@@ -1355,7 +1359,7 @@
}
Slog.d(TAG_AM, "quick sync unfreeze " + pid);
try {
- freezeBinder(pid, false);
+ freezeBinder(pid, false, FREEZE_BINDER_TIMEOUT_MS);
} catch (RuntimeException e) {
Slog.e(TAG_AM, "Unable to quick unfreeze binder for " + pid);
return;
@@ -1950,7 +1954,7 @@
// Freeze binder interface before the process, to flush any
// transactions that might be pending.
try {
- if (freezeBinder(pid, true) != 0) {
+ if (freezeBinder(pid, true, FREEZE_BINDER_TIMEOUT_MS) != 0) {
rescheduleFreeze(proc, "outstanding txns");
return;
}
diff --git a/services/core/java/com/android/server/am/ProcessList.java b/services/core/java/com/android/server/am/ProcessList.java
index d7b3848..42bfc4c 100644
--- a/services/core/java/com/android/server/am/ProcessList.java
+++ b/services/core/java/com/android/server/am/ProcessList.java
@@ -32,6 +32,7 @@
import static android.os.Process.getTotalMemory;
import static android.os.Process.killProcessQuiet;
import static android.os.Process.startWebView;
+import static android.system.OsConstants.*;
import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_LRU;
import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_NETWORK;
@@ -2711,6 +2712,50 @@
}
}
+ private static boolean freezePackageCgroup(int packageUID, boolean freeze) {
+ try {
+ Process.freezeCgroupUid(packageUID, freeze);
+ } catch (RuntimeException e) {
+ final String logtxt = freeze ? "freeze" : "unfreeze";
+ Slog.e(TAG, "Unable to " + logtxt + " cgroup uid: " + packageUID + ": " + e);
+ return false;
+ }
+ return true;
+ }
+
+ private static void freezeBinderAndPackageCgroup(ArrayList<Pair<ProcessRecord, Boolean>> procs,
+ int packageUID) {
+ // Freeze all binder processes under the target UID (whose cgroup is about to be frozen).
+ // Since we're going to kill these, we don't need to unfreze them later.
+ // The procs list may not include all processes under the UID cgroup, but unincluded
+ // processes (forks) should not be Binder users.
+ int N = procs.size();
+ for (int i = 0; i < N; i++) {
+ final int uid = procs.get(i).first.uid;
+ final int pid = procs.get(i).first.getPid();
+ int nRetries = 0;
+ // We only freeze the cgroup of the target package, so we do not need to freeze the
+ // Binder interfaces of dependant processes in other UIDs.
+ if (pid > 0 && uid == packageUID) {
+ try {
+ int rc;
+ do {
+ rc = CachedAppOptimizer.freezeBinder(pid, true, 10 /* timeout_ms */);
+ } while (rc == -EAGAIN && nRetries++ < 1);
+ if (rc != 0) Slog.e(TAG, "Unable to freeze binder for " + pid + ": " + rc);
+ } catch (RuntimeException e) {
+ Slog.e(TAG, "Unable to freeze binder for " + pid + ": " + e);
+ }
+ }
+ }
+
+ // We freeze the entire UID (parent) cgroup so that newly-specialized processes also freeze
+ // despite being added to a new child cgroup. The cgroups of package dependant processes are
+ // not frozen, since it's possible this would freeze processes with no dependency on the
+ // package being killed here.
+ freezePackageCgroup(packageUID, true);
+ }
+
@GuardedBy({"mService", "mProcLock"})
boolean killPackageProcessesLSP(String packageName, int appId,
int userId, int minOomAdj, boolean callerWillRestart, boolean allowRestart,
@@ -2763,7 +2808,7 @@
boolean shouldAllowRestart = false;
// If no package is specified, we call all processes under the
- // give user id.
+ // given user id.
if (packageName == null) {
if (userId != UserHandle.USER_ALL && app.userId != userId) {
continue;
@@ -2806,14 +2851,24 @@
}
}
+ final int packageUID = UserHandle.getUid(userId, appId);
+ final boolean doFreeze = appId >= Process.FIRST_APPLICATION_UID
+ && appId <= Process.LAST_APPLICATION_UID;
+ if (doFreeze) {
+ freezeBinderAndPackageCgroup(procs, packageUID);
+ }
+
int N = procs.size();
for (int i=0; i<N; i++) {
final Pair<ProcessRecord, Boolean> proc = procs.get(i);
removeProcessLocked(proc.first, callerWillRestart, allowRestart || proc.second,
- reasonCode, subReason, reason);
+ reasonCode, subReason, reason, !doFreeze /* async */);
}
killAppZygotesLocked(packageName, appId, userId, false /* force */);
mService.updateOomAdjLocked(OomAdjuster.OOM_ADJ_REASON_PROCESS_END);
+ if (doFreeze) {
+ freezePackageCgroup(packageUID, false);
+ }
return N > 0;
}
@@ -2821,12 +2876,19 @@
boolean removeProcessLocked(ProcessRecord app,
boolean callerWillRestart, boolean allowRestart, int reasonCode, String reason) {
return removeProcessLocked(app, callerWillRestart, allowRestart, reasonCode,
- ApplicationExitInfo.SUBREASON_UNKNOWN, reason);
+ ApplicationExitInfo.SUBREASON_UNKNOWN, reason, true);
}
@GuardedBy("mService")
boolean removeProcessLocked(ProcessRecord app, boolean callerWillRestart,
boolean allowRestart, int reasonCode, int subReason, String reason) {
+ return removeProcessLocked(app, callerWillRestart, allowRestart, reasonCode, subReason,
+ reason, true);
+ }
+
+ @GuardedBy("mService")
+ boolean removeProcessLocked(ProcessRecord app, boolean callerWillRestart,
+ boolean allowRestart, int reasonCode, int subReason, String reason, boolean async) {
final String name = app.processName;
final int uid = app.uid;
if (DEBUG_PROCESSES) Slog.d(TAG_PROCESSES,
@@ -2863,7 +2925,7 @@
needRestart = true;
}
}
- app.killLocked(reason, reasonCode, subReason, true);
+ app.killLocked(reason, reasonCode, subReason, true, async);
mService.handleAppDiedLocked(app, pid, willRestart, allowRestart,
false /* fromBinderDied */);
if (willRestart) {
diff --git a/services/core/java/com/android/server/am/ProcessRecord.java b/services/core/java/com/android/server/am/ProcessRecord.java
index 482e6a7..cc9071a 100644
--- a/services/core/java/com/android/server/am/ProcessRecord.java
+++ b/services/core/java/com/android/server/am/ProcessRecord.java
@@ -1056,18 +1056,30 @@
@GuardedBy("mService")
void killLocked(String reason, @Reason int reasonCode, boolean noisy) {
- killLocked(reason, reasonCode, ApplicationExitInfo.SUBREASON_UNKNOWN, noisy);
+ killLocked(reason, reasonCode, ApplicationExitInfo.SUBREASON_UNKNOWN, noisy, true);
}
@GuardedBy("mService")
void killLocked(String reason, @Reason int reasonCode, @SubReason int subReason,
boolean noisy) {
- killLocked(reason, reason, reasonCode, subReason, noisy);
+ killLocked(reason, reason, reasonCode, subReason, noisy, true);
}
@GuardedBy("mService")
void killLocked(String reason, String description, @Reason int reasonCode,
@SubReason int subReason, boolean noisy) {
+ killLocked(reason, description, reasonCode, subReason, noisy, true);
+ }
+
+ @GuardedBy("mService")
+ void killLocked(String reason, @Reason int reasonCode, @SubReason int subReason,
+ boolean noisy, boolean asyncKPG) {
+ killLocked(reason, reason, reasonCode, subReason, noisy, asyncKPG);
+ }
+
+ @GuardedBy("mService")
+ void killLocked(String reason, String description, @Reason int reasonCode,
+ @SubReason int subReason, boolean noisy, boolean asyncKPG) {
if (!mKilledByAm) {
Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "kill");
if (reasonCode == ApplicationExitInfo.REASON_ANR
@@ -1084,7 +1096,8 @@
EventLog.writeEvent(EventLogTags.AM_KILL,
userId, mPid, processName, mState.getSetAdj(), reason);
Process.killProcessQuiet(mPid);
- ProcessList.killProcessGroup(uid, mPid);
+ if (asyncKPG) ProcessList.killProcessGroup(uid, mPid);
+ else Process.killProcessGroup(uid, mPid);
} else {
mPendingStart = false;
}
diff --git a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
index 470de8c..f16347f 100644
--- a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
+++ b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
@@ -90,6 +90,7 @@
DeviceConfig.NAMESPACE_NETD_NATIVE,
DeviceConfig.NAMESPACE_NNAPI_NATIVE,
DeviceConfig.NAMESPACE_PROFCOLLECT_NATIVE_BOOT,
+ DeviceConfig.NAMESPACE_REMOTE_KEY_PROVISIONING_NATIVE,
DeviceConfig.NAMESPACE_RUNTIME_NATIVE,
DeviceConfig.NAMESPACE_RUNTIME_NATIVE_BOOT,
DeviceConfig.NAMESPACE_STATSD_NATIVE,
diff --git a/services/core/java/com/android/server/am/UserController.java b/services/core/java/com/android/server/am/UserController.java
index 82fb1e8..226c638 100644
--- a/services/core/java/com/android/server/am/UserController.java
+++ b/services/core/java/com/android/server/am/UserController.java
@@ -654,7 +654,7 @@
EventLog.writeEvent(EventLogTags.UC_FINISH_USER_UNLOCKING, userId);
logUserLifecycleEvent(userId, USER_LIFECYCLE_EVENT_UNLOCKING_USER,
USER_LIFECYCLE_EVENT_STATE_BEGIN);
- // Only keep marching forward if user is actually unlocked
+ // If the user key hasn't been unlocked yet, we cannot proceed.
if (!StorageManager.isUserKeyUnlocked(userId)) return false;
synchronized (mLock) {
// Do not proceed if unexpected state or a stale user
@@ -1776,28 +1776,19 @@
}
}
- boolean unlockUser(final @UserIdInt int userId, byte[] secret, IProgressListener listener) {
+ boolean unlockUser(@UserIdInt int userId, @Nullable IProgressListener listener) {
checkCallingPermission(INTERACT_ACROSS_USERS_FULL, "unlockUser");
EventLog.writeEvent(EventLogTags.UC_UNLOCK_USER, userId);
final long binderToken = Binder.clearCallingIdentity();
try {
- return unlockUserCleared(userId, secret, listener);
+ return maybeUnlockUser(userId, listener);
} finally {
Binder.restoreCallingIdentity(binderToken);
}
}
- /**
- * Attempt to unlock user without a secret. This typically succeeds when the
- * device doesn't have credential-encrypted storage, or when the
- * credential-encrypted storage isn't tied to a user-provided PIN or
- * pattern.
- */
- private boolean maybeUnlockUser(final @UserIdInt int userId) {
- return unlockUserCleared(userId, null, null);
- }
-
- private static void notifyFinished(@UserIdInt int userId, IProgressListener listener) {
+ private static void notifyFinished(@UserIdInt int userId,
+ @Nullable IProgressListener listener) {
if (listener == null) return;
try {
listener.onFinished(userId, null);
@@ -1805,8 +1796,18 @@
}
}
- private boolean unlockUserCleared(final @UserIdInt int userId, byte[] secret,
- IProgressListener listener) {
+ private boolean maybeUnlockUser(@UserIdInt int userId) {
+ return maybeUnlockUser(userId, null);
+ }
+
+ /**
+ * Tries to unlock the given user.
+ * <p>
+ * This will succeed only if the user's CE storage key is already unlocked or if the user
+ * doesn't have a lockscreen credential set.
+ */
+ private boolean maybeUnlockUser(@UserIdInt int userId, @Nullable IProgressListener listener) {
+
// Delay user unlocking for headless system user mode until the system boot
// completes. When the system boot completes, the {@link #onBootCompleted()}
// method unlocks all started users for headless system user mode. This is done
@@ -1825,14 +1826,8 @@
UserState uss;
if (!StorageManager.isUserKeyUnlocked(userId)) {
- final UserInfo userInfo = getUserInfo(userId);
- final IStorageManager storageManager = mInjector.getStorageManager();
- try {
- // We always want to unlock user storage, even user is not started yet
- storageManager.unlockUserKey(userId, userInfo.serialNumber, secret);
- } catch (RemoteException | RuntimeException e) {
- Slogf.w(TAG, "Failed to unlock: " + e.getMessage());
- }
+ // We always want to try to unlock the user key, even if the user is not started yet.
+ mLockPatternUtils.unlockUserKeyIfUnsecured(userId);
}
synchronized (mLock) {
// Register the given listener to watch for unlock progress
diff --git a/services/core/java/com/android/server/biometrics/sensors/face/FaceService.java b/services/core/java/com/android/server/biometrics/sensors/face/FaceService.java
index 85ec83c..271bce9 100644
--- a/services/core/java/com/android/server/biometrics/sensors/face/FaceService.java
+++ b/services/core/java/com/android/server/biometrics/sensors/face/FaceService.java
@@ -588,9 +588,8 @@
}
try {
final SensorProps[] props = face.getSensorProps();
- final FaceProvider provider = new FaceProvider(getContext(),
- mBiometricStateCallback, props, instance, mLockoutResetDispatcher,
- BiometricContext.getInstance(getContext()));
+ final FaceProvider provider = new FaceProvider(getContext(), props, instance,
+ mLockoutResetDispatcher, BiometricContext.getInstance(getContext()));
providers.add(provider);
} catch (RemoteException e) {
Slog.e(TAG, "Remote exception in getSensorProps: " + fqName);
@@ -601,14 +600,14 @@
}
@android.annotation.EnforcePermission(android.Manifest.permission.USE_BIOMETRIC_INTERNAL)
+ @Override // Binder call
public void registerAuthenticators(
@NonNull List<FaceSensorPropertiesInternal> hidlSensors) {
mRegistry.registerAll(() -> {
final List<ServiceProvider> providers = new ArrayList<>();
for (FaceSensorPropertiesInternal hidlSensor : hidlSensors) {
providers.add(
- Face10.newInstance(getContext(), mBiometricStateCallback,
- hidlSensor, mLockoutResetDispatcher));
+ Face10.newInstance(getContext(), hidlSensor, mLockoutResetDispatcher));
}
providers.addAll(getAidlProviders());
return providers;
diff --git a/services/core/java/com/android/server/biometrics/sensors/face/aidl/BiometricTestSessionImpl.java b/services/core/java/com/android/server/biometrics/sensors/face/aidl/BiometricTestSessionImpl.java
index 43b5896..73c272f 100644
--- a/services/core/java/com/android/server/biometrics/sensors/face/aidl/BiometricTestSessionImpl.java
+++ b/services/core/java/com/android/server/biometrics/sensors/face/aidl/BiometricTestSessionImpl.java
@@ -16,6 +16,8 @@
package com.android.server.biometrics.sensors.face.aidl;
+import static android.Manifest.permission.TEST_BIOMETRIC;
+
import android.annotation.NonNull;
import android.content.Context;
import android.hardware.biometrics.ITestSession;
@@ -31,8 +33,8 @@
import android.util.Slog;
import com.android.server.biometrics.HardwareAuthTokenUtils;
+import com.android.server.biometrics.Utils;
import com.android.server.biometrics.sensors.BaseClientMonitor;
-import com.android.server.biometrics.sensors.BiometricStateCallback;
import com.android.server.biometrics.sensors.ClientMonitorCallback;
import com.android.server.biometrics.sensors.face.FaceUtils;
diff --git a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceProvider.java b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceProvider.java
index 2d7b2e6..6bff179 100644
--- a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceProvider.java
+++ b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceProvider.java
@@ -52,10 +52,8 @@
import com.android.server.biometrics.log.BiometricLogger;
import com.android.server.biometrics.sensors.AuthenticationClient;
import com.android.server.biometrics.sensors.BaseClientMonitor;
-import com.android.server.biometrics.sensors.BiometricStateCallback;
import com.android.server.biometrics.sensors.ClientMonitorCallback;
import com.android.server.biometrics.sensors.ClientMonitorCallbackConverter;
-import com.android.server.biometrics.sensors.ClientMonitorCompositeCallback;
import com.android.server.biometrics.sensors.InvalidationRequesterClient;
import com.android.server.biometrics.sensors.LockoutResetDispatcher;
import com.android.server.biometrics.sensors.PerformanceTracker;
@@ -82,7 +80,6 @@
private boolean mTestHalEnabled;
@NonNull private final Context mContext;
- @NonNull private final BiometricStateCallback mBiometricStateCallback;
@NonNull private final String mHalInstanceName;
@NonNull @VisibleForTesting
final SparseArray<Sensor> mSensors; // Map of sensors that this HAL supports
@@ -124,14 +121,11 @@
}
}
- public FaceProvider(@NonNull Context context,
- @NonNull BiometricStateCallback biometricStateCallback,
- @NonNull SensorProps[] props,
+ public FaceProvider(@NonNull Context context, @NonNull SensorProps[] props,
@NonNull String halInstanceName,
@NonNull LockoutResetDispatcher lockoutResetDispatcher,
@NonNull BiometricContext biometricContext) {
mContext = context;
- mBiometricStateCallback = biometricStateCallback;
mHalInstanceName = halInstanceName;
mSensors = new SparseArray<>();
mHandler = new Handler(Looper.getMainLooper());
@@ -370,19 +364,8 @@
mBiometricContext, maxTemplatesPerUser, debugConsent);
scheduleForSensor(sensorId, client, new ClientMonitorCallback() {
@Override
- public void onClientStarted(@NonNull BaseClientMonitor clientMonitor) {
- mBiometricStateCallback.onClientStarted(clientMonitor);
- }
-
- @Override
- public void onBiometricAction(int action) {
- mBiometricStateCallback.onBiometricAction(action);
- }
-
- @Override
public void onClientFinished(@NonNull BaseClientMonitor clientMonitor,
boolean success) {
- mBiometricStateCallback.onClientFinished(clientMonitor, success);
if (success) {
scheduleLoadAuthenticatorIdsForUser(sensorId, userId);
scheduleInvalidationRequest(sensorId, userId);
@@ -412,7 +395,7 @@
token, id, callback, userId, opPackageName, sensorId,
createLogger(BiometricsProtoEnums.ACTION_AUTHENTICATE, statsClient),
mBiometricContext, isStrongBiometric);
- scheduleForSensor(sensorId, client, mBiometricStateCallback);
+ scheduleForSensor(sensorId, client);
});
return id;
@@ -439,7 +422,7 @@
mBiometricContext, isStrongBiometric,
mUsageStats, mSensors.get(sensorId).getLockoutCache(),
allowBackgroundAuthentication, isKeyguardBypassEnabled);
- scheduleForSensor(sensorId, client, mBiometricStateCallback);
+ scheduleForSensor(sensorId, client);
});
}
@@ -494,7 +477,7 @@
BiometricsProtoEnums.CLIENT_UNKNOWN),
mBiometricContext,
mSensors.get(sensorId).getAuthenticatorIds());
- scheduleForSensor(sensorId, client, mBiometricStateCallback);
+ scheduleForSensor(sensorId, client);
});
}
@@ -582,8 +565,7 @@
if (favorHalEnrollments) {
client.setFavorHalEnrollments();
}
- scheduleForSensor(sensorId, client, new ClientMonitorCompositeCallback(callback,
- mBiometricStateCallback));
+ scheduleForSensor(sensorId, client, callback);
});
}
diff --git a/services/core/java/com/android/server/biometrics/sensors/face/aidl/Sensor.java b/services/core/java/com/android/server/biometrics/sensors/face/aidl/Sensor.java
index f1c6512..800d4b8 100644
--- a/services/core/java/com/android/server/biometrics/sensors/face/aidl/Sensor.java
+++ b/services/core/java/com/android/server/biometrics/sensors/face/aidl/Sensor.java
@@ -57,7 +57,6 @@
import com.android.server.biometrics.sensors.AuthenticationConsumer;
import com.android.server.biometrics.sensors.BaseClientMonitor;
import com.android.server.biometrics.sensors.BiometricScheduler;
-import com.android.server.biometrics.sensors.BiometricStateCallback;
import com.android.server.biometrics.sensors.EnumerateConsumer;
import com.android.server.biometrics.sensors.ErrorConsumer;
import com.android.server.biometrics.sensors.Interruptable;
diff --git a/services/core/java/com/android/server/biometrics/sensors/face/hidl/BiometricTestSessionImpl.java b/services/core/java/com/android/server/biometrics/sensors/face/hidl/BiometricTestSessionImpl.java
index 698d064..14af216 100644
--- a/services/core/java/com/android/server/biometrics/sensors/face/hidl/BiometricTestSessionImpl.java
+++ b/services/core/java/com/android/server/biometrics/sensors/face/hidl/BiometricTestSessionImpl.java
@@ -16,6 +16,8 @@
package com.android.server.biometrics.sensors.face.hidl;
+import static android.Manifest.permission.TEST_BIOMETRIC;
+
import android.annotation.NonNull;
import android.content.Context;
import android.hardware.biometrics.ITestSession;
@@ -28,8 +30,8 @@
import android.os.RemoteException;
import android.util.Slog;
+import com.android.server.biometrics.Utils;
import com.android.server.biometrics.sensors.BaseClientMonitor;
-import com.android.server.biometrics.sensors.BiometricStateCallback;
import com.android.server.biometrics.sensors.ClientMonitorCallback;
import com.android.server.biometrics.sensors.face.FaceUtils;
@@ -51,7 +53,6 @@
@NonNull private final Set<Integer> mEnrollmentIds;
@NonNull private final Random mRandom;
-
private final IFaceServiceReceiver mReceiver = new IFaceServiceReceiver.Stub() {
@Override
public void onEnrollResult(Face face, int remaining) {
@@ -115,8 +116,7 @@
};
BiometricTestSessionImpl(@NonNull Context context, int sensorId,
- @NonNull ITestSessionCallback callback,
- @NonNull Face10 face10,
+ @NonNull ITestSessionCallback callback, @NonNull Face10 face10,
@NonNull Face10.HalResultController halResultController) {
mContext = context;
mSensorId = sensorId;
diff --git a/services/core/java/com/android/server/biometrics/sensors/face/hidl/Face10.java b/services/core/java/com/android/server/biometrics/sensors/face/hidl/Face10.java
index 0e0ee19..c0a119f 100644
--- a/services/core/java/com/android/server/biometrics/sensors/face/hidl/Face10.java
+++ b/services/core/java/com/android/server/biometrics/sensors/face/hidl/Face10.java
@@ -62,10 +62,8 @@
import com.android.server.biometrics.sensors.BaseClientMonitor;
import com.android.server.biometrics.sensors.BiometricNotificationUtils;
import com.android.server.biometrics.sensors.BiometricScheduler;
-import com.android.server.biometrics.sensors.BiometricStateCallback;
import com.android.server.biometrics.sensors.ClientMonitorCallback;
import com.android.server.biometrics.sensors.ClientMonitorCallbackConverter;
-import com.android.server.biometrics.sensors.ClientMonitorCompositeCallback;
import com.android.server.biometrics.sensors.EnumerateConsumer;
import com.android.server.biometrics.sensors.ErrorConsumer;
import com.android.server.biometrics.sensors.LockoutResetDispatcher;
@@ -112,7 +110,6 @@
private boolean mTestHalEnabled;
@NonNull private final FaceSensorPropertiesInternal mSensorProperties;
- @NonNull private final BiometricStateCallback mBiometricStateCallback;
@NonNull private final Context mContext;
@NonNull private final BiometricScheduler mScheduler;
@NonNull private final Handler mHandler;
@@ -339,7 +336,6 @@
@VisibleForTesting
Face10(@NonNull Context context,
- @NonNull BiometricStateCallback biometricStateCallback,
@NonNull FaceSensorPropertiesInternal sensorProps,
@NonNull LockoutResetDispatcher lockoutResetDispatcher,
@NonNull Handler handler,
@@ -347,7 +343,6 @@
@NonNull BiometricContext biometricContext) {
mSensorProperties = sensorProps;
mContext = context;
- mBiometricStateCallback = biometricStateCallback;
mSensorId = sensorProps.sensorId;
mScheduler = scheduler;
mHandler = handler;
@@ -371,12 +366,11 @@
}
public static Face10 newInstance(@NonNull Context context,
- @NonNull BiometricStateCallback biometricStateCallback,
@NonNull FaceSensorPropertiesInternal sensorProps,
@NonNull LockoutResetDispatcher lockoutResetDispatcher) {
final Handler handler = new Handler(Looper.getMainLooper());
- return new Face10(context, biometricStateCallback, sensorProps, lockoutResetDispatcher,
- handler, new BiometricScheduler(TAG, BiometricScheduler.SENSOR_TYPE_FACE,
+ return new Face10(context, sensorProps, lockoutResetDispatcher, handler,
+ new BiometricScheduler(TAG, BiometricScheduler.SENSOR_TYPE_FACE,
null /* gestureAvailabilityTracker */),
BiometricContext.getInstance(context));
}
@@ -621,19 +615,8 @@
mScheduler.scheduleClientMonitor(client, new ClientMonitorCallback() {
@Override
- public void onClientStarted(@NonNull BaseClientMonitor clientMonitor) {
- mBiometricStateCallback.onClientStarted(clientMonitor);
- }
-
- @Override
- public void onBiometricAction(int action) {
- mBiometricStateCallback.onBiometricAction(action);
- }
-
- @Override
public void onClientFinished(@NonNull BaseClientMonitor clientMonitor,
boolean success) {
- mBiometricStateCallback.onClientFinished(clientMonitor, success);
if (success) {
// Update authenticatorIds
scheduleUpdateActiveUserWithoutHandler(client.getTargetUserId());
@@ -678,7 +661,7 @@
createLogger(BiometricsProtoEnums.ACTION_AUTHENTICATE, statsClient),
mBiometricContext, isStrongBiometric, mLockoutTracker,
mUsageStats, allowBackgroundAuthentication, isKeyguardBypassEnabled);
- mScheduler.scheduleClientMonitor(client, mBiometricStateCallback);
+ mScheduler.scheduleClientMonitor(client);
});
}
@@ -713,7 +696,7 @@
createLogger(BiometricsProtoEnums.ACTION_REMOVE,
BiometricsProtoEnums.CLIENT_UNKNOWN),
mBiometricContext, mAuthenticatorIds);
- mScheduler.scheduleClientMonitor(client, mBiometricStateCallback);
+ mScheduler.scheduleClientMonitor(client);
});
}
@@ -731,7 +714,7 @@
createLogger(BiometricsProtoEnums.ACTION_REMOVE,
BiometricsProtoEnums.CLIENT_UNKNOWN),
mBiometricContext, mAuthenticatorIds);
- mScheduler.scheduleClientMonitor(client, mBiometricStateCallback);
+ mScheduler.scheduleClientMonitor(client);
});
}
@@ -823,15 +806,14 @@
BiometricsProtoEnums.CLIENT_UNKNOWN),
mBiometricContext, enrolledList,
FaceUtils.getLegacyInstance(mSensorId), mAuthenticatorIds);
- mScheduler.scheduleClientMonitor(client, new ClientMonitorCompositeCallback(callback,
- mBiometricStateCallback));
+ mScheduler.scheduleClientMonitor(client, callback);
});
}
@Override
public void scheduleInternalCleanup(int sensorId, int userId,
@Nullable ClientMonitorCallback callback) {
- scheduleInternalCleanup(userId, mBiometricStateCallback);
+ scheduleInternalCleanup(userId, callback);
}
@Override
@@ -1029,7 +1011,7 @@
@Override
public ITestSession createTestSession(int sensorId, @NonNull ITestSessionCallback callback,
@NonNull String opPackageName) {
- return new BiometricTestSessionImpl(mContext, mSensorId, callback,
- this, mHalResultController);
+ return new BiometricTestSessionImpl(mContext, mSensorId, callback, this,
+ mHalResultController);
}
}
diff --git a/services/core/java/com/android/server/display/AutomaticBrightnessController.java b/services/core/java/com/android/server/display/AutomaticBrightnessController.java
index b3aee22..7b60421 100644
--- a/services/core/java/com/android/server/display/AutomaticBrightnessController.java
+++ b/services/core/java/com/android/server/display/AutomaticBrightnessController.java
@@ -593,10 +593,14 @@
}
pw.println();
+ pw.println(" mAmbientBrightnessThresholds=");
mAmbientBrightnessThresholds.dump(pw);
+ pw.println(" mScreenBrightnessThresholds=");
mScreenBrightnessThresholds.dump(pw);
+ pw.println(" mScreenBrightnessThresholdsIdle=");
mScreenBrightnessThresholdsIdle.dump(pw);
- mScreenBrightnessThresholdsIdle.dump(pw);
+ pw.println(" mAmbientBrightnessThresholdsIdle=");
+ mAmbientBrightnessThresholdsIdle.dump(pw);
}
private String configStateToString(int state) {
@@ -861,6 +865,7 @@
Slog.d(TAG, "updateAmbientLux: "
+ ((mFastAmbientLux > mAmbientLux) ? "Brightened" : "Darkened") + ": "
+ "mBrighteningLuxThreshold=" + mAmbientBrighteningThreshold + ", "
+ + "mAmbientDarkeningThreshold=" + mAmbientDarkeningThreshold + ", "
+ "mAmbientLightRingBuffer=" + mAmbientLightRingBuffer + ", "
+ "mAmbientLux=" + mAmbientLux);
}
diff --git a/services/core/java/com/android/server/display/DisplayDeviceConfig.java b/services/core/java/com/android/server/display/DisplayDeviceConfig.java
index 4165186..81219ba 100644
--- a/services/core/java/com/android/server/display/DisplayDeviceConfig.java
+++ b/services/core/java/com/android/server/display/DisplayDeviceConfig.java
@@ -27,6 +27,7 @@
import android.os.PowerManager;
import android.text.TextUtils;
import android.util.MathUtils;
+import android.util.Pair;
import android.util.Slog;
import android.util.Spline;
import android.view.DisplayAddress;
@@ -51,7 +52,7 @@
import com.android.server.display.config.SensorDetails;
import com.android.server.display.config.ThermalStatus;
import com.android.server.display.config.ThermalThrottling;
-import com.android.server.display.config.Thresholds;
+import com.android.server.display.config.ThresholdPoint;
import com.android.server.display.config.XmlParser;
import org.xmlpull.v1.XmlPullParserException;
@@ -188,42 +189,153 @@
* <ambientLightHorizonLong>10001</ambientLightHorizonLong>
* <ambientLightHorizonShort>2001</ambientLightHorizonShort>
*
- * <displayBrightnessChangeThresholds> // Thresholds for screen changes
- * <brighteningThresholds> // Thresholds for active mode brightness changes.
- * <minimum>0.001</minimum> // Minimum change needed in screen brightness to brighten.
- * </brighteningThresholds>
- * <darkeningThresholds>
- * <minimum>0.002</minimum> // Minimum change needed in screen brightness to darken.
- * </darkeningThresholds>
- * </displayBrightnessChangeThresholds>
- *
- * <ambientBrightnessChangeThresholds> // Thresholds for lux changes
- * <brighteningThresholds> // Thresholds for active mode brightness changes.
- * <minimum>0.003</minimum> // Minimum change needed in ambient brightness to brighten.
- * </brighteningThresholds>
- * <darkeningThresholds>
- * <minimum>0.004</minimum> // Minimum change needed in ambient brightness to darken.
- * </darkeningThresholds>
- * </ambientBrightnessChangeThresholds>
- *
- * <displayBrightnessChangeThresholdsIdle> // Thresholds for screen changes in idle mode
- * <brighteningThresholds> // Thresholds for idle mode brightness changes.
- * <minimum>0.001</minimum> // Minimum change needed in screen brightness to brighten.
- * </brighteningThresholds>
- * <darkeningThresholds>
- * <minimum>0.002</minimum> // Minimum change needed in screen brightness to darken.
- * </darkeningThresholds>
- * </displayBrightnessChangeThresholdsIdle>
- *
- * <ambientBrightnessChangeThresholdsIdle> // Thresholds for lux changes in idle mode
- * <brighteningThresholds> // Thresholds for idle mode brightness changes.
- * <minimum>0.003</minimum> // Minimum change needed in ambient brightness to brighten.
- * </brighteningThresholds>
- * <darkeningThresholds>
- * <minimum>0.004</minimum> // Minimum change needed in ambient brightness to darken.
- * </darkeningThresholds>
- * </ambientBrightnessChangeThresholdsIdle>
- *
+ * <ambientBrightnessChangeThresholds> // Thresholds for lux changes
+ * <brighteningThresholds>
+ * // Minimum change needed in ambient brightness to brighten screen.
+ * <minimum>10</minimum>
+ * // Percentage increase of lux needed to increase the screen brightness at a lux range
+ * // above the specified threshold.
+ * <brightnessThresholdPoints>
+ * <brightnessThresholdPoint>
+ * <threshold>0</threshold><percentage>13</percentage>
+ * </brightnessThresholdPoint>
+ * <brightnessThresholdPoint>
+ * <threshold>100</threshold><percentage>14</percentage>
+ * </brightnessThresholdPoint>
+ * <brightnessThresholdPoint>
+ * <threshold>200</threshold><percentage>15</percentage>
+ * </brightnessThresholdPoint>
+ * </brightnessThresholdPoints>
+ * </brighteningThresholds>
+ * <darkeningThresholds>
+ * // Minimum change needed in ambient brightness to darken screen.
+ * <minimum>30</minimum>
+ * // Percentage increase of lux needed to decrease the screen brightness at a lux range
+ * // above the specified threshold.
+ * <brightnessThresholdPoints>
+ * <brightnessThresholdPoint>
+ * <threshold>0</threshold><percentage>15</percentage>
+ * </brightnessThresholdPoint>
+ * <brightnessThresholdPoint>
+ * <threshold>300</threshold><percentage>16</percentage>
+ * </brightnessThresholdPoint>
+ * <brightnessThresholdPoint>
+ * <threshold>400</threshold><percentage>17</percentage>
+ * </brightnessThresholdPoint>
+ * </brightnessThresholdPoints>
+ * </darkeningThresholds>
+ * </ambientBrightnessChangeThresholds>
+ * <displayBrightnessChangeThresholds> // Thresholds for screen brightness changes
+ * <brighteningThresholds>
+ * // Minimum change needed in screen brightness to brighten screen.
+ * <minimum>0.1</minimum>
+ * // Percentage increase of screen brightness needed to increase the screen brightness
+ * // at a lux range above the specified threshold.
+ * <brightnessThresholdPoints>
+ * <brightnessThresholdPoint>
+ * <threshold>0</threshold>
+ * <percentage>9</percentage>
+ * </brightnessThresholdPoint>
+ * <brightnessThresholdPoint>
+ * <threshold>0.10</threshold>
+ * <percentage>10</percentage>
+ * </brightnessThresholdPoint>
+ * <brightnessThresholdPoint>
+ * <threshold>0.20</threshold>
+ * <percentage>11</percentage>
+ * </brightnessThresholdPoint>
+ * </brightnessThresholdPoints>
+ * </brighteningThresholds>
+ * <darkeningThresholds>
+ * // Minimum change needed in screen brightness to darken screen.
+ * <minimum>0.3</minimum>
+ * // Percentage increase of screen brightness needed to decrease the screen brightness
+ * // at a lux range above the specified threshold.
+ * <brightnessThresholdPoints>
+ * <brightnessThresholdPoint>
+ * <threshold>0</threshold><percentage>11</percentage>
+ * </brightnessThresholdPoint>
+ * <brightnessThresholdPoint>
+ * <threshold>0.11</threshold><percentage>12</percentage>
+ * </brightnessThresholdPoint>
+ * <brightnessThresholdPoint>
+ * <threshold>0.21</threshold><percentage>13</percentage>
+ * </brightnessThresholdPoint>
+ * </brightnessThresholdPoints>
+ * </darkeningThresholds>
+ * </displayBrightnessChangeThresholds>
+ * <ambientBrightnessChangeThresholdsIdle> // Thresholds for lux changes in idle mode
+ * <brighteningThresholds>
+ * // Minimum change needed in ambient brightness to brighten screen in idle mode
+ * <minimum>20</minimum>
+ * // Percentage increase of lux needed to increase the screen brightness at a lux range
+ * // above the specified threshold whilst in idle mode.
+ * <brightnessThresholdPoints>
+ * <brightnessThresholdPoint>
+ * <threshold>0</threshold><percentage>21</percentage>
+ * </brightnessThresholdPoint>
+ * <brightnessThresholdPoint>
+ * <threshold>500</threshold><percentage>22</percentage>
+ * </brightnessThresholdPoint>
+ * <brightnessThresholdPoint>
+ * <threshold>600</threshold><percentage>23</percentage>
+ * </brightnessThresholdPoint>
+ * </brightnessThresholdPoints>
+ * </brighteningThresholds>
+ * <darkeningThresholds>
+ * // Minimum change needed in ambient brightness to darken screen in idle mode
+ * <minimum>40</minimum>
+ * // Percentage increase of lux needed to decrease the screen brightness at a lux range
+ * // above the specified threshold whilst in idle mode.
+ * <brightnessThresholdPoints>
+ * <brightnessThresholdPoint>
+ * <threshold>0</threshold><percentage>23</percentage>
+ * </brightnessThresholdPoint>
+ * <brightnessThresholdPoint>
+ * <threshold>700</threshold><percentage>24</percentage>
+ * </brightnessThresholdPoint>
+ * <brightnessThresholdPoint>
+ * <threshold>800</threshold><percentage>25</percentage>
+ * </brightnessThresholdPoint>
+ * </brightnessThresholdPoints>
+ * </darkeningThresholds>
+ * </ambientBrightnessChangeThresholdsIdle>
+ * <displayBrightnessChangeThresholdsIdle> // Thresholds for idle screen brightness changes
+ * <brighteningThresholds>
+ * // Minimum change needed in screen brightness to brighten screen in idle mode
+ * <minimum>0.2</minimum>
+ * // Percentage increase of screen brightness needed to increase the screen brightness
+ * // at a lux range above the specified threshold whilst in idle mode
+ * <brightnessThresholdPoints>
+ * <brightnessThresholdPoint>
+ * <threshold>0</threshold><percentage>17</percentage>
+ * </brightnessThresholdPoint>
+ * <brightnessThresholdPoint>
+ * <threshold>0.12</threshold><percentage>18</percentage>
+ * </brightnessThresholdPoint>
+ * <brightnessThresholdPoint>
+ * <threshold>0.22</threshold><percentage>19</percentage>
+ * </brightnessThresholdPoint>
+ * </brightnessThresholdPoints>
+ * </brighteningThresholds>
+ * <darkeningThresholds>
+ * // Minimum change needed in screen brightness to darken screen in idle mode
+ * <minimum>0.4</minimum>
+ * // Percentage increase of screen brightness needed to decrease the screen brightness
+ * // at a lux range above the specified threshold whilst in idle mode
+ * <brightnessThresholdPoints>
+ * <brightnessThresholdPoint>
+ * <threshold>0</threshold><percentage>19</percentage>
+ * </brightnessThresholdPoint>
+ * <brightnessThresholdPoint>
+ * <threshold>0.13</threshold><percentage>20</percentage>
+ * </brightnessThresholdPoint>
+ * <brightnessThresholdPoint>
+ * <threshold>0.23</threshold><percentage>21</percentage>
+ * </brightnessThresholdPoint>
+ * </brightnessThresholdPoints>
+ * </darkeningThresholds>
+ * </displayBrightnessChangeThresholdsIdle>
* </displayConfiguration>
* }
* </pre>
@@ -247,6 +359,13 @@
private static final String NO_SUFFIX_FORMAT = "%d";
private static final long STABLE_FLAG = 1L << 62;
+ private static final float[] DEFAULT_AMBIENT_THRESHOLD_LEVELS = new float[]{0f};
+ private static final float[] DEFAULT_AMBIENT_BRIGHTENING_THRESHOLDS = new float[]{100f};
+ private static final float[] DEFAULT_AMBIENT_DARKENING_THRESHOLDS = new float[]{200f};
+ private static final float[] DEFAULT_SCREEN_THRESHOLD_LEVELS = new float[]{0f};
+ private static final float[] DEFAULT_SCREEN_BRIGHTENING_THRESHOLDS = new float[]{100f};
+ private static final float[] DEFAULT_SCREEN_DARKENING_THRESHOLDS = new float[]{200f};
+
private static final int INTERPOLATION_DEFAULT = 0;
private static final int INTERPOLATION_LINEAR = 1;
@@ -344,6 +463,31 @@
private float mAmbientLuxBrighteningMinThresholdIdle = 0.0f;
private float mAmbientLuxDarkeningMinThreshold = 0.0f;
private float mAmbientLuxDarkeningMinThresholdIdle = 0.0f;
+
+ // Screen brightness thresholds levels & percentages
+ private float[] mScreenBrighteningLevels = DEFAULT_SCREEN_THRESHOLD_LEVELS;
+ private float[] mScreenBrighteningPercentages = DEFAULT_SCREEN_BRIGHTENING_THRESHOLDS;
+ private float[] mScreenDarkeningLevels = DEFAULT_SCREEN_THRESHOLD_LEVELS;
+ private float[] mScreenDarkeningPercentages = DEFAULT_SCREEN_DARKENING_THRESHOLDS;
+
+ // Screen brightness thresholds levels & percentages for idle mode
+ private float[] mScreenBrighteningLevelsIdle = DEFAULT_SCREEN_THRESHOLD_LEVELS;
+ private float[] mScreenBrighteningPercentagesIdle = DEFAULT_SCREEN_BRIGHTENING_THRESHOLDS;
+ private float[] mScreenDarkeningLevelsIdle = DEFAULT_SCREEN_THRESHOLD_LEVELS;
+ private float[] mScreenDarkeningPercentagesIdle = DEFAULT_SCREEN_DARKENING_THRESHOLDS;
+
+ // Ambient brightness thresholds levels & percentages
+ private float[] mAmbientBrighteningLevels = DEFAULT_AMBIENT_THRESHOLD_LEVELS;
+ private float[] mAmbientBrighteningPercentages = DEFAULT_AMBIENT_BRIGHTENING_THRESHOLDS;
+ private float[] mAmbientDarkeningLevels = DEFAULT_AMBIENT_THRESHOLD_LEVELS;
+ private float[] mAmbientDarkeningPercentages = DEFAULT_AMBIENT_DARKENING_THRESHOLDS;
+
+ // Ambient brightness thresholds levels & percentages for idle mode
+ private float[] mAmbientBrighteningLevelsIdle = DEFAULT_AMBIENT_THRESHOLD_LEVELS;
+ private float[] mAmbientBrighteningPercentagesIdle = DEFAULT_AMBIENT_BRIGHTENING_THRESHOLDS;
+ private float[] mAmbientDarkeningLevelsIdle = DEFAULT_AMBIENT_THRESHOLD_LEVELS;
+ private float[] mAmbientDarkeningPercentagesIdle = DEFAULT_AMBIENT_DARKENING_THRESHOLDS;
+
private Spline mBrightnessToBacklightSpline;
private Spline mBacklightToBrightnessSpline;
private Spline mBacklightToNitsSpline;
@@ -684,7 +828,7 @@
/**
* The minimum value for the ambient lux increase for a screen brightness change to actually
* occur.
- * @return float value in brightness scale of 0 - 1.
+ * @return float value in lux.
*/
public float getAmbientLuxBrighteningMinThreshold() {
return mAmbientLuxBrighteningMinThreshold;
@@ -693,7 +837,7 @@
/**
* The minimum value for the ambient lux decrease for a screen brightness change to actually
* occur.
- * @return float value in brightness scale of 0 - 1.
+ * @return float value in lux.
*/
public float getAmbientLuxDarkeningMinThreshold() {
return mAmbientLuxDarkeningMinThreshold;
@@ -702,7 +846,7 @@
/**
* The minimum value for the ambient lux increase for a screen brightness change to actually
* occur while in idle screen brightness mode.
- * @return float value in brightness scale of 0 - 1.
+ * @return float value in lux.
*/
public float getAmbientLuxBrighteningMinThresholdIdle() {
return mAmbientLuxBrighteningMinThresholdIdle;
@@ -711,12 +855,262 @@
/**
* The minimum value for the ambient lux decrease for a screen brightness change to actually
* occur while in idle screen brightness mode.
- * @return float value in brightness scale of 0 - 1.
+ * @return float value in lux.
*/
public float getAmbientLuxDarkeningMinThresholdIdle() {
return mAmbientLuxDarkeningMinThresholdIdle;
}
+ /**
+ * The array that describes the range of screen brightness that each threshold percentage
+ * applies within.
+ *
+ * The (zero-based) index is calculated as follows
+ * value = current screen brightness value
+ * level = mScreenBrighteningLevels
+ *
+ * condition return
+ * value < level[0] = 0.0f
+ * level[n] <= value < level[n+1] = mScreenBrighteningPercentages[n]
+ * level[MAX] <= value = mScreenBrighteningPercentages[MAX]
+ *
+ * @return the screen brightness levels between 0.0 and 1.0 for which each
+ * mScreenBrighteningPercentages applies
+ */
+ public float[] getScreenBrighteningLevels() {
+ return mScreenBrighteningLevels;
+ }
+
+ /**
+ * The array that describes the screen brightening threshold percentage change at each screen
+ * brightness level described in mScreenBrighteningLevels.
+ *
+ * @return the percentages between 0 and 100 of brightness increase required in order for the
+ * screen brightness to change
+ */
+ public float[] getScreenBrighteningPercentages() {
+ return mScreenBrighteningPercentages;
+ }
+
+ /**
+ * The array that describes the range of screen brightness that each threshold percentage
+ * applies within.
+ *
+ * The (zero-based) index is calculated as follows
+ * value = current screen brightness value
+ * level = mScreenDarkeningLevels
+ *
+ * condition return
+ * value < level[0] = 0.0f
+ * level[n] <= value < level[n+1] = mScreenDarkeningPercentages[n]
+ * level[MAX] <= value = mScreenDarkeningPercentages[MAX]
+ *
+ * @return the screen brightness levels between 0.0 and 1.0 for which each
+ * mScreenDarkeningPercentages applies
+ */
+ public float[] getScreenDarkeningLevels() {
+ return mScreenDarkeningLevels;
+ }
+
+ /**
+ * The array that describes the screen darkening threshold percentage change at each screen
+ * brightness level described in mScreenDarkeningLevels.
+ *
+ * @return the percentages between 0 and 100 of brightness decrease required in order for the
+ * screen brightness to change
+ */
+ public float[] getScreenDarkeningPercentages() {
+ return mScreenDarkeningPercentages;
+ }
+
+ /**
+ * The array that describes the range of ambient brightness that each threshold
+ * percentage applies within.
+ *
+ * The (zero-based) index is calculated as follows
+ * value = current ambient brightness value
+ * level = mAmbientBrighteningLevels
+ *
+ * condition return
+ * value < level[0] = 0.0f
+ * level[n] <= value < level[n+1] = mAmbientBrighteningPercentages[n]
+ * level[MAX] <= value = mAmbientBrighteningPercentages[MAX]
+ *
+ * @return the ambient brightness levels from 0 lux upwards for which each
+ * mAmbientBrighteningPercentages applies
+ */
+ public float[] getAmbientBrighteningLevels() {
+ return mAmbientBrighteningLevels;
+ }
+
+ /**
+ * The array that describes the ambient brightening threshold percentage change at each ambient
+ * brightness level described in mAmbientBrighteningLevels.
+ *
+ * @return the percentages between 0 and 100 of brightness increase required in order for the
+ * screen brightness to change
+ */
+ public float[] getAmbientBrighteningPercentages() {
+ return mAmbientBrighteningPercentages;
+ }
+
+ /**
+ * The array that describes the range of ambient brightness that each threshold percentage
+ * applies within.
+ *
+ * The (zero-based) index is calculated as follows
+ * value = current ambient brightness value
+ * level = mAmbientDarkeningLevels
+ *
+ * condition return
+ * value < level[0] = 0.0f
+ * level[n] <= value < level[n+1] = mAmbientDarkeningPercentages[n]
+ * level[MAX] <= value = mAmbientDarkeningPercentages[MAX]
+ *
+ * @return the ambient brightness levels from 0 lux upwards for which each
+ * mAmbientDarkeningPercentages applies
+ */
+ public float[] getAmbientDarkeningLevels() {
+ return mAmbientDarkeningLevels;
+ }
+
+ /**
+ * The array that describes the ambient darkening threshold percentage change at each ambient
+ * brightness level described in mAmbientDarkeningLevels.
+ *
+ * @return the percentages between 0 and 100 of brightness decrease required in order for the
+ * screen brightness to change
+ */
+ public float[] getAmbientDarkeningPercentages() {
+ return mAmbientDarkeningPercentages;
+ }
+
+ /**
+ * The array that describes the range of screen brightness that each threshold percentage
+ * applies within whilst in idle screen brightness mode.
+ *
+ * The (zero-based) index is calculated as follows
+ * value = current screen brightness value
+ * level = mScreenBrighteningLevelsIdle
+ *
+ * condition return
+ * value < level[0] = 0.0f
+ * level[n] <= value < level[n+1] = mScreenBrighteningPercentagesIdle[n]
+ * level[MAX] <= value = mScreenBrighteningPercentagesIdle[MAX]
+ *
+ * @return the screen brightness levels between 0.0 and 1.0 for which each
+ * mScreenBrighteningPercentagesIdle applies
+ */
+ public float[] getScreenBrighteningLevelsIdle() {
+ return mScreenBrighteningLevelsIdle;
+ }
+
+ /**
+ * The array that describes the screen brightening threshold percentage change at each screen
+ * brightness level described in mScreenBrighteningLevelsIdle.
+ *
+ * @return the percentages between 0 and 100 of brightness increase required in order for the
+ * screen brightness to change while in idle mode.
+ */
+ public float[] getScreenBrighteningPercentagesIdle() {
+ return mScreenBrighteningPercentagesIdle;
+ }
+
+ /**
+ * The array that describes the range of screen brightness that each threshold percentage
+ * applies within whilst in idle screen brightness mode.
+ *
+ * The (zero-based) index is calculated as follows
+ * value = current screen brightness value
+ * level = mScreenDarkeningLevelsIdle
+ *
+ * condition return
+ * value < level[0] = 0.0f
+ * level[n] <= value < level[n+1] = mScreenDarkeningPercentagesIdle[n]
+ * level[MAX] <= value = mScreenDarkeningPercentagesIdle[MAX]
+ *
+ * @return the screen brightness levels between 0.0 and 1.0 for which each
+ * mScreenDarkeningPercentagesIdle applies
+ */
+ public float[] getScreenDarkeningLevelsIdle() {
+ return mScreenDarkeningLevelsIdle;
+ }
+
+ /**
+ * The array that describes the screen darkening threshold percentage change at each screen
+ * brightness level described in mScreenDarkeningLevelsIdle.
+ *
+ * @return the percentages between 0 and 100 of brightness decrease required in order for the
+ * screen brightness to change while in idle mode.
+ */
+ public float[] getScreenDarkeningPercentagesIdle() {
+ return mScreenDarkeningPercentagesIdle;
+ }
+
+ /**
+ * The array that describes the range of ambient brightness that each threshold percentage
+ * applies within whilst in idle screen brightness mode.
+ *
+ * The (zero-based) index is calculated as follows
+ * value = current ambient brightness value
+ * level = mAmbientBrighteningLevelsIdle
+ *
+ * condition return
+ * value < level[0] = 0.0f
+ * level[n] <= value < level[n+1] = mAmbientBrighteningPercentagesIdle[n]
+ * level[MAX] <= value = mAmbientBrighteningPercentagesIdle[MAX]
+ *
+ * @return the ambient brightness levels from 0 lux upwards for which each
+ * mAmbientBrighteningPercentagesIdle applies
+ */
+ public float[] getAmbientBrighteningLevelsIdle() {
+ return mAmbientBrighteningLevelsIdle;
+ }
+
+ /**
+ * The array that describes the ambient brightness threshold percentage change whilst in
+ * idle screen brightness mode at each ambient brightness level described in
+ * mAmbientBrighteningLevelsIdle.
+ *
+ * @return the percentages between 0 and 100 of ambient brightness increase required in order
+ * for the screen brightness to change
+ */
+ public float[] getAmbientBrighteningPercentagesIdle() {
+ return mAmbientBrighteningPercentagesIdle;
+ }
+
+ /**
+ * The array that describes the range of ambient brightness that each threshold percentage
+ * applies within whilst in idle screen brightness mode.
+ *
+ * The (zero-based) index is calculated as follows
+ * value = current ambient brightness value
+ * level = mAmbientDarkeningLevelsIdle
+ *
+ * condition return
+ * value < level[0] = 0.0f
+ * level[n] <= value < level[n+1] = mAmbientDarkeningPercentagesIdle[n]
+ * level[MAX] <= value = mAmbientDarkeningPercentagesIdle[MAX]
+ *
+ * @return the ambient brightness levels from 0 lux upwards for which each
+ * mAmbientDarkeningPercentagesIdle applies
+ */
+ public float[] getAmbientDarkeningLevelsIdle() {
+ return mAmbientDarkeningLevelsIdle;
+ }
+
+ /**
+ * The array that describes the ambient brightness threshold percentage change whilst in
+ * idle screen brightness mode at each ambient brightness level described in
+ * mAmbientDarkeningLevelsIdle.
+ *
+ * @return the percentages between 0 and 100 of ambient brightness decrease required in order
+ * for the screen brightness to change
+ */
+ public float[] getAmbientDarkeningPercentagesIdle() {
+ return mAmbientDarkeningPercentagesIdle;
+ }
+
SensorData getAmbientLightSensor() {
return mAmbientLightSensor;
}
@@ -812,14 +1206,17 @@
+ ", mSdrToHdrRatioSpline=" + mSdrToHdrRatioSpline
+ ", mBrightnessThrottlingData=" + mBrightnessThrottlingData
+ ", mOriginalBrightnessThrottlingData=" + mOriginalBrightnessThrottlingData
+ + "\n"
+ ", mBrightnessRampFastDecrease=" + mBrightnessRampFastDecrease
+ ", mBrightnessRampFastIncrease=" + mBrightnessRampFastIncrease
+ ", mBrightnessRampSlowDecrease=" + mBrightnessRampSlowDecrease
+ ", mBrightnessRampSlowIncrease=" + mBrightnessRampSlowIncrease
+ ", mBrightnessRampDecreaseMaxMillis=" + mBrightnessRampDecreaseMaxMillis
+ ", mBrightnessRampIncreaseMaxMillis=" + mBrightnessRampIncreaseMaxMillis
+ + "\n"
+ ", mAmbientHorizonLong=" + mAmbientHorizonLong
+ ", mAmbientHorizonShort=" + mAmbientHorizonShort
+ + "\n"
+ ", mScreenDarkeningMinThreshold=" + mScreenDarkeningMinThreshold
+ ", mScreenDarkeningMinThresholdIdle=" + mScreenDarkeningMinThresholdIdle
+ ", mScreenBrighteningMinThreshold=" + mScreenBrighteningMinThreshold
@@ -829,6 +1226,41 @@
+ ", mAmbientLuxBrighteningMinThreshold=" + mAmbientLuxBrighteningMinThreshold
+ ", mAmbientLuxBrighteningMinThresholdIdle="
+ mAmbientLuxBrighteningMinThresholdIdle
+ + "\n"
+ + ", mScreenBrighteningLevels=" + Arrays.toString(
+ mScreenBrighteningLevels)
+ + ", mScreenBrighteningPercentages=" + Arrays.toString(
+ mScreenBrighteningPercentages)
+ + ", mScreenDarkeningLevels=" + Arrays.toString(
+ mScreenDarkeningLevels)
+ + ", mScreenDarkeningPercentages=" + Arrays.toString(
+ mScreenDarkeningPercentages)
+ + ", mAmbientBrighteningLevels=" + Arrays.toString(
+ mAmbientBrighteningLevels)
+ + ", mAmbientBrighteningPercentages=" + Arrays.toString(
+ mAmbientBrighteningPercentages)
+ + ", mAmbientDarkeningLevels=" + Arrays.toString(
+ mAmbientDarkeningLevels)
+ + ", mAmbientDarkeningPercentages=" + Arrays.toString(
+ mAmbientDarkeningPercentages)
+ + "\n"
+ + ", mAmbientBrighteningLevelsIdle=" + Arrays.toString(
+ mAmbientBrighteningLevelsIdle)
+ + ", mAmbientBrighteningPercentagesIdle=" + Arrays.toString(
+ mAmbientBrighteningPercentagesIdle)
+ + ", mAmbientDarkeningLevelsIdle=" + Arrays.toString(
+ mAmbientDarkeningLevelsIdle)
+ + ", mAmbientDarkeningPercentagesIdle=" + Arrays.toString(
+ mAmbientDarkeningPercentagesIdle)
+ + ", mScreenBrighteningLevelsIdle=" + Arrays.toString(
+ mScreenBrighteningLevelsIdle)
+ + ", mScreenBrighteningPercentagesIdle=" + Arrays.toString(
+ mScreenBrighteningPercentagesIdle)
+ + ", mScreenDarkeningLevelsIdle=" + Arrays.toString(
+ mScreenDarkeningLevelsIdle)
+ + ", mScreenDarkeningPercentagesIdle=" + Arrays.toString(
+ mScreenDarkeningPercentagesIdle)
+ + "\n"
+ ", mAmbientLightSensor=" + mAmbientLightSensor
+ ", mProximitySensor=" + mProximitySensor
+ ", mRefreshRateLimitations= " + Arrays.toString(mRefreshRateLimitations.toArray())
@@ -914,6 +1346,7 @@
loadBrightnessMapFromConfigXml();
loadBrightnessRampsFromConfigXml();
loadAmbientLightSensorFromConfigXml();
+ loadBrightnessChangeThresholdsFromXml();
setProxSensorUnspecified();
loadAutoBrightnessConfigsFromConfigXml();
mLoadedFrom = "<config.xml>";
@@ -1454,91 +1887,286 @@
}
}
+ private void loadBrightnessChangeThresholdsFromXml() {
+ loadBrightnessChangeThresholds(/* config= */ null);
+ }
+
private void loadBrightnessChangeThresholds(DisplayConfiguration config) {
- Thresholds displayBrightnessThresholds = config.getDisplayBrightnessChangeThresholds();
- Thresholds ambientBrightnessThresholds = config.getAmbientBrightnessChangeThresholds();
- Thresholds displayBrightnessThresholdsIdle =
- config.getDisplayBrightnessChangeThresholdsIdle();
- Thresholds ambientBrightnessThresholdsIdle =
- config.getAmbientBrightnessChangeThresholdsIdle();
-
- loadDisplayBrightnessThresholds(displayBrightnessThresholds);
- loadAmbientBrightnessThresholds(ambientBrightnessThresholds);
- loadIdleDisplayBrightnessThresholds(displayBrightnessThresholdsIdle);
- loadIdleAmbientBrightnessThresholds(ambientBrightnessThresholdsIdle);
+ loadDisplayBrightnessThresholds(config);
+ loadAmbientBrightnessThresholds(config);
+ loadDisplayBrightnessThresholdsIdle(config);
+ loadAmbientBrightnessThresholdsIdle(config);
}
- private void loadDisplayBrightnessThresholds(Thresholds displayBrightnessThresholds) {
- if (displayBrightnessThresholds != null) {
- BrightnessThresholds brighteningScreen =
- displayBrightnessThresholds.getBrighteningThresholds();
- BrightnessThresholds darkeningScreen =
- displayBrightnessThresholds.getDarkeningThresholds();
+ private void loadDisplayBrightnessThresholds(DisplayConfiguration config) {
+ BrightnessThresholds brighteningScreen = null;
+ BrightnessThresholds darkeningScreen = null;
+ if (config != null && config.getDisplayBrightnessChangeThresholds() != null) {
+ brighteningScreen =
+ config.getDisplayBrightnessChangeThresholds().getBrighteningThresholds();
+ darkeningScreen =
+ config.getDisplayBrightnessChangeThresholds().getDarkeningThresholds();
- if (brighteningScreen != null && brighteningScreen.getMinimum() != null) {
- mScreenBrighteningMinThreshold = brighteningScreen.getMinimum().floatValue();
- }
- if (darkeningScreen != null && darkeningScreen.getMinimum() != null) {
- mScreenDarkeningMinThreshold = darkeningScreen.getMinimum().floatValue();
- }
+ }
+
+ // Screen bright/darkening threshold levels for active mode
+ Pair<float[], float[]> screenBrighteningPair = getBrightnessLevelAndPercentage(
+ brighteningScreen,
+ com.android.internal.R.array.config_screenThresholdLevels,
+ com.android.internal.R.array.config_screenBrighteningThresholds,
+ DEFAULT_SCREEN_THRESHOLD_LEVELS, DEFAULT_SCREEN_BRIGHTENING_THRESHOLDS,
+ /* potentialOldBrightnessScale= */ true);
+
+ mScreenBrighteningLevels = screenBrighteningPair.first;
+ mScreenBrighteningPercentages = screenBrighteningPair.second;
+
+ Pair<float[], float[]> screenDarkeningPair = getBrightnessLevelAndPercentage(
+ darkeningScreen,
+ com.android.internal.R.array.config_screenThresholdLevels,
+ com.android.internal.R.array.config_screenDarkeningThresholds,
+ DEFAULT_SCREEN_THRESHOLD_LEVELS, DEFAULT_SCREEN_DARKENING_THRESHOLDS,
+ /* potentialOldBrightnessScale= */ true);
+ mScreenDarkeningLevels = screenDarkeningPair.first;
+ mScreenDarkeningPercentages = screenDarkeningPair.second;
+
+ // Screen bright/darkening threshold minimums for active mode
+ if (brighteningScreen != null && brighteningScreen.getMinimum() != null) {
+ mScreenBrighteningMinThreshold = brighteningScreen.getMinimum().floatValue();
+ }
+ if (darkeningScreen != null && darkeningScreen.getMinimum() != null) {
+ mScreenDarkeningMinThreshold = darkeningScreen.getMinimum().floatValue();
}
}
- private void loadAmbientBrightnessThresholds(Thresholds ambientBrightnessThresholds) {
- if (ambientBrightnessThresholds != null) {
- BrightnessThresholds brighteningAmbientLux =
- ambientBrightnessThresholds.getBrighteningThresholds();
- BrightnessThresholds darkeningAmbientLux =
- ambientBrightnessThresholds.getDarkeningThresholds();
+ private void loadAmbientBrightnessThresholds(DisplayConfiguration config) {
+ // Ambient Brightness Threshold Levels
+ BrightnessThresholds brighteningAmbientLux = null;
+ BrightnessThresholds darkeningAmbientLux = null;
+ if (config != null && config.getAmbientBrightnessChangeThresholds() != null) {
+ brighteningAmbientLux =
+ config.getAmbientBrightnessChangeThresholds().getBrighteningThresholds();
+ darkeningAmbientLux =
+ config.getAmbientBrightnessChangeThresholds().getDarkeningThresholds();
+ }
- final BigDecimal ambientBrighteningThreshold = brighteningAmbientLux.getMinimum();
- final BigDecimal ambientDarkeningThreshold = darkeningAmbientLux.getMinimum();
+ // Ambient bright/darkening threshold levels for active mode
+ Pair<float[], float[]> ambientBrighteningPair = getBrightnessLevelAndPercentage(
+ brighteningAmbientLux,
+ com.android.internal.R.array.config_ambientThresholdLevels,
+ com.android.internal.R.array.config_ambientBrighteningThresholds,
+ DEFAULT_AMBIENT_THRESHOLD_LEVELS, DEFAULT_AMBIENT_BRIGHTENING_THRESHOLDS);
+ mAmbientBrighteningLevels = ambientBrighteningPair.first;
+ mAmbientBrighteningPercentages = ambientBrighteningPair.second;
- if (ambientBrighteningThreshold != null) {
- mAmbientLuxBrighteningMinThreshold = ambientBrighteningThreshold.floatValue();
- }
- if (ambientDarkeningThreshold != null) {
- mAmbientLuxDarkeningMinThreshold = ambientDarkeningThreshold.floatValue();
- }
+ Pair<float[], float[]> ambientDarkeningPair = getBrightnessLevelAndPercentage(
+ darkeningAmbientLux,
+ com.android.internal.R.array.config_ambientThresholdLevels,
+ com.android.internal.R.array.config_ambientDarkeningThresholds,
+ DEFAULT_AMBIENT_THRESHOLD_LEVELS, DEFAULT_AMBIENT_DARKENING_THRESHOLDS);
+ mAmbientDarkeningLevels = ambientDarkeningPair.first;
+ mAmbientDarkeningPercentages = ambientDarkeningPair.second;
+
+ // Ambient bright/darkening threshold minimums for active/idle mode
+ if (brighteningAmbientLux != null && brighteningAmbientLux.getMinimum() != null) {
+ mAmbientLuxBrighteningMinThreshold =
+ brighteningAmbientLux.getMinimum().floatValue();
+ }
+
+ if (darkeningAmbientLux != null && darkeningAmbientLux.getMinimum() != null) {
+ mAmbientLuxDarkeningMinThreshold = darkeningAmbientLux.getMinimum().floatValue();
}
}
- private void loadIdleDisplayBrightnessThresholds(Thresholds idleDisplayBrightnessThresholds) {
- if (idleDisplayBrightnessThresholds != null) {
- BrightnessThresholds brighteningScreenIdle =
- idleDisplayBrightnessThresholds.getBrighteningThresholds();
- BrightnessThresholds darkeningScreenIdle =
- idleDisplayBrightnessThresholds.getDarkeningThresholds();
+ private void loadDisplayBrightnessThresholdsIdle(DisplayConfiguration config) {
+ BrightnessThresholds brighteningScreenIdle = null;
+ BrightnessThresholds darkeningScreenIdle = null;
+ if (config != null && config.getDisplayBrightnessChangeThresholdsIdle() != null) {
+ brighteningScreenIdle =
+ config.getDisplayBrightnessChangeThresholdsIdle().getBrighteningThresholds();
+ darkeningScreenIdle =
+ config.getDisplayBrightnessChangeThresholdsIdle().getDarkeningThresholds();
+ }
- if (brighteningScreenIdle != null
- && brighteningScreenIdle.getMinimum() != null) {
- mScreenBrighteningMinThresholdIdle =
- brighteningScreenIdle.getMinimum().floatValue();
- }
- if (darkeningScreenIdle != null && darkeningScreenIdle.getMinimum() != null) {
- mScreenDarkeningMinThresholdIdle =
- darkeningScreenIdle.getMinimum().floatValue();
- }
+ Pair<float[], float[]> screenBrighteningPair = getBrightnessLevelAndPercentage(
+ brighteningScreenIdle,
+ com.android.internal.R.array.config_screenThresholdLevels,
+ com.android.internal.R.array.config_screenBrighteningThresholds,
+ DEFAULT_SCREEN_THRESHOLD_LEVELS, DEFAULT_SCREEN_BRIGHTENING_THRESHOLDS,
+ /* potentialOldBrightnessScale= */ true);
+ mScreenBrighteningLevelsIdle = screenBrighteningPair.first;
+ mScreenBrighteningPercentagesIdle = screenBrighteningPair.second;
+
+ Pair<float[], float[]> screenDarkeningPair = getBrightnessLevelAndPercentage(
+ darkeningScreenIdle,
+ com.android.internal.R.array.config_screenThresholdLevels,
+ com.android.internal.R.array.config_screenDarkeningThresholds,
+ DEFAULT_SCREEN_THRESHOLD_LEVELS, DEFAULT_SCREEN_DARKENING_THRESHOLDS,
+ /* potentialOldBrightnessScale= */ true);
+ mScreenDarkeningLevelsIdle = screenDarkeningPair.first;
+ mScreenDarkeningPercentagesIdle = screenDarkeningPair.second;
+
+ if (brighteningScreenIdle != null
+ && brighteningScreenIdle.getMinimum() != null) {
+ mScreenBrighteningMinThresholdIdle =
+ brighteningScreenIdle.getMinimum().floatValue();
+ }
+ if (darkeningScreenIdle != null && darkeningScreenIdle.getMinimum() != null) {
+ mScreenDarkeningMinThresholdIdle =
+ darkeningScreenIdle.getMinimum().floatValue();
}
}
- private void loadIdleAmbientBrightnessThresholds(Thresholds idleAmbientBrightnessThresholds) {
- if (idleAmbientBrightnessThresholds != null) {
- BrightnessThresholds brighteningAmbientLuxIdle =
- idleAmbientBrightnessThresholds.getBrighteningThresholds();
- BrightnessThresholds darkeningAmbientLuxIdle =
- idleAmbientBrightnessThresholds.getDarkeningThresholds();
+ private void loadAmbientBrightnessThresholdsIdle(DisplayConfiguration config) {
+ BrightnessThresholds brighteningAmbientLuxIdle = null;
+ BrightnessThresholds darkeningAmbientLuxIdle = null;
+ if (config != null && config.getAmbientBrightnessChangeThresholdsIdle() != null) {
+ brighteningAmbientLuxIdle =
+ config.getAmbientBrightnessChangeThresholdsIdle().getBrighteningThresholds();
+ darkeningAmbientLuxIdle =
+ config.getAmbientBrightnessChangeThresholdsIdle().getDarkeningThresholds();
+ }
- if (brighteningAmbientLuxIdle != null
- && brighteningAmbientLuxIdle.getMinimum() != null) {
- mAmbientLuxBrighteningMinThresholdIdle =
- brighteningAmbientLuxIdle.getMinimum().floatValue();
+ Pair<float[], float[]> ambientBrighteningPair = getBrightnessLevelAndPercentage(
+ brighteningAmbientLuxIdle,
+ com.android.internal.R.array.config_ambientThresholdLevels,
+ com.android.internal.R.array.config_ambientBrighteningThresholds,
+ DEFAULT_AMBIENT_THRESHOLD_LEVELS, DEFAULT_AMBIENT_BRIGHTENING_THRESHOLDS);
+ mAmbientBrighteningLevelsIdle = ambientBrighteningPair.first;
+ mAmbientBrighteningPercentagesIdle = ambientBrighteningPair.second;
+
+ Pair<float[], float[]> ambientDarkeningPair = getBrightnessLevelAndPercentage(
+ darkeningAmbientLuxIdle,
+ com.android.internal.R.array.config_ambientThresholdLevels,
+ com.android.internal.R.array.config_ambientDarkeningThresholds,
+ DEFAULT_AMBIENT_THRESHOLD_LEVELS, DEFAULT_AMBIENT_DARKENING_THRESHOLDS);
+ mAmbientDarkeningLevelsIdle = ambientDarkeningPair.first;
+ mAmbientDarkeningPercentagesIdle = ambientDarkeningPair.second;
+
+ if (brighteningAmbientLuxIdle != null
+ && brighteningAmbientLuxIdle.getMinimum() != null) {
+ mAmbientLuxBrighteningMinThresholdIdle =
+ brighteningAmbientLuxIdle.getMinimum().floatValue();
+ }
+
+ if (darkeningAmbientLuxIdle != null && darkeningAmbientLuxIdle.getMinimum() != null) {
+ mAmbientLuxDarkeningMinThresholdIdle =
+ darkeningAmbientLuxIdle.getMinimum().floatValue();
+ }
+ }
+
+ private Pair<float[], float[]> getBrightnessLevelAndPercentage(BrightnessThresholds thresholds,
+ int configFallbackThreshold, int configFallbackPercentage, float[] defaultLevels,
+ float[] defaultPercentage) {
+ return getBrightnessLevelAndPercentage(thresholds, configFallbackThreshold,
+ configFallbackPercentage, defaultLevels, defaultPercentage, false);
+ }
+
+ // Returns two float arrays, one of the brightness levels and one of the corresponding threshold
+ // percentages for brightness levels at or above the lux value.
+ // Historically, config.xml would have an array for brightness levels that was 1 shorter than
+ // the levels array. Now we prepend a 0 to this array so they can be treated the same in the
+ // rest of the framework. Values were also defined in different units (permille vs percent).
+ private Pair<float[], float[]> getBrightnessLevelAndPercentage(BrightnessThresholds thresholds,
+ int configFallbackThreshold, int configFallbackPermille,
+ float[] defaultLevels, float[] defaultPercentage,
+ boolean potentialOldBrightnessScale) {
+ if (thresholds != null
+ && thresholds.getBrightnessThresholdPoints() != null
+ && thresholds.getBrightnessThresholdPoints()
+ .getBrightnessThresholdPoint().size() != 0) {
+
+ // The level and percentages arrays are equal length in the ddc (new system)
+ List<ThresholdPoint> points =
+ thresholds.getBrightnessThresholdPoints().getBrightnessThresholdPoint();
+ final int size = points.size();
+
+ float[] thresholdLevels = new float[size];
+ float[] thresholdPercentages = new float[size];
+
+ int i = 0;
+ for (ThresholdPoint point : points) {
+ thresholdLevels[i] = point.getThreshold().floatValue();
+ thresholdPercentages[i] = point.getPercentage().floatValue();
+ i++;
}
- if (darkeningAmbientLuxIdle != null && darkeningAmbientLuxIdle.getMinimum() != null) {
- mAmbientLuxDarkeningMinThresholdIdle =
- darkeningAmbientLuxIdle.getMinimum().floatValue();
+ return new Pair<>(thresholdLevels, thresholdPercentages);
+ } else {
+ // The level and percentages arrays are unequal length in config.xml (old system)
+ // We prefix the array with a 0 value to ensure they can be handled consistently
+ // with the new system.
+
+ // Load levels array
+ int[] configThresholdArray = mContext.getResources().getIntArray(
+ configFallbackThreshold);
+ int configThresholdsSize;
+ if (configThresholdArray == null || configThresholdArray.length == 0) {
+ configThresholdsSize = 1;
+ } else {
+ configThresholdsSize = configThresholdArray.length + 1;
+ }
+
+
+ // Load percentage array
+ int[] configPermille = mContext.getResources().getIntArray(
+ configFallbackPermille);
+
+ // Ensure lengths match up
+ boolean emptyArray = configPermille == null || configPermille.length == 0;
+ if (emptyArray && configThresholdsSize == 1) {
+ return new Pair<>(defaultLevels, defaultPercentage);
+ }
+ if (emptyArray || configPermille.length != configThresholdsSize) {
+ throw new IllegalArgumentException(
+ "Brightness threshold arrays do not align in length");
+ }
+
+ // Calculate levels array
+ float[] configThresholdWithZeroPrefixed = new float[configThresholdsSize];
+ // Start at 1, so that 0 index value is 0.0f (default)
+ for (int i = 1; i < configThresholdsSize; i++) {
+ configThresholdWithZeroPrefixed[i] = (float) configThresholdArray[i - 1];
+ }
+ if (potentialOldBrightnessScale) {
+ configThresholdWithZeroPrefixed =
+ constraintInRangeIfNeeded(configThresholdWithZeroPrefixed);
+ }
+
+ // Calculate percentages array
+ float[] configPercentage = new float[configThresholdsSize];
+ for (int i = 0; i < configPermille.length; i++) {
+ configPercentage[i] = configPermille[i] / 10.0f;
+ } return new Pair<>(configThresholdWithZeroPrefixed, configPercentage);
+ }
+ }
+
+ /**
+ * This check is due to historical reasons, where screen thresholdLevels used to be
+ * integer values in the range of [0-255], but then was changed to be float values from [0,1].
+ * To accommodate both the possibilities, we first check if all the thresholdLevels are in
+ * [0,1], and if not, we divide all the levels with 255 to bring them down to the same scale.
+ */
+ private float[] constraintInRangeIfNeeded(float[] thresholdLevels) {
+ if (isAllInRange(thresholdLevels, /* minValueInclusive= */ 0.0f,
+ /* maxValueInclusive= */ 1.0f)) {
+ return thresholdLevels;
+ }
+
+ Slog.w(TAG, "Detected screen thresholdLevels on a deprecated brightness scale");
+ float[] thresholdLevelsScaled = new float[thresholdLevels.length];
+ for (int index = 0; thresholdLevels.length > index; ++index) {
+ thresholdLevelsScaled[index] = thresholdLevels[index] / 255.0f;
+ }
+ return thresholdLevelsScaled;
+ }
+
+ private boolean isAllInRange(float[] configArray, float minValueInclusive,
+ float maxValueInclusive) {
+ for (float v : configArray) {
+ if (v < minValueInclusive || v > maxValueInclusive) {
+ return false;
}
}
+ return true;
}
private boolean thermalStatusIsValid(ThermalStatus value) {
diff --git a/services/core/java/com/android/server/display/DisplayPowerController.java b/services/core/java/com/android/server/display/DisplayPowerController.java
index 52d630b..4752044 100644
--- a/services/core/java/com/android/server/display/DisplayPowerController.java
+++ b/services/core/java/com/android/server/display/DisplayPowerController.java
@@ -970,53 +970,77 @@
com.android.internal.R.fraction.config_screenAutoBrightnessDozeScaleFactor,
1, 1);
- int[] ambientBrighteningThresholds = resources.getIntArray(
- com.android.internal.R.array.config_ambientBrighteningThresholds);
- int[] ambientDarkeningThresholds = resources.getIntArray(
- com.android.internal.R.array.config_ambientDarkeningThresholds);
- int[] ambientThresholdLevels = resources.getIntArray(
- com.android.internal.R.array.config_ambientThresholdLevels);
+ // Ambient Lux - Active Mode Brightness Thresholds
+ float[] ambientBrighteningThresholds =
+ mDisplayDeviceConfig.getAmbientBrighteningPercentages();
+ float[] ambientDarkeningThresholds =
+ mDisplayDeviceConfig.getAmbientDarkeningPercentages();
+ float[] ambientBrighteningLevels =
+ mDisplayDeviceConfig.getAmbientBrighteningLevels();
+ float[] ambientDarkeningLevels =
+ mDisplayDeviceConfig.getAmbientDarkeningLevels();
float ambientDarkeningMinThreshold =
mDisplayDeviceConfig.getAmbientLuxDarkeningMinThreshold();
float ambientBrighteningMinThreshold =
mDisplayDeviceConfig.getAmbientLuxBrighteningMinThreshold();
HysteresisLevels ambientBrightnessThresholds = new HysteresisLevels(
ambientBrighteningThresholds, ambientDarkeningThresholds,
- ambientThresholdLevels, ambientDarkeningMinThreshold,
+ ambientBrighteningLevels, ambientDarkeningLevels, ambientDarkeningMinThreshold,
ambientBrighteningMinThreshold);
- int[] screenBrighteningThresholds = resources.getIntArray(
- com.android.internal.R.array.config_screenBrighteningThresholds);
- int[] screenDarkeningThresholds = resources.getIntArray(
- com.android.internal.R.array.config_screenDarkeningThresholds);
- float[] screenThresholdLevels = BrightnessMappingStrategy.getFloatArray(resources
- .obtainTypedArray(com.android.internal.R.array.config_screenThresholdLevels));
+ // Display - Active Mode Brightness Thresholds
+ float[] screenBrighteningThresholds =
+ mDisplayDeviceConfig.getScreenBrighteningPercentages();
+ float[] screenDarkeningThresholds =
+ mDisplayDeviceConfig.getScreenDarkeningPercentages();
+ float[] screenBrighteningLevels =
+ mDisplayDeviceConfig.getScreenBrighteningLevels();
+ float[] screenDarkeningLevels =
+ mDisplayDeviceConfig.getScreenDarkeningLevels();
float screenDarkeningMinThreshold =
mDisplayDeviceConfig.getScreenDarkeningMinThreshold();
float screenBrighteningMinThreshold =
mDisplayDeviceConfig.getScreenBrighteningMinThreshold();
HysteresisLevels screenBrightnessThresholds = new HysteresisLevels(
- screenBrighteningThresholds, screenDarkeningThresholds, screenThresholdLevels,
- screenDarkeningMinThreshold, screenBrighteningMinThreshold);
+ screenBrighteningThresholds, screenDarkeningThresholds,
+ screenBrighteningLevels, screenDarkeningLevels, screenDarkeningMinThreshold,
+ screenBrighteningMinThreshold, true);
- // Idle screen thresholds
- float screenDarkeningMinThresholdIdle =
- mDisplayDeviceConfig.getScreenDarkeningMinThresholdIdle();
- float screenBrighteningMinThresholdIdle =
- mDisplayDeviceConfig.getScreenBrighteningMinThresholdIdle();
- HysteresisLevels screenBrightnessThresholdsIdle = new HysteresisLevels(
- screenBrighteningThresholds, screenDarkeningThresholds, screenThresholdLevels,
- screenDarkeningMinThresholdIdle, screenBrighteningMinThresholdIdle);
-
- // Idle ambient thresholds
+ // Ambient Lux - Idle Screen Brightness Thresholds
float ambientDarkeningMinThresholdIdle =
mDisplayDeviceConfig.getAmbientLuxDarkeningMinThresholdIdle();
float ambientBrighteningMinThresholdIdle =
mDisplayDeviceConfig.getAmbientLuxBrighteningMinThresholdIdle();
+ float[] ambientBrighteningThresholdsIdle =
+ mDisplayDeviceConfig.getAmbientBrighteningPercentagesIdle();
+ float[] ambientDarkeningThresholdsIdle =
+ mDisplayDeviceConfig.getAmbientDarkeningPercentagesIdle();
+ float[] ambientBrighteningLevelsIdle =
+ mDisplayDeviceConfig.getAmbientBrighteningLevelsIdle();
+ float[] ambientDarkeningLevelsIdle =
+ mDisplayDeviceConfig.getAmbientDarkeningLevelsIdle();
HysteresisLevels ambientBrightnessThresholdsIdle = new HysteresisLevels(
- ambientBrighteningThresholds, ambientDarkeningThresholds,
- ambientThresholdLevels, ambientDarkeningMinThresholdIdle,
- ambientBrighteningMinThresholdIdle);
+ ambientBrighteningThresholdsIdle, ambientDarkeningThresholdsIdle,
+ ambientBrighteningLevelsIdle, ambientDarkeningLevelsIdle,
+ ambientDarkeningMinThresholdIdle, ambientBrighteningMinThresholdIdle);
+
+ // Display - Idle Screen Brightness Thresholds
+ float screenDarkeningMinThresholdIdle =
+ mDisplayDeviceConfig.getScreenDarkeningMinThresholdIdle();
+ float screenBrighteningMinThresholdIdle =
+ mDisplayDeviceConfig.getScreenBrighteningMinThresholdIdle();
+ float[] screenBrighteningThresholdsIdle =
+ mDisplayDeviceConfig.getScreenBrighteningPercentagesIdle();
+ float[] screenDarkeningThresholdsIdle =
+ mDisplayDeviceConfig.getScreenDarkeningPercentagesIdle();
+ float[] screenBrighteningLevelsIdle =
+ mDisplayDeviceConfig.getScreenBrighteningLevelsIdle();
+ float[] screenDarkeningLevelsIdle =
+ mDisplayDeviceConfig.getScreenDarkeningLevelsIdle();
+ HysteresisLevels screenBrightnessThresholdsIdle = new HysteresisLevels(
+ screenBrighteningThresholdsIdle, screenDarkeningThresholdsIdle,
+ screenBrighteningLevelsIdle, screenDarkeningLevelsIdle,
+ screenDarkeningMinThresholdIdle, screenBrighteningMinThresholdIdle);
long brighteningLightDebounce = mDisplayDeviceConfig
.getAutoBrightnessBrighteningLightDebounce();
diff --git a/services/core/java/com/android/server/display/DisplayPowerController2.java b/services/core/java/com/android/server/display/DisplayPowerController2.java
index db4cd7f..172b4be 100644
--- a/services/core/java/com/android/server/display/DisplayPowerController2.java
+++ b/services/core/java/com/android/server/display/DisplayPowerController2.java
@@ -946,53 +946,77 @@
R.fraction.config_screenAutoBrightnessDozeScaleFactor,
1, 1);
- int[] ambientBrighteningThresholds = resources.getIntArray(
- R.array.config_ambientBrighteningThresholds);
- int[] ambientDarkeningThresholds = resources.getIntArray(
- R.array.config_ambientDarkeningThresholds);
- int[] ambientThresholdLevels = resources.getIntArray(
- R.array.config_ambientThresholdLevels);
+ // Ambient Lux - Active Mode Brightness Thresholds
+ float[] ambientBrighteningThresholds =
+ mDisplayDeviceConfig.getAmbientBrighteningPercentages();
+ float[] ambientDarkeningThresholds =
+ mDisplayDeviceConfig.getAmbientDarkeningPercentages();
+ float[] ambientBrighteningLevels =
+ mDisplayDeviceConfig.getAmbientBrighteningLevels();
+ float[] ambientDarkeningLevels =
+ mDisplayDeviceConfig.getAmbientDarkeningLevels();
float ambientDarkeningMinThreshold =
mDisplayDeviceConfig.getAmbientLuxDarkeningMinThreshold();
float ambientBrighteningMinThreshold =
mDisplayDeviceConfig.getAmbientLuxBrighteningMinThreshold();
HysteresisLevels ambientBrightnessThresholds = new HysteresisLevels(
ambientBrighteningThresholds, ambientDarkeningThresholds,
- ambientThresholdLevels, ambientDarkeningMinThreshold,
+ ambientBrighteningLevels, ambientDarkeningLevels, ambientDarkeningMinThreshold,
ambientBrighteningMinThreshold);
- int[] screenBrighteningThresholds = resources.getIntArray(
- R.array.config_screenBrighteningThresholds);
- int[] screenDarkeningThresholds = resources.getIntArray(
- R.array.config_screenDarkeningThresholds);
- float[] screenThresholdLevels = BrightnessMappingStrategy.getFloatArray(resources
- .obtainTypedArray(com.android.internal.R.array.config_screenThresholdLevels));
+ // Display - Active Mode Brightness Thresholds
+ float[] screenBrighteningThresholds =
+ mDisplayDeviceConfig.getScreenBrighteningPercentages();
+ float[] screenDarkeningThresholds =
+ mDisplayDeviceConfig.getScreenDarkeningPercentages();
+ float[] screenBrighteningLevels =
+ mDisplayDeviceConfig.getScreenBrighteningLevels();
+ float[] screenDarkeningLevels =
+ mDisplayDeviceConfig.getScreenDarkeningLevels();
float screenDarkeningMinThreshold =
mDisplayDeviceConfig.getScreenDarkeningMinThreshold();
float screenBrighteningMinThreshold =
mDisplayDeviceConfig.getScreenBrighteningMinThreshold();
HysteresisLevels screenBrightnessThresholds = new HysteresisLevels(
- screenBrighteningThresholds, screenDarkeningThresholds, screenThresholdLevels,
- screenDarkeningMinThreshold, screenBrighteningMinThreshold);
+ screenBrighteningThresholds, screenDarkeningThresholds,
+ screenBrighteningLevels, screenDarkeningLevels, screenDarkeningMinThreshold,
+ screenBrighteningMinThreshold, true);
- // Idle screen thresholds
- float screenDarkeningMinThresholdIdle =
- mDisplayDeviceConfig.getScreenDarkeningMinThresholdIdle();
- float screenBrighteningMinThresholdIdle =
- mDisplayDeviceConfig.getScreenBrighteningMinThresholdIdle();
- HysteresisLevels screenBrightnessThresholdsIdle = new HysteresisLevels(
- screenBrighteningThresholds, screenDarkeningThresholds, screenThresholdLevels,
- screenDarkeningMinThresholdIdle, screenBrighteningMinThresholdIdle);
-
- // Idle ambient thresholds
+ // Ambient Lux - Idle Screen Brightness Thresholds
float ambientDarkeningMinThresholdIdle =
mDisplayDeviceConfig.getAmbientLuxDarkeningMinThresholdIdle();
float ambientBrighteningMinThresholdIdle =
mDisplayDeviceConfig.getAmbientLuxBrighteningMinThresholdIdle();
+ float[] ambientBrighteningThresholdsIdle =
+ mDisplayDeviceConfig.getAmbientBrighteningPercentagesIdle();
+ float[] ambientDarkeningThresholdsIdle =
+ mDisplayDeviceConfig.getAmbientDarkeningPercentagesIdle();
+ float[] ambientBrighteningLevelsIdle =
+ mDisplayDeviceConfig.getAmbientBrighteningLevelsIdle();
+ float[] ambientDarkeningLevelsIdle =
+ mDisplayDeviceConfig.getAmbientDarkeningLevelsIdle();
HysteresisLevels ambientBrightnessThresholdsIdle = new HysteresisLevels(
- ambientBrighteningThresholds, ambientDarkeningThresholds,
- ambientThresholdLevels, ambientDarkeningMinThresholdIdle,
- ambientBrighteningMinThresholdIdle);
+ ambientBrighteningThresholdsIdle, ambientDarkeningThresholdsIdle,
+ ambientBrighteningLevelsIdle, ambientDarkeningLevelsIdle,
+ ambientDarkeningMinThresholdIdle, ambientBrighteningMinThresholdIdle);
+
+ // Display - Idle Screen Brightness Thresholds
+ float screenDarkeningMinThresholdIdle =
+ mDisplayDeviceConfig.getScreenDarkeningMinThresholdIdle();
+ float screenBrighteningMinThresholdIdle =
+ mDisplayDeviceConfig.getScreenBrighteningMinThresholdIdle();
+ float[] screenBrighteningThresholdsIdle =
+ mDisplayDeviceConfig.getScreenBrighteningPercentagesIdle();
+ float[] screenDarkeningThresholdsIdle =
+ mDisplayDeviceConfig.getScreenDarkeningPercentagesIdle();
+ float[] screenBrighteningLevelsIdle =
+ mDisplayDeviceConfig.getScreenBrighteningLevelsIdle();
+ float[] screenDarkeningLevelsIdle =
+ mDisplayDeviceConfig.getScreenDarkeningLevelsIdle();
+ HysteresisLevels screenBrightnessThresholdsIdle = new HysteresisLevels(
+ screenBrighteningThresholdsIdle, screenDarkeningThresholdsIdle,
+ screenBrighteningLevelsIdle, screenDarkeningLevelsIdle,
+ screenDarkeningMinThresholdIdle, screenBrighteningMinThresholdIdle);
long brighteningLightDebounce = mDisplayDeviceConfig
.getAutoBrightnessBrighteningLightDebounce();
diff --git a/services/core/java/com/android/server/display/HysteresisLevels.java b/services/core/java/com/android/server/display/HysteresisLevels.java
index abf8fe3..3c522e7 100644
--- a/services/core/java/com/android/server/display/HysteresisLevels.java
+++ b/services/core/java/com/android/server/display/HysteresisLevels.java
@@ -29,61 +29,60 @@
private static final boolean DEBUG = false;
- private final float[] mBrighteningThresholds;
- private final float[] mDarkeningThresholds;
- private final float[] mThresholdLevels;
+ private final float[] mBrighteningThresholdsPercentages;
+ private final float[] mDarkeningThresholdsPercentages;
+ private final float[] mBrighteningThresholdLevels;
+ private final float[] mDarkeningThresholdLevels;
private final float mMinDarkening;
private final float mMinBrightening;
/**
- * Creates a {@code HysteresisLevels} object for ambient brightness.
- * @param brighteningThresholds an array of brightening hysteresis constraint constants.
- * @param darkeningThresholds an array of darkening hysteresis constraint constants.
- * @param thresholdLevels a monotonically increasing array of threshold levels.
+ * Creates a {@code HysteresisLevels} object with the given equal-length
+ * float arrays.
+ * @param brighteningThresholdsPercentages 0-100 of thresholds
+ * @param darkeningThresholdsPercentages 0-100 of thresholds
+ * @param brighteningThresholdLevels float array of brightness values in the relevant units
* @param minBrighteningThreshold the minimum value for which the brightening value needs to
* return.
* @param minDarkeningThreshold the minimum value for which the darkening value needs to return.
+ * @param potentialOldBrightnessRange whether or not the values used could be from the old
+ * screen brightness range ie, between 1-255.
*/
- HysteresisLevels(int[] brighteningThresholds, int[] darkeningThresholds,
- int[] thresholdLevels, float minDarkeningThreshold, float minBrighteningThreshold) {
- if (brighteningThresholds.length != darkeningThresholds.length
- || darkeningThresholds.length != thresholdLevels.length + 1) {
+ HysteresisLevels(float[] brighteningThresholdsPercentages,
+ float[] darkeningThresholdsPercentages,
+ float[] brighteningThresholdLevels, float[] darkeningThresholdLevels,
+ float minDarkeningThreshold, float minBrighteningThreshold,
+ boolean potentialOldBrightnessRange) {
+ if (brighteningThresholdsPercentages.length != brighteningThresholdLevels.length
+ || darkeningThresholdsPercentages.length != darkeningThresholdLevels.length) {
throw new IllegalArgumentException("Mismatch between hysteresis array lengths.");
}
- mBrighteningThresholds = setArrayFormat(brighteningThresholds, 1000.0f);
- mDarkeningThresholds = setArrayFormat(darkeningThresholds, 1000.0f);
- mThresholdLevels = setArrayFormat(thresholdLevels, 1.0f);
+ mBrighteningThresholdsPercentages =
+ setArrayFormat(brighteningThresholdsPercentages, 100.0f);
+ mDarkeningThresholdsPercentages =
+ setArrayFormat(darkeningThresholdsPercentages, 100.0f);
+ mBrighteningThresholdLevels = setArrayFormat(brighteningThresholdLevels, 1.0f);
+ mDarkeningThresholdLevels = setArrayFormat(darkeningThresholdLevels, 1.0f);
mMinDarkening = minDarkeningThreshold;
mMinBrightening = minBrighteningThreshold;
}
- /**
- * Creates a {@code HysteresisLevels} object for screen brightness.
- * @param brighteningThresholds an array of brightening hysteresis constraint constants.
- * @param darkeningThresholds an array of darkening hysteresis constraint constants.
- * @param thresholdLevels a monotonically increasing array of threshold levels.
- * @param minBrighteningThreshold the minimum value for which the brightening value needs to
- * return.
- * @param minDarkeningThreshold the minimum value for which the darkening value needs to return.
- */
- HysteresisLevels(int[] brighteningThresholds, int[] darkeningThresholds,
- float[] thresholdLevels, float minDarkeningThreshold, float minBrighteningThreshold) {
- if (brighteningThresholds.length != darkeningThresholds.length
- || darkeningThresholds.length != thresholdLevels.length + 1) {
- throw new IllegalArgumentException("Mismatch between hysteresis array lengths.");
- }
- mBrighteningThresholds = setArrayFormat(brighteningThresholds, 1000.0f);
- mDarkeningThresholds = setArrayFormat(darkeningThresholds, 1000.0f);
- mThresholdLevels = constraintInRangeIfNeeded(thresholdLevels);
- mMinDarkening = minDarkeningThreshold;
- mMinBrightening = minBrighteningThreshold;
+ HysteresisLevels(float[] brighteningThresholdsPercentages,
+ float[] darkeningThresholdsPercentages,
+ float[] brighteningThresholdLevels, float[] darkeningThresholdLevels,
+ float minDarkeningThreshold, float minBrighteningThreshold) {
+ this(brighteningThresholdsPercentages, darkeningThresholdsPercentages,
+ brighteningThresholdLevels, darkeningThresholdLevels, minDarkeningThreshold,
+ minBrighteningThreshold, false);
}
/**
* Return the brightening hysteresis threshold for the given value level.
*/
public float getBrighteningThreshold(float value) {
- final float brightConstant = getReferenceLevel(value, mBrighteningThresholds);
+ final float brightConstant = getReferenceLevel(value,
+ mBrighteningThresholdLevels, mBrighteningThresholdsPercentages);
+
float brightThreshold = value * (1.0f + brightConstant);
if (DEBUG) {
Slog.d(TAG, "bright hysteresis constant=" + brightConstant + ", threshold="
@@ -98,7 +97,8 @@
* Return the darkening hysteresis threshold for the given value level.
*/
public float getDarkeningThreshold(float value) {
- final float darkConstant = getReferenceLevel(value, mDarkeningThresholds);
+ final float darkConstant = getReferenceLevel(value,
+ mDarkeningThresholdLevels, mDarkeningThresholdsPercentages);
float darkThreshold = value * (1.0f - darkConstant);
if (DEBUG) {
Slog.d(TAG, "dark hysteresis constant=: " + darkConstant + ", threshold="
@@ -111,60 +111,38 @@
/**
* Return the hysteresis constant for the closest threshold value from the given array.
*/
- private float getReferenceLevel(float value, float[] referenceLevels) {
- int index = 0;
- while (mThresholdLevels.length > index && value >= mThresholdLevels[index]) {
- ++index;
+ private float getReferenceLevel(float value, float[] thresholdLevels,
+ float[] thresholdPercentages) {
+ if (thresholdLevels == null || thresholdLevels.length == 0 || value < thresholdLevels[0]) {
+ return 0.0f;
}
- return referenceLevels[index];
+ int index = 0;
+ while (index < thresholdLevels.length - 1 && value >= thresholdLevels[index + 1]) {
+ index++;
+ }
+ return thresholdPercentages[index];
}
/**
* Return a float array where each i-th element equals {@code configArray[i]/divideFactor}.
*/
- private float[] setArrayFormat(int[] configArray, float divideFactor) {
+ private float[] setArrayFormat(float[] configArray, float divideFactor) {
float[] levelArray = new float[configArray.length];
for (int index = 0; levelArray.length > index; ++index) {
- levelArray[index] = (float) configArray[index] / divideFactor;
+ levelArray[index] = configArray[index] / divideFactor;
}
return levelArray;
}
- /**
- * This check is due to historical reasons, where screen thresholdLevels used to be
- * integer values in the range of [0-255], but then was changed to be float values from [0,1].
- * To accommodate both the possibilities, we first check if all the thresholdLevels are in [0,
- * 1], and if not, we divide all the levels with 255 to bring them down to the same scale.
- */
- private float[] constraintInRangeIfNeeded(float[] thresholdLevels) {
- if (isAllInRange(thresholdLevels, /* minValueInclusive = */ 0.0f, /* maxValueInclusive = */
- 1.0f)) {
- return thresholdLevels;
- }
-
- Slog.w(TAG, "Detected screen thresholdLevels on a deprecated brightness scale");
- float[] thresholdLevelsScaled = new float[thresholdLevels.length];
- for (int index = 0; thresholdLevels.length > index; ++index) {
- thresholdLevelsScaled[index] = thresholdLevels[index] / 255.0f;
- }
- return thresholdLevelsScaled;
- }
-
- private boolean isAllInRange(float[] configArray, float minValueInclusive,
- float maxValueInclusive) {
- int configArraySize = configArray.length;
- for (int index = 0; configArraySize > index; ++index) {
- if (configArray[index] < minValueInclusive || configArray[index] > maxValueInclusive) {
- return false;
- }
- }
- return true;
- }
-
void dump(PrintWriter pw) {
pw.println("HysteresisLevels");
- pw.println(" mBrighteningThresholds=" + Arrays.toString(mBrighteningThresholds));
- pw.println(" mDarkeningThresholds=" + Arrays.toString(mDarkeningThresholds));
- pw.println(" mThresholdLevels=" + Arrays.toString(mThresholdLevels));
+ pw.println(" mBrighteningThresholdLevels=" + Arrays.toString(mBrighteningThresholdLevels));
+ pw.println(" mBrighteningThresholdsPercentages="
+ + Arrays.toString(mBrighteningThresholdsPercentages));
+ pw.println(" mMinBrightening=" + mMinBrightening);
+ pw.println(" mDarkeningThresholdLevels=" + Arrays.toString(mDarkeningThresholdLevels));
+ pw.println(" mDarkeningThresholdsPercentages="
+ + Arrays.toString(mDarkeningThresholdsPercentages));
+ pw.println(" mMinDarkening=" + mMinDarkening);
}
}
diff --git a/services/core/java/com/android/server/inputmethod/IInputMethodInvoker.java b/services/core/java/com/android/server/inputmethod/IInputMethodInvoker.java
index 3e39746..82436cc 100644
--- a/services/core/java/com/android/server/inputmethod/IInputMethodInvoker.java
+++ b/services/core/java/com/android/server/inputmethod/IInputMethodInvoker.java
@@ -221,14 +221,12 @@
}
@AnyThread
- boolean updateEditorToolType(int toolType) {
+ void updateEditorToolType(@MotionEvent.ToolType int toolType) {
try {
mTarget.updateEditorToolType(toolType);
} catch (RemoteException e) {
logRemoteException(e);
- return false;
}
- return true;
}
@AnyThread
diff --git a/services/core/java/com/android/server/inputmethod/InputContentUriTokenHandler.java b/services/core/java/com/android/server/inputmethod/InputContentUriTokenHandler.java
index 5a0069a..789222e 100644
--- a/services/core/java/com/android/server/inputmethod/InputContentUriTokenHandler.java
+++ b/services/core/java/com/android/server/inputmethod/InputContentUriTokenHandler.java
@@ -1,18 +1,18 @@
/*
-** Copyright 2016, The Android Open Source Project
-**
-** Licensed under the Apache License, Version 2.0 (the "License");
-** you may not use this file except in compliance with the License.
-** You may obtain a copy of the License at
-**
-** http://www.apache.org/licenses/LICENSE-2.0
-**
-** Unless required by applicable law or agreed to in writing, software
-** distributed under the License is distributed on an "AS IS" BASIS,
-** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-** See the License for the specific language governing permissions and
-** limitations under the License.
-*/
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
package com.android.server.inputmethod;
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodDeviceConfigs.java b/services/core/java/com/android/server/inputmethod/InputMethodDeviceConfigs.java
index 2160b65..dc2799e 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodDeviceConfigs.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodDeviceConfigs.java
@@ -29,7 +29,7 @@
private boolean mHideImeWhenNoEditorFocus;
private final DeviceConfig.OnPropertiesChangedListener mDeviceConfigChangedListener;
- public InputMethodDeviceConfigs() {
+ InputMethodDeviceConfigs() {
mDeviceConfigChangedListener = properties -> {
if (!DeviceConfig.NAMESPACE_INPUT_METHOD_MANAGER.equals(properties.getNamespace())) {
return;
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
index 8a56afa..520a471 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
@@ -69,7 +69,6 @@
import android.app.ActivityManager;
import android.app.ActivityManagerInternal;
import android.app.AppGlobals;
-import android.app.AppOpsManager;
import android.app.KeyguardManager;
import android.app.Notification;
import android.app.NotificationManager;
@@ -307,7 +306,6 @@
final boolean mHasFeature;
private final ArrayMap<String, List<InputMethodSubtype>> mAdditionalSubtypeMap =
new ArrayMap<>();
- private final AppOpsManager mAppOpsManager;
private final UserManager mUserManager;
private final UserManagerInternal mUserManagerInternal;
private final InputMethodMenuController mMenuController;
@@ -1734,7 +1732,6 @@
mInputMethodDeviceConfigs = new InputMethodDeviceConfigs();
mImeDisplayValidator = mWindowManagerInternal::getDisplayImePolicy;
mDisplayManagerInternal = LocalServices.getService(DisplayManagerInternal.class);
- mAppOpsManager = mContext.getSystemService(AppOpsManager.class);
mUserManager = mContext.getSystemService(UserManager.class);
mUserManagerInternal = LocalServices.getService(UserManagerInternal.class);
mAccessibilityManager = AccessibilityManager.getInstance(context);
@@ -2520,7 +2517,7 @@
null, null, null, selectedMethodId, getSequenceNumberLocked(), null, false);
}
- if (!InputMethodUtils.checkIfPackageBelongsToUid(mAppOpsManager, cs.mUid,
+ if (!InputMethodUtils.checkIfPackageBelongsToUid(mPackageManagerInternal, cs.mUid,
editorInfo.packageName)) {
Slog.e(TAG, "Rejecting this client as it reported an invalid package name."
+ " uid=" + cs.mUid + " package=" + editorInfo.packageName);
@@ -3957,7 +3954,7 @@
return false;
}
if (getCurIntentLocked() != null && InputMethodUtils.checkIfPackageBelongsToUid(
- mAppOpsManager,
+ mPackageManagerInternal,
uid,
getCurIntentLocked().getComponent().getPackageName())) {
return true;
@@ -4156,6 +4153,7 @@
if (UserHandle.getCallingUserId() != userId) {
mContext.enforceCallingPermission(Manifest.permission.INTERACT_ACROSS_USERS_FULL, null);
}
+ final int callingUid = Binder.getCallingUid();
// By this IPC call, only a process which shares the same uid with the IME can add
// additional input method subtypes to the IME.
@@ -4176,7 +4174,7 @@
if (mSettings.getCurrentUserId() == userId) {
if (!mSettings.setAdditionalInputMethodSubtypes(imiId, toBeAdded,
- mAdditionalSubtypeMap, mIPackageManager)) {
+ mAdditionalSubtypeMap, mPackageManagerInternal, callingUid)) {
return;
}
final long ident = Binder.clearCallingIdentity();
@@ -4188,14 +4186,17 @@
return;
}
- final ArrayMap<String, InputMethodInfo> methodMap = queryMethodMapForUser(userId);
- final InputMethodSettings settings = new InputMethodSettings(mContext, methodMap,
- userId, false);
+ final ArrayMap<String, InputMethodInfo> methodMap = new ArrayMap<>();
+ final ArrayList<InputMethodInfo> methodList = new ArrayList<>();
final ArrayMap<String, List<InputMethodSubtype>> additionalSubtypeMap =
new ArrayMap<>();
AdditionalSubtypeUtils.load(additionalSubtypeMap, userId);
+ queryInputMethodServicesInternal(mContext, userId, additionalSubtypeMap, methodMap,
+ methodList, DirectBootAwareness.AUTO);
+ final InputMethodSettings settings = new InputMethodSettings(mContext, methodMap,
+ userId, false);
settings.setAdditionalInputMethodSubtypes(imiId, toBeAdded, additionalSubtypeMap,
- mIPackageManager);
+ mPackageManagerInternal, callingUid);
}
}
@@ -4208,8 +4209,8 @@
final int callingUid = Binder.getCallingUid();
final ComponentName imeComponentName =
imeId != null ? ComponentName.unflattenFromString(imeId) : null;
- if (imeComponentName == null || !InputMethodUtils.checkIfPackageBelongsToUid(mAppOpsManager,
- callingUid, imeComponentName.getPackageName())) {
+ if (imeComponentName == null || !InputMethodUtils.checkIfPackageBelongsToUid(
+ mPackageManagerInternal, callingUid, imeComponentName.getPackageName())) {
throw new SecurityException("Calling UID=" + callingUid + " does not belong to imeId="
+ imeId);
}
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodMenuController.java b/services/core/java/com/android/server/inputmethod/InputMethodMenuController.java
index 11e6923..a25630f 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodMenuController.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodMenuController.java
@@ -71,7 +71,7 @@
@Nullable
private InputMethodDialogWindowContext mDialogWindowContext;
- public InputMethodMenuController(InputMethodManagerService service) {
+ InputMethodMenuController(InputMethodManagerService service) {
mService = service;
mSettings = mService.mSettings;
mSwitchingController = mService.mSwitchingController;
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodUtils.java b/services/core/java/com/android/server/inputmethod/InputMethodUtils.java
index c57fe33..69b0661 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodUtils.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodUtils.java
@@ -20,16 +20,13 @@
import android.annotation.Nullable;
import android.annotation.UserHandleAware;
import android.annotation.UserIdInt;
-import android.app.AppOpsManager;
import android.content.ContentResolver;
import android.content.Context;
import android.content.pm.ApplicationInfo;
-import android.content.pm.IPackageManager;
import android.content.pm.PackageManager;
+import android.content.pm.PackageManagerInternal;
import android.content.res.Resources;
-import android.os.Binder;
import android.os.Build;
-import android.os.RemoteException;
import android.os.UserHandle;
import android.provider.Settings;
import android.text.TextUtils;
@@ -45,7 +42,6 @@
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.inputmethod.StartInputFlags;
-import com.android.internal.util.ArrayUtils;
import com.android.server.LocalServices;
import com.android.server.pm.UserManagerInternal;
import com.android.server.textservices.TextServicesManagerInternal;
@@ -80,29 +76,6 @@
}
// ----------------------------------------------------------------------
- // Utilities for debug
- static String getApiCallStack() {
- String apiCallStack = "";
- try {
- throw new RuntimeException();
- } catch (RuntimeException e) {
- final StackTraceElement[] frames = e.getStackTrace();
- for (int j = 1; j < frames.length; ++j) {
- final String tempCallStack = frames[j].toString();
- if (TextUtils.isEmpty(apiCallStack)) {
- // Overwrite apiCallStack if it's empty
- apiCallStack = tempCallStack;
- } else if (tempCallStack.indexOf("Transact(") < 0) {
- // Overwrite apiCallStack if it's not a binder call
- apiCallStack = tempCallStack;
- } else {
- break;
- }
- }
- }
- return apiCallStack;
- }
- // ----------------------------------------------------------------------
static boolean canAddToLastInputMethod(InputMethodSubtype subtype) {
if (subtype == null) return true;
@@ -210,28 +183,27 @@
return subtype != null
? TextUtils.concat(subtype.getDisplayName(context,
imi.getPackageName(), imi.getServiceInfo().applicationInfo),
- (TextUtils.isEmpty(imiLabel) ?
- "" : " - " + imiLabel))
+ (TextUtils.isEmpty(imiLabel) ? "" : " - " + imiLabel))
: imiLabel;
}
/**
* Returns true if a package name belongs to a UID.
*
- * <p>This is a simple wrapper of {@link AppOpsManager#checkPackage(int, String)}.</p>
- * @param appOpsManager the {@link AppOpsManager} object to be used for the validation.
+ * <p>This is a simple wrapper of
+ * {@link PackageManagerInternal#getPackageUid(String, long, int)}.</p>
+ * @param packageManagerInternal the {@link PackageManagerInternal} object to be used for the
+ * validation.
* @param uid the UID to be validated.
* @param packageName the package name.
* @return {@code true} if the package name belongs to the UID.
*/
- static boolean checkIfPackageBelongsToUid(AppOpsManager appOpsManager,
+ static boolean checkIfPackageBelongsToUid(PackageManagerInternal packageManagerInternal,
int uid, String packageName) {
- try {
- appOpsManager.checkPackage(uid, packageName);
- return true;
- } catch (SecurityException e) {
- return false;
- }
+ // PackageManagerInternal#getPackageUid() doesn't check MATCH_INSTANT/MATCH_APEX as of
+ // writing. So setting 0 should be fine.
+ return packageManagerInternal.getPackageUid(packageName, 0 /* flags */,
+ UserHandle.getUserId(uid)) == uid;
}
/**
@@ -674,8 +646,8 @@
List<InputMethodSubtype> implicitlyEnabledSubtypes =
SubtypeUtils.getImplicitlyApplicableSubtypesLocked(mRes, imi);
if (implicitlyEnabledSubtypes != null) {
- final int N = implicitlyEnabledSubtypes.size();
- for (int i = 0; i < N; ++i) {
+ final int numSubtypes = implicitlyEnabledSubtypes.size();
+ for (int i = 0; i < numSubtypes; ++i) {
final InputMethodSubtype st = implicitlyEnabledSubtypes.get(i);
if (String.valueOf(st.hashCode()).equals(subtypeHashCode)) {
return subtypeHashCode;
@@ -877,20 +849,13 @@
boolean setAdditionalInputMethodSubtypes(@NonNull String imeId,
@NonNull ArrayList<InputMethodSubtype> subtypes,
@NonNull ArrayMap<String, List<InputMethodSubtype>> additionalSubtypeMap,
- @NonNull IPackageManager packageManager) {
+ @NonNull PackageManagerInternal packageManagerInternal, int callingUid) {
final InputMethodInfo imi = mMethodMap.get(imeId);
if (imi == null) {
return false;
}
- final String[] packageInfos;
- try {
- packageInfos = packageManager.getPackagesForUid(Binder.getCallingUid());
- } catch (RemoteException e) {
- Slog.e(TAG, "Failed to get package infos");
- return false;
- }
- if (ArrayUtils.find(packageInfos,
- packageInfo -> TextUtils.equals(packageInfo, imi.getPackageName())) == null) {
+ if (!InputMethodUtils.checkIfPackageBelongsToUid(packageManagerInternal, callingUid,
+ imi.getPackageName())) {
return false;
}
diff --git a/services/core/java/com/android/server/inputmethod/LocaleUtils.java b/services/core/java/com/android/server/inputmethod/LocaleUtils.java
index 3d02b3a..f865e60 100644
--- a/services/core/java/com/android/server/inputmethod/LocaleUtils.java
+++ b/services/core/java/com/android/server/inputmethod/LocaleUtils.java
@@ -46,7 +46,7 @@
* @param desired The locale preferred by user.
* @return A score based on the locale matching for the default subtype enabling.
*/
- @IntRange(from=1, to=3)
+ @IntRange(from = 1, to = 3)
private static byte calculateMatchingSubScore(@NonNull final ULocale supported,
@NonNull final ULocale desired) {
// Assuming supported/desired is fully expanded.
@@ -111,7 +111,7 @@
* @return 1 if {@code left} is larger than {@code right}. -1 if {@code left} is less than
* {@code right}. 0 if {@code left} and {@code right} is equal.
*/
- @IntRange(from=-1, to=1)
+ @IntRange(from = -1, to = 1)
private static int compare(@NonNull byte[] left, @NonNull byte[] right) {
for (int i = 0; i < left.length; ++i) {
if (left[i] > right[i]) {
diff --git a/services/core/java/com/android/server/inputmethod/SubtypeUtils.java b/services/core/java/com/android/server/inputmethod/SubtypeUtils.java
index 7085868..f07539f 100644
--- a/services/core/java/com/android/server/inputmethod/SubtypeUtils.java
+++ b/services/core/java/com/android/server/inputmethod/SubtypeUtils.java
@@ -68,14 +68,14 @@
if (locale == null) {
return false;
}
- final int N = imi.getSubtypeCount();
- for (int i = 0; i < N; ++i) {
+ final int numSubtypes = imi.getSubtypeCount();
+ for (int i = 0; i < numSubtypes; ++i) {
final InputMethodSubtype subtype = imi.getSubtypeAt(i);
if (checkCountry) {
final Locale subtypeLocale = subtype.getLocaleObject();
- if (subtypeLocale == null ||
- !TextUtils.equals(subtypeLocale.getLanguage(), locale.getLanguage()) ||
- !TextUtils.equals(subtypeLocale.getCountry(), locale.getCountry())) {
+ if (subtypeLocale == null
+ || !TextUtils.equals(subtypeLocale.getLanguage(), locale.getLanguage())
+ || !TextUtils.equals(subtypeLocale.getCountry(), locale.getCountry())) {
continue;
}
} else {
@@ -260,8 +260,8 @@
boolean partialMatchFound = false;
InputMethodSubtype applicableSubtype = null;
InputMethodSubtype firstMatchedModeSubtype = null;
- final int N = subtypes.size();
- for (int i = 0; i < N; ++i) {
+ final int numSubtypes = subtypes.size();
+ for (int i = 0; i < numSubtypes; ++i) {
InputMethodSubtype subtype = subtypes.get(i);
final String subtypeLocale = subtype.getLocale();
final String subtypeLanguage = LocaleUtils.getLanguageFromLocaleString(subtypeLocale);
diff --git a/services/core/java/com/android/server/locksettings/LockSettingsService.java b/services/core/java/com/android/server/locksettings/LockSettingsService.java
index 533d1b0..58d677c 100644
--- a/services/core/java/com/android/server/locksettings/LockSettingsService.java
+++ b/services/core/java/com/android/server/locksettings/LockSettingsService.java
@@ -113,6 +113,7 @@
import android.util.LongSparseArray;
import android.util.Slog;
import android.util.SparseArray;
+import android.util.SparseIntArray;
import com.android.internal.R;
import com.android.internal.annotations.GuardedBy;
@@ -141,6 +142,7 @@
import com.android.server.locksettings.SyntheticPasswordManager.TokenType;
import com.android.server.locksettings.recoverablekeystore.RecoverableKeyStoreManager;
import com.android.server.pm.UserManagerInternal;
+import com.android.server.utils.Slogf;
import com.android.server.wm.WindowManagerInternal;
import libcore.util.HexEncoding;
@@ -243,6 +245,17 @@
private final RebootEscrowManager mRebootEscrowManager;
+ // Locking order is mUserCreationAndRemovalLock -> mSpManager.
+ private final Object mUserCreationAndRemovalLock = new Object();
+ // These two arrays are only used at boot time. To save memory, they are set to null when
+ // PHASE_BOOT_COMPLETED is reached.
+ @GuardedBy("mUserCreationAndRemovalLock")
+ private SparseIntArray mEarlyCreatedUsers = new SparseIntArray();
+ @GuardedBy("mUserCreationAndRemovalLock")
+ private SparseIntArray mEarlyRemovedUsers = new SparseIntArray();
+ @GuardedBy("mUserCreationAndRemovalLock")
+ private boolean mBootComplete;
+
// Current password metric for all users on the device. Updated when user unlocks
// the device or changes password. Removed when user is stopped.
@GuardedBy("this")
@@ -283,9 +296,16 @@
@Override
public void onBootPhase(int phase) {
super.onBootPhase(phase);
- if (phase == PHASE_ACTIVITY_MANAGER_READY) {
- mLockSettingsService.migrateOldDataAfterSystemReady();
- mLockSettingsService.loadEscrowData();
+ switch (phase) {
+ case PHASE_ACTIVITY_MANAGER_READY:
+ mLockSettingsService.migrateOldDataAfterSystemReady();
+ mLockSettingsService.loadEscrowData();
+ break;
+ case PHASE_BOOT_COMPLETED:
+ mLockSettingsService.bootCompleted();
+ break;
+ default:
+ break;
}
}
@@ -577,7 +597,6 @@
IntentFilter filter = new IntentFilter();
filter.addAction(Intent.ACTION_USER_ADDED);
filter.addAction(Intent.ACTION_USER_STARTING);
- filter.addAction(Intent.ACTION_USER_REMOVED);
injector.getContext().registerReceiverAsUser(mBroadcastReceiver, UserHandle.ALL, filter,
null, null);
@@ -720,28 +739,32 @@
}
/**
- * Clean up states associated with the given user, in case the userId is reused but LSS didn't
- * get a chance to do cleanup previously during ACTION_USER_REMOVED.
- *
- * Internally, LSS stores serial number for each user and check it against the current user's
- * serial number to determine if the userId is reused and invoke cleanup code.
+ * Removes the LSS state for the given userId if the userId was reused without its LSS state
+ * being fully removed.
+ * <p>
+ * This is primarily needed for users that were removed by Android 13 or earlier, which didn't
+ * guarantee removal of LSS state as it relied on the {@code ACTION_USER_REMOVED} intent. It is
+ * also needed because {@link #removeUser()} delays requests to remove LSS state until the
+ * {@code PHASE_BOOT_COMPLETED} boot phase, so they can be lost.
+ * <p>
+ * Stale state is detected by checking whether the user serial number changed. This works
+ * because user serial numbers are never reused.
*/
- private void cleanupDataForReusedUserIdIfNecessary(int userId) {
+ private void removeStateForReusedUserIdIfNecessary(@UserIdInt int userId, int serialNumber) {
if (userId == UserHandle.USER_SYSTEM) {
// Short circuit as we never clean up user 0.
return;
}
- // Serial number is never reusued, so we can use it as a distinguisher for user Id reuse.
- int serialNumber = mUserManager.getUserSerialNumber(userId);
-
int storedSerialNumber = mStorage.getInt(USER_SERIAL_NUMBER_KEY, -1, userId);
if (storedSerialNumber != serialNumber) {
// If LockSettingsStorage does not have a copy of the serial number, it could be either
// this is a user created before the serial number recording logic is introduced, or
// the user does not exist or was removed and cleaned up properly. In either case, don't
- // invoke removeUser().
+ // invoke removeUserState().
if (storedSerialNumber != -1) {
- removeUser(userId, /* unknownUser */ true);
+ Slogf.i(TAG, "Removing stale state for reused userId %d (serial %d => %d)", userId,
+ storedSerialNumber, serialNumber);
+ removeUserState(userId);
}
mStorage.setInt(USER_SERIAL_NUMBER_KEY, serialNumber, userId);
}
@@ -771,7 +794,6 @@
mHandler.post(new Runnable() {
@Override
public void run() {
- cleanupDataForReusedUserIdIfNecessary(userId);
ensureProfileKeystoreUnlocked(userId);
// Hide notification first, as tie managed profile lock takes time
hideEncryptionNotification(new UserHandle(userId));
@@ -779,38 +801,10 @@
if (isCredentialSharableWithParent(userId)) {
tieProfileLockIfNecessary(userId, LockscreenCredential.createNone());
}
-
- // If the user doesn't have a credential, try and derive their secret for the
- // AuthSecret HAL. The secret will have been enrolled if the user previously set a
- // credential and still needs to be passed to the HAL once that credential is
- // removed.
- if (mUserManager.getUserInfo(userId).isPrimary() && !isUserSecure(userId)) {
- tryDeriveVendorAuthSecretForUnsecuredPrimaryUser(userId);
- }
}
});
}
- private void tryDeriveVendorAuthSecretForUnsecuredPrimaryUser(@UserIdInt int userId) {
- synchronized (mSpManager) {
- // If there is no SP, then there is no vendor auth secret.
- if (!isSyntheticPasswordBasedCredentialLocked(userId)) {
- return;
- }
-
- final long protectorId = getCurrentLskfBasedProtectorId(userId);
- AuthenticationResult result =
- mSpManager.unlockLskfBasedProtector(getGateKeeperService(), protectorId,
- LockscreenCredential.createNone(), userId, null);
- if (result.syntheticPassword != null) {
- Slog.i(TAG, "Unwrapped SP for unsecured primary user " + userId);
- onSyntheticPasswordKnown(userId, result.syntheticPassword);
- } else {
- Slog.e(TAG, "Failed to unwrap SP for unsecured primary user " + userId);
- }
- }
- }
-
private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
@@ -821,11 +815,6 @@
} else if (Intent.ACTION_USER_STARTING.equals(intent.getAction())) {
final int userHandle = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, 0);
mStorage.prefetchUser(userHandle);
- } else if (Intent.ACTION_USER_REMOVED.equals(intent.getAction())) {
- final int userHandle = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, 0);
- if (userHandle > 0) {
- removeUser(userHandle, /* unknownUser= */ false);
- }
}
}
};
@@ -937,6 +926,79 @@
return success;
}
+ private void bootCompleted() {
+ synchronized (mUserCreationAndRemovalLock) {
+ // Handle delayed calls to LSS.removeUser() and LSS.createNewUser().
+ for (int i = 0; i < mEarlyRemovedUsers.size(); i++) {
+ int userId = mEarlyRemovedUsers.keyAt(i);
+ Slogf.i(TAG, "Removing locksettings state for removed user %d now that boot "
+ + "is complete", userId);
+ removeUserState(userId);
+ }
+ mEarlyRemovedUsers = null; // no longer needed
+ for (int i = 0; i < mEarlyCreatedUsers.size(); i++) {
+ int userId = mEarlyCreatedUsers.keyAt(i);
+ int serialNumber = mEarlyCreatedUsers.valueAt(i);
+
+ removeStateForReusedUserIdIfNecessary(userId, serialNumber);
+ synchronized (mSpManager) {
+ if (!isSyntheticPasswordBasedCredentialLocked(userId)) {
+ Slogf.i(TAG, "Creating locksettings state for user %d now that boot "
+ + "is complete", userId);
+ initializeSyntheticPasswordLocked(userId);
+ }
+ }
+ }
+ mEarlyCreatedUsers = null; // no longer needed
+
+ // Also do a one-time migration of all users to SP-based credentials with the CE key
+ // encrypted by the SP. This is needed for the system user on the first boot of a
+ // device, as the system user is special and never goes through the user creation flow
+ // that other users do. It is also needed for existing users on a device upgraded from
+ // Android 13 or earlier, where users with no LSKF didn't necessarily have an SP, and if
+ // they did have an SP then their CE key wasn't encrypted by it.
+ //
+ // If this gets interrupted (e.g. by the device powering off), there shouldn't be a
+ // problem since this will run again on the next boot, and setUserKeyProtection() is
+ // okay with the key being already protected by the given secret.
+ if (getString("migrated_all_users_to_sp_and_bound_ce", null, 0) == null) {
+ for (UserInfo user : mUserManager.getAliveUsers()) {
+ removeStateForReusedUserIdIfNecessary(user.id, user.serialNumber);
+ synchronized (mSpManager) {
+ migrateUserToSpWithBoundCeKeyLocked(user.id);
+ }
+ }
+ setString("migrated_all_users_to_sp_and_bound_ce", "true", 0);
+ }
+
+ mBootComplete = true;
+ }
+ }
+
+ @GuardedBy("mSpManager")
+ private void migrateUserToSpWithBoundCeKeyLocked(@UserIdInt int userId) {
+ if (isUserSecure(userId)) {
+ Slogf.d(TAG, "User %d is secured; no migration needed", userId);
+ return;
+ }
+ long protectorId = getCurrentLskfBasedProtectorId(userId);
+ if (protectorId == SyntheticPasswordManager.NULL_PROTECTOR_ID) {
+ Slogf.i(TAG, "Migrating unsecured user %d to SP-based credential", userId);
+ initializeSyntheticPasswordLocked(userId);
+ } else {
+ Slogf.i(TAG, "Existing unsecured user %d has a synthetic password; re-encrypting CE " +
+ "key with it", userId);
+ AuthenticationResult result = mSpManager.unlockLskfBasedProtector(
+ getGateKeeperService(), protectorId, LockscreenCredential.createNone(), userId,
+ null);
+ if (result.syntheticPassword == null) {
+ Slogf.wtf(TAG, "Failed to unwrap synthetic password for unsecured user %d", userId);
+ return;
+ }
+ setUserKeyProtection(userId, result.syntheticPassword.deriveFileBasedEncryptionKey());
+ }
+ }
+
/**
* Returns the lowest password quality that still presents the same UI for entering it.
*
@@ -1269,9 +1331,8 @@
* can end up calling into other system services to process user unlock request (via
* {@link com.android.server.SystemServiceManager#unlockUser} </em>
*/
- private void unlockUser(int userId, byte[] secret) {
- Slog.i(TAG, "Unlocking user " + userId + " with secret only, length "
- + (secret != null ? secret.length : 0));
+ private void unlockUser(@UserIdInt int userId) {
+ Slogf.i(TAG, "Unlocking user %d", userId);
// TODO: make this method fully async so we can update UI with progress strings
final boolean alreadyUnlocked = mUserManager.isUserUnlockingOrUnlocked(userId);
final CountDownLatch latch = new CountDownLatch(1);
@@ -1294,7 +1355,7 @@
};
try {
- mActivityManager.unlockUser(userId, null, secret, listener);
+ mActivityManager.unlockUser2(userId, listener);
} catch (RemoteException e) {
throw e.rethrowAsRuntimeException();
}
@@ -1587,6 +1648,7 @@
if (!savedCredential.isNone()) {
throw new IllegalStateException("Saved credential given, but user has no SP");
}
+ // TODO(b/232452368): this case is only needed by unit tests now; remove it.
initializeSyntheticPasswordLocked(userId);
} else if (savedCredential.isNone() && isProfileWithUnifiedLock(userId)) {
// get credential from keystore when profile has unified lock
@@ -1897,19 +1959,12 @@
mStorage.writeChildProfileLock(userId, ArrayUtils.concat(iv, ciphertext));
}
- private void setUserKeyProtection(int userId, byte[] key) {
- if (DEBUG) Slog.d(TAG, "setUserKeyProtection: user=" + userId);
- addUserKeyAuth(userId, key);
- }
-
- private void clearUserKeyProtection(int userId, byte[] secret) {
- if (DEBUG) Slog.d(TAG, "clearUserKeyProtection user=" + userId);
- final UserInfo userInfo = mUserManager.getUserInfo(userId);
+ private void setUserKeyProtection(@UserIdInt int userId, byte[] secret) {
final long callingId = Binder.clearCallingIdentity();
try {
- mStorageManager.clearUserKeyAuth(userId, userInfo.serialNumber, secret);
+ mStorageManager.setUserKeyProtection(userId, secret);
} catch (RemoteException e) {
- throw new IllegalStateException("clearUserKeyAuth failed user=" + userId);
+ throw new IllegalStateException("Failed to protect CE key for user " + userId, e);
} finally {
Binder.restoreCallingIdentity(callingId);
}
@@ -1924,40 +1979,51 @@
}
}
- /** Unlock file-based encryption */
- private void unlockUserKey(int userId, byte[] secret) {
+ /**
+ * Unlocks the user's CE (credential-encrypted) storage if it's not already unlocked.
+ * <p>
+ * This method doesn't throw exceptions because it is called opportunistically whenever a user
+ * is started. Whether it worked or not can be detected by whether the key got unlocked or not.
+ */
+ private void unlockUserKey(@UserIdInt int userId, SyntheticPassword sp) {
+ if (isUserKeyUnlocked(userId)) {
+ Slogf.d(TAG, "CE storage for user %d is already unlocked", userId);
+ return;
+ }
final UserInfo userInfo = mUserManager.getUserInfo(userId);
+ final String userType = isUserSecure(userId) ? "secured" : "unsecured";
+ final byte[] secret = sp.deriveFileBasedEncryptionKey();
try {
mStorageManager.unlockUserKey(userId, userInfo.serialNumber, secret);
+ Slogf.i(TAG, "Unlocked CE storage for %s user %d", userType, userId);
} catch (RemoteException e) {
- throw new IllegalStateException("Failed to unlock user key " + userId, e);
-
+ Slogf.wtf(TAG, e, "Failed to unlock CE storage for %s user %d", userType, userId);
+ } finally {
+ Arrays.fill(secret, (byte) 0);
}
}
- private void addUserKeyAuth(int userId, byte[] secret) {
- final UserInfo userInfo = mUserManager.getUserInfo(userId);
- final long callingId = Binder.clearCallingIdentity();
- try {
- mStorageManager.addUserKeyAuth(userId, userInfo.serialNumber, secret);
- } catch (RemoteException e) {
- throw new IllegalStateException("Failed to add new key to vold " + userId, e);
- } finally {
- Binder.restoreCallingIdentity(callingId);
- }
- }
-
- private void fixateNewestUserKeyAuth(int userId) {
- if (DEBUG) Slog.d(TAG, "fixateNewestUserKeyAuth: user=" + userId);
- final long callingId = Binder.clearCallingIdentity();
- try {
- mStorageManager.fixateNewestUserKeyAuth(userId);
- } catch (RemoteException e) {
- // OK to ignore the exception as vold would just accept both old and new
- // keys if this call fails, and will fix itself during the next boot
- Slog.w(TAG, "fixateNewestUserKeyAuth failed", e);
- } finally {
- Binder.restoreCallingIdentity(callingId);
+ private void unlockUserKeyIfUnsecured(@UserIdInt int userId) {
+ synchronized (mSpManager) {
+ if (isUserKeyUnlocked(userId)) {
+ Slogf.d(TAG, "CE storage for user %d is already unlocked", userId);
+ return;
+ }
+ if (isUserSecure(userId)) {
+ Slogf.d(TAG, "Not unlocking CE storage for user %d yet because user is secured",
+ userId);
+ return;
+ }
+ Slogf.i(TAG, "Unwrapping synthetic password for unsecured user %d", userId);
+ AuthenticationResult result = mSpManager.unlockLskfBasedProtector(
+ getGateKeeperService(), getCurrentLskfBasedProtectorId(userId),
+ LockscreenCredential.createNone(), userId, null);
+ if (result.syntheticPassword == null) {
+ Slogf.wtf(TAG, "Failed to unwrap synthetic password for unsecured user %d", userId);
+ return;
+ }
+ onSyntheticPasswordKnown(userId, result.syntheticPassword);
+ unlockUserKey(userId, result.syntheticPassword);
}
}
@@ -2228,8 +2294,50 @@
});
}
- private void removeUser(int userId, boolean unknownUser) {
- Slog.i(TAG, "RemoveUser: " + userId);
+ private void createNewUser(@UserIdInt int userId, int userSerialNumber) {
+ synchronized (mUserCreationAndRemovalLock) {
+ // Before PHASE_BOOT_COMPLETED, don't actually create the synthetic password yet, but
+ // rather automatically delay it to later. We do this because protecting the synthetic
+ // password requires the Weaver HAL if the device supports it, and some devices don't
+ // make Weaver available until fairly late in the boot process. This logic ensures a
+ // consistent flow across all devices, regardless of their Weaver implementation.
+ if (!mBootComplete) {
+ Slogf.i(TAG, "Delaying locksettings state creation for user %d until boot complete",
+ userId);
+ mEarlyCreatedUsers.put(userId, userSerialNumber);
+ mEarlyRemovedUsers.delete(userId);
+ return;
+ }
+ removeStateForReusedUserIdIfNecessary(userId, userSerialNumber);
+ synchronized (mSpManager) {
+ initializeSyntheticPasswordLocked(userId);
+ }
+ }
+ }
+
+ private void removeUser(@UserIdInt int userId) {
+ synchronized (mUserCreationAndRemovalLock) {
+ // Before PHASE_BOOT_COMPLETED, don't actually remove the LSS state yet, but rather
+ // automatically delay it to later. We do this because deleting synthetic password
+ // protectors requires the Weaver HAL if the device supports it, and some devices don't
+ // make Weaver available until fairly late in the boot process. This logic ensures a
+ // consistent flow across all devices, regardless of their Weaver implementation.
+ if (!mBootComplete) {
+ Slogf.i(TAG, "Delaying locksettings state removal for user %d until boot complete",
+ userId);
+ if (mEarlyCreatedUsers.indexOfKey(userId) >= 0) {
+ mEarlyCreatedUsers.delete(userId);
+ } else {
+ mEarlyRemovedUsers.put(userId, -1 /* unused */);
+ }
+ return;
+ }
+ Slogf.i(TAG, "Removing state for user %d", userId);
+ removeUserState(userId);
+ }
+ }
+
+ private void removeUserState(@UserIdInt int userId) {
removeBiometricsForUser(userId);
mSpManager.removeUser(getGateKeeperService(), userId);
mStrongAuth.removeUser(userId);
@@ -2238,11 +2346,9 @@
mManagedProfilePasswordCache.removePassword(userId);
gateKeeperClearSecureUserId(userId);
- if (unknownUser || isCredentialSharableWithParent(userId)) {
- removeKeystoreProfileKey(userId);
- }
- // Clean up storage last, this is to ensure that cleanupDataForReusedUserIdIfNecessary()
- // can make the assumption that no USER_SERIAL_NUMBER_KEY means user is fully removed.
+ removeKeystoreProfileKey(userId);
+ // Clean up storage last, so that removeStateForReusedUserIdIfNecessary() can assume that no
+ // USER_SERIAL_NUMBER_KEY means user is fully removed.
mStorage.removeUser(userId);
}
@@ -2497,8 +2603,11 @@
}
private void callToAuthSecretIfNeeded(@UserIdInt int userId, SyntheticPassword sp) {
- // Pass the primary user's auth secret to the HAL
- if (mAuthSecretService != null && mUserManager.getUserInfo(userId).isPrimary()) {
+ // If the given user is the primary user, pass the auth secret to the HAL. Only the system
+ // user can be primary. Check for the system user ID before calling getUserInfo(), as other
+ // users may still be under construction.
+ if (mAuthSecretService != null && userId == UserHandle.USER_SYSTEM &&
+ mUserManager.getUserInfo(userId).isPrimary()) {
try {
final byte[] rawSecret = sp.deriveVendorAuthSecret();
final ArrayList<Byte> secret = new ArrayList<>(rawSecret.length);
@@ -2513,9 +2622,13 @@
}
/**
- * Creates the synthetic password (SP) for the given user and protects it with an empty LSKF.
- * This is called just once in the lifetime of the user: the first time a nonempty LSKF is set,
- * or when an escrow token is activated on a device with an empty LSKF.
+ * Creates the synthetic password (SP) for the given user, protects it with an empty LSKF, and
+ * protects the user's CE key with a key derived from the SP.
+ * <p>
+ * This is called just once in the lifetime of the user: at user creation time (possibly delayed
+ * until {@code PHASE_BOOT_COMPLETED} to ensure that the Weaver HAL is available if the device
+ * supports it), or when upgrading from Android 13 or earlier where users with no LSKF didn't
+ * necessarily have an SP.
*/
@GuardedBy("mSpManager")
@VisibleForTesting
@@ -2529,6 +2642,7 @@
final long protectorId = mSpManager.createLskfBasedProtector(getGateKeeperService(),
LockscreenCredential.createNone(), sp, userId);
setCurrentLskfBasedProtectorId(protectorId, userId);
+ setUserKeyProtection(userId, sp.deriveFileBasedEncryptionKey());
onSyntheticPasswordKnown(userId, sp);
return sp;
}
@@ -2601,11 +2715,10 @@
unlockKeystore(sp.deriveKeyStorePassword(), userId);
- {
- final byte[] secret = sp.deriveFileBasedEncryptionKey();
- unlockUser(userId, secret);
- Arrays.fill(secret, (byte) 0);
- }
+ unlockUserKey(userId, sp);
+
+ unlockUser(userId);
+
activateEscrowTokens(sp, userId);
if (isProfileWithSeparatedLock(userId)) {
@@ -2626,9 +2739,9 @@
* be empty) and replacing the old LSKF-based protector with it. The SP itself is not changed.
*
* Also maintains the invariants described in {@link SyntheticPasswordManager} by
- * setting/clearing the protection (by the SP) on the user's file-based encryption key and
- * auth-bound Keystore keys when the LSKF is added/removed, respectively. If the new LSKF is
- * nonempty, then the Gatekeeper auth token is also refreshed.
+ * setting/clearing the protection (by the SP) on the user's auth-bound Keystore keys when the
+ * LSKF is added/removed, respectively. If the new LSKF is nonempty, then the Gatekeeper auth
+ * token is also refreshed.
*/
@GuardedBy("mSpManager")
private long setLockCredentialWithSpLocked(LockscreenCredential credential,
@@ -2648,8 +2761,6 @@
} else {
mSpManager.newSidForUser(getGateKeeperService(), sp, userId);
mSpManager.verifyChallenge(getGateKeeperService(), sp, 0L, userId);
- setUserKeyProtection(userId, sp.deriveFileBasedEncryptionKey());
- fixateNewestUserKeyAuth(userId);
setKeystorePassword(sp.deriveKeyStorePassword(), userId);
}
} else {
@@ -2659,9 +2770,7 @@
mSpManager.clearSidForUser(userId);
gateKeeperClearSecureUserId(userId);
- unlockUserKey(userId, sp.deriveFileBasedEncryptionKey());
- clearUserKeyProtection(userId, sp.deriveFileBasedEncryptionKey());
- fixateNewestUserKeyAuth(userId);
+ unlockUserKey(userId, sp);
unlockKeystore(sp.deriveKeyStorePassword(), userId);
setKeystorePassword(null, userId);
removeBiometricsForUser(userId);
@@ -2804,6 +2913,7 @@
if (!isUserSecure(userId)) {
long protectorId = getCurrentLskfBasedProtectorId(userId);
if (protectorId == SyntheticPasswordManager.NULL_PROTECTOR_ID) {
+ // TODO(b/232452368): this case is only needed by unit tests now; remove it.
sp = initializeSyntheticPasswordLocked(userId);
} else {
sp = mSpManager.unlockLskfBasedProtector(getGateKeeperService(), protectorId,
@@ -2888,7 +2998,7 @@
// If clearing credential, unlock the user manually in order to progress user start
// Call unlockUser() on a handler thread so no lock is held (either by LSS or by
// the caller like DPMS), otherwise it can lead to deadlock.
- mHandler.post(() -> unlockUser(userId, null));
+ mHandler.post(() -> unlockUser(userId));
}
notifyPasswordChanged(credential, userId);
notifySeparateProfileChallengeChanged(userId);
@@ -3041,6 +3151,9 @@
pw.decreaseIndent();
pw.println("PasswordHandleCount: " + mGatekeeperPasswords.size());
+ synchronized (mUserCreationAndRemovalLock) {
+ pw.println("BootComplete: " + mBootComplete);
+ }
}
private void dumpKeystoreKeys(IndentingPrintWriter pw) {
@@ -3197,6 +3310,21 @@
private final class LocalService extends LockSettingsInternal {
@Override
+ public void unlockUserKeyIfUnsecured(@UserIdInt int userId) {
+ LockSettingsService.this.unlockUserKeyIfUnsecured(userId);
+ }
+
+ @Override
+ public void createNewUser(@UserIdInt int userId, int userSerialNumber) {
+ LockSettingsService.this.createNewUser(userId, userSerialNumber);
+ }
+
+ @Override
+ public void removeUser(@UserIdInt int userId) {
+ LockSettingsService.this.removeUser(userId);
+ }
+
+ @Override
public long addEscrowToken(byte[] token, int userId,
EscrowTokenStateChangeCallback callback) {
return LockSettingsService.this.addEscrowToken(token, TOKEN_TYPE_STRONG, userId,
diff --git a/services/core/java/com/android/server/locksettings/SyntheticPasswordManager.java b/services/core/java/com/android/server/locksettings/SyntheticPasswordManager.java
index f1afb96..66bdadb 100644
--- a/services/core/java/com/android/server/locksettings/SyntheticPasswordManager.java
+++ b/services/core/java/com/android/server/locksettings/SyntheticPasswordManager.java
@@ -49,6 +49,7 @@
import com.android.internal.widget.LockscreenCredential;
import com.android.internal.widget.VerifyCredentialResponse;
import com.android.server.locksettings.LockSettingsStorage.PersistentData;
+import com.android.server.utils.Slogf;
import libcore.util.HexEncoding;
@@ -79,11 +80,12 @@
* LockscreenCredential. The LSKF may be empty (none). There may be escrow token-based
* protectors as well, only for specific use cases such as enterprise-managed users.
*
- * - While the user's LSKF is nonempty, the SP protects the user's CE (credential encrypted)
- * storage and auth-bound Keystore keys: the user's CE key is encrypted by an SP-derived secret,
- * and the user's Keystore and Gatekeeper passwords are other SP-derived secrets. However, while
- * the user's LSKF is empty, these protections are cleared; this is needed to invalidate the
- * auth-bound keys and make UserController.unlockUser() work with an empty secret.
+ * - The user's credential-encrypted storage is always protected by the SP.
+ *
+ * - The user's auth-bound Keystore keys are protected by the SP, but only while an LSKF is set.
+ * This works by setting the user's Keystore and Gatekeeper passwords to SP-derived secrets, but
+ * only while an LSKF is set. When the LSKF is removed, these passwords are cleared,
+ * invalidating the user's auth-bound keys.
*
* Files stored on disk for each user:
* For the SP itself, stored under NULL_PROTECTOR_ID:
@@ -1026,6 +1028,14 @@
long protectorId, @NonNull LockscreenCredential credential, int userId,
ICheckCredentialProgressCallback progressCallback) {
AuthenticationResult result = new AuthenticationResult();
+
+ if (protectorId == SyntheticPasswordManager.NULL_PROTECTOR_ID) {
+ // This should never happen, due to the migration done in LSS.bootCompleted().
+ Slogf.wtf(TAG, "Synthetic password not found for user %d", userId);
+ result.gkResponse = VerifyCredentialResponse.ERROR;
+ return result;
+ }
+
PasswordData pwd = PasswordData.fromBytes(loadState(PASSWORD_DATA_NAME, protectorId,
userId));
diff --git a/services/core/java/com/android/server/pm/ShortcutPackage.java b/services/core/java/com/android/server/pm/ShortcutPackage.java
index 0c601bf..890c891 100644
--- a/services/core/java/com/android/server/pm/ShortcutPackage.java
+++ b/services/core/java/com/android/server/pm/ShortcutPackage.java
@@ -1962,10 +1962,15 @@
continue;
case TAG_SHORTCUT:
- final ShortcutInfo si = parseShortcut(parser, packageName,
- shortcutUser.getUserId(), fromBackup);
- // Don't use addShortcut(), we don't need to save the icon.
- ret.mShortcuts.put(si.getId(), si);
+ try {
+ final ShortcutInfo si = parseShortcut(parser, packageName,
+ shortcutUser.getUserId(), fromBackup);
+ // Don't use addShortcut(), we don't need to save the icon.
+ ret.mShortcuts.put(si.getId(), si);
+ } catch (Exception e) {
+ // b/246540168 malformed shortcuts should be ignored
+ Slog.e(TAG, "Failed parsing shortcut.", e);
+ }
continue;
case TAG_SHARE_TARGET:
ret.mShareTargets.add(ShareTargetInfo.loadFromXml(parser));
diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java
index c39cbae..0a89d13 100644
--- a/services/core/java/com/android/server/pm/UserManagerService.java
+++ b/services/core/java/com/android/server/pm/UserManagerService.java
@@ -88,8 +88,6 @@
import android.os.storage.StorageManager;
import android.os.storage.StorageManagerInternal;
import android.provider.Settings;
-import android.security.GateKeeper;
-import android.service.gatekeeper.IGateKeeperService;
import android.service.voice.VoiceInteractionManagerInternal;
import android.stats.devicepolicy.DevicePolicyEnums;
import android.text.TextUtils;
@@ -4664,6 +4662,10 @@
StorageManager.FLAG_STORAGE_DE | StorageManager.FLAG_STORAGE_CE);
t.traceEnd();
+ t.traceBegin("LSS.createNewUser");
+ mLockPatternUtils.createNewUser(userId, userInfo.serialNumber);
+ t.traceEnd();
+
final Set<String> userTypeInstallablePackages =
mSystemPackageInstaller.getInstallablePackagesForUserType(userType);
t.traceBegin("PM.createNewUser");
@@ -5500,15 +5502,8 @@
Slog.i(LOG_TAG, "Destroying key for user " + userId + " failed, continuing anyway", e);
}
- // Cleanup gatekeeper secure user id
- try {
- final IGateKeeperService gk = GateKeeper.getService();
- if (gk != null) {
- gk.clearSecureUserId(userId);
- }
- } catch (Exception ex) {
- Slog.w(LOG_TAG, "unable to clear GK secure user id");
- }
+ // Cleanup lock settings
+ mLockPatternUtils.removeUser(userId);
// Cleanup package manager settings
mPm.cleanUpUser(this, userId);
diff --git a/services/core/java/com/android/server/power/PowerManagerService.java b/services/core/java/com/android/server/power/PowerManagerService.java
index 2d22b8f..f9352cb 100644
--- a/services/core/java/com/android/server/power/PowerManagerService.java
+++ b/services/core/java/com/android/server/power/PowerManagerService.java
@@ -5316,7 +5316,7 @@
private final class SuspendBlockerImpl implements SuspendBlocker {
private static final String UNKNOWN_ID = "unknown";
private final String mName;
- private final String mTraceName;
+ private final int mNameHash;
private int mReferenceCount;
// Maps suspend blocker IDs to a list (LongArray) of open acquisitions for the suspend
@@ -5325,7 +5325,7 @@
public SuspendBlockerImpl(String name) {
mName = name;
- mTraceName = "SuspendBlocker (" + name + ")";
+ mNameHash = mName.hashCode();
}
@Override
@@ -5336,7 +5336,8 @@
+ "\" was finalized without being released!");
mReferenceCount = 0;
mNativeWrapper.nativeReleaseSuspendBlocker(mName);
- Trace.asyncTraceEnd(Trace.TRACE_TAG_POWER, mTraceName, 0);
+ Trace.asyncTraceForTrackEnd(Trace.TRACE_TAG_POWER,
+ "SuspendBlockers", mNameHash);
}
} finally {
super.finalize();
@@ -5357,7 +5358,8 @@
if (DEBUG_SPEW) {
Slog.d(TAG, "Acquiring suspend blocker \"" + mName + "\".");
}
- Trace.asyncTraceBegin(Trace.TRACE_TAG_POWER, mTraceName, 0);
+ Trace.asyncTraceForTrackBegin(Trace.TRACE_TAG_POWER,
+ "SuspendBlockers", mName, mNameHash);
mNativeWrapper.nativeAcquireSuspendBlocker(mName);
}
}
@@ -5378,7 +5380,10 @@
Slog.d(TAG, "Releasing suspend blocker \"" + mName + "\".");
}
mNativeWrapper.nativeReleaseSuspendBlocker(mName);
- Trace.asyncTraceEnd(Trace.TRACE_TAG_POWER, mTraceName, 0);
+ if (Trace.isTagEnabled(Trace.TRACE_TAG_POWER)) {
+ Trace.asyncTraceForTrackEnd(Trace.TRACE_TAG_POWER,
+ "SuspendBlockers", mNameHash);
+ }
} else if (mReferenceCount < 0) {
Slog.wtf(TAG, "Suspend blocker \"" + mName
+ "\" was released without being acquired!", new Throwable());
diff --git a/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java b/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java
index c6128f9..8a84860 100644
--- a/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java
+++ b/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java
@@ -6813,20 +6813,31 @@
synchronized (mModemNetworkLock) {
if (displayTransport == TRANSPORT_CELLULAR) {
mModemIfaces = includeInStringArray(mModemIfaces, iface);
- if (DEBUG) Slog.d(TAG, "Note mobile iface " + iface + ": " + mModemIfaces);
+ if (DEBUG) {
+ Slog.d(TAG, "Note mobile iface " + iface + ": "
+ + Arrays.toString(mModemIfaces));
+ }
} else {
mModemIfaces = excludeFromStringArray(mModemIfaces, iface);
- if (DEBUG) Slog.d(TAG, "Note non-mobile iface " + iface + ": " + mModemIfaces);
+ if (DEBUG) {
+ Slog.d(TAG, "Note non-mobile iface " + iface + ": "
+ + Arrays.toString(mModemIfaces));
+ }
}
}
synchronized (mWifiNetworkLock) {
if (displayTransport == TRANSPORT_WIFI) {
mWifiIfaces = includeInStringArray(mWifiIfaces, iface);
- if (DEBUG) Slog.d(TAG, "Note wifi iface " + iface + ": " + mWifiIfaces);
+ if (DEBUG) {
+ Slog.d(TAG, "Note wifi iface " + iface + ": " + Arrays.toString(mWifiIfaces));
+ }
} else {
mWifiIfaces = excludeFromStringArray(mWifiIfaces, iface);
- if (DEBUG) Slog.d(TAG, "Note non-wifi iface " + iface + ": " + mWifiIfaces);
+ if (DEBUG) {
+ Slog.d(TAG, "Note non-wifi iface " + iface + ": "
+ + Arrays.toString(mWifiIfaces));
+ }
}
}
}
@@ -12622,8 +12633,7 @@
private void updateCpuMeasuredEnergyStatsLocked(@NonNull long[] clusterChargeUC,
@NonNull CpuDeltaPowerAccumulator accumulator) {
if (DEBUG_ENERGY) {
- Slog.d(TAG,
- "Updating cpu cluster stats: " + clusterChargeUC.toString());
+ Slog.d(TAG, "Updating cpu cluster stats: " + Arrays.toString(clusterChargeUC));
}
if (mGlobalMeasuredEnergyStats == null) {
return;
diff --git a/services/core/java/com/android/server/wm/AppTaskImpl.java b/services/core/java/com/android/server/wm/AppTaskImpl.java
index fd6c974..b160af6a 100644
--- a/services/core/java/com/android/server/wm/AppTaskImpl.java
+++ b/services/core/java/com/android/server/wm/AppTaskImpl.java
@@ -98,7 +98,7 @@
throw new IllegalArgumentException("Unable to find task ID " + mTaskId);
}
return mService.getRecentTasks().createRecentTaskInfo(task,
- false /* stripExtras */);
+ false /* stripExtras */, true /* getTasksAllowed */);
} finally {
Binder.restoreCallingIdentity(origId);
}
diff --git a/services/core/java/com/android/server/wm/RecentTasks.java b/services/core/java/com/android/server/wm/RecentTasks.java
index 4860762..1fc061b 100644
--- a/services/core/java/com/android/server/wm/RecentTasks.java
+++ b/services/core/java/com/android/server/wm/RecentTasks.java
@@ -976,7 +976,7 @@
continue;
}
- res.add(createRecentTaskInfo(task, true /* stripExtras */));
+ res.add(createRecentTaskInfo(task, true /* stripExtras */, getTasksAllowed));
}
return res;
}
@@ -1895,7 +1895,8 @@
/**
* Creates a new RecentTaskInfo from a Task.
*/
- ActivityManager.RecentTaskInfo createRecentTaskInfo(Task tr, boolean stripExtras) {
+ ActivityManager.RecentTaskInfo createRecentTaskInfo(Task tr, boolean stripExtras,
+ boolean getTasksAllowed) {
final ActivityManager.RecentTaskInfo rti = new ActivityManager.RecentTaskInfo();
// If the recent Task is detached, we consider it will be re-attached to the default
// TaskDisplayArea because we currently only support recent overview in the default TDA.
@@ -1907,6 +1908,9 @@
rti.id = rti.isRunning ? rti.taskId : INVALID_TASK_ID;
rti.persistentId = rti.taskId;
rti.lastSnapshotData.set(tr.mLastTaskSnapshotData);
+ if (!getTasksAllowed) {
+ Task.trimIneffectiveInfo(tr, rti);
+ }
// Fill in organized child task info for the task created by organizer.
if (tr.mCreatedByOrganizer) {
diff --git a/services/core/java/com/android/server/wm/RunningTasks.java b/services/core/java/com/android/server/wm/RunningTasks.java
index a753b55..9c85bc0 100644
--- a/services/core/java/com/android/server/wm/RunningTasks.java
+++ b/services/core/java/com/android/server/wm/RunningTasks.java
@@ -138,6 +138,10 @@
task.fillTaskInfo(rti, !mKeepIntentExtra);
// Fill in some deprecated values
rti.id = rti.taskId;
+
+ if (!mAllowed) {
+ Task.trimIneffectiveInfo(task, rti);
+ }
return rti;
}
}
diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java
index 62d93ad..5bd141a 100644
--- a/services/core/java/com/android/server/wm/Task.java
+++ b/services/core/java/com/android/server/wm/Task.java
@@ -3431,6 +3431,27 @@
info.isSleeping = shouldSleepActivities();
}
+ /**
+ * Removes the activity info if the activity belongs to a different uid, which is
+ * different from the app that hosts the task.
+ */
+ static void trimIneffectiveInfo(Task task, TaskInfo info) {
+ final ActivityRecord baseActivity = task.getActivity(r -> !r.finishing,
+ false /* traverseTopToBottom */);
+ final int baseActivityUid =
+ baseActivity != null ? baseActivity.getUid() : task.effectiveUid;
+
+ if (info.topActivityInfo != null
+ && task.effectiveUid != info.topActivityInfo.applicationInfo.uid) {
+ info.topActivity = null;
+ info.topActivityInfo = null;
+ }
+
+ if (task.effectiveUid != baseActivityUid) {
+ info.baseActivity = null;
+ }
+ }
+
@Nullable PictureInPictureParams getPictureInPictureParams() {
final Task topTask = getTopMostTask();
if (topTask == null) return null;
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index 1497a19..502b4bb 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -6012,7 +6012,11 @@
if (mFrozenDisplayId != INVALID_DISPLAY && mFrozenDisplayId == w.getDisplayId()
&& mWindowsFreezingScreen != WINDOWS_FREEZING_SCREENS_TIMEOUT) {
ProtoLog.v(WM_DEBUG_ORIENTATION, "Changing surface while display frozen: %s", w);
- w.setOrientationChanging(true);
+ // WindowsState#reportResized won't tell invisible requested window to redraw,
+ // so do not set it as changing orientation to avoid affecting draw state.
+ if (w.isVisibleRequested()) {
+ w.setOrientationChanging(true);
+ }
if (mWindowsFreezingScreen == WINDOWS_FREEZING_SCREENS_NONE) {
mWindowsFreezingScreen = WINDOWS_FREEZING_SCREENS_ACTIVE;
// XXX should probably keep timeout from
diff --git a/services/core/java/com/android/server/wm/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java
index e2f833c..2304ab4 100644
--- a/services/core/java/com/android/server/wm/WindowOrganizerController.java
+++ b/services/core/java/com/android/server/wm/WindowOrganizerController.java
@@ -46,6 +46,7 @@
import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_WINDOW_ORGANIZER;
import static com.android.server.wm.ActivityTaskManagerService.LAYOUT_REASON_CONFIG_CHANGED;
+import static com.android.server.wm.ActivityTaskManagerService.enforceTaskPermission;
import static com.android.server.wm.ActivityTaskSupervisor.PRESERVE_WINDOWS;
import static com.android.server.wm.Task.FLAG_FORCE_HIDDEN_FOR_PINNED_TASK;
import static com.android.server.wm.Task.FLAG_FORCE_HIDDEN_FOR_TASK_ORG;
@@ -240,8 +241,18 @@
}
@Override
- public IBinder startTransition(int type, @Nullable IBinder transitionToken,
+ public IBinder startNewTransition(int type, @Nullable WindowContainerTransaction t) {
+ return startTransition(type, null /* transitionToken */, t);
+ }
+
+ @Override
+ public void startTransition(@NonNull IBinder transitionToken,
@Nullable WindowContainerTransaction t) {
+ startTransition(-1 /* unused type */, transitionToken, t);
+ }
+
+ private IBinder startTransition(@WindowManager.TransitionType int type,
+ @Nullable IBinder transitionToken, @Nullable WindowContainerTransaction t) {
enforceTaskPermission("startTransition()");
final CallerInfo caller = new CallerInfo();
final long ident = Binder.clearCallingIdentity();
@@ -1549,10 +1560,6 @@
return (cfgChanges & CONTROLLABLE_CONFIGS) == 0;
}
- private void enforceTaskPermission(String func) {
- mService.enforceTaskPermission(func);
- }
-
private boolean isValidTransaction(@NonNull WindowContainerTransaction t) {
if (t.getTaskFragmentOrganizer() != null && !mTaskFragmentOrganizerController
.isOrganizerRegistered(t.getTaskFragmentOrganizer())) {
diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java
index 68fabc5..0f5184a 100644
--- a/services/core/java/com/android/server/wm/WindowState.java
+++ b/services/core/java/com/android/server/wm/WindowState.java
@@ -3868,7 +3868,7 @@
// configuration update when the window has requested to be hidden. Doing so can lead to
// the client erroneously accepting a configuration that would have otherwise caused an
// activity restart. We instead hand back the last reported {@link MergedConfiguration}.
- if (useLatestConfig || (relayoutVisible && (shouldCheckTokenVisibleRequested()
+ if (useLatestConfig || (relayoutVisible && (!shouldCheckTokenVisibleRequested()
|| mToken.isVisibleRequested()))) {
final Configuration globalConfig = getProcessGlobalConfiguration();
final Configuration overrideConfig = getMergedOverrideConfiguration();
diff --git a/services/core/jni/OWNERS b/services/core/jni/OWNERS
index 9abf107..2584b86 100644
--- a/services/core/jni/OWNERS
+++ b/services/core/jni/OWNERS
@@ -12,6 +12,7 @@
per-file com_android_server_am_BatteryStatsService.cpp = file:/BATTERY_STATS_OWNERS
per-file Android.bp = file:platform/build/soong:/OWNERS #{LAST_RESORT_SUGGESTION}
+per-file com_android_server_SystemClock* = file:/services/core/java/com/android/server/timedetector/OWNERS
per-file com_android_server_Usb* = file:/services/usb/OWNERS
per-file com_android_server_Vibrator* = file:/services/core/java/com/android/server/vibrator/OWNERS
per-file com_android_server_hdmi_* = file:/core/java/android/hardware/hdmi/OWNERS
diff --git a/services/core/jni/com_android_server_am_CachedAppOptimizer.cpp b/services/core/jni/com_android_server_am_CachedAppOptimizer.cpp
index 1d56078..d3d69ae 100644
--- a/services/core/jni/com_android_server_am_CachedAppOptimizer.cpp
+++ b/services/core/jni/com_android_server_am_CachedAppOptimizer.cpp
@@ -491,10 +491,10 @@
compactProcessOrFallback(pid, compactionFlags);
}
-static jint com_android_server_am_CachedAppOptimizer_freezeBinder(
- JNIEnv *env, jobject clazz, jint pid, jboolean freeze) {
-
- jint retVal = IPCThreadState::freeze(pid, freeze, 100 /* timeout [ms] */);
+static jint com_android_server_am_CachedAppOptimizer_freezeBinder(JNIEnv* env, jobject clazz,
+ jint pid, jboolean freeze,
+ jint timeout_ms) {
+ jint retVal = IPCThreadState::freeze(pid, freeze, timeout_ms);
if (retVal != 0 && retVal != -EAGAIN) {
jniThrowException(env, "java/lang/RuntimeException", "Unable to freeze/unfreeze binder");
}
@@ -548,7 +548,7 @@
(void*)com_android_server_am_CachedAppOptimizer_getMemoryFreedCompaction},
{"compactSystem", "()V", (void*)com_android_server_am_CachedAppOptimizer_compactSystem},
{"compactProcess", "(II)V", (void*)com_android_server_am_CachedAppOptimizer_compactProcess},
- {"freezeBinder", "(IZ)I", (void*)com_android_server_am_CachedAppOptimizer_freezeBinder},
+ {"freezeBinder", "(IZI)I", (void*)com_android_server_am_CachedAppOptimizer_freezeBinder},
{"getBinderFreezeInfo", "(I)I",
(void*)com_android_server_am_CachedAppOptimizer_getBinderFreezeInfo},
{"getFreezerCheckPath", "()Ljava/lang/String;",
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 267cff6..98e5f1d 100644
--- a/services/core/xsd/display-device-config/display-device-config.xsd
+++ b/services/core/xsd/display-device-config/display-device-config.xsd
@@ -351,6 +351,35 @@
<xs:annotation name="nonnull"/>
<xs:annotation name="final"/>
</xs:element>
+ <xs:sequence>
+ <!-- Thresholds as tenths of percent of current brightness level, at each level of
+ brightness -->
+ <xs:element name="brightnessThresholdPoints" type="thresholdPoints" maxOccurs="1" minOccurs="0">
+ <xs:annotation name="final"/>
+ </xs:element>
+ </xs:sequence>
+ </xs:complexType>
+
+ <xs:complexType name="thresholdPoints">
+ <xs:sequence>
+ <xs:element type="thresholdPoint" name="brightnessThresholdPoint" maxOccurs="unbounded" minOccurs="1">
+ <xs:annotation name="nonnull"/>
+ <xs:annotation name="final"/>
+ </xs:element>
+ </xs:sequence>
+ </xs:complexType>
+
+ <xs:complexType name="thresholdPoint">
+ <xs:sequence>
+ <xs:element type="nonNegativeDecimal" name="threshold">
+ <xs:annotation name="nonnull"/>
+ <xs:annotation name="final"/>
+ </xs:element>
+ <xs:element type="nonNegativeDecimal" name="percentage">
+ <xs:annotation name="nonnull"/>
+ <xs:annotation name="final"/>
+ </xs:element>
+ </xs:sequence>
</xs:complexType>
<xs:complexType name="autoBrightness">
diff --git a/services/core/xsd/display-device-config/schema/current.txt b/services/core/xsd/display-device-config/schema/current.txt
index f8bff75..748ef4b 100644
--- a/services/core/xsd/display-device-config/schema/current.txt
+++ b/services/core/xsd/display-device-config/schema/current.txt
@@ -13,7 +13,9 @@
public class BrightnessThresholds {
ctor public BrightnessThresholds();
+ method public final com.android.server.display.config.ThresholdPoints getBrightnessThresholdPoints();
method @NonNull public final java.math.BigDecimal getMinimum();
+ method public final void setBrightnessThresholdPoints(com.android.server.display.config.ThresholdPoints);
method public final void setMinimum(@NonNull java.math.BigDecimal);
}
@@ -204,6 +206,19 @@
method public final void setBrightnessThrottlingMap(@NonNull com.android.server.display.config.BrightnessThrottlingMap);
}
+ public class ThresholdPoint {
+ ctor public ThresholdPoint();
+ method @NonNull public final java.math.BigDecimal getPercentage();
+ method @NonNull public final java.math.BigDecimal getThreshold();
+ method public final void setPercentage(@NonNull java.math.BigDecimal);
+ method public final void setThreshold(@NonNull java.math.BigDecimal);
+ }
+
+ public class ThresholdPoints {
+ ctor public ThresholdPoints();
+ method @NonNull public final java.util.List<com.android.server.display.config.ThresholdPoint> getBrightnessThresholdPoint();
+ }
+
public class Thresholds {
ctor public Thresholds();
method @NonNull public final com.android.server.display.config.BrightnessThresholds getBrighteningThresholds();
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
index 972d4e8..a561307 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
@@ -7709,6 +7709,7 @@
Preconditions.checkCallAuthorization(
isProfileOwner(caller) || isDefaultDeviceOwner(caller)
|| hasCallingOrSelfPermission(permission.READ_NEARBY_STREAMING_POLICY));
+ Preconditions.checkCallAuthorization(hasCrossUsersPermission(caller, userId));
synchronized (getLockObject()) {
if (mOwners.hasProfileOwner(userId) || mOwners.hasDeviceOwner()) {
@@ -7749,6 +7750,7 @@
Preconditions.checkCallAuthorization(
isProfileOwner(caller) || isDefaultDeviceOwner(caller)
|| hasCallingOrSelfPermission(permission.READ_NEARBY_STREAMING_POLICY));
+ Preconditions.checkCallAuthorization(hasCrossUsersPermission(caller, userId));
synchronized (getLockObject()) {
if (mOwners.hasProfileOwner(userId) || mOwners.hasDeviceOwner()) {
diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java
index d8f282a..9e449ae 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -331,6 +331,8 @@
"com.android.server.wallpaper.WallpaperManagerService$Lifecycle";
private static final String AUTO_FILL_MANAGER_SERVICE_CLASS =
"com.android.server.autofill.AutofillManagerService";
+ private static final String CREDENTIAL_MANAGER_SERVICE_CLASS =
+ "com.android.server.credentials.CredentialManagerService";
private static final String CONTENT_CAPTURE_MANAGER_SERVICE_CLASS =
"com.android.server.contentcapture.ContentCaptureManagerService";
private static final String TRANSLATION_MANAGER_SERVICE_CLASS =
@@ -2571,6 +2573,12 @@
t.traceEnd();
}
+ if (mPackageManager.hasSystemFeature(PackageManager.FEATURE_CREDENTIALS)) {
+ t.traceBegin("StartCredentialManagerService");
+ mSystemServiceManager.startService(CREDENTIAL_MANAGER_SERVICE_CLASS);
+ t.traceEnd();
+ }
+
// Translation manager service
if (deviceHasConfigString(context, R.string.config_defaultTranslationService)) {
t.traceBegin("StartTranslationManagerService");
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 86d6169..f89d378 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java
@@ -364,7 +364,7 @@
mActiveProcesses.remove(r);
mRegisteredReceivers.remove(r.getPid());
return invocation.callRealMethod();
- }).when(r).killLocked(any(), any(), anyInt(), anyInt(), anyBoolean());
+ }).when(r).killLocked(any(), any(), anyInt(), anyInt(), anyBoolean(), anyBoolean());
// If we're entirely dead, rely on default behaviors above
if (dead) return r;
diff --git a/services/tests/servicestests/AndroidManifest.xml b/services/tests/servicestests/AndroidManifest.xml
index 7ae70eb..6551bde 100644
--- a/services/tests/servicestests/AndroidManifest.xml
+++ b/services/tests/servicestests/AndroidManifest.xml
@@ -113,7 +113,7 @@
<uses-sdk android:minSdkVersion="1"
android:targetSdkVersion="26"/>
- <application android:testOnly="true">
+ <application android:testOnly="true" android:debuggable="true">
<uses-library android:name="android.test.runner"/>
<service android:name="com.android.server.accounts.TestAccountType1AuthenticatorService"
diff --git a/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java b/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java
index 96c3823..2d2c76c 100644
--- a/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java
+++ b/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java
@@ -94,6 +94,7 @@
import androidx.test.filters.SmallTest;
+import com.android.internal.widget.LockPatternUtils;
import com.android.server.FgThread;
import com.android.server.SystemService;
import com.android.server.am.UserState.KeyEvictedCallback;
@@ -834,8 +835,7 @@
private void setUpAndStartUserInBackground(int userId) throws Exception {
setUpUser(userId, 0);
mUserController.startUser(userId, /* foreground= */ false);
- verify(mInjector.mStorageManagerMock, times(1))
- .unlockUserKey(userId, /* serialNumber= */ 0, /* secret= */ null);
+ verify(mInjector.mLockPatternUtilsMock, times(1)).unlockUserKeyIfUnsecured(userId);
mUserStates.put(userId, mUserController.getStartedUserState(userId));
}
@@ -843,8 +843,7 @@
setUpUser(userId, UserInfo.FLAG_PROFILE, false, UserManager.USER_TYPE_PROFILE_MANAGED);
assertThat(mUserController.startProfile(userId)).isTrue();
- verify(mInjector.mStorageManagerMock, times(1))
- .unlockUserKey(userId, /* serialNumber= */ 0, /* secret= */ null);
+ verify(mInjector.mLockPatternUtilsMock, times(1)).unlockUserKeyIfUnsecured(userId);
mUserStates.put(userId, mUserController.getStartedUserState(userId));
}
@@ -966,6 +965,7 @@
private final UserManagerInternal mUserManagerInternalMock;
private final WindowManagerService mWindowManagerMock;
private final KeyguardManager mKeyguardManagerMock;
+ private final LockPatternUtils mLockPatternUtilsMock;
private final Context mCtx;
@@ -982,6 +982,7 @@
mStorageManagerMock = mock(IStorageManager.class);
mKeyguardManagerMock = mock(KeyguardManager.class);
when(mKeyguardManagerMock.isDeviceSecure(anyInt())).thenReturn(true);
+ mLockPatternUtilsMock = mock(LockPatternUtils.class);
}
@Override
@@ -1081,6 +1082,11 @@
protected void dismissKeyguard(Runnable runnable, String reason) {
runnable.run();
}
+
+ @Override
+ protected LockPatternUtils getLockPatternUtils() {
+ return mLockPatternUtilsMock;
+ }
}
private static class TestHandler extends Handler {
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceProviderTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceProviderTest.java
index 41f7433..12b8264 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceProviderTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceProviderTest.java
@@ -39,7 +39,6 @@
import com.android.server.biometrics.log.BiometricContext;
import com.android.server.biometrics.sensors.BiometricScheduler;
-import com.android.server.biometrics.sensors.BiometricStateCallback;
import com.android.server.biometrics.sensors.HalClientMonitor;
import com.android.server.biometrics.sensors.LockoutResetDispatcher;
@@ -64,8 +63,6 @@
private IFace mDaemon;
@Mock
private BiometricContext mBiometricContext;
- @Mock
- private BiometricStateCallback mBiometricStateCallback;
private SensorProps[] mSensorProps;
private LockoutResetDispatcher mLockoutResetDispatcher;
@@ -94,8 +91,8 @@
mLockoutResetDispatcher = new LockoutResetDispatcher(mContext);
- mFaceProvider = new TestableFaceProvider(mDaemon, mContext, mBiometricStateCallback,
- mSensorProps, TAG, mLockoutResetDispatcher, mBiometricContext);
+ mFaceProvider = new TestableFaceProvider(mDaemon, mContext, mSensorProps, TAG,
+ mLockoutResetDispatcher, mBiometricContext);
}
@SuppressWarnings("rawtypes")
@@ -143,13 +140,11 @@
TestableFaceProvider(@NonNull IFace daemon,
@NonNull Context context,
- @NonNull BiometricStateCallback biometricStateCallback,
@NonNull SensorProps[] props,
@NonNull String halInstanceName,
@NonNull LockoutResetDispatcher lockoutResetDispatcher,
@NonNull BiometricContext biometricContext) {
- super(context, biometricStateCallback, props, halInstanceName, lockoutResetDispatcher,
- biometricContext);
+ super(context, props, halInstanceName, lockoutResetDispatcher, biometricContext);
mDaemon = daemon;
}
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/hidl/Face10Test.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/hidl/Face10Test.java
index a2cade7..116d2d5 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/hidl/Face10Test.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/hidl/Face10Test.java
@@ -43,7 +43,6 @@
import com.android.server.biometrics.log.BiometricContext;
import com.android.server.biometrics.sensors.BiometricScheduler;
-import com.android.server.biometrics.sensors.BiometricStateCallback;
import com.android.server.biometrics.sensors.LockoutResetDispatcher;
import org.junit.Before;
@@ -74,8 +73,6 @@
private BiometricScheduler mScheduler;
@Mock
private BiometricContext mBiometricContext;
- @Mock
- private BiometricStateCallback mBiometricStateCallback;
private final Handler mHandler = new Handler(Looper.getMainLooper());
private LockoutResetDispatcher mLockoutResetDispatcher;
@@ -106,8 +103,8 @@
resetLockoutRequiresChallenge);
Face10.sSystemClock = Clock.fixed(Instant.ofEpochMilli(100), ZoneId.of("PST"));
- mFace10 = new Face10(mContext, mBiometricStateCallback, sensorProps,
- mLockoutResetDispatcher, mHandler, mScheduler, mBiometricContext);
+ mFace10 = new Face10(mContext, sensorProps, mLockoutResetDispatcher, mHandler, mScheduler,
+ mBiometricContext);
mBinder = new Binder();
}
diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java
index 57ded99..6388c7d 100644
--- a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java
@@ -236,10 +236,9 @@
.setBlockedActivities(getBlockedActivities())
.build();
mDeviceImpl = new VirtualDeviceImpl(mContext,
- mAssociationInfo, new Binder(), /* uid */ 0, mInputController,
- (int associationId) -> {
- }, mPendingTrampolineCallback, mActivityListener, mRunningAppsChangedCallback,
- params);
+ mAssociationInfo, new Binder(), /* ownerUid */ 0, /* uniqueId */ 1,
+ mInputController, (int associationId) -> {}, mPendingTrampolineCallback,
+ mActivityListener, mRunningAppsChangedCallback, params);
}
@Test
diff --git a/services/tests/servicestests/src/com/android/server/display/AutomaticBrightnessControllerTest.java b/services/tests/servicestests/src/com/android/server/display/AutomaticBrightnessControllerTest.java
index fc2a4cf..0206839 100644
--- a/services/tests/servicestests/src/com/android/server/display/AutomaticBrightnessControllerTest.java
+++ b/services/tests/servicestests/src/com/android/server/display/AutomaticBrightnessControllerTest.java
@@ -420,13 +420,13 @@
@Test
public void testHysteresisLevels() {
- int[] ambientBrighteningThresholds = {100, 200};
- int[] ambientDarkeningThresholds = {400, 500};
- int[] ambientThresholdLevels = {500};
+ float[] ambientBrighteningThresholds = {50, 100};
+ float[] ambientDarkeningThresholds = {10, 20};
+ float[] ambientThresholdLevels = {0, 500};
float ambientDarkeningMinChangeThreshold = 3.0f;
float ambientBrighteningMinChangeThreshold = 1.5f;
HysteresisLevels hysteresisLevels = new HysteresisLevels(ambientBrighteningThresholds,
- ambientDarkeningThresholds, ambientThresholdLevels,
+ ambientDarkeningThresholds, ambientThresholdLevels, ambientThresholdLevels,
ambientDarkeningMinChangeThreshold, ambientBrighteningMinChangeThreshold);
// test low, activate minimum change thresholds.
@@ -435,16 +435,17 @@
assertEquals(1f, hysteresisLevels.getDarkeningThreshold(4.0f), EPSILON);
// test max
- assertEquals(12000f, hysteresisLevels.getBrighteningThreshold(10000.0f), EPSILON);
- assertEquals(5000f, hysteresisLevels.getDarkeningThreshold(10000.0f), EPSILON);
+ // epsilon is x2 here, since the next floating point value about 20,000 is 0.0019531 greater
+ assertEquals(20000f, hysteresisLevels.getBrighteningThreshold(10000.0f), EPSILON * 2);
+ assertEquals(8000f, hysteresisLevels.getDarkeningThreshold(10000.0f), EPSILON);
// test just below threshold
- assertEquals(548.9f, hysteresisLevels.getBrighteningThreshold(499f), EPSILON);
- assertEquals(299.4f, hysteresisLevels.getDarkeningThreshold(499f), EPSILON);
+ assertEquals(748.5f, hysteresisLevels.getBrighteningThreshold(499f), EPSILON);
+ assertEquals(449.1f, hysteresisLevels.getDarkeningThreshold(499f), EPSILON);
// test at (considered above) threshold
- assertEquals(600f, hysteresisLevels.getBrighteningThreshold(500f), EPSILON);
- assertEquals(250f, hysteresisLevels.getDarkeningThreshold(500f), EPSILON);
+ assertEquals(1000f, hysteresisLevels.getBrighteningThreshold(500f), EPSILON);
+ assertEquals(400f, hysteresisLevels.getDarkeningThreshold(500f), EPSILON);
}
@Test
diff --git a/services/tests/servicestests/src/com/android/server/display/DisplayDeviceConfigTest.java b/services/tests/servicestests/src/com/android/server/display/DisplayDeviceConfigTest.java
index 04702c4..a6a2419 100644
--- a/services/tests/servicestests/src/com/android/server/display/DisplayDeviceConfigTest.java
+++ b/services/tests/servicestests/src/com/android/server/display/DisplayDeviceConfigTest.java
@@ -31,6 +31,8 @@
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
+import com.android.internal.R;
+
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -46,6 +48,8 @@
@RunWith(AndroidJUnit4.class)
public final class DisplayDeviceConfigTest {
private DisplayDeviceConfig mDisplayDeviceConfig;
+ private static final float ZERO_DELTA = 0.0f;
+ private static final float SMALL_DELTA = 0.0001f;
@Mock
private Context mContext;
@@ -67,26 +71,74 @@
assertEquals(mDisplayDeviceConfig.getAmbientHorizonShort(), 50);
assertEquals(mDisplayDeviceConfig.getBrightnessRampDecreaseMaxMillis(), 3000);
assertEquals(mDisplayDeviceConfig.getBrightnessRampIncreaseMaxMillis(), 2000);
- assertEquals(mDisplayDeviceConfig.getAmbientLuxBrighteningMinThreshold(), 10.0f, 0.0f);
- assertEquals(mDisplayDeviceConfig.getAmbientLuxDarkeningMinThreshold(), 2.0f, 0.0f);
- assertEquals(mDisplayDeviceConfig.getBrightnessRampFastDecrease(), 0.01f, 0.0f);
- assertEquals(mDisplayDeviceConfig.getBrightnessRampFastIncrease(), 0.02f, 0.0f);
- assertEquals(mDisplayDeviceConfig.getBrightnessRampSlowIncrease(), 0.04f, 0.0f);
- assertEquals(mDisplayDeviceConfig.getBrightnessRampSlowDecrease(), 0.03f, 0.0f);
- assertEquals(mDisplayDeviceConfig.getBrightnessDefault(), 0.5f, 0.0f);
+ assertEquals(mDisplayDeviceConfig.getBrightnessRampFastDecrease(), 0.01f, ZERO_DELTA);
+ assertEquals(mDisplayDeviceConfig.getBrightnessRampFastIncrease(), 0.02f, ZERO_DELTA);
+ assertEquals(mDisplayDeviceConfig.getBrightnessRampSlowIncrease(), 0.04f, ZERO_DELTA);
+ assertEquals(mDisplayDeviceConfig.getBrightnessRampSlowDecrease(), 0.03f, ZERO_DELTA);
+ assertEquals(mDisplayDeviceConfig.getBrightnessDefault(), 0.5f, ZERO_DELTA);
assertArrayEquals(mDisplayDeviceConfig.getBrightness(), new float[]{0.0f, 0.62f, 1.0f},
- 0.0f);
- assertArrayEquals(mDisplayDeviceConfig.getNits(), new float[]{2.0f, 500.0f, 800.0f}, 0.0f);
+ ZERO_DELTA);
+ assertArrayEquals(mDisplayDeviceConfig.getNits(), new float[]{2.0f, 500.0f, 800.0f},
+ ZERO_DELTA);
assertArrayEquals(mDisplayDeviceConfig.getBacklight(), new float[]{0.0f, 0.62f, 1.0f},
- 0.0f);
- assertEquals(mDisplayDeviceConfig.getScreenBrighteningMinThreshold(), 0.001, 0.000001f);
- assertEquals(mDisplayDeviceConfig.getScreenDarkeningMinThreshold(), 0.002, 0.000001f);
+ ZERO_DELTA);
assertEquals(mDisplayDeviceConfig.getAutoBrightnessBrighteningLightDebounce(), 2000);
assertEquals(mDisplayDeviceConfig.getAutoBrightnessDarkeningLightDebounce(), 1000);
assertArrayEquals(mDisplayDeviceConfig.getAutoBrightnessBrighteningLevelsLux(), new
- float[]{0.0f, 50.0f, 80.0f}, 0.0f);
+ float[]{0.0f, 50.0f, 80.0f}, ZERO_DELTA);
assertArrayEquals(mDisplayDeviceConfig.getAutoBrightnessBrighteningLevelsNits(), new
- float[]{45.32f, 75.43f}, 0.0f);
+ float[]{45.32f, 75.43f}, ZERO_DELTA);
+
+ // Test thresholds
+ assertEquals(10, mDisplayDeviceConfig.getAmbientLuxBrighteningMinThreshold(),
+ ZERO_DELTA);
+ assertEquals(20, mDisplayDeviceConfig.getAmbientLuxBrighteningMinThresholdIdle(),
+ ZERO_DELTA);
+ assertEquals(30, mDisplayDeviceConfig.getAmbientLuxDarkeningMinThreshold(), ZERO_DELTA);
+ assertEquals(40, mDisplayDeviceConfig.getAmbientLuxDarkeningMinThresholdIdle(), ZERO_DELTA);
+
+ assertEquals(0.1f, mDisplayDeviceConfig.getScreenBrighteningMinThreshold(), ZERO_DELTA);
+ assertEquals(0.2f, mDisplayDeviceConfig.getScreenBrighteningMinThresholdIdle(), ZERO_DELTA);
+ assertEquals(0.3f, mDisplayDeviceConfig.getScreenDarkeningMinThreshold(), ZERO_DELTA);
+ assertEquals(0.4f, mDisplayDeviceConfig.getScreenDarkeningMinThresholdIdle(), ZERO_DELTA);
+
+ assertArrayEquals(new float[]{0, 0.10f, 0.20f},
+ mDisplayDeviceConfig.getScreenBrighteningLevels(), ZERO_DELTA);
+ assertArrayEquals(new float[]{9, 10, 11},
+ mDisplayDeviceConfig.getScreenBrighteningPercentages(), ZERO_DELTA);
+ assertArrayEquals(new float[]{0, 0.11f, 0.21f},
+ mDisplayDeviceConfig.getScreenDarkeningLevels(), ZERO_DELTA);
+ assertArrayEquals(new float[]{11, 12, 13},
+ mDisplayDeviceConfig.getScreenDarkeningPercentages(), ZERO_DELTA);
+
+ assertArrayEquals(new float[]{0, 100, 200},
+ mDisplayDeviceConfig.getAmbientBrighteningLevels(), ZERO_DELTA);
+ assertArrayEquals(new float[]{13, 14, 15},
+ mDisplayDeviceConfig.getAmbientBrighteningPercentages(), ZERO_DELTA);
+ assertArrayEquals(new float[]{0, 300, 400},
+ mDisplayDeviceConfig.getAmbientDarkeningLevels(), ZERO_DELTA);
+ assertArrayEquals(new float[]{15, 16, 17},
+ mDisplayDeviceConfig.getAmbientDarkeningPercentages(), ZERO_DELTA);
+
+ assertArrayEquals(new float[]{0, 0.12f, 0.22f},
+ mDisplayDeviceConfig.getScreenBrighteningLevelsIdle(), ZERO_DELTA);
+ assertArrayEquals(new float[]{17, 18, 19},
+ mDisplayDeviceConfig.getScreenBrighteningPercentagesIdle(), ZERO_DELTA);
+ assertArrayEquals(new float[]{0, 0.13f, 0.23f},
+ mDisplayDeviceConfig.getScreenDarkeningLevelsIdle(), ZERO_DELTA);
+ assertArrayEquals(new float[]{19, 20, 21},
+ mDisplayDeviceConfig.getScreenDarkeningPercentagesIdle(), ZERO_DELTA);
+
+ assertArrayEquals(new float[]{0, 500, 600},
+ mDisplayDeviceConfig.getAmbientBrighteningLevelsIdle(), ZERO_DELTA);
+ assertArrayEquals(new float[]{21, 22, 23},
+ mDisplayDeviceConfig.getAmbientBrighteningPercentagesIdle(), ZERO_DELTA);
+ assertArrayEquals(new float[]{0, 700, 800},
+ mDisplayDeviceConfig.getAmbientDarkeningLevelsIdle(), ZERO_DELTA);
+ assertArrayEquals(new float[]{23, 24, 25},
+ mDisplayDeviceConfig.getAmbientDarkeningPercentagesIdle(), ZERO_DELTA);
+
+
// Todo(brup): Add asserts for BrightnessThrottlingData, DensityMapping,
// HighBrightnessModeData AmbientLightSensor, RefreshRateLimitations and ProximitySensor.
}
@@ -95,9 +147,61 @@
public void testConfigValuesFromConfigResource() {
setupDisplayDeviceConfigFromConfigResourceFile();
assertArrayEquals(mDisplayDeviceConfig.getAutoBrightnessBrighteningLevelsNits(), new
- float[]{2.0f, 200.0f, 600.0f}, 0.0f);
+ float[]{2.0f, 200.0f, 600.0f}, ZERO_DELTA);
assertArrayEquals(mDisplayDeviceConfig.getAutoBrightnessBrighteningLevelsLux(), new
- float[]{0.0f, 0.0f, 110.0f, 500.0f}, 0.0f);
+ float[]{0.0f, 0.0f, 110.0f, 500.0f}, ZERO_DELTA);
+
+ // Test thresholds
+ assertEquals(0, mDisplayDeviceConfig.getAmbientLuxBrighteningMinThreshold(), ZERO_DELTA);
+ assertEquals(0, mDisplayDeviceConfig.getAmbientLuxBrighteningMinThresholdIdle(),
+ ZERO_DELTA);
+ assertEquals(0, mDisplayDeviceConfig.getAmbientLuxDarkeningMinThreshold(), ZERO_DELTA);
+ assertEquals(0, mDisplayDeviceConfig.getAmbientLuxDarkeningMinThresholdIdle(), ZERO_DELTA);
+
+ assertEquals(0, mDisplayDeviceConfig.getScreenBrighteningMinThreshold(), ZERO_DELTA);
+ assertEquals(0, mDisplayDeviceConfig.getScreenBrighteningMinThresholdIdle(), ZERO_DELTA);
+ assertEquals(0, mDisplayDeviceConfig.getScreenDarkeningMinThreshold(), ZERO_DELTA);
+ assertEquals(0, mDisplayDeviceConfig.getScreenDarkeningMinThresholdIdle(), ZERO_DELTA);
+
+ // screen levels will be considered "old screen brightness scale"
+ // and therefore will divide by 255
+ assertArrayEquals(new float[]{0, 42 / 255f, 43 / 255f},
+ mDisplayDeviceConfig.getScreenBrighteningLevels(), SMALL_DELTA);
+ assertArrayEquals(new float[]{35, 36, 37},
+ mDisplayDeviceConfig.getScreenBrighteningPercentages(), ZERO_DELTA);
+ assertArrayEquals(new float[]{0, 42 / 255f, 43 / 255f},
+ mDisplayDeviceConfig.getScreenDarkeningLevels(), SMALL_DELTA);
+ assertArrayEquals(new float[]{37, 38, 39},
+ mDisplayDeviceConfig.getScreenDarkeningPercentages(), ZERO_DELTA);
+
+ assertArrayEquals(new float[]{0, 30, 31},
+ mDisplayDeviceConfig.getAmbientBrighteningLevels(), ZERO_DELTA);
+ assertArrayEquals(new float[]{27, 28, 29},
+ mDisplayDeviceConfig.getAmbientBrighteningPercentages(), ZERO_DELTA);
+ assertArrayEquals(new float[]{0, 30, 31},
+ mDisplayDeviceConfig.getAmbientDarkeningLevels(), ZERO_DELTA);
+ assertArrayEquals(new float[]{29, 30, 31},
+ mDisplayDeviceConfig.getAmbientDarkeningPercentages(), ZERO_DELTA);
+
+ assertArrayEquals(new float[]{0, 42 / 255f, 43 / 255f},
+ mDisplayDeviceConfig.getScreenBrighteningLevelsIdle(), SMALL_DELTA);
+ assertArrayEquals(new float[]{35, 36, 37},
+ mDisplayDeviceConfig.getScreenBrighteningPercentagesIdle(), ZERO_DELTA);
+ assertArrayEquals(new float[]{0, 42 / 255f, 43 / 255f},
+ mDisplayDeviceConfig.getScreenDarkeningLevelsIdle(), SMALL_DELTA);
+ assertArrayEquals(new float[]{37, 38, 39},
+ mDisplayDeviceConfig.getScreenDarkeningPercentagesIdle(), ZERO_DELTA);
+
+ assertArrayEquals(new float[]{0, 30, 31},
+ mDisplayDeviceConfig.getAmbientBrighteningLevelsIdle(), ZERO_DELTA);
+ assertArrayEquals(new float[]{27, 28, 29},
+ mDisplayDeviceConfig.getAmbientBrighteningPercentagesIdle(), ZERO_DELTA);
+ assertArrayEquals(new float[]{0, 30, 31},
+ mDisplayDeviceConfig.getAmbientDarkeningLevelsIdle(), ZERO_DELTA);
+ assertArrayEquals(new float[]{29, 30, 31},
+ mDisplayDeviceConfig.getAmbientDarkeningPercentagesIdle(), ZERO_DELTA);
+
+
// Todo(brup): Add asserts for BrightnessThrottlingData, DensityMapping,
// HighBrightnessModeData AmbientLightSensor, RefreshRateLimitations and ProximitySensor.
}
@@ -152,11 +256,126 @@
+ "<ambientBrightnessChangeThresholds>\n"
+ "<brighteningThresholds>\n"
+ "<minimum>10</minimum>\n"
+ + "<brightnessThresholdPoints>\n"
+ + "<brightnessThresholdPoint>\n"
+ + "<threshold>0</threshold><percentage>13</percentage>\n"
+ + "</brightnessThresholdPoint>\n"
+ + "<brightnessThresholdPoint>\n"
+ + "<threshold>100</threshold><percentage>14</percentage>\n"
+ + "</brightnessThresholdPoint>\n"
+ + "<brightnessThresholdPoint>\n"
+ + "<threshold>200</threshold><percentage>15</percentage>\n"
+ + "</brightnessThresholdPoint>\n"
+ + "</brightnessThresholdPoints>\n"
+ "</brighteningThresholds>\n"
+ "<darkeningThresholds>\n"
- + "<minimum>2</minimum>\n"
+ + "<minimum>30</minimum>\n"
+ + "<brightnessThresholdPoints>\n"
+ + "<brightnessThresholdPoint>\n"
+ + "<threshold>0</threshold><percentage>15</percentage>\n"
+ + "</brightnessThresholdPoint>\n"
+ + "<brightnessThresholdPoint>\n"
+ + "<threshold>300</threshold><percentage>16</percentage>\n"
+ + "</brightnessThresholdPoint>\n"
+ + "<brightnessThresholdPoint>\n"
+ + "<threshold>400</threshold><percentage>17</percentage>\n"
+ + "</brightnessThresholdPoint>\n"
+ + "</brightnessThresholdPoints>\n"
+ "</darkeningThresholds>\n"
+ "</ambientBrightnessChangeThresholds>\n"
+ + "<displayBrightnessChangeThresholds>\n"
+ + "<brighteningThresholds>\n"
+ + "<minimum>0.1</minimum>\n"
+ + "<brightnessThresholdPoints>\n"
+ + "<brightnessThresholdPoint>\n"
+ + "<threshold>0</threshold>\n"
+ + "<percentage>9</percentage>\n"
+ + "</brightnessThresholdPoint>\n"
+ + "<brightnessThresholdPoint>\n"
+ + "<threshold>0.10</threshold>\n"
+ + "<percentage>10</percentage>\n"
+ + "</brightnessThresholdPoint>\n"
+ + "<brightnessThresholdPoint>\n"
+ + "<threshold>0.20</threshold>\n"
+ + "<percentage>11</percentage>\n"
+ + "</brightnessThresholdPoint>\n"
+ + "</brightnessThresholdPoints>\n"
+ + "</brighteningThresholds>\n"
+ + "<darkeningThresholds>\n"
+ + "<minimum>0.3</minimum>\n"
+ + "<brightnessThresholdPoints>\n"
+ + "<brightnessThresholdPoint>\n"
+ + "<threshold>0</threshold><percentage>11</percentage>\n"
+ + "</brightnessThresholdPoint>\n"
+ + "<brightnessThresholdPoint>\n"
+ + "<threshold>0.11</threshold><percentage>12</percentage>\n"
+ + "</brightnessThresholdPoint>\n"
+ + "<brightnessThresholdPoint>\n"
+ + "<threshold>0.21</threshold><percentage>13</percentage>\n"
+ + "</brightnessThresholdPoint>\n"
+ + "</brightnessThresholdPoints>\n"
+ + "</darkeningThresholds>\n"
+ + "</displayBrightnessChangeThresholds>\n"
+ + "<ambientBrightnessChangeThresholdsIdle>\n"
+ + "<brighteningThresholds>\n"
+ + "<minimum>20</minimum>\n"
+ + "<brightnessThresholdPoints>\n"
+ + "<brightnessThresholdPoint>\n"
+ + "<threshold>0</threshold><percentage>21</percentage>\n"
+ + "</brightnessThresholdPoint>\n"
+ + "<brightnessThresholdPoint>\n"
+ + "<threshold>500</threshold><percentage>22</percentage>\n"
+ + "</brightnessThresholdPoint>\n"
+ + "<brightnessThresholdPoint>\n"
+ + "<threshold>600</threshold><percentage>23</percentage>\n"
+ + "</brightnessThresholdPoint>\n"
+ + "</brightnessThresholdPoints>\n"
+ + "</brighteningThresholds>\n"
+ + "<darkeningThresholds>\n"
+ + "<minimum>40</minimum>\n"
+ + "<brightnessThresholdPoints>\n"
+ + "<brightnessThresholdPoint>\n"
+ + "<threshold>0</threshold><percentage>23</percentage>\n"
+ + "</brightnessThresholdPoint>\n"
+ + "<brightnessThresholdPoint>\n"
+ + "<threshold>700</threshold><percentage>24</percentage>\n"
+ + "</brightnessThresholdPoint>\n"
+ + "<brightnessThresholdPoint>\n"
+ + "<threshold>800</threshold><percentage>25</percentage>\n"
+ + "</brightnessThresholdPoint>\n"
+ + "</brightnessThresholdPoints>\n"
+ + "</darkeningThresholds>\n"
+ + "</ambientBrightnessChangeThresholdsIdle>\n"
+ + "<displayBrightnessChangeThresholdsIdle>\n"
+ + "<brighteningThresholds>\n"
+ + "<minimum>0.2</minimum>\n"
+ + "<brightnessThresholdPoints>\n"
+ + "<brightnessThresholdPoint>\n"
+ + "<threshold>0</threshold><percentage>17</percentage>\n"
+ + "</brightnessThresholdPoint>\n"
+ + "<brightnessThresholdPoint>\n"
+ + "<threshold>0.12</threshold><percentage>18</percentage>\n"
+ + "</brightnessThresholdPoint>\n"
+ + "<brightnessThresholdPoint>\n"
+ + "<threshold>0.22</threshold><percentage>19</percentage>\n"
+ + "</brightnessThresholdPoint>\n"
+ + "</brightnessThresholdPoints>\n"
+ + "</brighteningThresholds>\n"
+ + "<darkeningThresholds>\n"
+ + "<minimum>0.4</minimum>\n"
+ + "<brightnessThresholdPoints>\n"
+ + "<brightnessThresholdPoint>\n"
+ + "<threshold>0</threshold><percentage>19</percentage>\n"
+ + "</brightnessThresholdPoint>\n"
+ + "<brightnessThresholdPoint>\n"
+ + "<threshold>0.13</threshold><percentage>20</percentage>\n"
+ + "</brightnessThresholdPoint>\n"
+ + "<brightnessThresholdPoint>\n"
+ + "<threshold>0.23</threshold><percentage>21</percentage>\n"
+ + "</brightnessThresholdPoint>\n"
+ + "</brightnessThresholdPoints>\n"
+ + "</darkeningThresholds>\n"
+ + "</displayBrightnessChangeThresholdsIdle>\n"
+ "<screenBrightnessRampFastDecrease>0.01</screenBrightnessRampFastDecrease> "
+ "<screenBrightnessRampFastIncrease>0.02</screenBrightnessRampFastIncrease> "
+ "<screenBrightnessRampSlowDecrease>0.03</screenBrightnessRampSlowDecrease>"
@@ -169,18 +388,6 @@
+ "</screenBrightnessRampDecreaseMaxMillis>"
+ "<ambientLightHorizonLong>5000</ambientLightHorizonLong>\n"
+ "<ambientLightHorizonShort>50</ambientLightHorizonShort>\n"
- + "<displayBrightnessChangeThresholds>"
- + "<brighteningThresholds>"
- + "<minimum>"
- + "0.001"
- + "</minimum>"
- + "</brighteningThresholds>"
- + "<darkeningThresholds>"
- + "<minimum>"
- + "0.002"
- + "</minimum>"
- + "</darkeningThresholds>"
- + "</displayBrightnessChangeThresholds>"
+ "<screenBrightnessRampIncreaseMaxMillis>"
+ "2000"
+ "</screenBrightnessRampIncreaseMaxMillis>\n"
@@ -239,8 +446,24 @@
com.android.internal.R.array.config_autoBrightnessLevels))
.thenReturn(screenBrightnessLevelLux);
- mDisplayDeviceConfig = DisplayDeviceConfig.create(mContext, true);
+ // Thresholds
+ // Config.xml requires the levels arrays to be of length N and the thresholds arrays to be
+ // of length N+1
+ when(mResources.getIntArray(com.android.internal.R.array.config_ambientThresholdLevels))
+ .thenReturn(new int[]{30, 31});
+ when(mResources.getIntArray(com.android.internal.R.array.config_screenThresholdLevels))
+ .thenReturn(new int[]{42, 43});
+ when(mResources.getIntArray(
+ com.android.internal.R.array.config_ambientBrighteningThresholds))
+ .thenReturn(new int[]{270, 280, 290});
+ when(mResources.getIntArray(com.android.internal.R.array.config_ambientDarkeningThresholds))
+ .thenReturn(new int[]{290, 300, 310});
+ when(mResources.getIntArray(R.array.config_screenBrighteningThresholds))
+ .thenReturn(new int[]{350, 360, 370});
+ when(mResources.getIntArray(R.array.config_screenDarkeningThresholds))
+ .thenReturn(new int[]{370, 380, 390});
+ mDisplayDeviceConfig = DisplayDeviceConfig.create(mContext, true);
}
private TypedArray createFloatTypedArray(float[] vals) {
diff --git a/services/tests/servicestests/src/com/android/server/locksettings/BaseLockSettingsServiceTests.java b/services/tests/servicestests/src/com/android/server/locksettings/BaseLockSettingsServiceTests.java
index e220841..c934e65 100644
--- a/services/tests/servicestests/src/com/android/server/locksettings/BaseLockSettingsServiceTests.java
+++ b/services/tests/servicestests/src/com/android/server/locksettings/BaseLockSettingsServiceTests.java
@@ -168,16 +168,15 @@
allUsers.add(SECONDARY_USER_INFO);
when(mUserManager.getUsers()).thenReturn(allUsers);
- when(mActivityManager.unlockUser(anyInt(), any(), any(), any())).thenAnswer(
- new Answer<Boolean>() {
- @Override
- public Boolean answer(InvocationOnMock invocation) throws Throwable {
+ when(mActivityManager.unlockUser2(anyInt(), any())).thenAnswer(
+ invocation -> {
Object[] args = invocation.getArguments();
- mStorageManager.unlockUser((int)args[0], (byte[])args[2],
- (IProgressListener) args[3]);
+ int userId = (int) args[0];
+ IProgressListener listener = (IProgressListener) args[1];
+ listener.onStarted(userId, null);
+ listener.onFinished(userId, null);
return true;
- }
- });
+ });
// Adding a fake Device Owner app which will enable escrow token support in LSS.
when(mDevicePolicyManager.getDeviceOwnerComponentOnAnyUser()).thenReturn(
@@ -215,37 +214,20 @@
private IStorageManager setUpStorageManagerMock() throws RemoteException {
final IStorageManager sm = mock(IStorageManager.class);
- doAnswer(new Answer<Void>() {
- @Override
- public Void answer(InvocationOnMock invocation) throws Throwable {
- Object[] args = invocation.getArguments();
- mStorageManager.addUserKeyAuth((int) args[0] /* userId */,
- (int) args[1] /* serialNumber */,
- (byte[]) args[2] /* secret */);
- return null;
- }
- }).when(sm).addUserKeyAuth(anyInt(), anyInt(), any());
+ doAnswer(invocation -> {
+ Object[] args = invocation.getArguments();
+ mStorageManager.unlockUserKey(/* userId= */ (int) args[0],
+ /* secret= */ (byte[]) args[2]);
+ return null;
+ }).when(sm).unlockUserKey(anyInt(), anyInt(), any());
- doAnswer(new Answer<Void>() {
- @Override
- public Void answer(InvocationOnMock invocation) throws Throwable {
- Object[] args = invocation.getArguments();
- mStorageManager.clearUserKeyAuth((int) args[0] /* userId */,
- (int) args[1] /* serialNumber */,
- (byte[]) args[2] /* secret */);
- return null;
- }
- }).when(sm).clearUserKeyAuth(anyInt(), anyInt(), any());
+ doAnswer(invocation -> {
+ Object[] args = invocation.getArguments();
+ mStorageManager.setUserKeyProtection(/* userId= */ (int) args[0],
+ /* secret= */ (byte[]) args[1]);
+ return null;
+ }).when(sm).setUserKeyProtection(anyInt(), any());
- doAnswer(
- new Answer<Void>() {
- @Override
- public Void answer(InvocationOnMock invocation) throws Throwable {
- Object[] args = invocation.getArguments();
- mStorageManager.fixateNewestUserKeyAuth((int) args[0] /* userId */);
- return null;
- }
- }).when(sm).fixateNewestUserKeyAuth(anyInt());
return sm;
}
diff --git a/services/tests/servicestests/src/com/android/server/locksettings/FakeStorageManager.java b/services/tests/servicestests/src/com/android/server/locksettings/FakeStorageManager.java
index 619ef70..91f3fed 100644
--- a/services/tests/servicestests/src/com/android/server/locksettings/FakeStorageManager.java
+++ b/services/tests/servicestests/src/com/android/server/locksettings/FakeStorageManager.java
@@ -16,75 +16,26 @@
package com.android.server.locksettings;
-import android.os.IProgressListener;
-import android.os.RemoteException;
+import static com.google.common.truth.Truth.assertThat;
+
import android.util.ArrayMap;
-
-import junit.framework.AssertionFailedError;
-
-import java.util.ArrayList;
-import java.util.Arrays;
-
public class FakeStorageManager {
- private ArrayMap<Integer, ArrayList<byte[]>> mAuth = new ArrayMap<>();
- private boolean mIgnoreBadUnlock;
+ private final ArrayMap<Integer, byte[]> mUserSecrets = new ArrayMap<>();
- public void addUserKeyAuth(int userId, int serialNumber, byte[] secret) {
- getUserAuth(userId).add(secret);
- }
-
- public void clearUserKeyAuth(int userId, int serialNumber, byte[] secret) {
- ArrayList<byte[]> auths = getUserAuth(userId);
- if (secret == null) {
- return;
- }
- auths.remove(secret);
- auths.add(null);
- }
-
- public void fixateNewestUserKeyAuth(int userId) {
- ArrayList<byte[]> auths = mAuth.get(userId);
- byte[] latest = auths.get(auths.size() - 1);
- auths.clear();
- auths.add(latest);
- }
-
- private ArrayList<byte[]> getUserAuth(int userId) {
- if (!mAuth.containsKey(userId)) {
- ArrayList<byte[]> auths = new ArrayList<>();
- auths.add(null);
- mAuth.put(userId, auths);
- }
- return mAuth.get(userId);
+ public void setUserKeyProtection(int userId, byte[] secret) {
+ assertThat(mUserSecrets).doesNotContainKey(userId);
+ mUserSecrets.put(userId, secret);
}
public byte[] getUserUnlockToken(int userId) {
- ArrayList<byte[]> auths = getUserAuth(userId);
- if (auths.size() != 1) {
- throw new AssertionFailedError("More than one secret exists");
- }
- return auths.get(0);
+ byte[] secret = mUserSecrets.get(userId);
+ assertThat(secret).isNotNull();
+ return secret;
}
- public void unlockUser(int userId, byte[] secret, IProgressListener listener)
- throws RemoteException {
- listener.onStarted(userId, null);
- listener.onFinished(userId, null);
- ArrayList<byte[]> auths = getUserAuth(userId);
- if (auths.size() > 1) {
- throw new AssertionFailedError("More than one secret exists");
- }
- byte[] auth = auths.get(0);
- if (!Arrays.equals(secret, auth)) {
- if (!mIgnoreBadUnlock) {
- throw new AssertionFailedError("Invalid secret to unlock user " + userId);
- }
- }
- }
-
- public void setIgnoreBadUnlock(boolean ignore) {
- mIgnoreBadUnlock = ignore;
+ public void unlockUserKey(int userId, byte[] secret) {
+ assertThat(mUserSecrets.get(userId)).isEqualTo(secret);
}
}
diff --git a/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsServiceTests.java b/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsServiceTests.java
index 3477288..3f259e3 100644
--- a/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsServiceTests.java
+++ b/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsServiceTests.java
@@ -145,15 +145,9 @@
// Verify that profile which aren't running (e.g. turn off work) don't get unlocked
assertNull(mGateKeeperService.getAuthToken(TURNED_OFF_PROFILE_USER_ID));
- /* Currently in LockSettingsService.setLockCredential, unlockUser() is called with the new
- * credential as part of verifyCredential() before the new credential is committed in
- * StorageManager. So we relax the check in our mock StorageManager to allow that.
- */
- mStorageManager.setIgnoreBadUnlock(true);
// Change primary password and verify that profile SID remains
assertTrue(mService.setLockCredential(
secondUnifiedPassword, firstUnifiedPassword, PRIMARY_USER_ID));
- mStorageManager.setIgnoreBadUnlock(false);
assertEquals(profileSid, mGateKeeperService.getSecureUserId(MANAGED_PROFILE_USER_ID));
assertNull(mGateKeeperService.getAuthToken(TURNED_OFF_PROFILE_USER_ID));
@@ -172,15 +166,9 @@
assertTrue(mService.setLockCredential(primaryPassword,
nonePassword(),
PRIMARY_USER_ID));
- /* Currently in LockSettingsService.setLockCredential, unlockUser() is called with the new
- * credential as part of verifyCredential() before the new credential is committed in
- * StorageManager. So we relax the check in our mock StorageManager to allow that.
- */
- mStorageManager.setIgnoreBadUnlock(true);
assertTrue(mService.setLockCredential(profilePassword,
nonePassword(),
MANAGED_PROFILE_USER_ID));
- mStorageManager.setIgnoreBadUnlock(false);
final long primarySid = mGateKeeperService.getSecureUserId(PRIMARY_USER_ID);
final long profileSid = mGateKeeperService.getSecureUserId(MANAGED_PROFILE_USER_ID);
@@ -203,11 +191,8 @@
assertNotNull(mGateKeeperService.getAuthToken(MANAGED_PROFILE_USER_ID));
assertEquals(profileSid, mGateKeeperService.getSecureUserId(MANAGED_PROFILE_USER_ID));
- // Change primary credential and make sure we don't affect profile
- mStorageManager.setIgnoreBadUnlock(true);
assertTrue(mService.setLockCredential(
newPassword("pwd"), primaryPassword, PRIMARY_USER_ID));
- mStorageManager.setIgnoreBadUnlock(false);
assertEquals(VerifyCredentialResponse.RESPONSE_OK, mService.verifyCredential(
profilePassword, MANAGED_PROFILE_USER_ID, 0 /* flags */)
.getResponseCode());
diff --git a/services/tests/servicestests/src/com/android/server/locksettings/SyntheticPasswordTests.java b/services/tests/servicestests/src/com/android/server/locksettings/SyntheticPasswordTests.java
index 87beece..5e6cccc 100644
--- a/services/tests/servicestests/src/com/android/server/locksettings/SyntheticPasswordTests.java
+++ b/services/tests/servicestests/src/com/android/server/locksettings/SyntheticPasswordTests.java
@@ -27,6 +27,7 @@
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
+import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.never;
@@ -126,6 +127,7 @@
initializeCredential(password, PRIMARY_USER_ID);
assertEquals(VerifyCredentialResponse.RESPONSE_OK, mService.verifyCredential(
password, PRIMARY_USER_ID, 0 /* flags */).getResponseCode());
+ verify(mActivityManager).unlockUser2(eq(PRIMARY_USER_ID), any());
assertEquals(VerifyCredentialResponse.RESPONSE_ERROR, mService.verifyCredential(
badPassword, PRIMARY_USER_ID, 0 /* flags */).getResponseCode());
@@ -187,32 +189,13 @@
}
@Test
- public void testNoSyntheticPasswordOrCredentialDoesNotPassAuthSecret() throws RemoteException {
- mService.onUnlockUser(PRIMARY_USER_ID);
- flushHandlerTasks();
- verify(mAuthSecretService, never()).primaryUserCredential(any(ArrayList.class));
- }
-
- @Test
- public void testCredentialDoesNotPassAuthSecret() throws RemoteException {
- LockscreenCredential password = newPassword("password");
- initializeCredential(password, PRIMARY_USER_ID);
-
- reset(mAuthSecretService);
- mService.onUnlockUser(PRIMARY_USER_ID);
- flushHandlerTasks();
- verify(mAuthSecretService, never()).primaryUserCredential(any(ArrayList.class));
- }
-
- @Test
- public void testSyntheticPasswordButNoCredentialPassesAuthSecret() throws RemoteException {
+ public void testUnlockUserKeyIfUnsecuredPassesPrimaryUserAuthSecret() throws RemoteException {
LockscreenCredential password = newPassword("password");
initializeCredential(password, PRIMARY_USER_ID);
mService.setLockCredential(nonePassword(), password, PRIMARY_USER_ID);
reset(mAuthSecretService);
- mService.onUnlockUser(PRIMARY_USER_ID);
- flushHandlerTasks();
+ mLocalService.unlockUserKeyIfUnsecured(PRIMARY_USER_ID);
verify(mAuthSecretService).primaryUserCredential(any(ArrayList.class));
}
diff --git a/services/tests/uiservicestests/src/com/android/server/UiModeManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/UiModeManagerServiceTest.java
index 617a34f..91c2fe0 100644
--- a/services/tests/uiservicestests/src/com/android/server/UiModeManagerServiceTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/UiModeManagerServiceTest.java
@@ -54,6 +54,7 @@
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
@@ -83,7 +84,6 @@
import android.os.RemoteException;
import android.os.UserHandle;
import android.provider.Settings;
-import android.service.dreams.DreamManagerInternal;
import android.test.mock.MockContentResolver;
import android.testing.AndroidTestingRunner;
import android.testing.TestableLooper;
@@ -101,6 +101,7 @@
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
+import org.mockito.Spy;
import java.time.LocalDateTime;
import java.time.LocalTime;
@@ -137,8 +138,8 @@
private PackageManager mPackageManager;
@Mock
private IBinder mBinder;
- @Mock
- private DreamManagerInternal mDreamManager;
+ @Spy
+ private TestInjector mInjector;
@Captor
private ArgumentCaptor<Intent> mOrderedBroadcastIntent;
@Captor
@@ -207,10 +208,10 @@
addLocalService(WindowManagerInternal.class, mWindowManager);
addLocalService(PowerManagerInternal.class, mLocalPowerManager);
addLocalService(TwilightManager.class, mTwilightManager);
- addLocalService(DreamManagerInternal.class, mDreamManager);
-
+
+ mInjector = spy(new TestInjector());
mUiManagerService = new UiModeManagerService(mContext, /* setupWizardComplete= */ true,
- mTwilightManager, new TestInjector());
+ mTwilightManager, mInjector);
try {
mUiManagerService.onBootPhase(SystemService.PHASE_SYSTEM_SERVICES_READY);
} catch (SecurityException e) {/* ignore for permission denial */}
@@ -1321,84 +1322,53 @@
@Test
public void dreamWhenDocked() {
- setScreensaverActivateOnDock(true);
- setScreensaverEnabled(true);
-
triggerDockIntent();
verifyAndSendResultBroadcast();
- verify(mDreamManager).requestDream();
- }
-
- @Test
- public void noDreamWhenDocked_dreamsDisabled() {
- setScreensaverActivateOnDock(true);
- setScreensaverEnabled(false);
-
- triggerDockIntent();
- verifyAndSendResultBroadcast();
- verify(mDreamManager, never()).requestDream();
- }
-
- @Test
- public void noDreamWhenDocked_dreamsWhenDockedDisabled() {
- setScreensaverActivateOnDock(false);
- setScreensaverEnabled(true);
-
- triggerDockIntent();
- verifyAndSendResultBroadcast();
- verify(mDreamManager, never()).requestDream();
+ verify(mInjector).startDreamWhenDockedIfAppropriate(mContext);
}
@Test
public void noDreamWhenDocked_keyguardNotShowing_interactive() {
- setScreensaverActivateOnDock(true);
- setScreensaverEnabled(true);
mUiManagerService.setStartDreamImmediatelyOnDock(false);
when(mWindowManager.isKeyguardShowingAndNotOccluded()).thenReturn(false);
when(mPowerManager.isInteractive()).thenReturn(true);
triggerDockIntent();
verifyAndSendResultBroadcast();
- verify(mDreamManager, never()).requestDream();
+ verify(mInjector, never()).startDreamWhenDockedIfAppropriate(mContext);
}
@Test
public void dreamWhenDocked_keyguardShowing_interactive() {
- setScreensaverActivateOnDock(true);
- setScreensaverEnabled(true);
mUiManagerService.setStartDreamImmediatelyOnDock(false);
when(mWindowManager.isKeyguardShowingAndNotOccluded()).thenReturn(true);
when(mPowerManager.isInteractive()).thenReturn(false);
triggerDockIntent();
verifyAndSendResultBroadcast();
- verify(mDreamManager).requestDream();
+ verify(mInjector).startDreamWhenDockedIfAppropriate(mContext);
}
@Test
public void dreamWhenDocked_keyguardNotShowing_notInteractive() {
- setScreensaverActivateOnDock(true);
- setScreensaverEnabled(true);
mUiManagerService.setStartDreamImmediatelyOnDock(false);
when(mWindowManager.isKeyguardShowingAndNotOccluded()).thenReturn(false);
when(mPowerManager.isInteractive()).thenReturn(false);
triggerDockIntent();
verifyAndSendResultBroadcast();
- verify(mDreamManager).requestDream();
+ verify(mInjector).startDreamWhenDockedIfAppropriate(mContext);
}
@Test
public void dreamWhenDocked_keyguardShowing_notInteractive() {
- setScreensaverActivateOnDock(true);
- setScreensaverEnabled(true);
mUiManagerService.setStartDreamImmediatelyOnDock(false);
when(mWindowManager.isKeyguardShowingAndNotOccluded()).thenReturn(true);
when(mPowerManager.isInteractive()).thenReturn(false);
triggerDockIntent();
verifyAndSendResultBroadcast();
- verify(mDreamManager).requestDream();
+ verify(mInjector).startDreamWhenDockedIfAppropriate(mContext);
}
private void triggerDockIntent() {
@@ -1435,22 +1405,6 @@
mOrderedBroadcastIntent.getValue());
}
- private void setScreensaverEnabled(boolean enable) {
- Settings.Secure.putIntForUser(
- mContentResolver,
- Settings.Secure.SCREENSAVER_ENABLED,
- enable ? 1 : 0,
- UserHandle.USER_CURRENT);
- }
-
- private void setScreensaverActivateOnDock(boolean enable) {
- Settings.Secure.putIntForUser(
- mContentResolver,
- Settings.Secure.SCREENSAVER_ACTIVATE_ON_DOCK,
- enable ? 1 : 0,
- UserHandle.USER_CURRENT);
- }
-
private void requestAllPossibleProjectionTypes() throws RemoteException {
for (int i = 0; i < Integer.SIZE; ++i) {
mService.requestProjection(mBinder, 1 << i, PACKAGE_NAME);
@@ -1467,11 +1421,17 @@
}
public TestInjector(int callingUid) {
- this.callingUid = callingUid;
+ this.callingUid = callingUid;
}
+ @Override
public int getCallingUid() {
return callingUid;
}
+
+ @Override
+ public void startDreamWhenDockedIfAppropriate(Context context) {
+ // do nothing
+ }
}
}
diff --git a/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java b/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java
index 170b388..5def703 100644
--- a/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java
@@ -30,6 +30,7 @@
import static android.content.pm.ActivityInfo.LAUNCH_MULTIPLE;
import static android.content.pm.ActivityInfo.LAUNCH_SINGLE_INSTANCE;
import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
+import static android.os.Process.NOBODY_UID;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
@@ -1220,20 +1221,34 @@
@Test
public void testCreateRecentTaskInfo_detachedTask() {
- final Task task = createTaskBuilder(".Task").setCreateActivity(true).build();
+ final Task task = createTaskBuilder(".Task").build();
+ new ActivityBuilder(mSupervisor.mService)
+ .setTask(task)
+ .setUid(NOBODY_UID)
+ .setComponent(getUniqueComponentName())
+ .build();
final TaskDisplayArea tda = task.getDisplayArea();
assertTrue(task.isAttached());
assertTrue(task.supportsMultiWindow());
- RecentTaskInfo info = mRecentTasks.createRecentTaskInfo(task, true);
+ RecentTaskInfo info = mRecentTasks.createRecentTaskInfo(task, true /* stripExtras */,
+ true /* getTasksAllowed */);
assertTrue(info.supportsMultiWindow);
+ info = mRecentTasks.createRecentTaskInfo(task, true /* stripExtras */,
+ false /* getTasksAllowed */);
+
+ assertTrue(info.topActivity == null);
+ assertTrue(info.topActivityInfo == null);
+ assertTrue(info.baseActivity == null);
+
// The task can be put in split screen even if it is not attached now.
task.removeImmediately();
- info = mRecentTasks.createRecentTaskInfo(task, true);
+ info = mRecentTasks.createRecentTaskInfo(task, true /* stripExtras */,
+ true /* getTasksAllowed */);
assertTrue(info.supportsMultiWindow);
@@ -1242,7 +1257,8 @@
doReturn(false).when(tda).supportsNonResizableMultiWindow();
doReturn(false).when(task).isResizeable();
- info = mRecentTasks.createRecentTaskInfo(task, true);
+ info = mRecentTasks.createRecentTaskInfo(task, true /* stripExtras */,
+ true /* getTasksAllowed */);
assertFalse(info.supportsMultiWindow);
@@ -1250,7 +1266,8 @@
// the device supports it.
doReturn(true).when(tda).supportsNonResizableMultiWindow();
- info = mRecentTasks.createRecentTaskInfo(task, true);
+ info = mRecentTasks.createRecentTaskInfo(task, true /* stripExtras */,
+ true /* getTasksAllowed */);
assertTrue(info.supportsMultiWindow);
}
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 c714f01..8370691 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java
@@ -198,14 +198,25 @@
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[] outControls = new InsetsSourceControl[0];
+ final Bundle outBundle = new Bundle();
mWm.relayoutWindow(win.mSession, win.mClient, win.mAttrs, w, h, View.GONE, 0, 0, 0,
- new ClientWindowFrames(), new MergedConfiguration(), new SurfaceControl(),
- new InsetsState(), new InsetsSourceControl[0], new Bundle());
+ outFrames, outConfig, outSurfaceControl, outInsetsState, outControls, outBundle);
// Because the window is already invisible, it doesn't need to apply exiting animation
// and WMS#tryStartExitingAnimation() will destroy the surface directly.
assertFalse(win.mAnimatingExit);
assertFalse(win.mHasSurface);
assertNull(win.mWinAnimator.mSurfaceController);
+
+ doReturn(mSystemServicesTestRule.mTransaction).when(SurfaceControl::getGlobalTransaction);
+ // Invisible requested activity should not get the last config even if its view is visible.
+ mWm.relayoutWindow(win.mSession, win.mClient, win.mAttrs, w, h, View.VISIBLE, 0, 0, 0,
+ outFrames, outConfig, outSurfaceControl, outInsetsState, outControls, outBundle);
+ assertEquals(0, outConfig.getMergedConfiguration().densityDpi);
}
@Test
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 e8c8054..1636667 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java
@@ -735,6 +735,15 @@
assertTrue(mWm.mResizingWindows.contains(startingApp));
assertTrue(startingApp.isDrawn());
assertFalse(startingApp.getOrientationChanging());
+
+ // Even if the display is frozen, invisible requested window should not be affected.
+ startingApp.mActivityRecord.mVisibleRequested = false;
+ mWm.startFreezingDisplay(0, 0, mDisplayContent);
+ doReturn(true).when(mWm.mPolicy).isScreenOn();
+ startingApp.getWindowFrames().setInsetsChanged(true);
+ startingApp.updateResizingWindowIfNeeded();
+ assertTrue(startingApp.isDrawn());
+ assertFalse(startingApp.getOrientationChanging());
}
@SetupWindows(addWindows = W_ABOVE_ACTIVITY)
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
index 9c9f5db..b6110247 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
@@ -1746,7 +1746,7 @@
}
void startTransition() {
- mOrganizer.startTransition(mLastRequest.getType(), mLastTransit, null);
+ mOrganizer.startTransition(mLastTransit, null);
}
void onTransactionReady(SurfaceControl.Transaction t) {
diff --git a/telephony/common/com/android/internal/telephony/SmsApplication.java b/telephony/common/com/android/internal/telephony/SmsApplication.java
index 7b7c83f..f848c40 100644
--- a/telephony/common/com/android/internal/telephony/SmsApplication.java
+++ b/telephony/common/com/android/internal/telephony/SmsApplication.java
@@ -190,12 +190,11 @@
}
/**
- * Returns the userId of the Context object, if called from a system app,
+ * Returns the userId of the current process, if called from a system app,
* otherwise it returns the caller's userId
- * @param context The context object passed in by the caller.
- * @return
+ * @return userId of the caller.
*/
- private static int getIncomingUserId(Context context) {
+ private static int getIncomingUserId() {
int contextUserId = UserHandle.myUserId();
final int callingUid = Binder.getCallingUid();
if (DEBUG_MULTIUSER) {
@@ -231,7 +230,7 @@
*/
@UnsupportedAppUsage
public static Collection<SmsApplicationData> getApplicationCollection(Context context) {
- return getApplicationCollectionAsUser(context, getIncomingUserId(context));
+ return getApplicationCollectionAsUser(context, getIncomingUserId());
}
/**
@@ -590,7 +589,7 @@
*/
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
public static void setDefaultApplication(String packageName, Context context) {
- setDefaultApplicationAsUser(packageName, context, getIncomingUserId(context));
+ setDefaultApplicationAsUser(packageName, context, getIncomingUserId());
}
/**
@@ -952,7 +951,7 @@
*/
@UnsupportedAppUsage
public static ComponentName getDefaultSmsApplication(Context context, boolean updateIfNeeded) {
- return getDefaultSmsApplicationAsUser(context, updateIfNeeded, getIncomingUserId(context));
+ return getDefaultSmsApplicationAsUser(context, updateIfNeeded, getIncomingUserId());
}
/**
@@ -988,7 +987,18 @@
*/
@UnsupportedAppUsage
public static ComponentName getDefaultMmsApplication(Context context, boolean updateIfNeeded) {
- int userId = getIncomingUserId(context);
+ return getDefaultMmsApplicationAsUser(context, updateIfNeeded, getIncomingUserId());
+ }
+
+ /**
+ * Gets the default MMS application on a given user
+ * @param context context from the calling app
+ * @param updateIfNeeded update the default app if there is no valid default app configured.
+ * @param userId target user ID.
+ * @return component name of the app and class to deliver MMS messages to.
+ */
+ public static ComponentName getDefaultMmsApplicationAsUser(Context context,
+ boolean updateIfNeeded, int userId) {
final long token = Binder.clearCallingIdentity();
try {
ComponentName component = null;
@@ -1013,7 +1023,19 @@
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
public static ComponentName getDefaultRespondViaMessageApplication(Context context,
boolean updateIfNeeded) {
- int userId = getIncomingUserId(context);
+ return getDefaultRespondViaMessageApplicationAsUser(context, updateIfNeeded,
+ getIncomingUserId());
+ }
+
+ /**
+ * Gets the default Respond Via Message application on a given user
+ * @param context context from the calling app
+ * @param updateIfNeeded update the default app if there is no valid default app configured
+ * @param userId target user ID.
+ * @return component name of the app and class to direct Respond Via Message intent to
+ */
+ public static ComponentName getDefaultRespondViaMessageApplicationAsUser(Context context,
+ boolean updateIfNeeded, int userId) {
final long token = Binder.clearCallingIdentity();
try {
ComponentName component = null;
@@ -1039,7 +1061,7 @@
*/
public static ComponentName getDefaultSendToApplication(Context context,
boolean updateIfNeeded) {
- int userId = getIncomingUserId(context);
+ int userId = getIncomingUserId();
final long token = Binder.clearCallingIdentity();
try {
ComponentName component = null;
@@ -1064,7 +1086,20 @@
*/
public static ComponentName getDefaultExternalTelephonyProviderChangedApplication(
Context context, boolean updateIfNeeded) {
- int userId = getIncomingUserId(context);
+ return getDefaultExternalTelephonyProviderChangedApplicationAsUser(context, updateIfNeeded,
+ getIncomingUserId());
+ }
+
+ /**
+ * Gets the default application that handles external changes to the SmsProvider and
+ * MmsProvider on a given user.
+ * @param context context from the calling app
+ * @param updateIfNeeded update the default app if there is no valid default app configured
+ * @param userId target user ID.
+ * @return component name of the app and class to deliver change intents to.
+ */
+ public static ComponentName getDefaultExternalTelephonyProviderChangedApplicationAsUser(
+ Context context, boolean updateIfNeeded, int userId) {
final long token = Binder.clearCallingIdentity();
try {
ComponentName component = null;
@@ -1089,7 +1124,18 @@
*/
public static ComponentName getDefaultSimFullApplication(
Context context, boolean updateIfNeeded) {
- int userId = getIncomingUserId(context);
+ return getDefaultSimFullApplicationAsUser(context, updateIfNeeded, getIncomingUserId());
+ }
+
+ /**
+ * Gets the default application that handles sim full event on a given user.
+ * @param context context from the calling app
+ * @param updateIfNeeded update the default app if there is no valid default app configured
+ * @param userId target user ID.
+ * @return component name of the app and class to deliver change intents to
+ */
+ public static ComponentName getDefaultSimFullApplicationAsUser(Context context,
+ boolean updateIfNeeded, int userId) {
final long token = Binder.clearCallingIdentity();
try {
ComponentName component = null;
@@ -1114,7 +1160,12 @@
*/
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
public static boolean shouldWriteMessageForPackage(String packageName, Context context) {
- return !isDefaultSmsApplication(context, packageName);
+ return !shouldWriteMessageForPackageAsUser(packageName, context, getIncomingUserId());
+ }
+
+ public static boolean shouldWriteMessageForPackageAsUser(String packageName, Context context,
+ int userId) {
+ return !isDefaultSmsApplicationAsUser(context, packageName, userId);
}
/**
@@ -1126,28 +1177,42 @@
*/
@UnsupportedAppUsage
public static boolean isDefaultSmsApplication(Context context, String packageName) {
+ return isDefaultSmsApplicationAsUser(context, packageName, getIncomingUserId());
+ }
+
+ /**
+ * Check if a package is default sms app (or equivalent, like bluetooth) on a given user.
+ *
+ * @param context context from the calling app
+ * @param packageName the name of the package to be checked
+ * @param userId target user ID.
+ * @return true if the package is default sms app or bluetooth
+ */
+ public static boolean isDefaultSmsApplicationAsUser(Context context, String packageName,
+ int userId) {
if (packageName == null) {
return false;
}
- final String defaultSmsPackage = getDefaultSmsApplicationPackageName(context);
- final String bluetoothPackageName = context.getResources()
+ ComponentName component = getDefaultSmsApplicationAsUser(context, false,
+ userId);
+ if (component == null) {
+ return false;
+ }
+
+ String defaultSmsPackage = component.getPackageName();
+ if (defaultSmsPackage == null) {
+ return false;
+ }
+
+ String bluetoothPackageName = context.getResources()
.getString(com.android.internal.R.string.config_systemBluetoothStack);
- if ((defaultSmsPackage != null && defaultSmsPackage.equals(packageName))
- || bluetoothPackageName.equals(packageName)) {
+ if (defaultSmsPackage.equals(packageName) || bluetoothPackageName.equals(packageName)) {
return true;
}
return false;
}
- private static String getDefaultSmsApplicationPackageName(Context context) {
- final ComponentName component = getDefaultSmsApplication(context, false);
- if (component != null) {
- return component.getPackageName();
- }
- return null;
- }
-
/**
* Check if a package is default mms app (or equivalent, like bluetooth)
*
@@ -1157,25 +1222,40 @@
*/
@UnsupportedAppUsage
public static boolean isDefaultMmsApplication(Context context, String packageName) {
+ return isDefaultMmsApplicationAsUser(context, packageName, getIncomingUserId());
+ }
+
+ /**
+ * Check if a package is default mms app (or equivalent, like bluetooth) on a given user.
+ *
+ * @param context context from the calling app
+ * @param packageName the name of the package to be checked
+ * @param userId target user ID.
+ * @return true if the package is default mms app or bluetooth
+ */
+ public static boolean isDefaultMmsApplicationAsUser(Context context, String packageName,
+ int userId) {
if (packageName == null) {
return false;
}
- String defaultMmsPackage = getDefaultMmsApplicationPackageName(context);
+
+ ComponentName component = getDefaultMmsApplicationAsUser(context, false,
+ userId);
+ if (component == null) {
+ return false;
+ }
+
+ String defaultMmsPackage = component.getPackageName();
+ if (defaultMmsPackage == null) {
+ return false;
+ }
+
String bluetoothPackageName = context.getResources()
.getString(com.android.internal.R.string.config_systemBluetoothStack);
- if ((defaultMmsPackage != null && defaultMmsPackage.equals(packageName))
- || bluetoothPackageName.equals(packageName)) {
+ if (defaultMmsPackage.equals(packageName)|| bluetoothPackageName.equals(packageName)) {
return true;
}
return false;
}
-
- private static String getDefaultMmsApplicationPackageName(Context context) {
- ComponentName component = getDefaultMmsApplication(context, false);
- if (component != null) {
- return component.getPackageName();
- }
- return null;
- }
-}
+}
\ No newline at end of file
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/GestureHelper.java b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/GestureHelper.java
new file mode 100644
index 0000000..858cd76
--- /dev/null
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/GestureHelper.java
@@ -0,0 +1,224 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.wm.flicker.helpers;
+
+import android.annotation.NonNull;
+import android.app.Instrumentation;
+import android.app.UiAutomation;
+import android.os.SystemClock;
+import android.view.InputDevice;
+import android.view.InputEvent;
+import android.view.MotionEvent;
+import android.view.MotionEvent.PointerCoords;
+import android.view.MotionEvent.PointerProperties;
+
+/**
+ * Injects gestures given an {@link Instrumentation} object.
+ */
+public class GestureHelper {
+ // Inserted after each motion event injection.
+ private static final int MOTION_EVENT_INJECTION_DELAY_MILLIS = 5;
+
+ private final UiAutomation mUiAutomation;
+
+ /**
+ * A pair of floating point values.
+ */
+ public static class Tuple {
+ public float x;
+ public float y;
+
+ public Tuple(float x, float y) {
+ this.x = x;
+ this.y = y;
+ }
+ }
+
+ public GestureHelper(Instrumentation instrumentation) {
+ mUiAutomation = instrumentation.getUiAutomation();
+ }
+
+ /**
+ * Injects a series of {@link MotionEvent} objects to simulate a pinch gesture.
+ *
+ * @param startPoint1 initial coordinates of the first pointer
+ * @param startPoint2 initial coordinates of the second pointer
+ * @param endPoint1 final coordinates of the first pointer
+ * @param endPoint2 final coordinates of the second pointer
+ * @param steps number of steps to take to animate pinching
+ * @return true if gesture is injected successfully
+ */
+ public boolean pinch(@NonNull Tuple startPoint1, @NonNull Tuple startPoint2,
+ @NonNull Tuple endPoint1, @NonNull Tuple endPoint2, int steps) {
+ PointerProperties ptrProp1 = getPointerProp(0, MotionEvent.TOOL_TYPE_FINGER);
+ PointerProperties ptrProp2 = getPointerProp(1, MotionEvent.TOOL_TYPE_FINGER);
+
+ PointerCoords ptrCoord1 = getPointerCoord(startPoint1.x, startPoint1.y, 1, 1);
+ PointerCoords ptrCoord2 = getPointerCoord(startPoint2.x, startPoint2.y, 1, 1);
+
+ PointerProperties[] ptrProps = new PointerProperties[] {
+ ptrProp1, ptrProp2
+ };
+
+ PointerCoords[] ptrCoords = new PointerCoords[] {
+ ptrCoord1, ptrCoord2
+ };
+
+ long downTime = SystemClock.uptimeMillis();
+
+ if (!primaryPointerDown(ptrProp1, ptrCoord1, downTime)) {
+ return false;
+ }
+
+ if (!nonPrimaryPointerDown(ptrProps, ptrCoords, downTime, 1)) {
+ return false;
+ }
+
+ if (!movePointers(ptrProps, ptrCoords, new Tuple[] { endPoint1, endPoint2 },
+ downTime, steps)) {
+ return false;
+ }
+
+ if (!nonPrimaryPointerUp(ptrProps, ptrCoords, downTime, 1)) {
+ return false;
+ }
+
+ return primaryPointerUp(ptrProp1, ptrCoord1, downTime);
+ }
+
+ private boolean primaryPointerDown(@NonNull PointerProperties prop,
+ @NonNull PointerCoords coord, long downTime) {
+ MotionEvent event = getMotionEvent(downTime, downTime, MotionEvent.ACTION_DOWN, 1,
+ new PointerProperties[]{ prop }, new PointerCoords[]{ coord });
+
+ return injectEventSync(event);
+ }
+
+ private boolean nonPrimaryPointerDown(@NonNull PointerProperties[] props,
+ @NonNull PointerCoords[] coords, long downTime, int index) {
+ // at least 2 pointers are needed
+ if (props.length != coords.length || coords.length < 2) {
+ return false;
+ }
+
+ long eventTime = SystemClock.uptimeMillis();
+
+ MotionEvent event = getMotionEvent(downTime, eventTime, MotionEvent.ACTION_POINTER_DOWN
+ + (index << MotionEvent.ACTION_POINTER_INDEX_SHIFT), coords.length, props, coords);
+
+ return injectEventSync(event);
+ }
+
+ private boolean movePointers(@NonNull PointerProperties[] props,
+ @NonNull PointerCoords[] coords, @NonNull Tuple[] endPoints, long downTime, int steps) {
+ // the number of endpoints should be the same as the number of pointers
+ if (props.length != coords.length || coords.length != endPoints.length) {
+ return false;
+ }
+
+ // prevent division by 0 and negative number of steps
+ if (steps < 1) {
+ steps = 1;
+ }
+
+ // save the starting points before updating any pointers
+ Tuple[] startPoints = new Tuple[coords.length];
+
+ for (int i = 0; i < coords.length; i++) {
+ startPoints[i] = new Tuple(coords[i].x, coords[i].y);
+ }
+
+ MotionEvent event;
+ long eventTime;
+
+ for (int i = 0; i < steps; i++) {
+ // inject a delay between movements
+ SystemClock.sleep(MOTION_EVENT_INJECTION_DELAY_MILLIS);
+
+ // update the coordinates
+ for (int j = 0; j < coords.length; j++) {
+ coords[j].x += (endPoints[j].x - startPoints[j].x) / steps;
+ coords[j].y += (endPoints[j].y - startPoints[j].y) / steps;
+ }
+
+ eventTime = SystemClock.uptimeMillis();
+
+ event = getMotionEvent(downTime, eventTime, MotionEvent.ACTION_MOVE,
+ coords.length, props, coords);
+
+ boolean didInject = injectEventSync(event);
+
+ if (!didInject) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ private boolean primaryPointerUp(@NonNull PointerProperties prop,
+ @NonNull PointerCoords coord, long downTime) {
+ long eventTime = SystemClock.uptimeMillis();
+
+ MotionEvent event = getMotionEvent(downTime, eventTime, MotionEvent.ACTION_UP, 1,
+ new PointerProperties[]{ prop }, new PointerCoords[]{ coord });
+
+ return injectEventSync(event);
+ }
+
+ private boolean nonPrimaryPointerUp(@NonNull PointerProperties[] props,
+ @NonNull PointerCoords[] coords, long downTime, int index) {
+ // at least 2 pointers are needed
+ if (props.length != coords.length || coords.length < 2) {
+ return false;
+ }
+
+ long eventTime = SystemClock.uptimeMillis();
+
+ MotionEvent event = getMotionEvent(downTime, eventTime, MotionEvent.ACTION_POINTER_UP
+ + (index << MotionEvent.ACTION_POINTER_INDEX_SHIFT), coords.length, props, coords);
+
+ return injectEventSync(event);
+ }
+
+ private PointerCoords getPointerCoord(float x, float y, float pressure, float size) {
+ PointerCoords ptrCoord = new PointerCoords();
+ ptrCoord.x = x;
+ ptrCoord.y = y;
+ ptrCoord.pressure = pressure;
+ ptrCoord.size = size;
+ return ptrCoord;
+ }
+
+ private PointerProperties getPointerProp(int id, int toolType) {
+ PointerProperties ptrProp = new PointerProperties();
+ ptrProp.id = id;
+ ptrProp.toolType = toolType;
+ return ptrProp;
+ }
+
+ private static MotionEvent getMotionEvent(long downTime, long eventTime, int action,
+ int pointerCount, PointerProperties[] ptrProps, PointerCoords[] ptrCoords) {
+ return MotionEvent.obtain(downTime, eventTime, action, pointerCount,
+ ptrProps, ptrCoords, 0, 0, 1.0f, 1.0f,
+ 0, 0, InputDevice.SOURCE_TOUCHSCREEN, 0);
+ }
+
+ private boolean injectEventSync(InputEvent event) {
+ return mUiAutomation.injectInputEvent(event, true);
+ }
+}
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/PipAppHelper.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/PipAppHelper.kt
index e730f31..19ee09a 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/PipAppHelper.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/PipAppHelper.kt
@@ -22,6 +22,7 @@
import android.util.Log
import androidx.test.uiautomator.By
import androidx.test.uiautomator.Until
+import com.android.server.wm.flicker.helpers.GestureHelper.Tuple
import com.android.server.wm.flicker.testapp.ActivityOptions
import com.android.server.wm.traces.common.Rect
import com.android.server.wm.traces.common.WindowManagerConditionsFactory
@@ -44,6 +45,8 @@
get() =
mediaSessionManager.getActiveSessions(null).firstOrNull { it.packageName == `package` }
+ private val gestureHelper: GestureHelper = GestureHelper(mInstrumentation)
+
open fun clickObject(resId: String) {
val selector = By.res(`package`, resId)
val obj = uiDevice.findObject(selector) ?: error("Could not find `$resId` object")
@@ -52,6 +55,50 @@
}
/**
+ * Expands the PIP window my using the pinch out gesture.
+ *
+ * @param percent The percentage by which to increase the pip window size.
+ * @throws IllegalArgumentException if percentage isn't between 0.0f and 1.0f
+ */
+ fun pinchOpenPipWindow(wmHelper: WindowManagerStateHelper, percent: Float, steps: Int) {
+ // the percentage must be between 0.0f and 1.0f
+ if (percent <= 0.0f || percent > 1.0f) {
+ throw IllegalArgumentException("Percent must be between 0.0f and 1.0f")
+ }
+
+ val windowRect = getWindowRect(wmHelper)
+
+ // first pointer's initial x coordinate is halfway between the left edge and the center
+ val initLeftX = (windowRect.centerX() - windowRect.width / 4).toFloat()
+ // second pointer's initial x coordinate is halfway between the right edge and the center
+ val initRightX = (windowRect.centerX() + windowRect.width / 4).toFloat()
+
+ // horizontal distance the window should increase by
+ val distIncrease = windowRect.width * percent
+
+ // final x-coordinates
+ val finalLeftX = initLeftX - (distIncrease / 2)
+ val finalRightX = initRightX + (distIncrease / 2)
+
+ // y-coordinate is the same throughout this animation
+ val yCoord = windowRect.centerY().toFloat()
+
+ var adjustedSteps = MIN_STEPS_TO_ANIMATE
+
+ // if distance per step is at least 1, then we can use the number of steps requested
+ if (distIncrease.toInt() / (steps * 2) >= 1) {
+ adjustedSteps = steps
+ }
+
+ // if the distance per step is less than 1, carry out the animation in two steps
+ gestureHelper.pinch(
+ Tuple(initLeftX, yCoord), Tuple(initRightX, yCoord),
+ Tuple(finalLeftX, yCoord), Tuple(finalRightX, yCoord), adjustedSteps)
+
+ waitForPipWindowToExpandFrom(wmHelper, Region.from(windowRect))
+ }
+
+ /**
* Launches the app through an intent instead of interacting with the launcher and waits until
* the app window is in PIP mode
*/
@@ -194,5 +241,8 @@
private const val MEDIA_SESSION_START_RADIO_BUTTON_ID = "media_session_start"
private const val ENTER_PIP_ON_USER_LEAVE_HINT = "enter_pip_on_leave_manual"
private const val ENTER_PIP_AUTOENTER = "enter_pip_on_leave_autoenter"
+ // minimum number of steps to take, when animating gestures, needs to be 2
+ // so that there is at least a single intermediate layer that flicker tests can check
+ private const val MIN_STEPS_TO_ANIMATE = 2
}
}
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/SwitchImeWindowsFromGestureNavTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/SwitchImeWindowsFromGestureNavTest.kt
index 02b3b13..0ca6457 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/SwitchImeWindowsFromGestureNavTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/SwitchImeWindowsFromGestureNavTest.kt
@@ -62,6 +62,7 @@
override val transition: FlickerBuilder.() -> Unit = {
setup {
tapl.setExpectedRotationCheckEnabled(false)
+ tapl.setIgnoreTaskbarVisibility(true)
this.setRotation(testSpec.startRotation)
testApp.launchViaIntent(wmHelper)
wmHelper.StateSyncBuilder().withFullScreenApp(testApp).waitForAndVerify()
@@ -78,14 +79,16 @@
imeTestApp.exit(wmHelper)
}
transitions {
- // [Step1]: Swipe right from imeTestApp to testApp task
+ // [Step1]: Swipe right from testApp task to imeTestApp
createTag(TAG_IME_VISIBLE)
+ // Expect taskBar invisible when switching to imeTestApp on the large screen device.
tapl.launchedAppState.quickSwitchToPreviousApp()
wmHelper.StateSyncBuilder().withFullScreenApp(testApp).waitForAndVerify()
createTag(TAG_IME_INVISIBLE)
}
transitions {
- // [Step2]: Swipe left to back to imeTestApp task
+ // [Step2]: Swipe left to back to testApp task
+ // Expect taskBar visible when switching to testApp on the large screen device.
tapl.launchedAppState.quickSwitchToPreviousAppSwipeLeft()
wmHelper.StateSyncBuilder().withFullScreenApp(imeTestApp).waitForAndVerify()
}
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchBetweenTwoAppsBackTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchBetweenTwoAppsBackTest.kt
index 3f1a418..46186bc 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchBetweenTwoAppsBackTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchBetweenTwoAppsBackTest.kt
@@ -68,6 +68,7 @@
override val transition: FlickerBuilder.() -> Unit = {
setup {
tapl.setExpectedRotation(testSpec.startRotation)
+ tapl.setIgnoreTaskbarVisibility(true)
testApp1.launchViaIntent(wmHelper)
testApp2.launchViaIntent(wmHelper)
startDisplayBounds =
diff --git a/tests/TelephonyCommonTests/src/com/android/internal/telephony/tests/SmsApplicationTest.java b/tests/TelephonyCommonTests/src/com/android/internal/telephony/tests/SmsApplicationTest.java
index 052ce3a..7a2af72 100644
--- a/tests/TelephonyCommonTests/src/com/android/internal/telephony/tests/SmsApplicationTest.java
+++ b/tests/TelephonyCommonTests/src/com/android/internal/telephony/tests/SmsApplicationTest.java
@@ -153,6 +153,20 @@
}
@Test
+ public void testGetDefaultMmsApplication() {
+ assertEquals(TEST_COMPONENT_NAME,
+ SmsApplication.getDefaultMmsApplicationAsUser(mContext, false,
+ UserHandle.USER_SYSTEM));
+ }
+
+ @Test
+ public void testGetDefaultExternalTelephonyProviderChangedApplication() {
+ assertEquals(TEST_COMPONENT_NAME,
+ SmsApplication.getDefaultExternalTelephonyProviderChangedApplicationAsUser(mContext,
+ false, UserHandle.USER_SYSTEM));
+ }
+
+ @Test
public void testGetDefaultSmsApplicationWithAppOpsFix() throws Exception {
when(mAppOpsManager.unsafeCheckOp(AppOpsManager.OPSTR_READ_SMS, SMS_APP_UID,
TEST_COMPONENT_NAME.getPackageName()))
diff --git a/tools/aapt2/Android.bp b/tools/aapt2/Android.bp
index 7ddbe95..aa337e5 100644
--- a/tools/aapt2/Android.bp
+++ b/tools/aapt2/Android.bp
@@ -105,6 +105,7 @@
"format/Container.cpp",
"format/binary/BinaryResourceParser.cpp",
"format/binary/ResChunkPullParser.cpp",
+ "format/binary/ResEntryWriter.cpp",
"format/binary/TableFlattener.cpp",
"format/binary/XmlFlattener.cpp",
"format/proto/ProtoDeserialize.cpp",
diff --git a/tools/aapt2/ResourceUtils.cpp b/tools/aapt2/ResourceUtils.cpp
index 945f45b..41c7435 100644
--- a/tools/aapt2/ResourceUtils.cpp
+++ b/tools/aapt2/ResourceUtils.cpp
@@ -43,8 +43,9 @@
static std::optional<ResourceNamedType> ToResourceNamedType(const char16_t* type16,
const char* type, size_t type_len) {
std::optional<ResourceNamedTypeRef> parsed_type;
+ std::string converted;
if (type16) {
- auto converted = android::util::Utf16ToUtf8(StringPiece16(type16, type_len));
+ converted = android::util::Utf16ToUtf8(StringPiece16(type16, type_len));
parsed_type = ParseResourceNamedType(converted);
} else if (type) {
parsed_type = ParseResourceNamedType(StringPiece(type, type_len));
diff --git a/tools/aapt2/format/binary/ResEntryWriter.cpp b/tools/aapt2/format/binary/ResEntryWriter.cpp
new file mode 100644
index 0000000..8832c24
--- /dev/null
+++ b/tools/aapt2/format/binary/ResEntryWriter.cpp
@@ -0,0 +1,278 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "format/binary/ResEntryWriter.h"
+
+#include "ValueVisitor.h"
+#include "androidfw/BigBuffer.h"
+#include "androidfw/ResourceTypes.h"
+#include "androidfw/Util.h"
+#include "format/binary/ResourceTypeExtensions.h"
+
+namespace aapt {
+
+using android::BigBuffer;
+using android::Res_value;
+using android::ResTable_entry;
+using android::ResTable_map;
+
+struct less_style_entries {
+ bool operator()(const Style::Entry* a, const Style::Entry* b) const {
+ if (a->key.id) {
+ if (b->key.id) {
+ return cmp_ids_dynamic_after_framework(a->key.id.value(), b->key.id.value());
+ }
+ return true;
+ }
+ if (!b->key.id) {
+ return a->key.name.value() < b->key.name.value();
+ }
+ return false;
+ }
+};
+
+class MapFlattenVisitor : public ConstValueVisitor {
+ public:
+ using ConstValueVisitor::Visit;
+
+ MapFlattenVisitor(ResTable_entry_ext* out_entry, BigBuffer* buffer)
+ : out_entry_(out_entry), buffer_(buffer) {
+ }
+
+ void Visit(const Attribute* attr) override {
+ {
+ Reference key = Reference(ResourceId(ResTable_map::ATTR_TYPE));
+ BinaryPrimitive val(Res_value::TYPE_INT_DEC, attr->type_mask);
+ FlattenEntry(&key, &val);
+ }
+
+ if (attr->min_int != std::numeric_limits<int32_t>::min()) {
+ Reference key = Reference(ResourceId(ResTable_map::ATTR_MIN));
+ BinaryPrimitive val(Res_value::TYPE_INT_DEC, static_cast<uint32_t>(attr->min_int));
+ FlattenEntry(&key, &val);
+ }
+
+ if (attr->max_int != std::numeric_limits<int32_t>::max()) {
+ Reference key = Reference(ResourceId(ResTable_map::ATTR_MAX));
+ BinaryPrimitive val(Res_value::TYPE_INT_DEC, static_cast<uint32_t>(attr->max_int));
+ FlattenEntry(&key, &val);
+ }
+
+ for (const Attribute::Symbol& s : attr->symbols) {
+ BinaryPrimitive val(s.type, s.value);
+ FlattenEntry(&s.symbol, &val);
+ }
+ }
+
+ void Visit(const Style* style) override {
+ if (style->parent) {
+ const Reference& parent_ref = style->parent.value();
+ CHECK(bool(parent_ref.id)) << "parent has no ID";
+ out_entry_->parent.ident = android::util::HostToDevice32(parent_ref.id.value().id);
+ }
+
+ // Sort the style.
+ std::vector<const Style::Entry*> sorted_entries;
+ for (const auto& entry : style->entries) {
+ sorted_entries.emplace_back(&entry);
+ }
+
+ std::sort(sorted_entries.begin(), sorted_entries.end(), less_style_entries());
+
+ for (const Style::Entry* entry : sorted_entries) {
+ FlattenEntry(&entry->key, entry->value.get());
+ }
+ }
+
+ void Visit(const Styleable* styleable) override {
+ for (auto& attr_ref : styleable->entries) {
+ BinaryPrimitive val(Res_value{});
+ FlattenEntry(&attr_ref, &val);
+ }
+ }
+
+ void Visit(const Array* array) override {
+ const size_t count = array->elements.size();
+ for (size_t i = 0; i < count; i++) {
+ Reference key(android::ResTable_map::ATTR_MIN + i);
+ FlattenEntry(&key, array->elements[i].get());
+ }
+ }
+
+ void Visit(const Plural* plural) override {
+ const size_t count = plural->values.size();
+ for (size_t i = 0; i < count; i++) {
+ if (!plural->values[i]) {
+ continue;
+ }
+
+ ResourceId q;
+ switch (i) {
+ case Plural::Zero:
+ q.id = android::ResTable_map::ATTR_ZERO;
+ break;
+
+ case Plural::One:
+ q.id = android::ResTable_map::ATTR_ONE;
+ break;
+
+ case Plural::Two:
+ q.id = android::ResTable_map::ATTR_TWO;
+ break;
+
+ case Plural::Few:
+ q.id = android::ResTable_map::ATTR_FEW;
+ break;
+
+ case Plural::Many:
+ q.id = android::ResTable_map::ATTR_MANY;
+ break;
+
+ case Plural::Other:
+ q.id = android::ResTable_map::ATTR_OTHER;
+ break;
+
+ default:
+ LOG(FATAL) << "unhandled plural type";
+ break;
+ }
+
+ Reference key(q);
+ FlattenEntry(&key, plural->values[i].get());
+ }
+ }
+
+ /**
+ * Call this after visiting a Value. This will finish any work that
+ * needs to be done to prepare the entry.
+ */
+ void Finish() {
+ out_entry_->count = android::util::HostToDevice32(entry_count_);
+ }
+
+ private:
+ DISALLOW_COPY_AND_ASSIGN(MapFlattenVisitor);
+
+ void FlattenKey(const Reference* key, ResTable_map* out_entry) {
+ CHECK(bool(key->id)) << "key has no ID";
+ out_entry->name.ident = android::util::HostToDevice32(key->id.value().id);
+ }
+
+ void FlattenValue(const Item* value, ResTable_map* out_entry) {
+ CHECK(value->Flatten(&out_entry->value)) << "flatten failed";
+ }
+
+ void FlattenEntry(const Reference* key, Item* value) {
+ ResTable_map* out_entry = buffer_->NextBlock<ResTable_map>();
+ FlattenKey(key, out_entry);
+ FlattenValue(value, out_entry);
+ out_entry->value.size = android::util::HostToDevice16(sizeof(out_entry->value));
+ entry_count_++;
+ }
+
+ ResTable_entry_ext* out_entry_;
+ BigBuffer* buffer_;
+ size_t entry_count_ = 0;
+};
+
+template <typename T>
+void WriteEntry(const FlatEntry* entry, T* out_result) {
+ static_assert(std::is_same_v<ResTable_entry, T> || std::is_same_v<ResTable_entry_ext, T>,
+ "T must be ResTable_entry or ResTable_entry_ext");
+
+ ResTable_entry* out_entry = (ResTable_entry*)out_result;
+ if (entry->entry->visibility.level == Visibility::Level::kPublic) {
+ out_entry->flags |= ResTable_entry::FLAG_PUBLIC;
+ }
+
+ if (entry->value->IsWeak()) {
+ out_entry->flags |= ResTable_entry::FLAG_WEAK;
+ }
+
+ if constexpr (std::is_same_v<ResTable_entry_ext, T>) {
+ out_entry->flags |= ResTable_entry::FLAG_COMPLEX;
+ }
+
+ out_entry->flags = android::util::HostToDevice16(out_entry->flags);
+ out_entry->key.index = android::util::HostToDevice32(entry->entry_key);
+ out_entry->size = android::util::HostToDevice16(sizeof(T));
+}
+
+int32_t WriteMapToBuffer(const FlatEntry* map_entry, BigBuffer* buffer) {
+ int32_t offset = buffer->size();
+ ResTable_entry_ext* out_entry = buffer->NextBlock<ResTable_entry_ext>();
+ WriteEntry<ResTable_entry_ext>(map_entry, out_entry);
+
+ MapFlattenVisitor visitor(out_entry, buffer);
+ map_entry->value->Accept(&visitor);
+ visitor.Finish();
+ return offset;
+}
+
+void WriteItemToPair(const FlatEntry* item_entry, ResEntryValuePair* out_pair) {
+ static_assert(sizeof(ResEntryValuePair) == sizeof(ResTable_entry) + sizeof(Res_value),
+ "ResEntryValuePair must not have padding between entry and value.");
+
+ WriteEntry<ResTable_entry>(item_entry, &out_pair->entry);
+
+ CHECK(ValueCast<Item>(item_entry->value)->Flatten(&out_pair->value)) << "flatten failed";
+ out_pair->value.size = android::util::HostToDevice16(sizeof(out_pair->value));
+}
+
+int32_t SequentialResEntryWriter::WriteMap(const FlatEntry* entry) {
+ return WriteMapToBuffer(entry, entries_buffer_);
+}
+
+int32_t SequentialResEntryWriter::WriteItem(const FlatEntry* entry) {
+ int32_t offset = entries_buffer_->size();
+ auto* out_pair = entries_buffer_->NextBlock<ResEntryValuePair>();
+ WriteItemToPair(entry, out_pair);
+ return offset;
+}
+
+std::size_t ResEntryValuePairContentHasher::operator()(const ResEntryValuePairRef& ref) const {
+ return android::JenkinsHashMixBytes(0, ref.ptr, sizeof(ResEntryValuePair));
+}
+
+bool ResEntryValuePairContentEqualTo::operator()(const ResEntryValuePairRef& a,
+ const ResEntryValuePairRef& b) const {
+ return std::memcmp(a.ptr, b.ptr, sizeof(ResEntryValuePair)) == 0;
+}
+
+int32_t DeduplicateItemsResEntryWriter::WriteMap(const FlatEntry* entry) {
+ return WriteMapToBuffer(entry, entries_buffer_);
+}
+
+int32_t DeduplicateItemsResEntryWriter::WriteItem(const FlatEntry* entry) {
+ int32_t initial_offset = entries_buffer_->size();
+
+ auto* out_pair = entries_buffer_->NextBlock<ResEntryValuePair>();
+ WriteItemToPair(entry, out_pair);
+
+ auto ref = ResEntryValuePairRef{*out_pair};
+ auto [it, inserted] = entry_offsets.insert({ref, initial_offset});
+ if (inserted) {
+ // If inserted just return a new offset as this is a first time we store
+ // this entry.
+ return initial_offset;
+ }
+ // If not inserted this means that this is a duplicate, backup allocated block to the buffer
+ // and return offset of previously stored entry.
+ entries_buffer_->BackUp(sizeof(ResEntryValuePair));
+ return it->second;
+}
+
+} // namespace aapt
\ No newline at end of file
diff --git a/tools/aapt2/format/binary/ResEntryWriter.h b/tools/aapt2/format/binary/ResEntryWriter.h
new file mode 100644
index 0000000..a36ceec
--- /dev/null
+++ b/tools/aapt2/format/binary/ResEntryWriter.h
@@ -0,0 +1,135 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef AAPT_FORMAT_BINARY_RESENTRY_SERIALIZER_H
+#define AAPT_FORMAT_BINARY_RESENTRY_SERIALIZER_H
+
+#include <unordered_map>
+
+#include "ResourceTable.h"
+#include "ValueVisitor.h"
+#include "android-base/macros.h"
+#include "androidfw/BigBuffer.h"
+#include "androidfw/ResourceTypes.h"
+
+namespace aapt {
+
+struct FlatEntry {
+ const ResourceTableEntryView* entry;
+ const Value* value;
+
+ // The entry string pool index to the entry's name.
+ uint32_t entry_key;
+};
+
+// Pair of ResTable_entry and Res_value. These pairs are stored sequentially in values buffer.
+// We introduce this structure for ResEntryWriter to a have single allocation using
+// BigBuffer::NextBlock which allows to return it back with BigBuffer::Backup.
+struct ResEntryValuePair {
+ android::ResTable_entry entry;
+ android::Res_value value;
+};
+
+// References ResEntryValuePair object stored in BigBuffer used as a key in std::unordered_map.
+// Allows access to memory address where ResEntryValuePair is stored.
+union ResEntryValuePairRef {
+ const std::reference_wrapper<const ResEntryValuePair> pair;
+ const u_char* ptr;
+
+ explicit ResEntryValuePairRef(const ResEntryValuePair& ref) : pair(ref) {
+ }
+};
+
+// Hasher which computes hash of ResEntryValuePair using its bytes representation in memory.
+struct ResEntryValuePairContentHasher {
+ std::size_t operator()(const ResEntryValuePairRef& ref) const;
+};
+
+// Equaler which compares ResEntryValuePairs using theirs bytes representation in memory.
+struct ResEntryValuePairContentEqualTo {
+ bool operator()(const ResEntryValuePairRef& a, const ResEntryValuePairRef& b) const;
+};
+
+// Base class that allows to write FlatEntries into entries_buffer.
+class ResEntryWriter {
+ public:
+ virtual ~ResEntryWriter() = default;
+
+ // Writes resource table entry and its value into 'entries_buffer_' and returns offset
+ // in the buffer where entry was written.
+ int32_t Write(const FlatEntry* entry) {
+ if (ValueCast<Item>(entry->value) != nullptr) {
+ return WriteItem(entry);
+ } else {
+ return WriteMap(entry);
+ }
+ }
+
+ protected:
+ ResEntryWriter(android::BigBuffer* entries_buffer) : entries_buffer_(entries_buffer) {
+ }
+ android::BigBuffer* entries_buffer_;
+
+ virtual int32_t WriteItem(const FlatEntry* entry) = 0;
+
+ virtual int32_t WriteMap(const FlatEntry* entry) = 0;
+
+ private:
+ DISALLOW_COPY_AND_ASSIGN(ResEntryWriter);
+};
+
+// ResEntryWriter which writes FlatEntries sequentially into entries_buffer.
+// Next entry is always written right after previous one in the buffer.
+class SequentialResEntryWriter : public ResEntryWriter {
+ public:
+ explicit SequentialResEntryWriter(android::BigBuffer* entries_buffer)
+ : ResEntryWriter(entries_buffer) {
+ }
+ ~SequentialResEntryWriter() override = default;
+
+ int32_t WriteItem(const FlatEntry* entry) override;
+
+ int32_t WriteMap(const FlatEntry* entry) override;
+
+ private:
+ DISALLOW_COPY_AND_ASSIGN(SequentialResEntryWriter);
+};
+
+// ResEntryWriter that writes only unique entry and value pairs into entries_buffer.
+// Next entry is written into buffer only if there is no entry with the same bytes representation
+// in memory written before. Otherwise returns offset of already written entry.
+class DeduplicateItemsResEntryWriter : public ResEntryWriter {
+ public:
+ explicit DeduplicateItemsResEntryWriter(android::BigBuffer* entries_buffer)
+ : ResEntryWriter(entries_buffer) {
+ }
+ ~DeduplicateItemsResEntryWriter() override = default;
+
+ int32_t WriteItem(const FlatEntry* entry) override;
+
+ int32_t WriteMap(const FlatEntry* entry) override;
+
+ private:
+ DISALLOW_COPY_AND_ASSIGN(DeduplicateItemsResEntryWriter);
+
+ std::unordered_map<ResEntryValuePairRef, int32_t, ResEntryValuePairContentHasher,
+ ResEntryValuePairContentEqualTo>
+ entry_offsets;
+};
+
+} // namespace aapt
+
+#endif
\ No newline at end of file
diff --git a/tools/aapt2/format/binary/ResEntryWriter_test.cpp b/tools/aapt2/format/binary/ResEntryWriter_test.cpp
new file mode 100644
index 0000000..56ca133
--- /dev/null
+++ b/tools/aapt2/format/binary/ResEntryWriter_test.cpp
@@ -0,0 +1,138 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "format/binary/ResEntryWriter.h"
+
+#include "androidfw/BigBuffer.h"
+#include "format/binary/ResourceTypeExtensions.h"
+#include "test/Test.h"
+#include "util/Util.h"
+
+using ::android::BigBuffer;
+using ::android::Res_value;
+using ::android::ResTable_map;
+using ::testing::Eq;
+using ::testing::Ge;
+using ::testing::IsNull;
+using ::testing::Ne;
+using ::testing::NotNull;
+
+namespace aapt {
+
+using SequentialResEntryWriterTest = CommandTestFixture;
+using DeduplicateItemsResEntryWriterTest = CommandTestFixture;
+
+std::vector<int32_t> WriteAllEntries(const ResourceTableView& table, ResEntryWriter& writer) {
+ std::vector<int32_t> result = {};
+ for (const auto& type : table.packages[0].types) {
+ for (const auto& entry : type.entries) {
+ for (const auto& value : entry.values) {
+ auto flat_entry = FlatEntry{&entry, value->value.get(), 0};
+ result.push_back(writer.Write(&flat_entry));
+ }
+ }
+ }
+ return result;
+}
+
+TEST_F(SequentialResEntryWriterTest, WriteEntriesOneByOne) {
+ std::unique_ptr<ResourceTable> table =
+ test::ResourceTableBuilder()
+ .AddSimple("com.app.test:id/id1", ResourceId(0x7f010000))
+ .AddSimple("com.app.test:id/id2", ResourceId(0x7f010001))
+ .AddSimple("com.app.test:id/id3", ResourceId(0x7f010002))
+ .Build();
+
+ BigBuffer out(512);
+ SequentialResEntryWriter writer(&out);
+ auto offsets = WriteAllEntries(table->GetPartitionedView(), writer);
+
+ std::vector<int32_t> expected_offsets{0, sizeof(ResEntryValuePair),
+ 2 * sizeof(ResEntryValuePair)};
+ EXPECT_EQ(out.size(), 3 * sizeof(ResEntryValuePair));
+ EXPECT_EQ(offsets, expected_offsets);
+};
+
+TEST_F(SequentialResEntryWriterTest, WriteMapEntriesOneByOne) {
+ std::unique_ptr<Array> array1 = util::make_unique<Array>();
+ array1->elements.push_back(
+ util::make_unique<BinaryPrimitive>(uint8_t(Res_value::TYPE_INT_DEC), 1u));
+ array1->elements.push_back(
+ util::make_unique<BinaryPrimitive>(uint8_t(Res_value::TYPE_INT_DEC), 2u));
+ std::unique_ptr<Array> array2 = util::make_unique<Array>();
+ array2->elements.push_back(
+ util::make_unique<BinaryPrimitive>(uint8_t(Res_value::TYPE_INT_DEC), 1u));
+ array2->elements.push_back(
+ util::make_unique<BinaryPrimitive>(uint8_t(Res_value::TYPE_INT_DEC), 2u));
+
+ std::unique_ptr<ResourceTable> table = test::ResourceTableBuilder()
+ .AddValue("com.app.test:array/arr1", std::move(array1))
+ .AddValue("com.app.test:array/arr2", std::move(array2))
+ .Build();
+
+ BigBuffer out(512);
+ SequentialResEntryWriter writer(&out);
+ auto offsets = WriteAllEntries(table->GetPartitionedView(), writer);
+
+ std::vector<int32_t> expected_offsets{0, sizeof(ResTable_entry_ext) + 2 * sizeof(ResTable_map)};
+ EXPECT_EQ(out.size(), 2 * (sizeof(ResTable_entry_ext) + 2 * sizeof(ResTable_map)));
+ EXPECT_EQ(offsets, expected_offsets);
+};
+
+TEST_F(DeduplicateItemsResEntryWriterTest, DeduplicateItemEntries) {
+ std::unique_ptr<ResourceTable> table =
+ test::ResourceTableBuilder()
+ .AddSimple("com.app.test:id/id1", ResourceId(0x7f010000))
+ .AddSimple("com.app.test:id/id2", ResourceId(0x7f010001))
+ .AddSimple("com.app.test:id/id3", ResourceId(0x7f010002))
+ .Build();
+
+ BigBuffer out(512);
+ DeduplicateItemsResEntryWriter writer(&out);
+ auto offsets = WriteAllEntries(table->GetPartitionedView(), writer);
+
+ std::vector<int32_t> expected_offsets{0, 0, 0};
+ EXPECT_EQ(out.size(), sizeof(ResEntryValuePair));
+ EXPECT_EQ(offsets, expected_offsets);
+};
+
+TEST_F(DeduplicateItemsResEntryWriterTest, WriteMapEntriesOneByOne) {
+ std::unique_ptr<Array> array1 = util::make_unique<Array>();
+ array1->elements.push_back(
+ util::make_unique<BinaryPrimitive>(uint8_t(Res_value::TYPE_INT_DEC), 1u));
+ array1->elements.push_back(
+ util::make_unique<BinaryPrimitive>(uint8_t(Res_value::TYPE_INT_DEC), 2u));
+ std::unique_ptr<Array> array2 = util::make_unique<Array>();
+ array2->elements.push_back(
+ util::make_unique<BinaryPrimitive>(uint8_t(Res_value::TYPE_INT_DEC), 1u));
+ array2->elements.push_back(
+ util::make_unique<BinaryPrimitive>(uint8_t(Res_value::TYPE_INT_DEC), 2u));
+
+ std::unique_ptr<ResourceTable> table = test::ResourceTableBuilder()
+ .AddValue("com.app.test:array/arr1", std::move(array1))
+ .AddValue("com.app.test:array/arr2", std::move(array2))
+ .Build();
+
+ BigBuffer out(512);
+ DeduplicateItemsResEntryWriter writer(&out);
+ auto offsets = WriteAllEntries(table->GetPartitionedView(), writer);
+
+ std::vector<int32_t> expected_offsets{0, sizeof(ResTable_entry_ext) + 2 * sizeof(ResTable_map)};
+ EXPECT_EQ(out.size(), 2 * (sizeof(ResTable_entry_ext) + 2 * sizeof(ResTable_map)));
+ EXPECT_EQ(offsets, expected_offsets);
+};
+
+} // namespace aapt
\ No newline at end of file
diff --git a/tools/aapt2/format/binary/TableFlattener.cpp b/tools/aapt2/format/binary/TableFlattener.cpp
index 22f278c..7dc9d26 100644
--- a/tools/aapt2/format/binary/TableFlattener.cpp
+++ b/tools/aapt2/format/binary/TableFlattener.cpp
@@ -16,21 +16,20 @@
#include "format/binary/TableFlattener.h"
-#include <algorithm>
-#include <numeric>
#include <sstream>
#include <type_traits>
+#include <variant>
#include "ResourceTable.h"
#include "ResourceValues.h"
#include "SdkConstants.h"
-#include "ValueVisitor.h"
#include "android-base/logging.h"
#include "android-base/macros.h"
#include "android-base/stringprintf.h"
#include "androidfw/BigBuffer.h"
#include "androidfw/ResourceUtils.h"
#include "format/binary/ChunkWriter.h"
+#include "format/binary/ResEntryWriter.h"
#include "format/binary/ResourceTypeExtensions.h"
#include "trace/TraceBuffer.h"
@@ -58,170 +57,6 @@
dst[i] = 0;
}
-static bool cmp_style_entries(const Style::Entry* a, const Style::Entry* b) {
- if (a->key.id) {
- if (b->key.id) {
- return cmp_ids_dynamic_after_framework(a->key.id.value(), b->key.id.value());
- }
- return true;
- } else if (!b->key.id) {
- return a->key.name.value() < b->key.name.value();
- }
- return false;
-}
-
-struct FlatEntry {
- const ResourceTableEntryView* entry;
- const Value* value;
-
- // The entry string pool index to the entry's name.
- uint32_t entry_key;
-};
-
-class MapFlattenVisitor : public ConstValueVisitor {
- public:
- using ConstValueVisitor::Visit;
-
- MapFlattenVisitor(ResTable_entry_ext* out_entry, BigBuffer* buffer)
- : out_entry_(out_entry), buffer_(buffer) {
- }
-
- void Visit(const Attribute* attr) override {
- {
- Reference key = Reference(ResourceId(ResTable_map::ATTR_TYPE));
- BinaryPrimitive val(Res_value::TYPE_INT_DEC, attr->type_mask);
- FlattenEntry(&key, &val);
- }
-
- if (attr->min_int != std::numeric_limits<int32_t>::min()) {
- Reference key = Reference(ResourceId(ResTable_map::ATTR_MIN));
- BinaryPrimitive val(Res_value::TYPE_INT_DEC, static_cast<uint32_t>(attr->min_int));
- FlattenEntry(&key, &val);
- }
-
- if (attr->max_int != std::numeric_limits<int32_t>::max()) {
- Reference key = Reference(ResourceId(ResTable_map::ATTR_MAX));
- BinaryPrimitive val(Res_value::TYPE_INT_DEC, static_cast<uint32_t>(attr->max_int));
- FlattenEntry(&key, &val);
- }
-
- for (const Attribute::Symbol& s : attr->symbols) {
- BinaryPrimitive val(s.type, s.value);
- FlattenEntry(&s.symbol, &val);
- }
- }
-
- void Visit(const Style* style) override {
- if (style->parent) {
- const Reference& parent_ref = style->parent.value();
- CHECK(bool(parent_ref.id)) << "parent has no ID";
- out_entry_->parent.ident = android::util::HostToDevice32(parent_ref.id.value().id);
- }
-
- // Sort the style.
- std::vector<const Style::Entry*> sorted_entries;
- for (const auto& entry : style->entries) {
- sorted_entries.emplace_back(&entry);
- }
-
- std::sort(sorted_entries.begin(), sorted_entries.end(), cmp_style_entries);
-
- for (const Style::Entry* entry : sorted_entries) {
- FlattenEntry(&entry->key, entry->value.get());
- }
- }
-
- void Visit(const Styleable* styleable) override {
- for (auto& attr_ref : styleable->entries) {
- BinaryPrimitive val(Res_value{});
- FlattenEntry(&attr_ref, &val);
- }
- }
-
- void Visit(const Array* array) override {
- const size_t count = array->elements.size();
- for (size_t i = 0; i < count; i++) {
- Reference key(android::ResTable_map::ATTR_MIN + i);
- FlattenEntry(&key, array->elements[i].get());
- }
- }
-
- void Visit(const Plural* plural) override {
- const size_t count = plural->values.size();
- for (size_t i = 0; i < count; i++) {
- if (!plural->values[i]) {
- continue;
- }
-
- ResourceId q;
- switch (i) {
- case Plural::Zero:
- q.id = android::ResTable_map::ATTR_ZERO;
- break;
-
- case Plural::One:
- q.id = android::ResTable_map::ATTR_ONE;
- break;
-
- case Plural::Two:
- q.id = android::ResTable_map::ATTR_TWO;
- break;
-
- case Plural::Few:
- q.id = android::ResTable_map::ATTR_FEW;
- break;
-
- case Plural::Many:
- q.id = android::ResTable_map::ATTR_MANY;
- break;
-
- case Plural::Other:
- q.id = android::ResTable_map::ATTR_OTHER;
- break;
-
- default:
- LOG(FATAL) << "unhandled plural type";
- break;
- }
-
- Reference key(q);
- FlattenEntry(&key, plural->values[i].get());
- }
- }
-
- /**
- * Call this after visiting a Value. This will finish any work that
- * needs to be done to prepare the entry.
- */
- void Finish() {
- out_entry_->count = android::util::HostToDevice32(entry_count_);
- }
-
- private:
- DISALLOW_COPY_AND_ASSIGN(MapFlattenVisitor);
-
- void FlattenKey(const Reference* key, ResTable_map* out_entry) {
- CHECK(bool(key->id)) << "key has no ID";
- out_entry->name.ident = android::util::HostToDevice32(key->id.value().id);
- }
-
- void FlattenValue(const Item* value, ResTable_map* out_entry) {
- CHECK(value->Flatten(&out_entry->value)) << "flatten failed";
- }
-
- void FlattenEntry(const Reference* key, Item* value) {
- ResTable_map* out_entry = buffer_->NextBlock<ResTable_map>();
- FlattenKey(key, out_entry);
- FlattenValue(value, out_entry);
- out_entry->value.size = android::util::HostToDevice16(sizeof(out_entry->value));
- entry_count_++;
- }
-
- ResTable_entry_ext* out_entry_;
- BigBuffer* buffer_;
- size_t entry_count_ = 0;
-};
-
struct OverlayableChunk {
std::string actor;
android::Source source;
@@ -233,14 +68,16 @@
PackageFlattener(IAaptContext* context, const ResourceTablePackageView& package,
const std::map<size_t, std::string>* shared_libs,
SparseEntriesMode sparse_entries, bool collapse_key_stringpool,
- const std::set<ResourceName>& name_collapse_exemptions)
+ const std::set<ResourceName>& name_collapse_exemptions,
+ bool deduplicate_entry_values)
: context_(context),
diag_(context->GetDiagnostics()),
package_(package),
shared_libs_(shared_libs),
sparse_entries_(sparse_entries),
collapse_key_stringpool_(collapse_key_stringpool),
- name_collapse_exemptions_(name_collapse_exemptions) {
+ name_collapse_exemptions_(name_collapse_exemptions),
+ deduplicate_entry_values_(deduplicate_entry_values) {
}
bool FlattenPackage(BigBuffer* buffer) {
@@ -298,47 +135,6 @@
private:
DISALLOW_COPY_AND_ASSIGN(PackageFlattener);
- template <typename T, bool IsItem>
- T* WriteEntry(FlatEntry* entry, BigBuffer* buffer) {
- static_assert(
- std::is_same<ResTable_entry, T>::value || std::is_same<ResTable_entry_ext, T>::value,
- "T must be ResTable_entry or ResTable_entry_ext");
-
- T* result = buffer->NextBlock<T>();
- ResTable_entry* out_entry = (ResTable_entry*)result;
- if (entry->entry->visibility.level == Visibility::Level::kPublic) {
- out_entry->flags |= ResTable_entry::FLAG_PUBLIC;
- }
-
- if (entry->value->IsWeak()) {
- out_entry->flags |= ResTable_entry::FLAG_WEAK;
- }
-
- if (!IsItem) {
- out_entry->flags |= ResTable_entry::FLAG_COMPLEX;
- }
-
- out_entry->flags = android::util::HostToDevice16(out_entry->flags);
- out_entry->key.index = android::util::HostToDevice32(entry->entry_key);
- out_entry->size = android::util::HostToDevice16(sizeof(T));
- return result;
- }
-
- bool FlattenValue(FlatEntry* entry, BigBuffer* buffer) {
- if (const Item* item = ValueCast<Item>(entry->value)) {
- WriteEntry<ResTable_entry, true>(entry, buffer);
- Res_value* outValue = buffer->NextBlock<Res_value>();
- CHECK(item->Flatten(outValue)) << "flatten failed";
- outValue->size = android::util::HostToDevice16(sizeof(*outValue));
- } else {
- ResTable_entry_ext* out_entry = WriteEntry<ResTable_entry_ext, false>(entry, buffer);
- MapFlattenVisitor visitor(out_entry, buffer);
- entry->value->Accept(&visitor);
- visitor.Finish();
- }
- return true;
- }
-
bool FlattenConfig(const ResourceTableTypeView& type, const ConfigDescription& config,
const size_t num_total_entries, std::vector<FlatEntry>* entries,
BigBuffer* buffer) {
@@ -355,16 +151,18 @@
offsets.resize(num_total_entries, 0xffffffffu);
android::BigBuffer values_buffer(512);
+ std::variant<std::monostate, DeduplicateItemsResEntryWriter, SequentialResEntryWriter>
+ writer_variant;
+ ResEntryWriter* res_entry_writer;
+ if (deduplicate_entry_values_) {
+ res_entry_writer = &writer_variant.emplace<DeduplicateItemsResEntryWriter>(&values_buffer);
+ } else {
+ res_entry_writer = &writer_variant.emplace<SequentialResEntryWriter>(&values_buffer);
+ }
+
for (FlatEntry& flat_entry : *entries) {
CHECK(static_cast<size_t>(flat_entry.entry->id.value()) < num_total_entries);
- offsets[flat_entry.entry->id.value()] = values_buffer.size();
- if (!FlattenValue(&flat_entry, &values_buffer)) {
- diag_->Error(android::DiagMessage()
- << "failed to flatten resource '"
- << ResourceNameRef(package_.name, type.named_type, flat_entry.entry->name)
- << "' for configuration '" << config << "'");
- return false;
- }
+ offsets[flat_entry.entry->id.value()] = res_entry_writer->Write(&flat_entry);
}
bool sparse_encode = sparse_entries_ == SparseEntriesMode::Enabled ||
@@ -720,6 +518,7 @@
bool collapse_key_stringpool_;
const std::set<ResourceName>& name_collapse_exemptions_;
std::map<uint32_t, uint32_t> aliases_;
+ bool deduplicate_entry_values_;
};
} // namespace
@@ -771,7 +570,8 @@
PackageFlattener flattener(context, package, &table->included_packages_,
options_.sparse_entries, options_.collapse_key_stringpool,
- options_.name_collapse_exemptions);
+ options_.name_collapse_exemptions,
+ options_.deduplicate_entry_values);
if (!flattener.FlattenPackage(&package_buffer)) {
return false;
}
diff --git a/tools/aapt2/format/binary/TableFlattener.h b/tools/aapt2/format/binary/TableFlattener.h
index c6d3033..6151b7e 100644
--- a/tools/aapt2/format/binary/TableFlattener.h
+++ b/tools/aapt2/format/binary/TableFlattener.h
@@ -54,6 +54,20 @@
// Map from original resource paths to shortened resource paths.
std::map<std::string, std::string> shortened_path_map;
+
+ // When enabled, only unique pairs of entry and value are stored in type chunks.
+ //
+ // By default, all such pairs are unique because a reference to resource name in the string pool
+ // is a part of the pair. But when resource names are collapsed (using 'collapse_key_stringpool'
+ // flag or manually) the same data might be duplicated multiple times in the same type chunk.
+ //
+ // For example: an application has 3 boolean resources with collapsed names and 3 'true' values
+ // are defined for these resources in 'default' configuration. All pairs of entry and value for
+ // these resources will have the same binary representation and stored only once in type chunk
+ // instead of three times when this flag is disabled.
+ //
+ // This applies only to simple entries (entry->flags & ResTable_entry::FLAG_COMPLEX == 0).
+ bool deduplicate_entry_values = false;
};
class TableFlattener : public IResourceTableConsumer {
diff --git a/tools/aapt2/format/binary/TableFlattener_test.cpp b/tools/aapt2/format/binary/TableFlattener_test.cpp
index b69dadd..2097a63 100644
--- a/tools/aapt2/format/binary/TableFlattener_test.cpp
+++ b/tools/aapt2/format/binary/TableFlattener_test.cpp
@@ -669,6 +669,87 @@
ResourceId(0x7f050000), {}, Res_value::TYPE_STRING, (uint32_t)*idx, 0u));
}
+TEST_F(TableFlattenerTest, ObfuscatingResourceNamesWithDeduplicationSucceeds) {
+ std::unique_ptr<ResourceTable> table =
+ test::ResourceTableBuilder()
+ .AddSimple("com.app.test:id/one", ResourceId(0x7f020000))
+ .AddSimple("com.app.test:id/two", ResourceId(0x7f020001))
+ .AddValue("com.app.test:id/three", ResourceId(0x7f020002),
+ test::BuildReference("com.app.test:id/one", ResourceId(0x7f020000)))
+ .AddValue("com.app.test:integer/one", ResourceId(0x7f030000),
+ util::make_unique<BinaryPrimitive>(uint8_t(Res_value::TYPE_INT_DEC), 1u))
+ .AddValue("com.app.test:integer/one", test::ParseConfigOrDie("v1"),
+ ResourceId(0x7f030000),
+ util::make_unique<BinaryPrimitive>(uint8_t(Res_value::TYPE_INT_DEC), 2u))
+ .AddString("com.app.test:string/test1", ResourceId(0x7f040000), "foo")
+ .AddString("com.app.test:string/test2", ResourceId(0x7f040001), "foo")
+ .AddString("com.app.test:string/test3", ResourceId(0x7f040002), "bar")
+ .AddString("com.app.test:string/test4", ResourceId(0x7f040003), "foo")
+ .AddString("com.app.test:layout/bar1", ResourceId(0x7f050000), "res/layout/bar.xml")
+ .AddString("com.app.test:layout/bar2", ResourceId(0x7f050001), "res/layout/bar.xml")
+ .Build();
+
+ TableFlattenerOptions options;
+ options.collapse_key_stringpool = true;
+ options.deduplicate_entry_values = true;
+
+ ResTable res_table;
+
+ ASSERT_TRUE(Flatten(context_.get(), options, table.get(), &res_table));
+
+ EXPECT_TRUE(Exists(&res_table, "com.app.test:id/0_resource_name_obfuscated",
+ ResourceId(0x7f020000), {}, Res_value::TYPE_INT_BOOLEAN, 0u, 0u));
+
+ EXPECT_TRUE(Exists(&res_table, "com.app.test:id/0_resource_name_obfuscated",
+ ResourceId(0x7f020001), {}, Res_value::TYPE_INT_BOOLEAN, 0u, 0u));
+
+ EXPECT_TRUE(Exists(&res_table, "com.app.test:id/0_resource_name_obfuscated",
+ ResourceId(0x7f020002), {}, Res_value::TYPE_REFERENCE, 0x7f020000u, 0u));
+
+ EXPECT_TRUE(Exists(&res_table, "com.app.test:integer/0_resource_name_obfuscated",
+ ResourceId(0x7f030000), {}, Res_value::TYPE_INT_DEC, 1u,
+ ResTable_config::CONFIG_VERSION));
+
+ EXPECT_TRUE(Exists(&res_table, "com.app.test:integer/0_resource_name_obfuscated",
+ ResourceId(0x7f030000), test::ParseConfigOrDie("v1"), Res_value::TYPE_INT_DEC,
+ 2u, ResTable_config::CONFIG_VERSION));
+
+ std::u16string foo_str = u"foo";
+ std::u16string bar_str = u"bar";
+ auto foo_idx = res_table.getTableStringBlock(0)->indexOfString(foo_str.data(), foo_str.size());
+ auto bar_idx = res_table.getTableStringBlock(0)->indexOfString(bar_str.data(), bar_str.size());
+ ASSERT_TRUE(foo_idx.has_value());
+ EXPECT_TRUE(Exists(&res_table, "com.app.test:string/0_resource_name_obfuscated",
+ ResourceId(0x7f040000), {}, Res_value::TYPE_STRING, (uint32_t)*foo_idx, 0u));
+ EXPECT_TRUE(Exists(&res_table, "com.app.test:string/0_resource_name_obfuscated",
+ ResourceId(0x7f040001), {}, Res_value::TYPE_STRING, (uint32_t)*foo_idx, 0u));
+ EXPECT_TRUE(Exists(&res_table, "com.app.test:string/0_resource_name_obfuscated",
+ ResourceId(0x7f040002), {}, Res_value::TYPE_STRING, (uint32_t)*bar_idx, 0u));
+ EXPECT_TRUE(Exists(&res_table, "com.app.test:string/0_resource_name_obfuscated",
+ ResourceId(0x7f040003), {}, Res_value::TYPE_STRING, (uint32_t)*foo_idx, 0u));
+
+ std::u16string bar_path = u"res/layout/bar.xml";
+ auto bar_path_idx =
+ res_table.getTableStringBlock(0)->indexOfString(bar_path.data(), bar_path.size());
+ ASSERT_TRUE(bar_path_idx.has_value());
+ EXPECT_TRUE(Exists(&res_table, "com.app.test:layout/0_resource_name_obfuscated",
+ ResourceId(0x7f050000), {}, Res_value::TYPE_STRING, (uint32_t)*bar_path_idx,
+ 0u));
+ EXPECT_TRUE(Exists(&res_table, "com.app.test:layout/0_resource_name_obfuscated",
+ ResourceId(0x7f050001), {}, Res_value::TYPE_STRING, (uint32_t)*bar_path_idx,
+ 0u));
+
+ std::string deduplicated_output;
+ std::string sequential_output;
+ Flatten(context_.get(), options, table.get(), &deduplicated_output);
+ options.deduplicate_entry_values = false;
+ Flatten(context_.get(), options, table.get(), &sequential_output);
+
+ // We have 4 duplicates: 0x7f020001 id, 0x7f040001 string, 0x7f040003 string, 0x7f050001 layout.
+ EXPECT_EQ(sequential_output.size(),
+ deduplicated_output.size() + 4 * (sizeof(ResTable_entry) + sizeof(Res_value)));
+}
+
TEST_F(TableFlattenerTest, ObfuscatingResourceNamesWithNameCollapseExemptionsSucceeds) {
std::unique_ptr<ResourceTable> table =
test::ResourceTableBuilder()
diff --git a/tools/lint/checks/src/main/java/com/google/android/lint/AndroidFrameworkIssueRegistry.kt b/tools/lint/checks/src/main/java/com/google/android/lint/AndroidFrameworkIssueRegistry.kt
index 8aa3e25..4d69d26 100644
--- a/tools/lint/checks/src/main/java/com/google/android/lint/AndroidFrameworkIssueRegistry.kt
+++ b/tools/lint/checks/src/main/java/com/google/android/lint/AndroidFrameworkIssueRegistry.kt
@@ -42,6 +42,8 @@
SaferParcelChecker.ISSUE_UNSAFE_API_USAGE,
PackageVisibilityDetector.ISSUE_PACKAGE_NAME_NO_PACKAGE_VISIBILITY_FILTERS,
RegisterReceiverFlagDetector.ISSUE_RECEIVER_EXPORTED_FLAG,
+ PermissionMethodDetector.ISSUE_PERMISSION_METHOD_USAGE,
+ PermissionMethodDetector.ISSUE_CAN_BE_PERMISSION_METHOD,
)
override val api: Int
diff --git a/tools/lint/checks/src/main/java/com/google/android/lint/Constants.kt b/tools/lint/checks/src/main/java/com/google/android/lint/Constants.kt
index 82eb8ed..3d5d01c 100644
--- a/tools/lint/checks/src/main/java/com/google/android/lint/Constants.kt
+++ b/tools/lint/checks/src/main/java/com/google/android/lint/Constants.kt
@@ -34,3 +34,7 @@
Method(CLASS_ACTIVITY_MANAGER_SERVICE, "checkPermission"),
Method(CLASS_ACTIVITY_MANAGER_INTERNAL, "enforceCallingPermission")
)
+
+const val ANNOTATION_PERMISSION_METHOD = "android.content.pm.PermissionMethod"
+const val ANNOTATION_PERMISSION_NAME = "android.content.pm.PermissionName"
+const val ANNOTATION_PERMISSION_RESULT = "android.content.pm.PackageManager.PermissionResult"
diff --git a/tools/lint/checks/src/main/java/com/google/android/lint/PermissionMethodDetector.kt b/tools/lint/checks/src/main/java/com/google/android/lint/PermissionMethodDetector.kt
new file mode 100644
index 0000000..68a450d
--- /dev/null
+++ b/tools/lint/checks/src/main/java/com/google/android/lint/PermissionMethodDetector.kt
@@ -0,0 +1,201 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.lint
+
+import com.android.tools.lint.client.api.UElementHandler
+import com.android.tools.lint.detector.api.Category
+import com.android.tools.lint.detector.api.Detector
+import com.android.tools.lint.detector.api.Implementation
+import com.android.tools.lint.detector.api.Issue
+import com.android.tools.lint.detector.api.JavaContext
+import com.android.tools.lint.detector.api.Scope
+import com.android.tools.lint.detector.api.Severity
+import com.android.tools.lint.detector.api.SourceCodeScanner
+import com.android.tools.lint.detector.api.getUMethod
+import com.intellij.psi.PsiType
+import org.jetbrains.uast.UAnnotation
+import org.jetbrains.uast.UBlockExpression
+import org.jetbrains.uast.UCallExpression
+import org.jetbrains.uast.UElement
+import org.jetbrains.uast.UExpression
+import org.jetbrains.uast.UIfExpression
+import org.jetbrains.uast.UMethod
+import org.jetbrains.uast.UQualifiedReferenceExpression
+import org.jetbrains.uast.UReturnExpression
+import org.jetbrains.uast.getContainingUMethod
+
+/**
+ * Stops incorrect usage of {@link PermissionMethod}
+ * TODO: add tests once re-enabled (b/240445172, b/247542171)
+ */
+class PermissionMethodDetector : Detector(), SourceCodeScanner {
+
+ override fun getApplicableUastTypes(): List<Class<out UElement>> =
+ listOf(UAnnotation::class.java, UMethod::class.java)
+
+ override fun createUastHandler(context: JavaContext): UElementHandler =
+ PermissionMethodHandler(context)
+
+ private inner class PermissionMethodHandler(val context: JavaContext) : UElementHandler() {
+ override fun visitMethod(node: UMethod) {
+ if (hasPermissionMethodAnnotation(node)) return
+ if (onlyCallsPermissionMethod(node)) {
+ val location = context.getLocation(node.javaPsi.modifierList)
+ val fix = fix()
+ .annotate(ANNOTATION_PERMISSION_METHOD)
+ .range(location)
+ .autoFix()
+ .build()
+
+ context.report(
+ ISSUE_CAN_BE_PERMISSION_METHOD,
+ location,
+ "Annotate method with @PermissionMethod",
+ fix
+ )
+ }
+ }
+
+ override fun visitAnnotation(node: UAnnotation) {
+ if (node.qualifiedName != ANNOTATION_PERMISSION_METHOD) return
+ val method = node.getContainingUMethod() ?: return
+
+ if (!isPermissionMethodReturnType(method)) {
+ context.report(
+ ISSUE_PERMISSION_METHOD_USAGE,
+ context.getLocation(node),
+ """
+ Methods annotated with `@PermissionMethod` should return `void`, \
+ `boolean`, or `@PackageManager.PermissionResult int`."
+ """.trimIndent()
+ )
+ }
+
+ if (method.returnType == PsiType.INT &&
+ method.annotations.none { it.hasQualifiedName(ANNOTATION_PERMISSION_RESULT) }
+ ) {
+ context.report(
+ ISSUE_PERMISSION_METHOD_USAGE,
+ context.getLocation(node),
+ """
+ Methods annotated with `@PermissionMethod` that return `int` should \
+ also be annotated with `@PackageManager.PermissionResult.`"
+ """.trimIndent()
+ )
+ }
+ }
+ }
+
+ companion object {
+
+ private val EXPLANATION_PERMISSION_METHOD_USAGE = """
+ `@PermissionMethod` should annotate methods that ONLY perform permission lookups. \
+ Said methods should return `boolean`, `@PackageManager.PermissionResult int`, or return \
+ `void` and potentially throw `SecurityException`.
+ """.trimIndent()
+
+ @JvmField
+ val ISSUE_PERMISSION_METHOD_USAGE = Issue.create(
+ id = "PermissionMethodUsage",
+ briefDescription = "@PermissionMethod used incorrectly",
+ explanation = EXPLANATION_PERMISSION_METHOD_USAGE,
+ category = Category.CORRECTNESS,
+ priority = 5,
+ severity = Severity.ERROR,
+ implementation = Implementation(
+ PermissionMethodDetector::class.java,
+ Scope.JAVA_FILE_SCOPE
+ ),
+ enabledByDefault = true
+ )
+
+ private val EXPLANATION_CAN_BE_PERMISSION_METHOD = """
+ Methods that only call other methods annotated with @PermissionMethod (and do NOTHING else) can themselves \
+ be annotated with @PermissionMethod. For example:
+ ```
+ void wrapperHelper() {
+ // Context.enforceCallingPermission is annotated with @PermissionMethod
+ context.enforceCallingPermission(SOME_PERMISSION)
+ }
+ ```
+ """.trimIndent()
+
+ @JvmField
+ val ISSUE_CAN_BE_PERMISSION_METHOD = Issue.create(
+ id = "CanBePermissionMethod",
+ briefDescription = "Method can be annotated with @PermissionMethod",
+ explanation = EXPLANATION_CAN_BE_PERMISSION_METHOD,
+ category = Category.SECURITY,
+ priority = 5,
+ severity = Severity.WARNING,
+ implementation = Implementation(
+ PermissionMethodDetector::class.java,
+ Scope.JAVA_FILE_SCOPE
+ ),
+ enabledByDefault = false
+ )
+
+ private fun hasPermissionMethodAnnotation(method: UMethod): Boolean = method.annotations
+ .any {
+ it.hasQualifiedName(ANNOTATION_PERMISSION_METHOD)
+ }
+
+ private fun isPermissionMethodReturnType(method: UMethod): Boolean =
+ listOf(PsiType.VOID, PsiType.INT, PsiType.BOOLEAN).contains(method.returnType)
+
+ /**
+ * Identifies methods that...
+ * DO call other methods annotated with @PermissionMethod
+ * DO NOT do anything else
+ */
+ private fun onlyCallsPermissionMethod(method: UMethod): Boolean {
+ val body = method.uastBody as? UBlockExpression ?: return false
+ if (body.expressions.isEmpty()) return false
+ for (expression in body.expressions) {
+ when (expression) {
+ is UQualifiedReferenceExpression -> {
+ if (!isPermissionMethodCall(expression.selector)) return false
+ }
+ is UReturnExpression -> {
+ if (!isPermissionMethodCall(expression.returnExpression)) return false
+ }
+ is UCallExpression -> {
+ if (!isPermissionMethodCall(expression)) return false
+ }
+ is UIfExpression -> {
+ if (expression.thenExpression !is UReturnExpression) return false
+ if (!isPermissionMethodCall(expression.condition)) return false
+ }
+ else -> return false
+ }
+ }
+ return true
+ }
+
+ private fun isPermissionMethodCall(expression: UExpression?): Boolean {
+ return when (expression) {
+ is UQualifiedReferenceExpression ->
+ return isPermissionMethodCall(expression.selector)
+ is UCallExpression -> {
+ val calledMethod = expression.resolve()?.getUMethod() ?: return false
+ return hasPermissionMethodAnnotation(calledMethod)
+ }
+ else -> false
+ }
+ }
+ }
+}
diff --git a/tools/processors/immutability/src/android/processor/immutability/ImmutabilityProcessor.kt b/tools/processors/immutability/src/android/processor/immutability/ImmutabilityProcessor.kt
index 3ab09a8..dfebdcc 100644
--- a/tools/processors/immutability/src/android/processor/immutability/ImmutabilityProcessor.kt
+++ b/tools/processors/immutability/src/android/processor/immutability/ImmutabilityProcessor.kt
@@ -37,10 +37,11 @@
class ImmutabilityProcessor : AbstractProcessor() {
companion object {
+
/**
- * Types that are already immutable.
+ * Types that are already immutable. Will also ignore subclasses.
*/
- private val IGNORED_TYPES = listOf(
+ private val IGNORED_SUPER_TYPES = listOf(
"java.io.File",
"java.lang.Boolean",
"java.lang.Byte",
@@ -56,6 +57,15 @@
"android.os.Parcelable.Creator",
)
+ /**
+ * Types that are already immutable. Must be an exact match, does not include any super
+ * or sub classes.
+ */
+ private val IGNORED_EXACT_TYPES = listOf(
+ "java.lang.Class",
+ "java.lang.Object",
+ )
+
private val IGNORED_METHODS = listOf(
"writeToParcel",
)
@@ -64,7 +74,8 @@
private lateinit var collectionType: TypeMirror
private lateinit var mapType: TypeMirror
- private lateinit var ignoredTypes: List<TypeMirror>
+ private lateinit var ignoredSuperTypes: List<TypeMirror>
+ private lateinit var ignoredExactTypes: List<TypeMirror>
private val seenTypesByPolicy = mutableMapOf<Set<Immutable.Policy.Exception>, Set<Type>>()
@@ -76,7 +87,8 @@
super.init(processingEnv)
collectionType = processingEnv.erasedType("java.util.Collection")!!
mapType = processingEnv.erasedType("java.util.Map")!!
- ignoredTypes = IGNORED_TYPES.mapNotNull { processingEnv.erasedType(it) }
+ ignoredSuperTypes = IGNORED_SUPER_TYPES.mapNotNull { processingEnv.erasedType(it) }
+ ignoredExactTypes = IGNORED_EXACT_TYPES.mapNotNull { processingEnv.erasedType(it) }
}
override fun process(
@@ -109,7 +121,7 @@
classType: Symbol.TypeSymbol,
parentPolicyExceptions: Set<Immutable.Policy.Exception>,
): Boolean {
- if (classType.getAnnotation(Immutable.Ignore::class.java) != null) return false
+ if (isIgnored(classType)) return false
val policyAnnotation = classType.getAnnotation(Immutable.Policy::class.java)
val newPolicyExceptions = parentPolicyExceptions + policyAnnotation?.exceptions.orEmpty()
@@ -131,7 +143,7 @@
.fold(false) { anyError, field ->
if (field.isStatic) {
if (!field.isPrivate) {
- var finalityError = !field.modifiers.contains(Modifier.FINAL)
+ val finalityError = !field.modifiers.contains(Modifier.FINAL)
if (finalityError) {
printError(parentChain, field, MessageUtils.staticNonFinalFailure())
}
@@ -177,8 +189,10 @@
val newChain = parentChain + "$classType"
val hasMethodError = filteredElements
+ .asSequence()
.filter { it.getKind() == ElementKind.METHOD }
.map { it as Symbol.MethodSymbol }
+ .filterNot { it.isStatic }
.filterNot { IGNORED_METHODS.contains(it.name.toString()) }
.fold(false) { anyError, method ->
// Must call visitMethod first so it doesn't get short circuited by the ||
@@ -208,6 +222,14 @@
}
}
+ // Check all of the super classes, since methods in those classes are also accessible
+ (classType as? Symbol.ClassSymbol)?.run {
+ (interfaces + superclass).forEach {
+ val element = it.asElement() ?: return@forEach
+ visitClass(parentChain, seenTypesByPolicy, element, element, newPolicyExceptions)
+ }
+ }
+
if (isRegularClass && !anyError && allowFinalClassesFinalFields &&
!classType.modifiers.contains(Modifier.FINAL)
) {
@@ -301,16 +323,14 @@
parentPolicyExceptions: Set<Immutable.Policy.Exception>,
nonInterfaceClassFailure: () -> String = { MessageUtils.nonInterfaceReturnFailure() },
): Boolean {
+ if (isIgnored(symbol)) return false
+ if (isIgnored(type)) return false
if (type.isPrimitive) return false
if (type.isPrimitiveOrVoid) {
printError(parentChain, symbol, MessageUtils.voidReturnFailure())
return true
}
- if (ignoredTypes.any { processingEnv.typeUtils.isAssignable(type, it) }) {
- return false
- }
-
val policyAnnotation = symbol.getAnnotation(Immutable.Policy::class.java)
val newPolicyExceptions = parentPolicyExceptions + policyAnnotation?.exceptions.orEmpty()
@@ -357,16 +377,38 @@
message: String,
) = processingEnv.messager.printMessage(
Diagnostic.Kind.ERROR,
- // Drop one from the parent chain so that the directly enclosing class isn't logged.
- // It exists in the list at this point in the traversal so that further children can
- // include the right reference.
- parentChain.dropLast(1).joinToString() + "\n\t" + message,
+ parentChain.plus(element.simpleName).joinToString() + "\n\t " + message,
element,
)
private fun ProcessingEnvironment.erasedType(typeName: String) =
elementUtils.getTypeElement(typeName)?.asType()?.let(typeUtils::erasure)
- private fun isIgnored(symbol: Symbol) =
- symbol.getAnnotation(Immutable.Ignore::class.java) != null
+ private fun isIgnored(type: Type) =
+ (type.getAnnotation(Immutable.Ignore::class.java) != null)
+ || (ignoredSuperTypes.any { type.isAssignable(it) })
+ || (ignoredExactTypes.any { type.isSameType(it) })
+
+ private fun isIgnored(symbol: Symbol) = when {
+ // Anything annotated as @Ignore is always ignored
+ symbol.getAnnotation(Immutable.Ignore::class.java) != null -> true
+ // Then ignore exact types, regardless of what kind they are
+ ignoredExactTypes.any { symbol.type.isSameType(it) } -> true
+ // Then only allow methods through, since other types (fields) are usually a failure
+ symbol.getKind() != ElementKind.METHOD -> false
+ // Finally, check for any ignored super types
+ else -> ignoredSuperTypes.any { symbol.type.isAssignable(it) }
+ }
+
+ private fun TypeMirror.isAssignable(type: TypeMirror) = try {
+ processingEnv.typeUtils.isAssignable(this, type)
+ } catch (ignored: Exception) {
+ false
+ }
+
+ private fun TypeMirror.isSameType(type: TypeMirror) = try {
+ processingEnv.typeUtils.isSameType(this, type)
+ } catch (ignored: Exception) {
+ false
+ }
}
diff --git a/tools/processors/immutability/test/android/processor/ImmutabilityProcessorTest.kt b/tools/processors/immutability/test/android/processor/ImmutabilityProcessorTest.kt
index f26357f..2f7d59a 100644
--- a/tools/processors/immutability/test/android/processor/ImmutabilityProcessorTest.kt
+++ b/tools/processors/immutability/test/android/processor/ImmutabilityProcessorTest.kt
@@ -90,7 +90,7 @@
@Test
fun validInterface() = test(
- JavaFileObjects.forSourceString(
+ source = JavaFileObjects.forSourceString(
"$PACKAGE_PREFIX.$DATA_CLASS_NAME",
/* language=JAVA */ """
package $PACKAGE_PREFIX;
@@ -227,49 +227,114 @@
nonInterfaceReturnFailure(line = 9),
nonInterfaceReturnFailure(line = 10, index = 0),
classNotFinalFailure(line = 13, "NonFinalClassFinalFields"),
- ), otherErrors = listOf(
- memberNotMethodFailure(line = 4) to FINAL_CLASSES[1],
- memberNotMethodFailure(line = 4) to FINAL_CLASSES[3],
+ ), otherErrors = mapOf(
+ FINAL_CLASSES[1] to listOf(
+ memberNotMethodFailure(line = 4),
+ ),
+ FINAL_CLASSES[3] to listOf(
+ memberNotMethodFailure(line = 4),
+ ),
)
)
+ @Test
+ fun superClass() {
+ val superClass = JavaFileObjects.forSourceString(
+ "$PACKAGE_PREFIX.SuperClass",
+ /* language=JAVA */ """
+ package $PACKAGE_PREFIX;
+
+ import java.util.List;
+
+ public interface SuperClass {
+ InnerClass getInnerClassOne();
+
+ final class InnerClass {
+ public String innerField;
+ }
+ }
+ """.trimIndent()
+ )
+
+ val dataClass = JavaFileObjects.forSourceString(
+ "$PACKAGE_PREFIX.$DATA_CLASS_NAME",
+ /* language=JAVA */ """
+ package $PACKAGE_PREFIX;
+
+ import java.util.List;
+
+ @Immutable
+ public interface $DATA_CLASS_NAME extends SuperClass {
+ String[] getArray();
+ }
+ """.trimIndent()
+ )
+
+ test(
+ sources = arrayOf(superClass, dataClass),
+ fileToErrors = mapOf(
+ superClass to listOf(
+ classNotImmutableFailure(line = 5, className = "SuperClass"),
+ nonInterfaceReturnFailure(line = 6),
+ nonInterfaceClassFailure(8),
+ classNotImmutableFailure(line = 8, className = "InnerClass"),
+ memberNotMethodFailure(line = 9),
+ ),
+ dataClass to listOf(
+ arrayFailure(line = 7),
+ )
+ )
+ )
+ }
+
private fun test(
source: JavaFileObject,
errors: List<CompilationError>,
- otherErrors: List<Pair<CompilationError, JavaFileObject>> = emptyList(),
+ otherErrors: Map<JavaFileObject, List<CompilationError>> = emptyMap(),
+ ) = test(
+ sources = arrayOf(source),
+ fileToErrors = otherErrors + (source to errors),
+ )
+
+ private fun test(
+ vararg sources: JavaFileObject,
+ fileToErrors: Map<JavaFileObject, List<CompilationError>> = emptyMap(),
) {
val compilation = javac()
.withProcessors(ImmutabilityProcessor())
- .compile(FINAL_CLASSES + ANNOTATION + listOf(source))
- val allErrors = otherErrors + errors.map { it to source }
- allErrors.forEach { (error, file) ->
- try {
- assertThat(compilation)
- .hadErrorContaining(error.message)
- .inFile(file)
- .onLine(error.line)
- } catch (e: AssertionError) {
- // Wrap the exception so that the line number is logged
- val wrapped = AssertionError("Expected $error, ${e.message}").apply {
- stackTrace = e.stackTrace
- }
+ .compile(FINAL_CLASSES + ANNOTATION + sources)
- // Wrap again with Expect so that all errors are reported. This is very bad code
- // but can only be fixed by updating compile-testing with a better Truth Subject
- // implementation.
- expect.that(wrapped).isNull()
+ fileToErrors.forEach { (file, errors) ->
+ errors.forEach { error ->
+ try {
+ assertThat(compilation)
+ .hadErrorContaining(error.message)
+ .inFile(file)
+ .onLine(error.line)
+ } catch (e: AssertionError) {
+ // Wrap the exception so that the line number is logged
+ val wrapped = AssertionError("Expected $error, ${e.message}").apply {
+ stackTrace = e.stackTrace
+ }
+
+ // Wrap again with Expect so that all errors are reported. This is very bad code
+ // but can only be fixed by updating compile-testing with a better Truth Subject
+ // implementation.
+ expect.that(wrapped).isNull()
+ }
}
}
- try {
- assertThat(compilation).hadErrorCount(allErrors.size)
- } catch (e: AssertionError) {
+ expect.that(compilation.errors().size).isEqualTo(fileToErrors.values.sumOf { it.size })
+
+ if (expect.hasFailures()) {
expect.withMessage(
compilation.errors()
+ .sortedBy { it.lineNumber }
.joinToString(separator = "\n") {
"${it.lineNumber}: ${it.getMessage(Locale.ENGLISH)?.trim()}"
}
- ).that(e).isNull()
+ ).fail()
}
}
@@ -307,4 +372,4 @@
val line: Long,
val message: String,
)
-}
\ No newline at end of file
+}