Merge "InputManagerService - call PermissionEnforcer helper on methods annotated with @EnforcePermission"
diff --git a/core/api/current.txt b/core/api/current.txt
index 53dbef8..b64afea 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -7669,6 +7669,7 @@
method public boolean updateOverrideApn(@NonNull android.content.ComponentName, int, @NonNull android.telephony.data.ApnSetting);
method public void wipeData(int);
method public void wipeData(int, @NonNull CharSequence);
+ method public void wipeDevice(int);
field public static final String ACTION_ADD_DEVICE_ADMIN = "android.app.action.ADD_DEVICE_ADMIN";
field public static final String ACTION_ADMIN_POLICY_COMPLIANCE = "android.app.action.ADMIN_POLICY_COMPLIANCE";
field public static final String ACTION_APPLICATION_DELEGATION_SCOPES_CHANGED = "android.app.action.APPLICATION_DELEGATION_SCOPES_CHANGED";
@@ -16597,6 +16598,7 @@
field public static final int FONT_WEIGHT_NORMAL = 400; // 0x190
field public static final int FONT_WEIGHT_SEMI_BOLD = 600; // 0x258
field public static final int FONT_WEIGHT_THIN = 100; // 0x64
+ field public static final int FONT_WEIGHT_UNSPECIFIED = -1; // 0xffffffff
}
public final class FontVariationAxis {
@@ -40389,6 +40391,7 @@
field public static final String EVENT_DISPLAY_DIAGNOSTIC_MESSAGE = "android.telecom.event.DISPLAY_DIAGNOSTIC_MESSAGE";
field public static final String EXTRA_DIAGNOSTIC_MESSAGE = "android.telecom.extra.DIAGNOSTIC_MESSAGE";
field public static final String EXTRA_DIAGNOSTIC_MESSAGE_ID = "android.telecom.extra.DIAGNOSTIC_MESSAGE_ID";
+ field public static final String EXTRA_IS_SUPPRESSED_BY_DO_NOT_DISTURB = "android.telecom.extra.IS_SUPPRESSED_BY_DO_NOT_DISTURB";
field public static final String EXTRA_LAST_EMERGENCY_CALLBACK_TIME_MILLIS = "android.telecom.extra.LAST_EMERGENCY_CALLBACK_TIME_MILLIS";
field public static final String EXTRA_SILENT_RINGING_REQUESTED = "android.telecom.extra.SILENT_RINGING_REQUESTED";
field public static final String EXTRA_SUGGESTED_PHONE_ACCOUNTS = "android.telecom.extra.SUGGESTED_PHONE_ACCOUNTS";
@@ -44117,7 +44120,7 @@
field public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_ENTITLEMENT_CHECK_FAILED = 13; // 0xd
field public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_FEATURE_NOT_SUPPORTED = 10; // 0xa
field public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_NETWORK_NOT_AVAILABLE = 12; // 0xc
- field public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_NOT_DEFAULT_DATA_SUB = 14; // 0xe
+ field public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_NOT_DEFAULT_DATA_SUBSCRIPTION = 14; // 0xe
field public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_OVERRIDDEN = 5; // 0x5
field public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_PENDING_NETWORK_SETUP = 15; // 0xf
field public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_REQUEST_FAILED = 11; // 0xb
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index b5b87c1..db1e4e0 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -12120,6 +12120,7 @@
method public static int getMaxScore();
method @Nullable public android.media.MediaSyncEvent getMediaSyncEvent();
method public int getPersonalizedScore();
+ method public int getProximity();
method public int getScore();
method public boolean isHotwordDetectionPersonalized();
method public void writeToParcel(@NonNull android.os.Parcel, int);
@@ -12133,6 +12134,9 @@
field public static final int CONFIDENCE_LEVEL_VERY_HIGH = 6; // 0x6
field @NonNull public static final android.os.Parcelable.Creator<android.service.voice.HotwordDetectedResult> CREATOR;
field public static final int HOTWORD_OFFSET_UNSET = -1; // 0xffffffff
+ field public static final int PROXIMITY_FAR = 2; // 0x2
+ field public static final int PROXIMITY_NEAR = 1; // 0x1
+ field public static final int PROXIMITY_UNKNOWN = -1; // 0xffffffff
}
public static final class HotwordDetectedResult.Builder {
@@ -12469,6 +12473,8 @@
field public static final int CS_BOUND = 6; // 0x6
field public static final int DIRECT_TO_VM_FINISHED = 103; // 0x67
field public static final int DIRECT_TO_VM_INITIATED = 102; // 0x66
+ field public static final int DND_CHECK_COMPLETED = 110; // 0x6e
+ field public static final int DND_CHECK_INITIATED = 109; // 0x6d
field public static final int FILTERING_COMPLETED = 107; // 0x6b
field public static final int FILTERING_INITIATED = 106; // 0x6a
field public static final int FILTERING_TIMED_OUT = 108; // 0x6c
@@ -12508,6 +12514,7 @@
field @NonNull public static final android.os.Parcelable.Creator<android.telecom.ParcelableCallAnalytics.EventTiming> CREATOR;
field public static final int DIRECT_TO_VM_FINISHED_TIMING = 8; // 0x8
field public static final int DISCONNECT_TIMING = 2; // 0x2
+ field public static final int DND_PRE_CALL_PRE_CHECK_TIMING = 12; // 0xc
field public static final int FILTERING_COMPLETED_TIMING = 10; // 0xa
field public static final int FILTERING_TIMED_OUT_TIMING = 11; // 0xb
field public static final int HOLD_TIMING = 3; // 0x3
diff --git a/core/api/test-current.txt b/core/api/test-current.txt
index d8419a8..535f6ce 100644
--- a/core/api/test-current.txt
+++ b/core/api/test-current.txt
@@ -758,6 +758,7 @@
method public int getUserId();
method public void setAutofillOptions(@Nullable android.content.AutofillOptions);
method public void setContentCaptureOptions(@Nullable android.content.ContentCaptureOptions);
+ field public static final String ATTENTION_SERVICE = "attention";
field public static final String CONTENT_CAPTURE_MANAGER_SERVICE = "content_capture";
field public static final String DEVICE_IDLE_CONTROLLER = "deviceidle";
field public static final String DREAM_SERVICE = "dream";
@@ -2499,6 +2500,10 @@
method @NonNull public android.service.voice.AlwaysOnHotwordDetector.EventPayload.Builder setKeyphraseRecognitionExtras(@NonNull java.util.List<android.hardware.soundtrigger.SoundTrigger.KeyphraseRecognitionExtra>);
}
+ public abstract class HotwordDetectionService extends android.app.Service {
+ field public static final boolean ENABLE_PROXIMITY_RESULT = true;
+ }
+
public final class VisibleActivityInfo implements android.os.Parcelable {
ctor public VisibleActivityInfo(int, @NonNull android.os.IBinder);
}
diff --git a/core/java/android/app/AppOpsManager.java b/core/java/android/app/AppOpsManager.java
index dc325ff..860bfc5 100644
--- a/core/java/android/app/AppOpsManager.java
+++ b/core/java/android/app/AppOpsManager.java
@@ -2403,6 +2403,9 @@
"SYSTEM_EXEMPT_FROM_FORCED_APP_STANDBY").build()
};
+ // The number of longs needed to form a full bitmask of app ops
+ private static final int BITMASK_LEN = ((_NUM_OP - 1) / Long.SIZE) + 1;
+
/**
* @hide
*/
@@ -2437,8 +2440,8 @@
* @see #getNotedOpCollectionMode
* @see #collectNotedOpSync
*/
- private static final ThreadLocal<ArrayMap<String, long[]>> sAppOpsNotedInThisBinderTransaction =
- new ThreadLocal<>();
+ private static final ThreadLocal<ArrayMap<String, BitSet>>
+ sAppOpsNotedInThisBinderTransaction = new ThreadLocal<>();
static {
if (sAppOpInfos.length != _NUM_OP) {
@@ -8684,10 +8687,10 @@
*/
public static class PausedNotedAppOpsCollection {
final int mUid;
- final @Nullable ArrayMap<String, long[]> mCollectedNotedAppOps;
+ final @Nullable ArrayMap<String, BitSet> mCollectedNotedAppOps;
PausedNotedAppOpsCollection(int uid, @Nullable ArrayMap<String,
- long[]> collectedNotedAppOps) {
+ BitSet> collectedNotedAppOps) {
mUid = uid;
mCollectedNotedAppOps = collectedNotedAppOps;
}
@@ -8705,7 +8708,7 @@
public static @Nullable PausedNotedAppOpsCollection pauseNotedAppOpsCollection() {
Integer previousUid = sBinderThreadCallingUid.get();
if (previousUid != null) {
- ArrayMap<String, long[]> previousCollectedNotedAppOps =
+ ArrayMap<String, BitSet> previousCollectedNotedAppOps =
sAppOpsNotedInThisBinderTransaction.get();
sBinderThreadCallingUid.remove();
@@ -8779,23 +8782,19 @@
// We are inside of a two-way binder call. Delivered to caller via
// {@link #prefixParcelWithAppOpsIfNeeded}
int op = sOpStrToOp.get(syncOp.getOp());
- ArrayMap<String, long[]> appOpsNoted = sAppOpsNotedInThisBinderTransaction.get();
+ ArrayMap<String, BitSet> appOpsNoted = sAppOpsNotedInThisBinderTransaction.get();
if (appOpsNoted == null) {
appOpsNoted = new ArrayMap<>(1);
sAppOpsNotedInThisBinderTransaction.set(appOpsNoted);
}
- long[] appOpsNotedForAttribution = appOpsNoted.get(syncOp.getAttributionTag());
+ BitSet appOpsNotedForAttribution = appOpsNoted.get(syncOp.getAttributionTag());
if (appOpsNotedForAttribution == null) {
- appOpsNotedForAttribution = new long[2];
+ appOpsNotedForAttribution = new BitSet(_NUM_OP);
appOpsNoted.put(syncOp.getAttributionTag(), appOpsNotedForAttribution);
}
- if (op < 64) {
- appOpsNotedForAttribution[0] |= 1L << op;
- } else {
- appOpsNotedForAttribution[1] |= 1L << (op - 64);
- }
+ appOpsNotedForAttribution.set(op);
}
/** @hide */
@@ -8869,7 +8868,7 @@
*/
// TODO (b/186872903) Refactor how sync noted ops are propagated.
public static void prefixParcelWithAppOpsIfNeeded(@NonNull Parcel p) {
- ArrayMap<String, long[]> notedAppOps = sAppOpsNotedInThisBinderTransaction.get();
+ ArrayMap<String, BitSet> notedAppOps = sAppOpsNotedInThisBinderTransaction.get();
if (notedAppOps == null) {
return;
}
@@ -8881,8 +8880,15 @@
for (int i = 0; i < numAttributionWithNotesAppOps; i++) {
p.writeString(notedAppOps.keyAt(i));
- p.writeLong(notedAppOps.valueAt(i)[0]);
- p.writeLong(notedAppOps.valueAt(i)[1]);
+ // Bitmask's toLongArray will truncate the array, if upper bits arent used
+ long[] notedOpsMask = notedAppOps.valueAt(i).toLongArray();
+ for (int j = 0; j < BITMASK_LEN; j++) {
+ if (j < notedOpsMask.length) {
+ p.writeLong(notedOpsMask[j]);
+ } else {
+ p.writeLong(0);
+ }
+ }
}
}
@@ -8901,12 +8907,13 @@
for (int i = 0; i < numAttributionsWithNotedAppOps; i++) {
String attributionTag = p.readString();
- long[] rawNotedAppOps = new long[2];
- rawNotedAppOps[0] = p.readLong();
- rawNotedAppOps[1] = p.readLong();
+ long[] rawNotedAppOps = new long[BITMASK_LEN];
+ for (int j = 0; j < rawNotedAppOps.length; j++) {
+ rawNotedAppOps[j] = p.readLong();
+ }
+ BitSet notedAppOps = BitSet.valueOf(rawNotedAppOps);
- if (rawNotedAppOps[0] != 0 || rawNotedAppOps[1] != 0) {
- BitSet notedAppOps = BitSet.valueOf(rawNotedAppOps);
+ if (!notedAppOps.isEmpty()) {
synchronized (sLock) {
for (int code = notedAppOps.nextSetBit(0); code != -1;
diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java
index f4cee5a..6fedb41 100644
--- a/core/java/android/app/admin/DevicePolicyManager.java
+++ b/core/java/android/app/admin/DevicePolicyManager.java
@@ -171,6 +171,7 @@
private final boolean mParentInstance;
private final DevicePolicyResourcesManager mResourcesManager;
+
/** @hide */
public DevicePolicyManager(Context context, IDevicePolicyManager service) {
this(context, service, false);
@@ -6207,46 +6208,46 @@
public static final int WIPE_SILENTLY = 0x0008;
/**
- * Ask that all user data be wiped. If called as a secondary user, the user will be removed and
- * other users will remain unaffected. Calling from the primary user will cause the device to
- * reboot, erasing all device data - including all the secondary users and their data - while
- * booting up.
- * <p>
- * The calling device admin must have requested {@link DeviceAdminInfo#USES_POLICY_WIPE_DATA} to
- * be able to call this method; if it has not, a security exception will be thrown.
- *
- * If the caller is a profile owner of an organization-owned managed profile, it may
- * additionally call this method on the parent instance.
- * Calling this method on the parent {@link DevicePolicyManager} instance would wipe the
- * entire device, while calling it on the current profile instance would relinquish the device
- * for personal use, removing the managed profile and all policies set by the profile owner.
+ * See {@link #wipeData(int, CharSequence)}
*
* @param flags Bit mask of additional options: currently supported flags are
- * {@link #WIPE_EXTERNAL_STORAGE}, {@link #WIPE_RESET_PROTECTION_DATA},
- * {@link #WIPE_EUICC} and {@link #WIPE_SILENTLY}.
- * @throws SecurityException if the calling application does not own an active administrator
- * that uses {@link DeviceAdminInfo#USES_POLICY_WIPE_DATA} or is not granted the
- * {@link android.Manifest.permission#MASTER_CLEAR} permission.
+ * {@link #WIPE_EXTERNAL_STORAGE}, {@link #WIPE_RESET_PROTECTION_DATA},
+ * {@link #WIPE_EUICC} and {@link #WIPE_SILENTLY}.
+ * @throws SecurityException if the calling application does not own an active
+ * administrator
+ * that uses {@link DeviceAdminInfo#USES_POLICY_WIPE_DATA} and is
+ * not granted the
+ * {@link android.Manifest.permission#MASTER_CLEAR} permission.
+ * @throws IllegalStateException if called on last full-user or system-user
+ * @see #wipeDevice(int)
+ * @see #wipeData(int, CharSequence)
*/
public void wipeData(int flags) {
- wipeDataInternal(flags, "");
+ wipeDataInternal(flags,
+ /* wipeReasonForUser= */ "",
+ /* factoryReset= */ false);
}
/**
- * Ask that all user data be wiped. If called as a secondary user, the user will be removed and
- * other users will remain unaffected, the provided reason for wiping data can be shown to
- * user. Calling from the primary user will cause the device to reboot, erasing all device data
- * - including all the secondary users and their data - while booting up. In this case, we don't
- * show the reason to the user since the device would be factory reset.
- * <p>
- * The calling device admin must have requested {@link DeviceAdminInfo#USES_POLICY_WIPE_DATA} to
- * be able to call this method; if it has not, a security exception will be thrown.
+ * Ask that all user data be wiped.
*
- * If the caller is a profile owner of an organization-owned managed profile, it may
- * additionally call this method on the parent instance.
- * Calling this method on the parent {@link DevicePolicyManager} instance would wipe the
- * entire device, while calling it on the current profile instance would relinquish the device
- * for personal use, removing the managed profile and all policies set by the profile owner.
+ * <p>
+ * If called as a secondary user or managed profile, the user itself and its associated user
+ * data will be wiped. In particular, If the caller is a profile owner of an
+ * organization-owned managed profile, calling this method will relinquish the device for
+ * personal use, removing the managed profile and all policies set by the profile owner.
+ * </p>
+ *
+ * <p>
+ * Calling this method from the primary user will only work if the calling app is targeting
+ * Android 13 or below, in which case it will cause the device to reboot, erasing all device
+ * data - including all the secondary users and their data - while booting up. If an app
+ * targeting Android 13+ is calling this method from the primary user or last full user,
+ * {@link IllegalStateException} will be thrown.
+ * </p>
+ *
+ * If an app wants to wipe the entire device irrespective of which user they are from, they
+ * should use {@link #wipeDevice} instead.
*
* @param flags Bit mask of additional options: currently supported flags are
* {@link #WIPE_EXTERNAL_STORAGE}, {@link #WIPE_RESET_PROTECTION_DATA} and
@@ -6254,30 +6255,61 @@
* @param reason a string that contains the reason for wiping data, which can be
* presented to the user.
* @throws SecurityException if the calling application does not own an active administrator
- * that uses {@link DeviceAdminInfo#USES_POLICY_WIPE_DATA} or is not granted the
+ * that uses {@link DeviceAdminInfo#USES_POLICY_WIPE_DATA} and is not granted the
* {@link android.Manifest.permission#MASTER_CLEAR} permission.
* @throws IllegalArgumentException if the input reason string is null or empty, or if
* {@link #WIPE_SILENTLY} is set.
+ * @throws IllegalStateException if called on last full-user or system-user
+ * @see #wipeDevice(int)
+ * @see #wipeData(int)
*/
public void wipeData(int flags, @NonNull CharSequence reason) {
Objects.requireNonNull(reason, "reason string is null");
Preconditions.checkStringNotEmpty(reason, "reason string is empty");
Preconditions.checkArgument((flags & WIPE_SILENTLY) == 0, "WIPE_SILENTLY cannot be set");
- wipeDataInternal(flags, reason.toString());
+ wipeDataInternal(flags, reason.toString(), /* factoryReset= */ false);
}
/**
- * Internal function for both {@link #wipeData(int)} and
- * {@link #wipeData(int, CharSequence)} to call.
+ * Ask that the device be wiped and factory reset.
*
+ * <p>
+ * The calling Device Owner or Organization Owned Profile Owner must have requested
+ * {@link DeviceAdminInfo#USES_POLICY_WIPE_DATA} to be able to call this method; if it has
+ * not, a security exception will be thrown.
+ *
+ * @param flags Bit mask of additional options: currently supported flags are
+ * {@link #WIPE_EXTERNAL_STORAGE}, {@link #WIPE_RESET_PROTECTION_DATA},
+ * {@link #WIPE_EUICC} and {@link #WIPE_SILENTLY}.
+ * @throws SecurityException if the calling application does not own an active administrator
+ * that uses {@link DeviceAdminInfo#USES_POLICY_WIPE_DATA} and is not
+ * granted the {@link android.Manifest.permission#MASTER_CLEAR}
+ * permission.
* @see #wipeData(int)
* @see #wipeData(int, CharSequence)
- * @hide
*/
- private void wipeDataInternal(int flags, @NonNull String wipeReasonForUser) {
+ // TODO(b/255323293) Add host-side tests
+ public void wipeDevice(int flags) {
+ wipeDataInternal(flags,
+ /* wipeReasonForUser= */ "",
+ /* factoryReset= */ true);
+ }
+
+ /**
+ * Internal function for {@link #wipeData(int)}, {@link #wipeData(int, CharSequence)}
+ * and {@link #wipeDevice(int)} to call.
+ *
+ * @hide
+ * @see #wipeData(int)
+ * @see #wipeData(int, CharSequence)
+ * @see #wipeDevice(int)
+ */
+ private void wipeDataInternal(int flags, @NonNull String wipeReasonForUser,
+ boolean factoryReset) {
if (mService != null) {
try {
- mService.wipeDataWithReason(flags, wipeReasonForUser, mParentInstance);
+ mService.wipeDataWithReason(flags, wipeReasonForUser, mParentInstance,
+ factoryReset);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -8642,7 +8674,7 @@
public void reportFailedPasswordAttempt(int userHandle) {
if (mService != null) {
try {
- mService.reportFailedPasswordAttempt(userHandle);
+ mService.reportFailedPasswordAttempt(userHandle, mParentInstance);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
diff --git a/core/java/android/app/admin/IDevicePolicyManager.aidl b/core/java/android/app/admin/IDevicePolicyManager.aidl
index 75bfc25..6c27dd7 100644
--- a/core/java/android/app/admin/IDevicePolicyManager.aidl
+++ b/core/java/android/app/admin/IDevicePolicyManager.aidl
@@ -117,7 +117,10 @@
void lockNow(int flags, boolean parent);
- void wipeDataWithReason(int flags, String wipeReasonForUser, boolean parent);
+ /**
+ * @param factoryReset only applicable when `targetSdk >= U`, either tries to factoryReset/fail or removeUser/fail otherwise
+ **/
+ void wipeDataWithReason(int flags, String wipeReasonForUser, boolean parent, boolean factoryReset);
void setFactoryResetProtectionPolicy(in ComponentName who, in FactoryResetProtectionPolicy policy);
FactoryResetProtectionPolicy getFactoryResetProtectionPolicy(in ComponentName who);
@@ -161,7 +164,7 @@
boolean hasGrantedPolicy(in ComponentName policyReceiver, int usesPolicy, int userHandle);
void reportPasswordChanged(in PasswordMetrics metrics, int userId);
- void reportFailedPasswordAttempt(int userHandle);
+ void reportFailedPasswordAttempt(int userHandle, boolean parent);
void reportSuccessfulPasswordAttempt(int userHandle);
void reportFailedBiometricAttempt(int userHandle);
void reportSuccessfulBiometricAttempt(int userHandle);
diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java
index 1df0fa8..ae1f689 100644
--- a/core/java/android/content/Context.java
+++ b/core/java/android/content/Context.java
@@ -4917,6 +4917,7 @@
* @see android.server.attention.AttentionManagerService
* @hide
*/
+ @TestApi
public static final String ATTENTION_SERVICE = "attention";
/**
diff --git a/core/java/android/content/pm/UserProperties.java b/core/java/android/content/pm/UserProperties.java
index 0d3c8db..fd35378 100644
--- a/core/java/android/content/pm/UserProperties.java
+++ b/core/java/android/content/pm/UserProperties.java
@@ -45,6 +45,7 @@
private static final String ATTR_START_WITH_PARENT = "startWithParent";
private static final String ATTR_SHOW_IN_SETTINGS = "showInSettings";
private static final String ATTR_INHERIT_DEVICE_POLICY = "inheritDevicePolicy";
+ private static final String ATTR_USE_PARENTS_CONTACTS = "useParentsContacts";
/** Index values of each property (to indicate whether they are present in this object). */
@IntDef(prefix = "INDEX_", value = {
@@ -52,6 +53,7 @@
INDEX_START_WITH_PARENT,
INDEX_SHOW_IN_SETTINGS,
INDEX_INHERIT_DEVICE_POLICY,
+ INDEX_USE_PARENTS_CONTACTS,
})
@Retention(RetentionPolicy.SOURCE)
private @interface PropertyIndex {
@@ -60,6 +62,7 @@
private static final int INDEX_START_WITH_PARENT = 1;
private static final int INDEX_SHOW_IN_SETTINGS = 2;
private static final int INDEX_INHERIT_DEVICE_POLICY = 3;
+ private static final int INDEX_USE_PARENTS_CONTACTS = 4;
/** A bit set, mapping each PropertyIndex to whether it is present (1) or absent (0). */
private long mPropertiesPresent = 0;
@@ -200,6 +203,7 @@
if (hasManagePermission) {
// Add items that require MANAGE_USERS or stronger.
setShowInSettings(orig.getShowInSettings());
+ setUseParentsContacts(orig.getUseParentsContacts());
}
if (hasQueryOrManagePermission) {
// Add items that require QUERY_USERS or stronger.
@@ -317,6 +321,39 @@
}
private @InheritDevicePolicy int mInheritDevicePolicy;
+ /**
+ * Returns whether the current user must use parent user's contacts. If true, writes to the
+ * ContactsProvider corresponding to the current user will be disabled and reads will be
+ * redirected to the parent.
+ *
+ * This only applies to users that have parents (i.e. profiles) and is used to ensure
+ * they can access contacts from the parent profile. This will be generally inapplicable for
+ * non-profile users.
+ *
+ * Please note that in case of the clone profiles, only the allow-listed apps would be allowed
+ * to access contacts across profiles and other apps will not see any contacts.
+ * TODO(b/256126819) Add link to the method returning apps allow-listed for app-cloning
+ *
+ * @return whether contacts access from an associated profile is enabled for the user
+ * @hide
+ */
+ public boolean getUseParentsContacts() {
+ if (isPresent(INDEX_USE_PARENTS_CONTACTS)) return mUseParentsContacts;
+ if (mDefaultProperties != null) return mDefaultProperties.mUseParentsContacts;
+ throw new SecurityException("You don't have permission to query useParentsContacts");
+ }
+ /** @hide */
+ public void setUseParentsContacts(boolean val) {
+ this.mUseParentsContacts = val;
+ setPresent(INDEX_USE_PARENTS_CONTACTS);
+ }
+ /**
+ * Indicates whether the current user should use parent user's contacts.
+ * If this property is set true, the user will be blocked from storing any contacts in its
+ * own contacts database and will serve all read contacts calls through the parent's contacts.
+ */
+ private boolean mUseParentsContacts;
+
@Override
public String toString() {
// Please print in increasing order of PropertyIndex.
@@ -326,6 +363,7 @@
+ ", mStartWithParent=" + getStartWithParent()
+ ", mShowInSettings=" + getShowInSettings()
+ ", mInheritDevicePolicy=" + getInheritDevicePolicy()
+ + ", mUseParentsContacts=" + getUseParentsContacts()
+ "}";
}
@@ -341,6 +379,7 @@
pw.println(prefix + " mStartWithParent=" + getStartWithParent());
pw.println(prefix + " mShowInSettings=" + getShowInSettings());
pw.println(prefix + " mInheritDevicePolicy=" + getInheritDevicePolicy());
+ pw.println(prefix + " mUseParentsContacts=" + getUseParentsContacts());
}
/**
@@ -386,6 +425,9 @@
case ATTR_INHERIT_DEVICE_POLICY:
setInheritDevicePolicy(parser.getAttributeInt(i));
break;
+ case ATTR_USE_PARENTS_CONTACTS:
+ setUseParentsContacts(parser.getAttributeBoolean(i));
+ break;
default:
Slog.w(LOG_TAG, "Skipping unknown property " + attributeName);
}
@@ -416,6 +458,10 @@
serializer.attributeInt(null, ATTR_INHERIT_DEVICE_POLICY,
mInheritDevicePolicy);
}
+ if (isPresent(INDEX_USE_PARENTS_CONTACTS)) {
+ serializer.attributeBoolean(null, ATTR_USE_PARENTS_CONTACTS,
+ mUseParentsContacts);
+ }
}
// For use only with an object that has already had any permission-lacking fields stripped out.
@@ -426,6 +472,7 @@
dest.writeBoolean(mStartWithParent);
dest.writeInt(mShowInSettings);
dest.writeInt(mInheritDevicePolicy);
+ dest.writeBoolean(mUseParentsContacts);
}
/**
@@ -440,6 +487,7 @@
mStartWithParent = source.readBoolean();
mShowInSettings = source.readInt();
mInheritDevicePolicy = source.readInt();
+ mUseParentsContacts = source.readBoolean();
}
@Override
@@ -468,6 +516,7 @@
private boolean mStartWithParent = false;
private @ShowInSettings int mShowInSettings = SHOW_IN_SETTINGS_WITH_PARENT;
private @InheritDevicePolicy int mInheritDevicePolicy = INHERIT_DEVICE_POLICY_NO;
+ private boolean mUseParentsContacts = false;
public Builder setShowInLauncher(@ShowInLauncher int showInLauncher) {
mShowInLauncher = showInLauncher;
@@ -492,13 +541,19 @@
return this;
}
+ public Builder setUseParentsContacts(boolean useParentsContacts) {
+ mUseParentsContacts = useParentsContacts;
+ return this;
+ }
+
/** Builds a UserProperties object with *all* values populated. */
public UserProperties build() {
return new UserProperties(
mShowInLauncher,
mStartWithParent,
mShowInSettings,
- mInheritDevicePolicy);
+ mInheritDevicePolicy,
+ mUseParentsContacts);
}
} // end Builder
@@ -507,12 +562,14 @@
@ShowInLauncher int showInLauncher,
boolean startWithParent,
@ShowInSettings int showInSettings,
- @InheritDevicePolicy int inheritDevicePolicy) {
+ @InheritDevicePolicy int inheritDevicePolicy,
+ boolean useParentsContacts) {
mDefaultProperties = null;
setShowInLauncher(showInLauncher);
setStartWithParent(startWithParent);
setShowInSettings(showInSettings);
setInheritDevicePolicy(inheritDevicePolicy);
+ setUseParentsContacts(useParentsContacts);
}
}
diff --git a/core/java/android/inputmethodservice/IInputMethodWrapper.java b/core/java/android/inputmethodservice/IInputMethodWrapper.java
index d23fb36..d55367f 100644
--- a/core/java/android/inputmethodservice/IInputMethodWrapper.java
+++ b/core/java/android/inputmethodservice/IInputMethodWrapper.java
@@ -32,6 +32,7 @@
import android.util.Log;
import android.view.InputChannel;
import android.view.MotionEvent;
+import android.view.inputmethod.ImeTracker;
import android.view.inputmethod.InputBinding;
import android.view.inputmethod.InputConnection;
import android.view.inputmethod.InputMethod;
@@ -93,9 +94,10 @@
final int mTargetSdkVersion;
/**
- * This is not {@null} only between {@link #bindInput(InputBinding)} and {@link #unbindInput()}
- * so that {@link RemoteInputConnection} can query if {@link #unbindInput()} has already been
- * called or not, mainly to avoid unnecessary blocking operations.
+ * This is not {@code null} only between {@link #bindInput(InputBinding)} and
+ * {@link #unbindInput()} so that {@link RemoteInputConnection} can query if
+ * {@link #unbindInput()} has already been called or not, mainly to avoid unnecessary
+ * blocking operations.
*
* <p>This field must be set and cleared only from the binder thread(s), where the system
* guarantees that {@link #bindInput(InputBinding)},
@@ -219,18 +221,26 @@
return;
case DO_SHOW_SOFT_INPUT: {
final SomeArgs args = (SomeArgs) msg.obj;
+ final ImeTracker.Token statsToken = (ImeTracker.Token) args.arg3;
if (isValid(inputMethod, target, "DO_SHOW_SOFT_INPUT")) {
+ ImeTracker.get().onProgress(statsToken, ImeTracker.PHASE_IME_WRAPPER_DISPATCH);
inputMethod.showSoftInputWithToken(
- msg.arg1, (ResultReceiver) args.arg2, (IBinder) args.arg1);
+ msg.arg1, (ResultReceiver) args.arg2, (IBinder) args.arg1, statsToken);
+ } else {
+ ImeTracker.get().onFailed(statsToken, ImeTracker.PHASE_IME_WRAPPER_DISPATCH);
}
args.recycle();
return;
}
case DO_HIDE_SOFT_INPUT: {
final SomeArgs args = (SomeArgs) msg.obj;
+ final ImeTracker.Token statsToken = (ImeTracker.Token) args.arg3;
if (isValid(inputMethod, target, "DO_HIDE_SOFT_INPUT")) {
+ ImeTracker.get().onProgress(statsToken, ImeTracker.PHASE_IME_WRAPPER_DISPATCH);
inputMethod.hideSoftInputWithToken(msg.arg1, (ResultReceiver) args.arg2,
- (IBinder) args.arg1);
+ (IBinder) args.arg1, statsToken);
+ } else {
+ ImeTracker.get().onFailed(statsToken, ImeTracker.PHASE_IME_WRAPPER_DISPATCH);
}
args.recycle();
return;
@@ -416,16 +426,20 @@
@BinderThread
@Override
- public void showSoftInput(IBinder showInputToken, int flags, ResultReceiver resultReceiver) {
- mCaller.executeOrSendMessage(mCaller.obtainMessageIOO(DO_SHOW_SOFT_INPUT,
- flags, showInputToken, resultReceiver));
+ public void showSoftInput(IBinder showInputToken, @Nullable ImeTracker.Token statsToken,
+ int flags, ResultReceiver resultReceiver) {
+ ImeTracker.get().onProgress(statsToken, ImeTracker.PHASE_IME_WRAPPER);
+ mCaller.executeOrSendMessage(mCaller.obtainMessageIOOO(DO_SHOW_SOFT_INPUT,
+ flags, showInputToken, resultReceiver, statsToken));
}
@BinderThread
@Override
- public void hideSoftInput(IBinder hideInputToken, int flags, ResultReceiver resultReceiver) {
- mCaller.executeOrSendMessage(mCaller.obtainMessageIOO(DO_HIDE_SOFT_INPUT,
- flags, hideInputToken, resultReceiver));
+ public void hideSoftInput(IBinder hideInputToken, @Nullable ImeTracker.Token statsToken,
+ int flags, ResultReceiver resultReceiver) {
+ ImeTracker.get().onProgress(statsToken, ImeTracker.PHASE_IME_WRAPPER);
+ mCaller.executeOrSendMessage(mCaller.obtainMessageIOOO(DO_HIDE_SOFT_INPUT,
+ flags, hideInputToken, resultReceiver, statsToken));
}
@BinderThread
diff --git a/core/java/android/inputmethodservice/InputMethodService.java b/core/java/android/inputmethodservice/InputMethodService.java
index d902486..bf4fc4a 100644
--- a/core/java/android/inputmethodservice/InputMethodService.java
+++ b/core/java/android/inputmethodservice/InputMethodService.java
@@ -124,6 +124,7 @@
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.ExtractedText;
import android.view.inputmethod.ExtractedTextRequest;
+import android.view.inputmethod.ImeTracker;
import android.view.inputmethod.InlineSuggestionsRequest;
import android.view.inputmethod.InlineSuggestionsResponse;
import android.view.inputmethod.InputBinding;
@@ -669,6 +670,10 @@
*/
private IBinder mCurHideInputToken;
+ /** The token tracking the current IME request or {@code null} otherwise. */
+ @Nullable
+ private ImeTracker.Token mCurStatsToken;
+
final ViewTreeObserver.OnComputeInternalInsetsListener mInsetsComputer = info -> {
onComputeInsets(mTmpInsets);
if (!mViewsCreated) {
@@ -870,10 +875,12 @@
@MainThread
@Override
public void hideSoftInputWithToken(int flags, ResultReceiver resultReceiver,
- IBinder hideInputToken) {
+ IBinder hideInputToken, @Nullable ImeTracker.Token statsToken) {
mSystemCallingHideSoftInput = true;
mCurHideInputToken = hideInputToken;
+ mCurStatsToken = statsToken;
hideSoftInput(flags, resultReceiver);
+ mCurStatsToken = null;
mCurHideInputToken = null;
mSystemCallingHideSoftInput = false;
}
@@ -884,6 +891,7 @@
@MainThread
@Override
public void hideSoftInput(int flags, ResultReceiver resultReceiver) {
+ ImeTracker.get().onProgress(mCurStatsToken, ImeTracker.PHASE_IME_HIDE_SOFT_INPUT);
if (DEBUG) Log.v(TAG, "hideSoftInput()");
if (getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.R
&& !mSystemCallingHideSoftInput) {
@@ -918,12 +926,17 @@
@MainThread
@Override
public void showSoftInputWithToken(int flags, ResultReceiver resultReceiver,
- IBinder showInputToken) {
+ IBinder showInputToken, @Nullable ImeTracker.Token statsToken) {
mSystemCallingShowSoftInput = true;
mCurShowInputToken = showInputToken;
- showSoftInput(flags, resultReceiver);
- mCurShowInputToken = null;
- mSystemCallingShowSoftInput = false;
+ mCurStatsToken = statsToken;
+ try {
+ showSoftInput(flags, resultReceiver);
+ } finally {
+ mCurStatsToken = null;
+ mCurShowInputToken = null;
+ mSystemCallingShowSoftInput = false;
+ }
}
/**
@@ -932,6 +945,7 @@
@MainThread
@Override
public void showSoftInput(int flags, ResultReceiver resultReceiver) {
+ ImeTracker.get().onProgress(mCurStatsToken, ImeTracker.PHASE_IME_SHOW_SOFT_INPUT);
if (DEBUG) Log.v(TAG, "showSoftInput()");
// TODO(b/148086656): Disallow IME developers from calling InputMethodImpl methods.
if (getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.R
@@ -947,7 +961,12 @@
null /* icProto */);
final boolean wasVisible = isInputViewShown();
if (dispatchOnShowInputRequested(flags, false)) {
+ ImeTracker.get().onProgress(mCurStatsToken,
+ ImeTracker.PHASE_IME_ON_SHOW_SOFT_INPUT_TRUE);
showWindow(true);
+ } else {
+ ImeTracker.get().onFailed(mCurStatsToken,
+ ImeTracker.PHASE_IME_ON_SHOW_SOFT_INPUT_TRUE);
}
setImeWindowStatus(mapToImeWindowStatus(), mBackDisposition);
@@ -2923,8 +2942,10 @@
ImeTracing.getInstance().triggerServiceDump(
"InputMethodService#applyVisibilityInInsetsConsumerIfNecessary", mDumper,
null /* icProto */);
+ ImeTracker.get().onProgress(mCurStatsToken,
+ ImeTracker.PHASE_IME_APPLY_VISIBILITY_INSETS_CONSUMER);
mPrivOps.applyImeVisibilityAsync(setVisible
- ? mCurShowInputToken : mCurHideInputToken, setVisible);
+ ? mCurShowInputToken : mCurHideInputToken, setVisible, mCurStatsToken);
}
private void finishViews(boolean finishingInput) {
diff --git a/core/java/android/os/FileUtils.java b/core/java/android/os/FileUtils.java
index d5c3de1..b478a379 100644
--- a/core/java/android/os/FileUtils.java
+++ b/core/java/android/os/FileUtils.java
@@ -84,7 +84,6 @@
import java.util.Arrays;
import java.util.Collection;
import java.util.Comparator;
-import java.util.List;
import java.util.Objects;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
@@ -1314,31 +1313,31 @@
private static long toBytes(long value, String unit) {
unit = unit.toUpperCase();
- if (List.of("B").contains(unit)) {
+ if ("B".equals(unit)) {
return value;
}
- if (List.of("K", "KB").contains(unit)) {
+ if ("K".equals(unit) || "KB".equals(unit)) {
return DataUnit.KILOBYTES.toBytes(value);
}
- if (List.of("M", "MB").contains(unit)) {
+ if ("M".equals(unit) || "MB".equals(unit)) {
return DataUnit.MEGABYTES.toBytes(value);
}
- if (List.of("G", "GB").contains(unit)) {
+ if ("G".equals(unit) || "GB".equals(unit)) {
return DataUnit.GIGABYTES.toBytes(value);
}
- if (List.of("KI", "KIB").contains(unit)) {
+ if ("KI".equals(unit) || "KIB".equals(unit)) {
return DataUnit.KIBIBYTES.toBytes(value);
}
- if (List.of("MI", "MIB").contains(unit)) {
+ if ("MI".equals(unit) || "MIB".equals(unit)) {
return DataUnit.MEBIBYTES.toBytes(value);
}
- if (List.of("GI", "GIB").contains(unit)) {
+ if ("GI".equals(unit) || "GIB".equals(unit)) {
return DataUnit.GIBIBYTES.toBytes(value);
}
@@ -1370,7 +1369,7 @@
sign = -1;
}
- fmtSize = fmtSize.replace(first + "", "");
+ fmtSize = fmtSize.substring(1);
}
int index = 0;
diff --git a/core/java/android/os/SystemVibrator.java b/core/java/android/os/SystemVibrator.java
index 6091bf9..bf72b1d 100644
--- a/core/java/android/os/SystemVibrator.java
+++ b/core/java/android/os/SystemVibrator.java
@@ -21,6 +21,7 @@
import android.annotation.Nullable;
import android.compat.annotation.UnsupportedAppUsage;
import android.content.Context;
+import android.hardware.vibrator.IVibrator;
import android.util.ArrayMap;
import android.util.Log;
import android.util.Range;
@@ -313,8 +314,14 @@
private static final float EPSILON = 1e-5f;
public MultiVibratorInfo(VibratorInfo[] vibrators) {
+ // Need to use an extra constructor to share the computation in super initialization.
+ this(vibrators, frequencyProfileIntersection(vibrators));
+ }
+
+ private MultiVibratorInfo(VibratorInfo[] vibrators,
+ VibratorInfo.FrequencyProfile mergedProfile) {
super(/* id= */ -1,
- capabilitiesIntersection(vibrators),
+ capabilitiesIntersection(vibrators, mergedProfile.isEmpty()),
supportedEffectsIntersection(vibrators),
supportedBrakingIntersection(vibrators),
supportedPrimitivesAndDurationsIntersection(vibrators),
@@ -323,14 +330,19 @@
integerLimitIntersection(vibrators, VibratorInfo::getPwlePrimitiveDurationMax),
integerLimitIntersection(vibrators, VibratorInfo::getPwleSizeMax),
floatPropertyIntersection(vibrators, VibratorInfo::getQFactor),
- frequencyProfileIntersection(vibrators));
+ mergedProfile);
}
- private static int capabilitiesIntersection(VibratorInfo[] infos) {
+ private static int capabilitiesIntersection(VibratorInfo[] infos,
+ boolean frequencyProfileIsEmpty) {
int intersection = ~0;
for (VibratorInfo info : infos) {
intersection &= info.getCapabilities();
}
+ if (frequencyProfileIsEmpty) {
+ // Revoke frequency control if the merged frequency profile ended up empty.
+ intersection &= ~IVibrator.CAP_FREQUENCY_CONTROL;
+ }
return intersection;
}
diff --git a/core/java/android/os/UserManager.java b/core/java/android/os/UserManager.java
index bda2a35..8aed2ac 100644
--- a/core/java/android/os/UserManager.java
+++ b/core/java/android/os/UserManager.java
@@ -1637,6 +1637,16 @@
/** @hide */
public static final String SYSTEM_USER_MODE_EMULATION_HEADLESS = "headless";
+ /**
+ * System Property used to override whether users can be created even if their type is disabled
+ * or their limit is reached. Set value to 1 to enable.
+ *
+ * <p>Only used on non-user builds.
+ *
+ * @hide
+ */
+ public static final String DEV_CREATE_OVERRIDE_PROPERTY = "debug.user.creation_override";
+
private static final String ACTION_CREATE_USER = "android.os.action.CREATE_USER";
/**
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index ad39c0c..52b1adb 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -10431,11 +10431,11 @@
public static final String QS_AUTO_ADDED_TILES = "qs_auto_tiles";
/**
- * The duration of timeout, in milliseconds, to switch from a non-primary user to the
- * primary user when the device is docked.
+ * The duration of timeout, in milliseconds, to switch from a non-Dock User to the
+ * Dock User when the device is docked.
* @hide
*/
- public static final String TIMEOUT_TO_USER_ZERO = "timeout_to_user_zero";
+ public static final String TIMEOUT_TO_DOCK_USER = "timeout_to_dock_user";
/**
* Backup manager behavioral parameters.
@@ -18674,6 +18674,9 @@
/**
* Activity Action: For system or preinstalled apps to show their {@link Activity} embedded
* in Settings app on large screen devices.
+ *
+ * Developers should resolve the Intent action before using it.
+ *
* <p>
* Input: {@link #EXTRA_SETTINGS_EMBEDDED_DEEP_LINK_INTENT_URI} must be included to
* specify the intent for the activity which will be embedded in Settings app.
diff --git a/core/java/android/service/credentials/CredentialEntry.java b/core/java/android/service/credentials/CredentialEntry.java
index 1d4ac25..98c537a 100644
--- a/core/java/android/service/credentials/CredentialEntry.java
+++ b/core/java/android/service/credentials/CredentialEntry.java
@@ -173,7 +173,7 @@
*/
public @NonNull Builder setPendingIntent(@Nullable PendingIntent pendingIntent) {
if (pendingIntent != null) {
- Preconditions.checkState(mCredential != null,
+ Preconditions.checkState(mCredential == null,
"credential is already set. Cannot set both the pendingIntent "
+ "and the credential");
}
@@ -189,7 +189,7 @@
*/
public @NonNull Builder setCredential(@Nullable Credential credential) {
if (credential != null) {
- Preconditions.checkState(mPendingIntent != null,
+ Preconditions.checkState(mPendingIntent == null,
"pendingIntent is already set. Cannot set both the "
+ "pendingIntent and the credential");
}
@@ -215,10 +215,10 @@
* is set, or if both are set.
*/
public @NonNull CredentialEntry build() {
- Preconditions.checkState(mPendingIntent == null && mCredential == null,
- "Either pendingIntent or credential must be set");
- Preconditions.checkState(mPendingIntent != null && mCredential != null,
- "Cannot set both the pendingIntent and credential");
+ Preconditions.checkState(((mPendingIntent != null && mCredential == null)
+ || (mPendingIntent == null && mCredential != null)),
+ "Either pendingIntent or credential must be set, and both cannot"
+ + "be set at the same time");
return new CredentialEntry(mType, mSlice, mPendingIntent,
mCredential, mAutoSelectAllowed);
}
diff --git a/core/java/android/service/credentials/CredentialProviderInfo.java b/core/java/android/service/credentials/CredentialProviderInfo.java
index 2c7a983..f89ad8e 100644
--- a/core/java/android/service/credentials/CredentialProviderInfo.java
+++ b/core/java/android/service/credentials/CredentialProviderInfo.java
@@ -24,7 +24,6 @@
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
-import android.content.pm.PackageItemInfo;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.content.pm.ServiceInfo;
@@ -96,6 +95,8 @@
mLabel = mServiceInfo.loadSafeLabel(
mContext.getPackageManager(), 0 /* do not ellipsize */,
TextUtils.SAFE_STRING_FLAG_FIRST_LINE | TextUtils.SAFE_STRING_FLAG_TRIM);
+ Log.i(TAG, "mLabel is : " + mLabel + ", for: " + mServiceInfo.getComponentName()
+ .flattenToString());
populateProviderCapabilities(context, serviceInfo);
}
diff --git a/core/java/android/service/credentials/CredentialProviderService.java b/core/java/android/service/credentials/CredentialProviderService.java
index b1b08f4..6f3e786 100644
--- a/core/java/android/service/credentials/CredentialProviderService.java
+++ b/core/java/android/service/credentials/CredentialProviderService.java
@@ -41,6 +41,18 @@
* @hide
*/
public abstract class CredentialProviderService extends Service {
+ /** Extra to be used by provider to populate the credential when ending the activity started
+ * through the {@code pendingIntent} on the selected {@link SaveEntry}. **/
+ public static final String EXTRA_SAVE_CREDENTIAL =
+ "android.service.credentials.extra.SAVE_CREDENTIAL";
+
+ /**
+ * Provider must read the value against this extra to receive the complete create credential
+ * request parameters, when a pending intent is launched.
+ */
+ public static final String EXTRA_CREATE_CREDENTIAL_REQUEST_PARAMS =
+ "android.service.credentials.extra.CREATE_CREDENTIAL_REQUEST_PARAMS";
+
private static final String TAG = "CredProviderService";
public static final String CAPABILITY_META_DATA_KEY = "android.credentials.capabilities";
@@ -64,7 +76,7 @@
}
@Override
- public final @NonNull IBinder onBind(@NonNull Intent intent) {
+ @NonNull public final IBinder onBind(@NonNull Intent intent) {
if (SERVICE_INTERFACE.equals(intent.getAction())) {
return mInterface.asBinder();
}
diff --git a/core/java/android/service/credentials/GetCredentialsRequest.java b/core/java/android/service/credentials/GetCredentialsRequest.java
index e06be44..03ba20e 100644
--- a/core/java/android/service/credentials/GetCredentialsRequest.java
+++ b/core/java/android/service/credentials/GetCredentialsRequest.java
@@ -119,9 +119,9 @@
*/
public @NonNull Builder setGetCredentialOptions(
@NonNull List<GetCredentialOption> getCredentialOptions) {
- Preconditions.checkCollectionNotEmpty(mGetCredentialOptions,
+ Preconditions.checkCollectionNotEmpty(getCredentialOptions,
"getCredentialOptions");
- Preconditions.checkCollectionElementsNotNull(mGetCredentialOptions,
+ Preconditions.checkCollectionElementsNotNull(getCredentialOptions,
"getCredentialOptions");
mGetCredentialOptions = getCredentialOptions;
return this;
diff --git a/core/java/android/service/voice/HotwordDetectedResult.java b/core/java/android/service/voice/HotwordDetectedResult.java
index b4f20c9..dee560b 100644
--- a/core/java/android/service/voice/HotwordDetectedResult.java
+++ b/core/java/android/service/voice/HotwordDetectedResult.java
@@ -31,6 +31,8 @@
import com.android.internal.util.DataClass;
import com.android.internal.util.Preconditions;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@@ -99,14 +101,30 @@
private static final int LIMIT_AUDIO_CHANNEL_MAX_VALUE = 63;
/**
- * The bundle key for proximity value
+ * The bundle key for proximity
*
* TODO(b/238896013): Move the proximity logic out of bundle to proper API.
- *
- * @hide
*/
- public static final String EXTRA_PROXIMITY_METERS =
- "android.service.voice.extra.PROXIMITY_METERS";
+ private static final String EXTRA_PROXIMITY =
+ "android.service.voice.extra.PROXIMITY";
+
+ /** Users’ proximity is unknown (proximity sensing was inconclusive and is unsupported). */
+ public static final int PROXIMITY_UNKNOWN = -1;
+
+ /** Proximity value that represents that the object is near. */
+ public static final int PROXIMITY_NEAR = 1;
+
+ /** Proximity value that represents that the object is far. */
+ public static final int PROXIMITY_FAR = 2;
+
+ /** @hide */
+ @IntDef(prefix = {"PROXIMITY"}, value = {
+ PROXIMITY_UNKNOWN,
+ PROXIMITY_NEAR,
+ PROXIMITY_FAR
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface ProximityValue {}
/** Confidence level in the trigger outcome. */
@HotwordConfidenceLevelValue
@@ -220,12 +238,14 @@
* versions of Android.
*
* <p>After the trigger happens, a special case of proximity-related extra, with the key of
- * 'android.service.voice.extra.PROXIMITY_METERS' and the value of distance in meters (double),
- * will be stored to enable proximity logic. The proximity meters is provided by the system,
- * on devices that support detecting proximity of nearby users, to help disambiguate which
- * nearby device should respond. When the proximity is unknown, the proximity value will not
- * be stored. This mapping will be excluded from the max bundle size calculation because this
- * mapping is included after the result is returned from the hotword detector service.
+ * 'android.service.voice.extra.PROXIMITY_VALUE' and the value of proximity value (integer)
+ * will be stored to enable proximity logic. {@link HotwordDetectedResult#PROXIMITY_NEAR} will
+ * indicate 'NEAR' proximity and {@link HotwordDetectedResult#PROXIMITY_FAR} will indicate 'FAR'
+ * proximity. The proximity value is provided by the system, on devices that support detecting
+ * proximity of nearby users, to help disambiguate which nearby device should respond. When the
+ * proximity is unknown, the proximity value will not be stored. This mapping will be excluded
+ * from the max bundle size calculation because this mapping is included after the result is
+ * returned from the hotword detector service.
*
* <p>This is a PersistableBundle so it doesn't allow any remotable objects or other contents
* that can be used to communicate with other processes.
@@ -348,16 +368,16 @@
// Remove the proximity key from the bundle before checking the bundle size. The
// proximity value is added after the privileged module and can avoid the
// maxBundleSize limitation.
- if (mExtras.containsKey(EXTRA_PROXIMITY_METERS)) {
- double proximityMeters = mExtras.getDouble(EXTRA_PROXIMITY_METERS);
- mExtras.remove(EXTRA_PROXIMITY_METERS);
+ if (mExtras.containsKey(EXTRA_PROXIMITY)) {
+ int proximityValue = mExtras.getInt(EXTRA_PROXIMITY);
+ mExtras.remove(EXTRA_PROXIMITY);
// Skip checking parcelable size if the new bundle size is 0. Newly empty bundle
// has parcelable size of 4, but the default bundle has parcelable size of 0.
if (mExtras.size() > 0) {
Preconditions.checkArgumentInRange(getParcelableSize(mExtras), 0,
getMaxBundleSize(), "extras");
}
- mExtras.putDouble(EXTRA_PROXIMITY_METERS, proximityMeters);
+ mExtras.putInt(EXTRA_PROXIMITY, proximityValue);
} else {
Preconditions.checkArgumentInRange(getParcelableSize(mExtras), 0,
getMaxBundleSize(), "extras");
@@ -372,6 +392,52 @@
return List.copyOf(mAudioStreams);
}
+ /**
+ * Adds proximity level, either near or far, that is mapped for the given distance into
+ * the bundle. The proximity value is provided by the system, on devices that support detecting
+ * proximity of nearby users, to help disambiguate which nearby device should respond.
+ * This mapping will be excluded from the max bundle size calculation because this mapping is
+ * included after the result is returned from the hotword detector service. The value will not
+ * be included if the proximity was unknown.
+ *
+ * @hide
+ */
+ public void setProximity(double distance) {
+ int proximityLevel = convertToProximityLevel(distance);
+ if (proximityLevel != PROXIMITY_UNKNOWN) {
+ mExtras.putInt(EXTRA_PROXIMITY, proximityLevel);
+ }
+ }
+
+ /**
+ * Returns proximity level, which can be either of {@link HotwordDetectedResult#PROXIMITY_NEAR}
+ * or {@link HotwordDetectedResult#PROXIMITY_FAR}. If the proximity is unknown, it will
+ * return {@link HotwordDetectedResult#PROXIMITY_UNKNOWN}.
+ */
+ @ProximityValue
+ public int getProximity() {
+ return mExtras.getInt(EXTRA_PROXIMITY, PROXIMITY_UNKNOWN);
+ }
+
+ /**
+ * Mapping of the proximity distance (meters) to proximity values, unknown, near, and far.
+ * Currently, this mapping is handled by HotwordDetectedResult because it handles just
+ * HotwordDetectionConnection which we know the mapping of. However, the mapping will need to
+ * move to a more centralized place once there are more clients.
+ *
+ * TODO(b/258531144): Move the proximity mapping to a central location
+ */
+ @ProximityValue
+ private int convertToProximityLevel(double distance) {
+ if (distance < 0) {
+ return PROXIMITY_UNKNOWN;
+ } else if (distance <= 3) {
+ return PROXIMITY_NEAR;
+ } else {
+ return PROXIMITY_FAR;
+ }
+ }
+
@DataClass.Suppress("addAudioStreams")
abstract static class BaseBuilder {
/**
@@ -432,7 +498,7 @@
CONFIDENCE_LEVEL_HIGH,
CONFIDENCE_LEVEL_VERY_HIGH
})
- @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE)
+ @Retention(RetentionPolicy.SOURCE)
@DataClass.Generated.Member
public @interface ConfidenceLevel {}
@@ -463,7 +529,7 @@
LIMIT_HOTWORD_OFFSET_MAX_VALUE,
LIMIT_AUDIO_CHANNEL_MAX_VALUE
})
- @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE)
+ @Retention(RetentionPolicy.SOURCE)
@DataClass.Generated.Member
/* package-private */ @interface Limit {}
@@ -479,6 +545,30 @@
}
}
+ /** @hide */
+ @IntDef(prefix = "PROXIMITY_", value = {
+ PROXIMITY_UNKNOWN,
+ PROXIMITY_NEAR,
+ PROXIMITY_FAR
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ @DataClass.Generated.Member
+ public @interface Proximity {}
+
+ /** @hide */
+ @DataClass.Generated.Member
+ public static String proximityToString(@Proximity int value) {
+ switch (value) {
+ case PROXIMITY_UNKNOWN:
+ return "PROXIMITY_UNKNOWN";
+ case PROXIMITY_NEAR:
+ return "PROXIMITY_NEAR";
+ case PROXIMITY_FAR:
+ return "PROXIMITY_FAR";
+ default: return Integer.toHexString(value);
+ }
+ }
+
@DataClass.Generated.Member
/* package-private */ HotwordDetectedResult(
@HotwordConfidenceLevelValue int confidenceLevel,
@@ -605,12 +695,14 @@
* versions of Android.
*
* <p>After the trigger happens, a special case of proximity-related extra, with the key of
- * 'android.service.voice.extra.PROXIMITY_METERS' and the value of distance in meters (double),
- * will be stored to enable proximity logic. The proximity meters is provided by the system,
- * on devices that support detecting proximity of nearby users, to help disambiguate which
- * nearby device should respond. When the proximity is unknown, the proximity value will not
- * be stored. This mapping will be excluded from the max bundle size calculation because this
- * mapping is included after the result is returned from the hotword detector service.
+ * 'android.service.voice.extra.PROXIMITY_VALUE' and the value of proximity value (integer)
+ * will be stored to enable proximity logic. {@link HotwordDetectedResult#PROXIMITY_NEAR} will
+ * indicate 'NEAR' proximity and {@link HotwordDetectedResult#PROXIMITY_FAR} will indicate 'FAR'
+ * proximity. The proximity value is provided by the system, on devices that support detecting
+ * proximity of nearby users, to help disambiguate which nearby device should respond. When the
+ * proximity is unknown, the proximity value will not be stored. This mapping will be excluded
+ * from the max bundle size calculation because this mapping is included after the result is
+ * returned from the hotword detector service.
*
* <p>This is a PersistableBundle so it doesn't allow any remotable objects or other contents
* that can be used to communicate with other processes.
@@ -923,12 +1015,14 @@
* versions of Android.
*
* <p>After the trigger happens, a special case of proximity-related extra, with the key of
- * 'android.service.voice.extra.PROXIMITY_METERS' and the value of distance in meters (double),
- * will be stored to enable proximity logic. The proximity meters is provided by the system,
- * on devices that support detecting proximity of nearby users, to help disambiguate which
- * nearby device should respond. When the proximity is unknown, the proximity value will not
- * be stored. This mapping will be excluded from the max bundle size calculation because this
- * mapping is included after the result is returned from the hotword detector service.
+ * 'android.service.voice.extra.PROXIMITY_VALUE' and the value of proximity value (integer)
+ * will be stored to enable proximity logic. {@link HotwordDetectedResult#PROXIMITY_NEAR} will
+ * indicate 'NEAR' proximity and {@link HotwordDetectedResult#PROXIMITY_FAR} will indicate 'FAR'
+ * proximity. The proximity value is provided by the system, on devices that support detecting
+ * proximity of nearby users, to help disambiguate which nearby device should respond. When the
+ * proximity is unknown, the proximity value will not be stored. This mapping will be excluded
+ * from the max bundle size calculation because this mapping is included after the result is
+ * returned from the hotword detector service.
*
* <p>This is a PersistableBundle so it doesn't allow any remotable objects or other contents
* that can be used to communicate with other processes.
@@ -1003,10 +1097,10 @@
}
@DataClass.Generated(
- time = 1666342044844L,
+ time = 1668385264834L,
codegenVersion = "1.0.23",
sourceFile = "frameworks/base/core/java/android/service/voice/HotwordDetectedResult.java",
- inputSignatures = "public static final int CONFIDENCE_LEVEL_NONE\npublic static final int CONFIDENCE_LEVEL_LOW\npublic static final int CONFIDENCE_LEVEL_LOW_MEDIUM\npublic static final int CONFIDENCE_LEVEL_MEDIUM\npublic static final int CONFIDENCE_LEVEL_MEDIUM_HIGH\npublic static final int CONFIDENCE_LEVEL_HIGH\npublic static final int CONFIDENCE_LEVEL_VERY_HIGH\npublic static final int HOTWORD_OFFSET_UNSET\npublic static final int AUDIO_CHANNEL_UNSET\nprivate static final int LIMIT_HOTWORD_OFFSET_MAX_VALUE\nprivate static final int LIMIT_AUDIO_CHANNEL_MAX_VALUE\npublic static final java.lang.String EXTRA_PROXIMITY_METERS\nprivate final @android.service.voice.HotwordDetectedResult.HotwordConfidenceLevelValue int mConfidenceLevel\nprivate @android.annotation.Nullable android.media.MediaSyncEvent mMediaSyncEvent\nprivate int mHotwordOffsetMillis\nprivate int mHotwordDurationMillis\nprivate int mAudioChannel\nprivate boolean mHotwordDetectionPersonalized\nprivate final int mScore\nprivate final int mPersonalizedScore\nprivate final int mHotwordPhraseId\nprivate final @android.annotation.NonNull java.util.List<android.service.voice.HotwordAudioStream> mAudioStreams\nprivate final @android.annotation.NonNull android.os.PersistableBundle mExtras\nprivate static int sMaxBundleSize\nprivate static int defaultConfidenceLevel()\nprivate static int defaultScore()\nprivate static int defaultPersonalizedScore()\npublic static int getMaxScore()\nprivate static int defaultHotwordPhraseId()\npublic static int getMaxHotwordPhraseId()\nprivate static java.util.List<android.service.voice.HotwordAudioStream> defaultAudioStreams()\nprivate static android.os.PersistableBundle defaultExtras()\npublic static int getMaxBundleSize()\npublic @android.annotation.Nullable android.media.MediaSyncEvent getMediaSyncEvent()\npublic static int getParcelableSize(android.os.Parcelable)\npublic static int getUsageSize(android.service.voice.HotwordDetectedResult)\nprivate static int bitCount(long)\nprivate void onConstructed()\npublic @android.annotation.NonNull java.util.List<android.service.voice.HotwordAudioStream> getAudioStreams()\npublic android.service.voice.HotwordDetectedResult.Builder buildUpon()\nclass HotwordDetectedResult extends java.lang.Object implements [android.os.Parcelable]\npublic @android.annotation.NonNull android.service.voice.HotwordDetectedResult.Builder setAudioStreams(java.util.List<android.service.voice.HotwordAudioStream>)\nclass BaseBuilder extends java.lang.Object implements []\n@com.android.internal.util.DataClass(genConstructor=false, genBuilder=true, genEqualsHashCode=true, genHiddenConstDefs=true, genParcelable=true, genToString=true)\npublic @android.annotation.NonNull android.service.voice.HotwordDetectedResult.Builder setAudioStreams(java.util.List<android.service.voice.HotwordAudioStream>)\nclass BaseBuilder extends java.lang.Object implements []")
+ inputSignatures = "public static final int CONFIDENCE_LEVEL_NONE\npublic static final int CONFIDENCE_LEVEL_LOW\npublic static final int CONFIDENCE_LEVEL_LOW_MEDIUM\npublic static final int CONFIDENCE_LEVEL_MEDIUM\npublic static final int CONFIDENCE_LEVEL_MEDIUM_HIGH\npublic static final int CONFIDENCE_LEVEL_HIGH\npublic static final int CONFIDENCE_LEVEL_VERY_HIGH\npublic static final int HOTWORD_OFFSET_UNSET\npublic static final int AUDIO_CHANNEL_UNSET\nprivate static final int LIMIT_HOTWORD_OFFSET_MAX_VALUE\nprivate static final int LIMIT_AUDIO_CHANNEL_MAX_VALUE\nprivate static final java.lang.String EXTRA_PROXIMITY\npublic static final int PROXIMITY_UNKNOWN\npublic static final int PROXIMITY_NEAR\npublic static final int PROXIMITY_FAR\nprivate final @android.service.voice.HotwordDetectedResult.HotwordConfidenceLevelValue int mConfidenceLevel\nprivate @android.annotation.Nullable android.media.MediaSyncEvent mMediaSyncEvent\nprivate int mHotwordOffsetMillis\nprivate int mHotwordDurationMillis\nprivate int mAudioChannel\nprivate boolean mHotwordDetectionPersonalized\nprivate final int mScore\nprivate final int mPersonalizedScore\nprivate final int mHotwordPhraseId\nprivate final @android.annotation.NonNull java.util.List<android.service.voice.HotwordAudioStream> mAudioStreams\nprivate final @android.annotation.NonNull android.os.PersistableBundle mExtras\nprivate static int sMaxBundleSize\nprivate static int defaultConfidenceLevel()\nprivate static int defaultScore()\nprivate static int defaultPersonalizedScore()\npublic static int getMaxScore()\nprivate static int defaultHotwordPhraseId()\npublic static int getMaxHotwordPhraseId()\nprivate static java.util.List<android.service.voice.HotwordAudioStream> defaultAudioStreams()\nprivate static android.os.PersistableBundle defaultExtras()\npublic static int getMaxBundleSize()\npublic @android.annotation.Nullable android.media.MediaSyncEvent getMediaSyncEvent()\npublic static int getParcelableSize(android.os.Parcelable)\npublic static int getUsageSize(android.service.voice.HotwordDetectedResult)\nprivate static int bitCount(long)\nprivate void onConstructed()\npublic @android.annotation.NonNull java.util.List<android.service.voice.HotwordAudioStream> getAudioStreams()\npublic void setProximity(double)\npublic @android.service.voice.HotwordDetectedResult.ProximityValue int getProximity()\nprivate @android.service.voice.HotwordDetectedResult.ProximityValue int convertToProximityLevel(double)\npublic android.service.voice.HotwordDetectedResult.Builder buildUpon()\nclass HotwordDetectedResult extends java.lang.Object implements [android.os.Parcelable]\npublic @android.annotation.NonNull android.service.voice.HotwordDetectedResult.Builder setAudioStreams(java.util.List<android.service.voice.HotwordAudioStream>)\nclass BaseBuilder extends java.lang.Object implements []\n@com.android.internal.util.DataClass(genConstructor=false, genBuilder=true, genEqualsHashCode=true, genHiddenConstDefs=true, genParcelable=true, genToString=true)\npublic @android.annotation.NonNull android.service.voice.HotwordDetectedResult.Builder setAudioStreams(java.util.List<android.service.voice.HotwordAudioStream>)\nclass BaseBuilder extends java.lang.Object implements []")
@Deprecated
private void __metadata() {}
diff --git a/core/java/android/service/voice/HotwordDetectionService.java b/core/java/android/service/voice/HotwordDetectionService.java
index df69cc0..552a793 100644
--- a/core/java/android/service/voice/HotwordDetectionService.java
+++ b/core/java/android/service/voice/HotwordDetectionService.java
@@ -25,6 +25,7 @@
import android.annotation.SdkConstant;
import android.annotation.SuppressLint;
import android.annotation.SystemApi;
+import android.annotation.TestApi;
import android.app.Service;
import android.content.ContentCaptureOptions;
import android.content.Context;
@@ -90,10 +91,10 @@
/**
* Feature flag for Attention Service.
*
- * TODO(b/247920386): Add TestApi annotation
* @hide
*/
- public static final boolean ENABLE_PROXIMITY_RESULT = false;
+ @TestApi
+ public static final boolean ENABLE_PROXIMITY_RESULT = true;
/**
* Indicates that the updated status is successful.
diff --git a/core/java/android/text/style/TextAppearanceSpan.java b/core/java/android/text/style/TextAppearanceSpan.java
index 85b7ae9..d61228b 100644
--- a/core/java/android/text/style/TextAppearanceSpan.java
+++ b/core/java/android/text/style/TextAppearanceSpan.java
@@ -149,7 +149,7 @@
}
mTextFontWeight = a.getInt(com.android.internal.R.styleable
- .TextAppearance_textFontWeight, -1);
+ .TextAppearance_textFontWeight, /*defValue*/ FontStyle.FONT_WEIGHT_UNSPECIFIED);
final String localeString = a.getString(com.android.internal.R.styleable
.TextAppearance_textLocale);
@@ -215,7 +215,7 @@
mTextColorLink = linkColor;
mTypeface = null;
- mTextFontWeight = -1;
+ mTextFontWeight = FontStyle.FONT_WEIGHT_UNSPECIFIED;
mTextLocales = null;
mShadowRadius = 0.0f;
@@ -359,8 +359,8 @@
}
/**
- * Returns the text font weight specified by this span, or <code>-1</code>
- * if it does not specify one.
+ * Returns the text font weight specified by this span, or
+ * <code>FontStyle.FONT_WEIGHT_UNSPECIFIED</code> if it does not specify one.
*/
public int getTextFontWeight() {
return mTextFontWeight;
diff --git a/core/java/android/view/IDisplayWindowInsetsController.aidl b/core/java/android/view/IDisplayWindowInsetsController.aidl
index 0769f12..91270d4 100644
--- a/core/java/android/view/IDisplayWindowInsetsController.aidl
+++ b/core/java/android/view/IDisplayWindowInsetsController.aidl
@@ -19,6 +19,7 @@
import android.content.ComponentName;
import android.view.InsetsSourceControl;
import android.view.InsetsState;
+import android.view.inputmethod.ImeTracker;
/**
* Singular controller of insets to use when there isn't another obvious controller available.
@@ -48,10 +49,10 @@
/**
* @see IWindow#showInsets
*/
- void showInsets(int types, boolean fromIme);
+ void showInsets(int types, boolean fromIme, in @nullable ImeTracker.Token statsToken);
/**
* @see IWindow#hideInsets
*/
- void hideInsets(int types, boolean fromIme);
+ void hideInsets(int types, boolean fromIme, in @nullable ImeTracker.Token statsToken);
}
diff --git a/core/java/android/view/IWindow.aidl b/core/java/android/view/IWindow.aidl
index a856474..8e16f24 100644
--- a/core/java/android/view/IWindow.aidl
+++ b/core/java/android/view/IWindow.aidl
@@ -29,6 +29,7 @@
import android.view.IScrollCaptureResponseListener;
import android.view.KeyEvent;
import android.view.MotionEvent;
+import android.view.inputmethod.ImeTracker;
import android.window.ClientWindowFrames;
import com.android.internal.os.IResultReceiver;
@@ -68,16 +69,18 @@
*
* @param types internal insets types (WindowInsets.Type.InsetsType) to show
* @param fromIme true if this request originated from IME (InputMethodService).
+ * @param statsToken the token tracking the current IME show request or {@code null} otherwise.
*/
- void showInsets(int types, boolean fromIme);
+ void showInsets(int types, boolean fromIme, in @nullable ImeTracker.Token statsToken);
/**
* Called when a set of insets source window should be hidden by policy.
*
* @param types internal insets types (WindowInsets.Type.InsetsType) to hide
* @param fromIme true if this request originated from IME (InputMethodService).
+ * @param statsToken the token tracking the current IME hide request or {@code null} otherwise.
*/
- void hideInsets(int types, boolean fromIme);
+ void hideInsets(int types, boolean fromIme, in @nullable ImeTracker.Token statsToken);
void moved(int newX, int newY);
void dispatchAppVisibility(boolean visible);
diff --git a/core/java/android/view/InsetsAnimationControlImpl.java b/core/java/android/view/InsetsAnimationControlImpl.java
index 27b4d87..e775969 100644
--- a/core/java/android/view/InsetsAnimationControlImpl.java
+++ b/core/java/android/view/InsetsAnimationControlImpl.java
@@ -58,6 +58,7 @@
import android.view.WindowInsetsAnimation.Bounds;
import android.view.WindowManager.LayoutParams;
import android.view.animation.Interpolator;
+import android.view.inputmethod.ImeTracker;
import com.android.internal.annotations.VisibleForTesting;
@@ -68,7 +69,7 @@
* Implements {@link WindowInsetsAnimationController}
* @hide
*/
-@VisibleForTesting
+@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
public class InsetsAnimationControlImpl implements InternalInsetsAnimationController,
InsetsAnimationControlRunner {
@@ -96,6 +97,8 @@
/** @see WindowInsetsAnimationController#hasZeroInsetsIme */
private final boolean mHasZeroInsetsIme;
private final CompatibilityInfo.Translator mTranslator;
+ @Nullable
+ private final ImeTracker.Token mStatsToken;
private Insets mCurrentInsets;
private Insets mPendingInsets;
private float mPendingFraction;
@@ -114,7 +117,7 @@
@InsetsType int types, InsetsAnimationControlCallbacks controller, long durationMs,
Interpolator interpolator, @AnimationType int animationType,
@LayoutInsetsDuringAnimation int layoutInsetsDuringAnimation,
- CompatibilityInfo.Translator translator) {
+ CompatibilityInfo.Translator translator, @Nullable ImeTracker.Token statsToken) {
mControls = controls;
mListener = listener;
mTypes = types;
@@ -152,6 +155,7 @@
mAnimationType = animationType;
mLayoutInsetsDuringAnimation = layoutInsetsDuringAnimation;
mTranslator = translator;
+ mStatsToken = statsToken;
mController.startAnimation(this, listener, types, mAnimation,
new Bounds(mHiddenInsets, mShownInsets));
}
@@ -228,6 +232,11 @@
}
@Override
+ public ImeTracker.Token getStatsToken() {
+ return mStatsToken;
+ }
+
+ @Override
public void setInsetsAndAlpha(Insets insets, float alpha, float fraction) {
setInsetsAndAlpha(insets, alpha, fraction, false /* allowWhenFinished */);
}
@@ -253,10 +262,10 @@
}
}
- @VisibleForTesting
/**
* @return Whether the finish callback of this animation should be invoked.
*/
+ @VisibleForTesting
public boolean applyChangeInsets(@Nullable InsetsState outState) {
if (mCancelled) {
if (DEBUG) Log.d(TAG, "applyChangeInsets canceled");
diff --git a/core/java/android/view/InsetsAnimationControlRunner.java b/core/java/android/view/InsetsAnimationControlRunner.java
index 291351e..cf40e7e 100644
--- a/core/java/android/view/InsetsAnimationControlRunner.java
+++ b/core/java/android/view/InsetsAnimationControlRunner.java
@@ -16,10 +16,12 @@
package android.view;
+import android.annotation.Nullable;
import android.util.SparseArray;
import android.util.proto.ProtoOutputStream;
import android.view.InsetsController.AnimationType;
import android.view.WindowInsets.Type.InsetsType;
+import android.view.inputmethod.ImeTracker;
/**
* Interface representing a runner for an insets animation.
@@ -74,6 +76,12 @@
@AnimationType int getAnimationType();
/**
+ * @return The token tracking the current IME request or {@code null} otherwise.
+ */
+ @Nullable
+ ImeTracker.Token getStatsToken();
+
+ /**
*
* Export the state of classes that implement this interface into a protocol buffer
* output stream.
diff --git a/core/java/android/view/InsetsAnimationThreadControlRunner.java b/core/java/android/view/InsetsAnimationThreadControlRunner.java
index fc97541..f7b9aa2 100644
--- a/core/java/android/view/InsetsAnimationThreadControlRunner.java
+++ b/core/java/android/view/InsetsAnimationThreadControlRunner.java
@@ -34,6 +34,7 @@
import android.view.WindowInsets.Type.InsetsType;
import android.view.WindowInsetsAnimation.Bounds;
import android.view.animation.Interpolator;
+import android.view.inputmethod.ImeTracker;
/**
* Insets animation runner that uses {@link InsetsAnimationThread} to run the animation off from the
@@ -112,12 +113,13 @@
@InsetsType int types, InsetsAnimationControlCallbacks controller, long durationMs,
Interpolator interpolator, @AnimationType int animationType,
@LayoutInsetsDuringAnimation int layoutInsetsDuringAnimation,
- CompatibilityInfo.Translator translator, Handler mainThreadHandler) {
+ CompatibilityInfo.Translator translator, Handler mainThreadHandler,
+ @Nullable ImeTracker.Token statsToken) {
mMainThreadHandler = mainThreadHandler;
mOuterCallbacks = controller;
mControl = new InsetsAnimationControlImpl(controls, frame, state, listener, types,
mCallbacks, durationMs, interpolator, animationType, layoutInsetsDuringAnimation,
- translator);
+ translator, statsToken);
InsetsAnimationThread.getHandler().post(() -> {
if (mControl.isCancelled()) {
return;
@@ -141,6 +143,11 @@
}
@Override
+ public ImeTracker.Token getStatsToken() {
+ return mControl.getStatsToken();
+ }
+
+ @Override
@UiThread
public int getTypes() {
return mControl.getTypes();
diff --git a/core/java/android/view/InsetsController.java b/core/java/android/view/InsetsController.java
index 35838a3..fbd8226 100644
--- a/core/java/android/view/InsetsController.java
+++ b/core/java/android/view/InsetsController.java
@@ -44,6 +44,7 @@
import android.os.Handler;
import android.os.IBinder;
import android.os.Trace;
+import android.text.TextUtils;
import android.util.ArraySet;
import android.util.Log;
import android.util.Pair;
@@ -60,6 +61,7 @@
import android.view.animation.Interpolator;
import android.view.animation.LinearInterpolator;
import android.view.animation.PathInterpolator;
+import android.view.inputmethod.ImeTracker;
import android.view.inputmethod.InputMethodManager;
import com.android.internal.annotations.VisibleForTesting;
@@ -929,10 +931,12 @@
hideTypes[0] &= ~animatingTypes;
if (showTypes[0] != 0) {
- applyAnimation(showTypes[0], true /* show */, false /* fromIme */);
+ applyAnimation(showTypes[0], true /* show */, false /* fromIme */,
+ null /* statsToken */);
}
if (hideTypes[0] != 0) {
- applyAnimation(hideTypes[0], false /* show */, false /* fromIme */);
+ applyAnimation(hideTypes[0], false /* show */, false /* fromIme */,
+ null /* statsToken */);
}
if (mControllableTypes != controllableTypes) {
@@ -948,11 +952,12 @@
@Override
public void show(@InsetsType int types) {
- show(types, false /* fromIme */);
+ show(types, false /* fromIme */, null /* statsToken */);
}
- @VisibleForTesting
- public void show(@InsetsType int types, boolean fromIme) {
+ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+ public void show(@InsetsType int types, boolean fromIme,
+ @Nullable ImeTracker.Token statsToken) {
if ((types & ime()) != 0) {
Log.d(TAG, "show(ime(), fromIme=" + fromIme + ")");
}
@@ -979,7 +984,7 @@
true /* fromIme */, pendingRequest.durationMs, pendingRequest.interpolator,
pendingRequest.animationType,
pendingRequest.layoutInsetsDuringAnimation,
- pendingRequest.useInsetsAnimationThread);
+ pendingRequest.useInsetsAnimationThread, statsToken);
return;
}
@@ -990,8 +995,9 @@
if ((types & type) == 0) {
continue;
}
- final @AnimationType int animationType = getAnimationType(type);
+ @AnimationType final int animationType = getAnimationType(type);
final boolean requestedVisible = (type & mRequestedVisibleTypes) != 0;
+ final boolean isImeAnimation = type == ime();
if (requestedVisible && animationType == ANIMATION_TYPE_NONE
|| animationType == ANIMATION_TYPE_SHOW) {
// no-op: already shown or animating in (because window visibility is
@@ -999,25 +1005,36 @@
if (DEBUG) Log.d(TAG, String.format(
"show ignored for type: %d animType: %d requestedVisible: %s",
type, animationType, requestedVisible));
+ if (isImeAnimation) {
+ ImeTracker.get().onCancelled(statsToken,
+ ImeTracker.PHASE_CLIENT_APPLY_ANIMATION);
+ }
continue;
}
if (fromIme && animationType == ANIMATION_TYPE_USER) {
// App is already controlling the IME, don't cancel it.
+ if (isImeAnimation) {
+ ImeTracker.get().onFailed(statsToken, ImeTracker.PHASE_CLIENT_APPLY_ANIMATION);
+ }
continue;
}
+ if (isImeAnimation) {
+ ImeTracker.get().onProgress(statsToken, ImeTracker.PHASE_CLIENT_APPLY_ANIMATION);
+ }
typesReady |= type;
}
if (DEBUG) Log.d(TAG, "show typesReady: " + typesReady);
- applyAnimation(typesReady, true /* show */, fromIme);
+ applyAnimation(typesReady, true /* show */, fromIme, statsToken);
}
@Override
public void hide(@InsetsType int types) {
- hide(types, false /* fromIme */);
+ hide(types, false /* fromIme */, null /* statsToken */);
}
@VisibleForTesting
- public void hide(@InsetsType int types, boolean fromIme) {
+ public void hide(@InsetsType int types, boolean fromIme,
+ @Nullable ImeTracker.Token statsToken) {
if (fromIme) {
ImeTracing.getInstance().triggerClientDump("InsetsController#hide",
mHost.getInputMethodManager(), null /* icProto */);
@@ -1030,16 +1047,25 @@
if ((types & type) == 0) {
continue;
}
- final @AnimationType int animationType = getAnimationType(type);
+ @AnimationType final int animationType = getAnimationType(type);
final boolean requestedVisible = (type & mRequestedVisibleTypes) != 0;
+ final boolean isImeAnimation = type == ime();
if (!requestedVisible && animationType == ANIMATION_TYPE_NONE
|| animationType == ANIMATION_TYPE_HIDE) {
- // no-op: already hidden or animating out.
+ // no-op: already hidden or animating out (because window visibility is
+ // applied before starting animation).
+ if (isImeAnimation) {
+ ImeTracker.get().onCancelled(statsToken,
+ ImeTracker.PHASE_CLIENT_APPLY_ANIMATION);
+ }
continue;
}
+ if (isImeAnimation) {
+ ImeTracker.get().onProgress(statsToken, ImeTracker.PHASE_CLIENT_APPLY_ANIMATION);
+ }
typesReady |= type;
}
- applyAnimation(typesReady, false /* show */, fromIme /* fromIme */);
+ applyAnimation(typesReady, false /* show */, fromIme, statsToken);
}
@Override
@@ -1068,7 +1094,7 @@
controlAnimationUnchecked(types, cancellationSignal, listener, mFrame, fromIme, durationMs,
interpolator, animationType, getLayoutInsetsDuringAnimationMode(types),
- false /* useInsetsAnimationThread */);
+ false /* useInsetsAnimationThread */, null /* statsToken */);
}
private void controlAnimationUnchecked(@InsetsType int types,
@@ -1077,7 +1103,8 @@
long durationMs, Interpolator interpolator,
@AnimationType int animationType,
@LayoutInsetsDuringAnimation int layoutInsetsDuringAnimation,
- boolean useInsetsAnimationThread) {
+ boolean useInsetsAnimationThread, @Nullable ImeTracker.Token statsToken) {
+ ImeTracker.get().onProgress(statsToken, ImeTracker.PHASE_CLIENT_CONTROL_ANIMATION);
if ((types & mTypesBeingCancelled) != 0) {
throw new IllegalStateException("Cannot start a new insets animation of "
+ Type.toString(types)
@@ -1152,14 +1179,16 @@
? new InsetsAnimationThreadControlRunner(controls,
frame, mState, listener, typesReady, this, durationMs, interpolator,
animationType, layoutInsetsDuringAnimation, mHost.getTranslator(),
- mHost.getHandler())
+ mHost.getHandler(), statsToken)
: new InsetsAnimationControlImpl(controls,
frame, mState, listener, typesReady, this, durationMs, interpolator,
- animationType, layoutInsetsDuringAnimation, mHost.getTranslator());
+ animationType, layoutInsetsDuringAnimation, mHost.getTranslator(),
+ statsToken);
if ((typesReady & WindowInsets.Type.ime()) != 0) {
ImeTracing.getInstance().triggerClientDump("InsetsAnimationControlImpl",
mHost.getInputMethodManager(), null /* icProto */);
}
+ ImeTracker.get().onProgress(statsToken, ImeTracker.PHASE_CLIENT_ANIMATION_RUNNING);
mRunningAnimations.add(new RunningAnimation(runner, animationType));
if (DEBUG) Log.d(TAG, "Animation added to runner. useInsetsAnimationThread: "
+ useInsetsAnimationThread);
@@ -1311,11 +1340,18 @@
// requested visibility.
return;
}
+ final ImeTracker.Token statsToken = runner.getStatsToken();
if (shown) {
+ ImeTracker.get().onProgress(statsToken,
+ ImeTracker.PHASE_CLIENT_ANIMATION_FINISHED_SHOW);
showDirectly(runner.getTypes(), true /* fromIme */);
+ ImeTracker.get().onShown(statsToken);
} else {
+ ImeTracker.get().onProgress(statsToken,
+ ImeTracker.PHASE_CLIENT_ANIMATION_FINISHED_HIDE);
hideDirectly(runner.getTypes(), true /* animationFinished */,
runner.getAnimationType(), true /* fromIme */);
+ ImeTracker.get().onHidden(statsToken);
}
}
@@ -1339,10 +1375,19 @@
}
private void cancelAnimation(InsetsAnimationControlRunner control, boolean invokeCallback) {
- if (DEBUG) Log.d(TAG, String.format("cancelAnimation of types: %d, animType: %d, host: %s",
- control.getTypes(), control.getAnimationType(), mHost.getRootViewTitle()));
if (invokeCallback) {
+ ImeTracker.get().onCancelled(control.getStatsToken(),
+ ImeTracker.PHASE_CLIENT_ANIMATION_CANCEL);
control.cancel();
+ } else {
+ // Succeeds if invokeCallback is false (i.e. when called from notifyFinished).
+ ImeTracker.get().onProgress(control.getStatsToken(),
+ ImeTracker.PHASE_CLIENT_ANIMATION_CANCEL);
+ }
+ if (DEBUG) {
+ Log.d(TAG, TextUtils.formatSimple(
+ "cancelAnimation of types: %d, animType: %d, host: %s",
+ control.getTypes(), control.getAnimationType(), mHost.getRootViewTitle()));
}
boolean stateChanged = false;
for (int i = mRunningAnimations.size() - 1; i >= 0; i--) {
@@ -1452,7 +1497,8 @@
}
@VisibleForTesting
- public void applyAnimation(@InsetsType final int types, boolean show, boolean fromIme) {
+ public void applyAnimation(@InsetsType final int types, boolean show, boolean fromIme,
+ @Nullable ImeTracker.Token statsToken) {
// TODO(b/166736352): We should only skip the animation of specific types, not all types.
boolean skipAnim = false;
if ((types & ime()) != 0) {
@@ -1465,12 +1511,12 @@
&& consumer.hasViewFocusWhenWindowFocusGain();
}
}
- applyAnimation(types, show, fromIme, skipAnim);
+ applyAnimation(types, show, fromIme, skipAnim, statsToken);
}
@VisibleForTesting
public void applyAnimation(@InsetsType final int types, boolean show, boolean fromIme,
- boolean skipAnim) {
+ boolean skipAnim, @Nullable ImeTracker.Token statsToken) {
if (types == 0) {
// nothing to animate.
if (DEBUG) Log.d(TAG, "applyAnimation, nothing to animate");
@@ -1490,12 +1536,11 @@
listener.getDurationMs(), listener.getInsetsInterpolator(),
show ? ANIMATION_TYPE_SHOW : ANIMATION_TYPE_HIDE,
show ? LAYOUT_INSETS_DURING_ANIMATION_SHOWN : LAYOUT_INSETS_DURING_ANIMATION_HIDDEN,
- !hasAnimationCallbacks /* useInsetsAnimationThread */);
+ !hasAnimationCallbacks /* useInsetsAnimationThread */, statsToken);
}
- private void hideDirectly(
- @InsetsType int types, boolean animationFinished, @AnimationType int animationType,
- boolean fromIme) {
+ private void hideDirectly(@InsetsType int types, boolean animationFinished,
+ @AnimationType int animationType, boolean fromIme) {
if ((types & ime()) != 0) {
ImeTracing.getInstance().triggerClientDump("InsetsController#hideDirectly",
mHost.getInputMethodManager(), null /* icProto */);
diff --git a/core/java/android/view/InsetsResizeAnimationRunner.java b/core/java/android/view/InsetsResizeAnimationRunner.java
index edcfc95..778c677 100644
--- a/core/java/android/view/InsetsResizeAnimationRunner.java
+++ b/core/java/android/view/InsetsResizeAnimationRunner.java
@@ -37,6 +37,7 @@
import android.view.WindowInsets.Type.InsetsType;
import android.view.WindowInsetsAnimation.Bounds;
import android.view.animation.Interpolator;
+import android.view.inputmethod.ImeTracker;
/**
* Runs a fake animation of resizing insets to produce insets animation callbacks.
@@ -92,6 +93,12 @@
}
@Override
+ public ImeTracker.Token getStatsToken() {
+ // Return null as resizing the IME view is not explicitly tracked.
+ return null;
+ }
+
+ @Override
public void cancel() {
if (mCancelled || mFinished) {
return;
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index 43f0947..63602393 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -192,6 +192,7 @@
import android.view.contentcapture.ContentCaptureManager;
import android.view.contentcapture.ContentCaptureSession;
import android.view.contentcapture.MainContentCaptureSession;
+import android.view.inputmethod.ImeTracker;
import android.view.inputmethod.InputMethodManager;
import android.widget.Scroller;
import android.window.ClientWindowFrames;
@@ -5649,17 +5650,23 @@
break;
}
case MSG_SHOW_INSETS: {
+ final ImeTracker.Token statsToken = (ImeTracker.Token) msg.obj;
+ ImeTracker.get().onProgress(statsToken,
+ ImeTracker.PHASE_CLIENT_HANDLE_SHOW_INSETS);
if (mView == null) {
Log.e(TAG,
String.format("Calling showInsets(%d,%b) on window that no longer"
+ " has views.", msg.arg1, msg.arg2 == 1));
}
clearLowProfileModeIfNeeded(msg.arg1, msg.arg2 == 1);
- mInsetsController.show(msg.arg1, msg.arg2 == 1);
+ mInsetsController.show(msg.arg1, msg.arg2 == 1, statsToken);
break;
}
case MSG_HIDE_INSETS: {
- mInsetsController.hide(msg.arg1, msg.arg2 == 1);
+ final ImeTracker.Token statsToken = (ImeTracker.Token) msg.obj;
+ ImeTracker.get().onProgress(statsToken,
+ ImeTracker.PHASE_CLIENT_HANDLE_HIDE_INSETS);
+ mInsetsController.hide(msg.arg1, msg.arg2 == 1, statsToken);
break;
}
case MSG_WINDOW_MOVED:
@@ -8811,12 +8818,14 @@
mHandler.obtainMessage(MSG_INSETS_CONTROL_CHANGED, args).sendToTarget();
}
- private void showInsets(@InsetsType int types, boolean fromIme) {
- mHandler.obtainMessage(MSG_SHOW_INSETS, types, fromIme ? 1 : 0).sendToTarget();
+ private void showInsets(@InsetsType int types, boolean fromIme,
+ @Nullable ImeTracker.Token statsToken) {
+ mHandler.obtainMessage(MSG_SHOW_INSETS, types, fromIme ? 1 : 0, statsToken).sendToTarget();
}
- private void hideInsets(@InsetsType int types, boolean fromIme) {
- mHandler.obtainMessage(MSG_HIDE_INSETS, types, fromIme ? 1 : 0).sendToTarget();
+ private void hideInsets(@InsetsType int types, boolean fromIme,
+ @Nullable ImeTracker.Token statsToken) {
+ mHandler.obtainMessage(MSG_HIDE_INSETS, types, fromIme ? 1 : 0, statsToken).sendToTarget();
}
public void dispatchMoved(int newX, int newY) {
@@ -10180,7 +10189,8 @@
}
@Override
- public void showInsets(@InsetsType int types, boolean fromIme) {
+ public void showInsets(@InsetsType int types, boolean fromIme,
+ @Nullable ImeTracker.Token statsToken) {
final ViewRootImpl viewAncestor = mViewAncestor.get();
if (fromIme) {
ImeTracing.getInstance().triggerClientDump("ViewRootImpl.W#showInsets",
@@ -10188,13 +10198,16 @@
null /* icProto */);
}
if (viewAncestor != null) {
- viewAncestor.showInsets(types, fromIme);
+ ImeTracker.get().onProgress(statsToken, ImeTracker.PHASE_CLIENT_SHOW_INSETS);
+ viewAncestor.showInsets(types, fromIme, statsToken);
+ } else {
+ ImeTracker.get().onFailed(statsToken, ImeTracker.PHASE_CLIENT_SHOW_INSETS);
}
}
@Override
- public void hideInsets(@InsetsType int types, boolean fromIme) {
-
+ public void hideInsets(@InsetsType int types, boolean fromIme,
+ @Nullable ImeTracker.Token statsToken) {
final ViewRootImpl viewAncestor = mViewAncestor.get();
if (fromIme) {
ImeTracing.getInstance().triggerClientDump("ViewRootImpl.W#hideInsets",
@@ -10202,7 +10215,10 @@
null /* icProto */);
}
if (viewAncestor != null) {
- viewAncestor.hideInsets(types, fromIme);
+ ImeTracker.get().onProgress(statsToken, ImeTracker.PHASE_CLIENT_HIDE_INSETS);
+ viewAncestor.hideInsets(types, fromIme, statsToken);
+ } else {
+ ImeTracker.get().onFailed(statsToken, ImeTracker.PHASE_CLIENT_HIDE_INSETS);
}
}
diff --git a/core/java/android/view/inputmethod/IInputMethodManagerGlobalInvoker.java b/core/java/android/view/inputmethod/IInputMethodManagerGlobalInvoker.java
index a66c67b..6eae63a 100644
--- a/core/java/android/view/inputmethod/IInputMethodManagerGlobalInvoker.java
+++ b/core/java/android/view/inputmethod/IInputMethodManagerGlobalInvoker.java
@@ -275,15 +275,16 @@
@AnyThread
static boolean showSoftInput(@NonNull IInputMethodClient client, @Nullable IBinder windowToken,
- int flags, int lastClickToolType, @Nullable ResultReceiver resultReceiver,
+ @Nullable ImeTracker.Token statsToken, int flags, int lastClickToolType,
+ @Nullable ResultReceiver resultReceiver,
@SoftInputShowHideReason int reason) {
final IInputMethodManager service = getService();
if (service == null) {
return false;
}
try {
- return service.showSoftInput(
- client, windowToken, flags, lastClickToolType, resultReceiver, reason);
+ return service.showSoftInput(client, windowToken, statsToken, flags, lastClickToolType,
+ resultReceiver, reason);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -291,14 +292,15 @@
@AnyThread
static boolean hideSoftInput(@NonNull IInputMethodClient client, @Nullable IBinder windowToken,
- int flags, @Nullable ResultReceiver resultReceiver,
- @SoftInputShowHideReason int reason) {
+ @Nullable ImeTracker.Token statsToken, int flags,
+ @Nullable ResultReceiver resultReceiver, @SoftInputShowHideReason int reason) {
final IInputMethodManager service = getService();
if (service == null) {
return false;
}
try {
- return service.hideSoftInput(client, windowToken, flags, resultReceiver, reason);
+ return service.hideSoftInput(client, windowToken, statsToken, flags, resultReceiver,
+ reason);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
diff --git a/core/java/android/view/inputmethod/ImeTracker.aidl b/core/java/android/view/inputmethod/ImeTracker.aidl
new file mode 100644
index 0000000..1988f48
--- /dev/null
+++ b/core/java/android/view/inputmethod/ImeTracker.aidl
@@ -0,0 +1,19 @@
+/*
+ * 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 android.view.inputmethod;
+
+parcelable ImeTracker.Token;
diff --git a/core/java/android/view/inputmethod/ImeTracker.java b/core/java/android/view/inputmethod/ImeTracker.java
new file mode 100644
index 0000000..f4ecdff
--- /dev/null
+++ b/core/java/android/view/inputmethod/ImeTracker.java
@@ -0,0 +1,488 @@
+/*
+ * 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 android.view.inputmethod;
+
+import static android.view.inputmethod.ImeTracker.Debug.originToString;
+import static android.view.inputmethod.ImeTracker.Debug.phaseToString;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.ActivityThread;
+import android.os.Binder;
+import android.os.IBinder;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.SystemProperties;
+import android.util.Log;
+
+import com.android.internal.inputmethod.InputMethodDebug;
+import com.android.internal.inputmethod.SoftInputShowHideReason;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.reflect.Field;
+import java.util.Arrays;
+import java.util.Map;
+import java.util.Random;
+import java.util.stream.Collectors;
+
+/** @hide */
+public interface ImeTracker {
+
+ String TAG = "ImeTracker";
+
+ /**
+ * The origin of the IME request
+ *
+ * The name follows the format {@code PHASE_x_...} where {@code x} denotes
+ * where the origin is (i.e. {@code PHASE_SERVER_...} occurs in the server).
+ */
+ @IntDef(prefix = { "ORIGIN_" }, value = {
+ ORIGIN_CLIENT_SHOW_SOFT_INPUT,
+ ORIGIN_CLIENT_HIDE_SOFT_INPUT,
+ ORIGIN_SERVER_START_INPUT,
+ ORIGIN_SERVER_HIDE_INPUT
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ @interface Origin {}
+
+ /**
+ * The IME show request originated in the client.
+ */
+ int ORIGIN_CLIENT_SHOW_SOFT_INPUT = 0;
+
+ /**
+ * The IME hide request originated in the client.
+ */
+ int ORIGIN_CLIENT_HIDE_SOFT_INPUT = 1;
+
+ /**
+ * The IME show request originated in the server.
+ */
+ int ORIGIN_SERVER_START_INPUT = 2;
+
+ /**
+ * The IME hide request originated in the server.
+ */
+ int ORIGIN_SERVER_HIDE_INPUT = 3;
+
+ /**
+ * The current phase of the IME request.
+ *
+ * The name follows the format {@code PHASE_x_...} where {@code x} denotes
+ * where the phase is (i.e. {@code PHASE_SERVER_...} occurs in the server).
+ */
+ @IntDef(prefix = { "PHASE_" }, value = {
+ PHASE_CLIENT_VIEW_SERVED,
+ PHASE_SERVER_CLIENT_KNOWN,
+ PHASE_SERVER_CLIENT_FOCUSED,
+ PHASE_SERVER_ACCESSIBILITY,
+ PHASE_SERVER_SYSTEM_READY,
+ PHASE_SERVER_HIDE_IMPLICIT,
+ PHASE_SERVER_HIDE_NOT_ALWAYS,
+ PHASE_SERVER_WAIT_IME,
+ PHASE_SERVER_HAS_IME,
+ PHASE_SERVER_SHOULD_HIDE,
+ PHASE_IME_WRAPPER,
+ PHASE_IME_WRAPPER_DISPATCH,
+ PHASE_IME_SHOW_SOFT_INPUT,
+ PHASE_IME_HIDE_SOFT_INPUT,
+ PHASE_IME_ON_SHOW_SOFT_INPUT_TRUE,
+ PHASE_IME_APPLY_VISIBILITY_INSETS_CONSUMER,
+ PHASE_SERVER_APPLY_IME_VISIBILITY,
+ PHASE_WM_SHOW_IME_RUNNER,
+ PHASE_WM_SHOW_IME_READY,
+ PHASE_WM_HAS_IME_INSETS_CONTROL_TARGET,
+ PHASE_WM_WINDOW_INSETS_CONTROL_TARGET_SHOW_INSETS,
+ PHASE_WM_WINDOW_INSETS_CONTROL_TARGET_HIDE_INSETS,
+ PHASE_WM_REMOTE_INSETS_CONTROL_TARGET_SHOW_INSETS,
+ PHASE_WM_REMOTE_INSETS_CONTROL_TARGET_HIDE_INSETS,
+ PHASE_WM_REMOTE_INSETS_CONTROLLER,
+ PHASE_WM_ANIMATION_CREATE,
+ PHASE_WM_ANIMATION_RUNNING,
+ PHASE_CLIENT_SHOW_INSETS,
+ PHASE_CLIENT_HIDE_INSETS,
+ PHASE_CLIENT_HANDLE_SHOW_INSETS,
+ PHASE_CLIENT_HANDLE_HIDE_INSETS,
+ PHASE_CLIENT_APPLY_ANIMATION,
+ PHASE_CLIENT_CONTROL_ANIMATION,
+ PHASE_CLIENT_ANIMATION_RUNNING,
+ PHASE_CLIENT_ANIMATION_CANCEL,
+ PHASE_CLIENT_ANIMATION_FINISHED_SHOW,
+ PHASE_CLIENT_ANIMATION_FINISHED_HIDE
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ @interface Phase {}
+
+ /** The view that requested the IME has been served by the IMM. */
+ int PHASE_CLIENT_VIEW_SERVED = 0;
+
+ /** The IME client that requested the IME has window manager focus. */
+ int PHASE_SERVER_CLIENT_KNOWN = 1;
+
+ /** The IME client that requested the IME has IME focus. */
+ int PHASE_SERVER_CLIENT_FOCUSED = 2;
+
+ /** The IME request complies with the current accessibility settings. */
+ int PHASE_SERVER_ACCESSIBILITY = 3;
+
+ /** The server is ready to run third party code. */
+ int PHASE_SERVER_SYSTEM_READY = 4;
+
+ /** Checked the implicit hide request against any explicit show requests. */
+ int PHASE_SERVER_HIDE_IMPLICIT = 5;
+
+ /** Checked the not-always hide request against any forced show requests. */
+ int PHASE_SERVER_HIDE_NOT_ALWAYS = 6;
+
+ /** The server is waiting for a connection to the IME. */
+ int PHASE_SERVER_WAIT_IME = 7;
+
+ /** The server has a connection to the IME. */
+ int PHASE_SERVER_HAS_IME = 8;
+
+ /** The server decided the IME should be hidden. */
+ int PHASE_SERVER_SHOULD_HIDE = 9;
+
+ /** Reached the IME wrapper. */
+ int PHASE_IME_WRAPPER = 10;
+
+ /** Dispatched from the IME wrapper to the IME. */
+ int PHASE_IME_WRAPPER_DISPATCH = 11;
+
+ /** Reached the IME' showSoftInput method. */
+ int PHASE_IME_SHOW_SOFT_INPUT = 12;
+
+ /** Reached the IME' hideSoftInput method. */
+ int PHASE_IME_HIDE_SOFT_INPUT = 13;
+
+ /** The server decided the IME should be shown. */
+ int PHASE_IME_ON_SHOW_SOFT_INPUT_TRUE = 14;
+
+ /** Requested applying the IME visibility in the insets source consumer. */
+ int PHASE_IME_APPLY_VISIBILITY_INSETS_CONSUMER = 15;
+
+ /** Applied the IME visibility. */
+ int PHASE_SERVER_APPLY_IME_VISIBILITY = 16;
+
+ /** Created the show IME runner. */
+ int PHASE_WM_SHOW_IME_RUNNER = 17;
+
+ /** Ready to show IME. */
+ int PHASE_WM_SHOW_IME_READY = 18;
+
+ /** The Window Manager has a connection to the IME insets control target. */
+ int PHASE_WM_HAS_IME_INSETS_CONTROL_TARGET = 19;
+
+ /** Reached the window insets control target's show insets method. */
+ int PHASE_WM_WINDOW_INSETS_CONTROL_TARGET_SHOW_INSETS = 20;
+
+ /** Reached the window insets control target's hide insets method. */
+ int PHASE_WM_WINDOW_INSETS_CONTROL_TARGET_HIDE_INSETS = 21;
+
+ /** Reached the remote insets control target's show insets method. */
+ int PHASE_WM_REMOTE_INSETS_CONTROL_TARGET_SHOW_INSETS = 22;
+
+ /** Reached the remote insets control target's hide insets method. */
+ int PHASE_WM_REMOTE_INSETS_CONTROL_TARGET_HIDE_INSETS = 23;
+
+ /** Reached the remote insets controller. */
+ int PHASE_WM_REMOTE_INSETS_CONTROLLER = 24;
+
+ /** Created the IME window insets show animation. */
+ int PHASE_WM_ANIMATION_CREATE = 25;
+
+ /** Started the IME window insets show animation. */
+ int PHASE_WM_ANIMATION_RUNNING = 26;
+
+ /** Reached the client's show insets method. */
+ int PHASE_CLIENT_SHOW_INSETS = 27;
+
+ /** Reached the client's hide insets method. */
+ int PHASE_CLIENT_HIDE_INSETS = 28;
+
+ /** Handling the IME window insets show request. */
+ int PHASE_CLIENT_HANDLE_SHOW_INSETS = 29;
+
+ /** Handling the IME window insets hide request. */
+ int PHASE_CLIENT_HANDLE_HIDE_INSETS = 30;
+
+ /** Applied the IME window insets show animation. */
+ int PHASE_CLIENT_APPLY_ANIMATION = 31;
+
+ /** Started the IME window insets show animation. */
+ int PHASE_CLIENT_CONTROL_ANIMATION = 32;
+
+ /** Queued the IME window insets show animation. */
+ int PHASE_CLIENT_ANIMATION_RUNNING = 33;
+
+ /** Cancelled the IME window insets show animation. */
+ int PHASE_CLIENT_ANIMATION_CANCEL = 34;
+
+ /** Finished the IME window insets show animation. */
+ int PHASE_CLIENT_ANIMATION_FINISHED_SHOW = 35;
+
+ /** Finished the IME window insets hide animation. */
+ int PHASE_CLIENT_ANIMATION_FINISHED_HIDE = 36;
+
+ /**
+ * Called when an IME show request is created.
+ *
+ * @param token the token tracking the current IME show request or {@code null} otherwise.
+ * @param origin the origin of the IME show request.
+ * @param reason the reason why the IME show request was created.
+ */
+ void onRequestShow(@Nullable Token token, @Origin int origin,
+ @SoftInputShowHideReason int reason);
+
+ /**
+ * Called when an IME hide request is created.
+ *
+ * @param token the token tracking the current IME hide request or {@code null} otherwise.
+ * @param origin the origin of the IME hide request.
+ * @param reason the reason why the IME hide request was created.
+ */
+ void onRequestHide(@Nullable Token token, @Origin int origin,
+ @SoftInputShowHideReason int reason);
+
+ /**
+ * Called when an IME request progresses to a further phase.
+ *
+ * @param token the token tracking the current IME request or {@code null} otherwise.
+ * @param phase the new phase the IME request reached.
+ */
+ void onProgress(@Nullable Token token, @Phase int phase);
+
+ /**
+ * Called when an IME request fails.
+ *
+ * @param token the token tracking the current IME request or {@code null} otherwise.
+ * @param phase the phase the IME request failed at.
+ */
+ void onFailed(@Nullable Token token, @Phase int phase);
+
+ /**
+ * Called when an IME request reached a flow that is not yet implemented.
+ *
+ * @param token the token tracking the current IME request or {@code null} otherwise.
+ * @param phase the phase the IME request was currently at.
+ */
+ void onTodo(@Nullable Token token, @Phase int phase);
+
+ /**
+ * Called when an IME request is cancelled.
+ *
+ * @param token the token tracking the current IME request or {@code null} otherwise.
+ * @param phase the phase the IME request was cancelled at.
+ */
+ void onCancelled(@Nullable Token token, @Phase int phase);
+
+ /**
+ * Called when the IME show request is successful.
+ *
+ * @param token the token tracking the current IME show request or {@code null} otherwise.
+ */
+ void onShown(@Nullable Token token);
+
+ /**
+ * Called when the IME hide request is successful.
+ *
+ * @param token the token tracking the current IME hide request or {@code null} otherwise.
+ */
+ void onHidden(@Nullable Token token);
+
+ /**
+ * Get the singleton instance of this class.
+ *
+ * @return the singleton instance of this class
+ */
+ @NonNull
+ static ImeTracker get() {
+ return SystemProperties.getBoolean("persist.debug.imetracker", false)
+ ? LOGGER
+ : NOOP_LOGGER;
+ }
+
+ /** The singleton IME tracker instance. */
+ ImeTracker LOGGER = new ImeTracker() {
+
+ @Override
+ public void onRequestShow(@Nullable Token token, int origin,
+ @SoftInputShowHideReason int reason) {
+ if (token == null) return;
+ Log.i(TAG, token.mTag + ": onRequestShow at " + originToString(origin)
+ + " reason " + InputMethodDebug.softInputDisplayReasonToString(reason));
+ }
+
+ @Override
+ public void onRequestHide(@Nullable Token token, int origin,
+ @SoftInputShowHideReason int reason) {
+ if (token == null) return;
+ Log.i(TAG, token.mTag + ": onRequestHide at " + originToString(origin)
+ + " reason " + InputMethodDebug.softInputDisplayReasonToString(reason));
+ }
+
+ @Override
+ public void onProgress(@Nullable Token token, int phase) {
+ if (token == null) return;
+ Log.i(TAG, token.mTag + ": onProgress at " + phaseToString(phase));
+ }
+
+ @Override
+ public void onFailed(@Nullable Token token, int phase) {
+ if (token == null) return;
+ Log.i(TAG, token.mTag + ": onFailed at " + phaseToString(phase));
+ }
+
+ @Override
+ public void onTodo(@Nullable Token token, int phase) {
+ if (token == null) return;
+ Log.i(TAG, token.mTag + ": onTodo at " + phaseToString(phase));
+ }
+
+ @Override
+ public void onCancelled(@Nullable Token token, int phase) {
+ if (token == null) return;
+ Log.i(TAG, token.mTag + ": onCancelled at " + phaseToString(phase));
+ }
+
+ @Override
+ public void onShown(@Nullable Token token) {
+ if (token == null) return;
+ Log.i(TAG, token.mTag + ": onShown");
+ }
+
+ @Override
+ public void onHidden(@Nullable Token token) {
+ if (token == null) return;
+ Log.i(TAG, token.mTag + ": onHidden");
+ }
+ };
+
+ /** The singleton no-op IME tracker instance. */
+ ImeTracker NOOP_LOGGER = new ImeTracker() {
+
+ @Override
+ public void onRequestShow(@Nullable Token token, int origin,
+ @SoftInputShowHideReason int reason) {}
+
+ @Override
+ public void onRequestHide(@Nullable Token token, int origin,
+ @SoftInputShowHideReason int reason) {}
+
+ @Override
+ public void onProgress(@Nullable Token token, int phase) {}
+
+ @Override
+ public void onFailed(@Nullable Token token, int phase) {}
+
+ @Override
+ public void onTodo(@Nullable Token token, int phase) {}
+
+ @Override
+ public void onCancelled(@Nullable Token token, int phase) {}
+
+ @Override
+ public void onShown(@Nullable Token token) {}
+
+ @Override
+ public void onHidden(@Nullable Token token) {}
+ };
+
+ /** A token that tracks the progress of an IME request. */
+ class Token implements Parcelable {
+
+ private final IBinder mBinder;
+ private final String mTag;
+
+ public Token() {
+ this(ActivityThread.currentProcessName());
+ }
+
+ public Token(String component) {
+ this(new Binder(), component + ":" + Integer.toHexString((new Random().nextInt())));
+ }
+
+ private Token(IBinder binder, String tag) {
+ mBinder = binder;
+ mTag = tag;
+ }
+
+ /** For Parcelable, no special marshalled objects. */
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeStrongBinder(mBinder);
+ dest.writeString8(mTag);
+ }
+
+ @NonNull
+ public static final Creator<Token> CREATOR = new Creator<>() {
+ @Override
+ public Token createFromParcel(Parcel source) {
+ IBinder binder = source.readStrongBinder();
+ String tag = source.readString8();
+ return new Token(binder, tag);
+ }
+
+ @Override
+ public Token[] newArray(int size) {
+ return new Token[size];
+ }
+ };
+ }
+
+ /**
+ * Utilities for mapping phases and origins IntDef values to their names.
+ *
+ * Note: This is held in a separate class so that it only gets initialized when actually needed.
+ */
+ class Debug {
+
+ private static final Map<Integer, String> sOrigins =
+ getFieldMapping(ImeTracker.class, "ORIGIN_");
+ private static final Map<Integer, String> sPhases =
+ getFieldMapping(ImeTracker.class, "PHASE_");
+
+ public static String originToString(int origin) {
+ return sOrigins.getOrDefault(origin, "ORIGIN_" + origin);
+ }
+
+ public static String phaseToString(int phase) {
+ return sPhases.getOrDefault(phase, "PHASE_" + phase);
+ }
+
+ private static Map<Integer, String> getFieldMapping(Class<?> cls, String fieldPrefix) {
+ return Arrays.stream(cls.getDeclaredFields())
+ .filter(field -> field.getName().startsWith(fieldPrefix))
+ .collect(Collectors.toMap(Debug::getFieldValue, Field::getName));
+ }
+
+ private static int getFieldValue(Field field) {
+ try {
+ return field.getInt(null);
+ } catch (IllegalAccessException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ }
+}
diff --git a/core/java/android/view/inputmethod/InputMethod.java b/core/java/android/view/inputmethod/InputMethod.java
index 4d5a17d..92380ed 100644
--- a/core/java/android/view/inputmethod/InputMethod.java
+++ b/core/java/android/view/inputmethod/InputMethod.java
@@ -300,11 +300,12 @@
* @param showInputToken an opaque {@link android.os.Binder} token to identify which API call
* of {@link InputMethodManager#showSoftInput(View, int)} is associated with
* this callback.
+ * @param statsToken the token tracking the current IME show request or {@code null} otherwise.
* @hide
*/
@MainThread
- default public void showSoftInputWithToken(int flags, ResultReceiver resultReceiver,
- IBinder showInputToken) {
+ public default void showSoftInputWithToken(int flags, ResultReceiver resultReceiver,
+ IBinder showInputToken, @Nullable ImeTracker.Token statsToken) {
showSoftInput(flags, resultReceiver);
}
@@ -338,11 +339,14 @@
* @param hideInputToken an opaque {@link android.os.Binder} token to identify which API call
* of {@link InputMethodManager#hideSoftInputFromWindow(IBinder, int)}} is associated
* with this callback.
+ * @param statsToken the token tracking the current IME hide request or {@code null} otherwise.
* @hide
*/
@MainThread
- public void hideSoftInputWithToken(int flags, ResultReceiver resultReceiver,
- IBinder hideInputToken);
+ public default void hideSoftInputWithToken(int flags, ResultReceiver resultReceiver,
+ IBinder hideInputToken, @Nullable ImeTracker.Token statsToken) {
+ hideSoftInput(flags, resultReceiver);
+ }
/**
* Request that any soft input part of the input method be hidden from the user.
@@ -369,7 +373,7 @@
/**
* Checks if IME is ready to start stylus handwriting session.
- * If yes, {@link #startStylusHandwriting(InputChannel, List)} is called.
+ * If yes, {@link #startStylusHandwriting(int, InputChannel, List)} is called.
* @param requestId
* @hide
*/
diff --git a/core/java/android/view/inputmethod/InputMethodManager.java b/core/java/android/view/inputmethod/InputMethodManager.java
index 74afced..ee31fd5 100644
--- a/core/java/android/view/inputmethod/InputMethodManager.java
+++ b/core/java/android/view/inputmethod/InputMethodManager.java
@@ -632,6 +632,8 @@
private final DelegateImpl mDelegate = new DelegateImpl();
+ private static boolean sPreventImeStartupUnlessTextEditor;
+
// -----------------------------------------------------------
private static final int MSG_DUMP = 1;
@@ -1435,6 +1437,10 @@
// display case.
final Looper looper = displayId == Display.DEFAULT_DISPLAY
? Looper.getMainLooper() : context.getMainLooper();
+ // Keep track of whether to expect the IME to be unavailable so as to avoid log spam in
+ // sendInputEventOnMainLooperLocked() by not logging a verbose message on every DPAD event
+ sPreventImeStartupUnlessTextEditor = context.getResources().getBoolean(
+ com.android.internal.R.bool.config_preventImeStartupUnlessTextEditor);
return forContextInternal(displayId, looper);
}
@@ -2001,6 +2007,10 @@
private boolean showSoftInput(View view, int flags, ResultReceiver resultReceiver,
@SoftInputShowHideReason int reason) {
+ final ImeTracker.Token statsToken = new ImeTracker.Token();
+ ImeTracker.get().onRequestShow(statsToken, ImeTracker.ORIGIN_CLIENT_SHOW_SOFT_INPUT,
+ reason);
+
ImeTracing.getInstance().triggerClientDump("InputMethodManager#showSoftInput", this,
null /* icProto */);
// Re-dispatch if there is a context mismatch.
@@ -2012,10 +2022,13 @@
checkFocus();
synchronized (mH) {
if (!hasServedByInputMethodLocked(view)) {
+ ImeTracker.get().onFailed(statsToken, ImeTracker.PHASE_CLIENT_VIEW_SERVED);
Log.w(TAG, "Ignoring showSoftInput() as view=" + view + " is not served.");
return false;
}
+ ImeTracker.get().onProgress(statsToken, ImeTracker.PHASE_CLIENT_VIEW_SERVED);
+
// Makes sure to call ImeInsetsSourceConsumer#onShowRequested on the UI thread.
// TODO(b/229426865): call WindowInsetsController#show instead.
mH.executeOrSendMessage(Message.obtain(mH, MSG_ON_SHOW_REQUESTED));
@@ -2024,6 +2037,7 @@
return IInputMethodManagerGlobalInvoker.showSoftInput(
mClient,
view.getWindowToken(),
+ statsToken,
flags,
mCurRootView.getLastClickToolType(),
resultReceiver,
@@ -2043,19 +2057,28 @@
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 123768499)
public void showSoftInputUnchecked(int flags, ResultReceiver resultReceiver) {
synchronized (mH) {
+ final ImeTracker.Token statsToken = new ImeTracker.Token();
+ ImeTracker.get().onRequestShow(statsToken, ImeTracker.ORIGIN_CLIENT_SHOW_SOFT_INPUT,
+ SoftInputShowHideReason.SHOW_SOFT_INPUT);
+
Log.w(TAG, "showSoftInputUnchecked() is a hidden method, which will be"
+ " removed soon. If you are using androidx.appcompat.widget.SearchView,"
+ " please update to version 26.0 or newer version.");
if (mCurRootView == null || mCurRootView.getView() == null) {
+ ImeTracker.get().onFailed(statsToken, ImeTracker.PHASE_CLIENT_VIEW_SERVED);
Log.w(TAG, "No current root view, ignoring showSoftInputUnchecked()");
return;
}
+
+ ImeTracker.get().onProgress(statsToken, ImeTracker.PHASE_CLIENT_VIEW_SERVED);
+
// Makes sure to call ImeInsetsSourceConsumer#onShowRequested on the UI thread.
// TODO(b/229426865): call WindowInsetsController#show instead.
mH.executeOrSendMessage(Message.obtain(mH, MSG_ON_SHOW_REQUESTED));
IInputMethodManagerGlobalInvoker.showSoftInput(
mClient,
mCurRootView.getView().getWindowToken(),
+ statsToken,
flags,
mCurRootView.getLastClickToolType(),
resultReceiver,
@@ -2125,17 +2148,24 @@
private boolean hideSoftInputFromWindow(IBinder windowToken, int flags,
ResultReceiver resultReceiver, @SoftInputShowHideReason int reason) {
+ final ImeTracker.Token statsToken = new ImeTracker.Token();
+ ImeTracker.get().onRequestHide(statsToken, ImeTracker.ORIGIN_CLIENT_HIDE_SOFT_INPUT,
+ reason);
+
ImeTracing.getInstance().triggerClientDump("InputMethodManager#hideSoftInputFromWindow",
this, null /* icProto */);
checkFocus();
synchronized (mH) {
final View servedView = getServedViewLocked();
if (servedView == null || servedView.getWindowToken() != windowToken) {
+ ImeTracker.get().onFailed(statsToken, ImeTracker.PHASE_CLIENT_VIEW_SERVED);
return false;
}
- return IInputMethodManagerGlobalInvoker.hideSoftInput(mClient, windowToken, flags,
- resultReceiver, reason);
+ ImeTracker.get().onProgress(statsToken, ImeTracker.PHASE_CLIENT_VIEW_SERVED);
+
+ return IInputMethodManagerGlobalInvoker.hideSoftInput(mClient, windowToken, statsToken,
+ flags, resultReceiver, reason);
}
}
@@ -2763,14 +2793,23 @@
@UnsupportedAppUsage
void closeCurrentInput() {
+ final ImeTracker.Token statsToken = new ImeTracker.Token();
+ ImeTracker.get().onRequestHide(statsToken, ImeTracker.ORIGIN_CLIENT_HIDE_SOFT_INPUT,
+ SoftInputShowHideReason.HIDE_SOFT_INPUT);
+
synchronized (mH) {
if (mCurRootView == null || mCurRootView.getView() == null) {
+ ImeTracker.get().onFailed(statsToken, ImeTracker.PHASE_CLIENT_VIEW_SERVED);
Log.w(TAG, "No current root view, ignoring closeCurrentInput()");
return;
}
+
+ ImeTracker.get().onProgress(statsToken, ImeTracker.PHASE_CLIENT_VIEW_SERVED);
+
IInputMethodManagerGlobalInvoker.hideSoftInput(
mClient,
mCurRootView.getView().getWindowToken(),
+ statsToken,
HIDE_NOT_ALWAYS,
null,
SoftInputShowHideReason.HIDE_SOFT_INPUT);
@@ -2839,15 +2878,24 @@
* @hide
*/
public void notifyImeHidden(IBinder windowToken) {
+ final ImeTracker.Token statsToken = new ImeTracker.Token();
+ ImeTracker.get().onRequestHide(statsToken, ImeTracker.ORIGIN_CLIENT_HIDE_SOFT_INPUT,
+ SoftInputShowHideReason.HIDE_SOFT_INPUT_BY_INSETS_API);
+
ImeTracing.getInstance().triggerClientDump("InputMethodManager#notifyImeHidden", this,
null /* icProto */);
synchronized (mH) {
- if (isImeSessionAvailableLocked() && mCurRootView != null
- && mCurRootView.getWindowToken() == windowToken) {
- IInputMethodManagerGlobalInvoker.hideSoftInput(mClient, windowToken, 0 /* flags */,
- null /* resultReceiver */,
- SoftInputShowHideReason.HIDE_SOFT_INPUT_BY_INSETS_API);
+ if (!isImeSessionAvailableLocked() || mCurRootView == null
+ || mCurRootView.getWindowToken() != windowToken) {
+ ImeTracker.get().onFailed(statsToken, ImeTracker.PHASE_CLIENT_VIEW_SERVED);
+ return;
}
+
+ ImeTracker.get().onProgress(statsToken, ImeTracker.PHASE_CLIENT_VIEW_SERVED);
+
+ IInputMethodManagerGlobalInvoker.hideSoftInput(mClient, windowToken, statsToken,
+ 0 /* flags */, null /* resultReceiver */,
+ SoftInputShowHideReason.HIDE_SOFT_INPUT_BY_INSETS_API);
}
}
@@ -3364,8 +3412,12 @@
return DISPATCH_IN_PROGRESS;
}
- Log.w(TAG, "Unable to send input event to IME: " + getImeIdLocked()
- + " dropping: " + event);
+ if (sPreventImeStartupUnlessTextEditor) {
+ Log.d(TAG, "Dropping event because IME is evicted: " + event);
+ } else {
+ Log.w(TAG, "Unable to send input event to IME: " + getImeIdLocked()
+ + " dropping: " + event);
+ }
}
return DISPATCH_NOT_HANDLED;
}
@@ -3967,7 +4019,7 @@
/**
* As reported by {@link InputBindResult}. This value is determined by
- * {@link com.android.internal.R.styleable#InputMethod_suppressesSpellChecking}.
+ * {@link com.android.internal.R.styleable#InputMethod_suppressesSpellChecker}.
*/
final boolean mIsInputMethodSuppressingSpellChecker;
diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java
index 810fde2..bf1a2bd 100644
--- a/core/java/android/widget/TextView.java
+++ b/core/java/android/widget/TextView.java
@@ -2289,11 +2289,13 @@
* @param familyName family name string, e.g. "serif"
* @param typefaceIndex an index of the typeface enum, e.g. SANS, SERIF.
* @param style a typeface style
- * @param weight a weight value for the Typeface or -1 if not specified.
+ * @param weight a weight value for the Typeface or {@code FontStyle.FONT_WEIGHT_UNSPECIFIED}
+ * if not specified.
*/
private void setTypefaceFromAttrs(@Nullable Typeface typeface, @Nullable String familyName,
@XMLTypefaceAttr int typefaceIndex, @Typeface.Style int style,
- @IntRange(from = -1, to = FontStyle.FONT_WEIGHT_MAX) int weight) {
+ @IntRange(from = FontStyle.FONT_WEIGHT_UNSPECIFIED, to = FontStyle.FONT_WEIGHT_MAX)
+ int weight) {
if (typeface == null && familyName != null) {
// Lookup normal Typeface from system font map.
final Typeface normalTypeface = Typeface.create(familyName, Typeface.NORMAL);
@@ -2320,7 +2322,8 @@
}
private void resolveStyleAndSetTypeface(@NonNull Typeface typeface, @Typeface.Style int style,
- @IntRange(from = -1, to = FontStyle.FONT_WEIGHT_MAX) int weight) {
+ @IntRange(from = FontStyle.FONT_WEIGHT_UNSPECIFIED, to = FontStyle.FONT_WEIGHT_MAX)
+ int weight) {
if (weight >= 0) {
weight = Math.min(FontStyle.FONT_WEIGHT_MAX, weight);
final boolean italic = (style & Typeface.ITALIC) != 0;
@@ -4021,7 +4024,7 @@
boolean mFontFamilyExplicit = false;
int mTypefaceIndex = -1;
int mTextStyle = 0;
- int mFontWeight = -1;
+ int mFontWeight = FontStyle.FONT_WEIGHT_UNSPECIFIED;
boolean mAllCaps = false;
int mShadowColor = 0;
float mShadowDx = 0, mShadowDy = 0, mShadowRadius = 0;
@@ -6946,18 +6949,18 @@
if (isPassword) {
setTransformationMethod(PasswordTransformationMethod.getInstance());
setTypefaceFromAttrs(null/* fontTypeface */, null /* fontFamily */, MONOSPACE,
- Typeface.NORMAL, -1 /* weight, not specifeid */);
+ Typeface.NORMAL, FontStyle.FONT_WEIGHT_UNSPECIFIED);
} else if (isVisiblePassword) {
if (mTransformation == PasswordTransformationMethod.getInstance()) {
forceUpdate = true;
}
setTypefaceFromAttrs(null/* fontTypeface */, null /* fontFamily */, MONOSPACE,
- Typeface.NORMAL, -1 /* weight, not specified */);
+ Typeface.NORMAL, FontStyle.FONT_WEIGHT_UNSPECIFIED);
} else if (wasPassword || wasVisiblePassword) {
// not in password mode, clean up typeface and transformation
setTypefaceFromAttrs(null/* fontTypeface */, null /* fontFamily */,
DEFAULT_TYPEFACE /* typeface index */, Typeface.NORMAL,
- -1 /* weight, not specified */);
+ FontStyle.FONT_WEIGHT_UNSPECIFIED);
if (mTransformation == PasswordTransformationMethod.getInstance()) {
forceUpdate = true;
}
diff --git a/core/java/android/window/DisplayWindowPolicyController.java b/core/java/android/window/DisplayWindowPolicyController.java
index a5aefd5..f55932e 100644
--- a/core/java/android/window/DisplayWindowPolicyController.java
+++ b/core/java/android/window/DisplayWindowPolicyController.java
@@ -16,6 +16,8 @@
package android.window;
+import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
+
import android.annotation.NonNull;
import android.app.WindowConfiguration;
import android.content.ComponentName;
@@ -142,6 +144,14 @@
*/
public void onRunningAppsChanged(ArraySet<Integer> runningUids) {}
+ /**
+ * This is called when an Activity is entering PIP.
+ * Returns {@code true} if the Activity is allowed to enter PIP.
+ */
+ public boolean isEnteringPipAllowed(int uid) {
+ return isWindowingModeSupported(WINDOWING_MODE_PINNED);
+ }
+
/** Dump debug data */
public void dump(String prefix, final PrintWriter pw) {
pw.println(prefix + "DisplayWindowPolicyController{" + super.toString() + "}");
diff --git a/core/java/com/android/internal/inputmethod/IInputMethod.aidl b/core/java/com/android/internal/inputmethod/IInputMethod.aidl
index c62fba9..1e3714e 100644
--- a/core/java/com/android/internal/inputmethod/IInputMethod.aidl
+++ b/core/java/com/android/internal/inputmethod/IInputMethod.aidl
@@ -21,6 +21,7 @@
import android.view.InputChannel;
import android.view.MotionEvent;
import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.ImeTracker;
import android.view.inputmethod.InputBinding;
import android.view.inputmethod.InputMethodSubtype;
import android.window.ImeOnBackInvokedDispatcher;
@@ -69,9 +70,11 @@
void setSessionEnabled(IInputMethodSession session, boolean enabled);
- void showSoftInput(in IBinder showInputToken, int flags, in ResultReceiver resultReceiver);
+ void showSoftInput(in IBinder showInputToken, in @nullable ImeTracker.Token statsToken,
+ int flags, in ResultReceiver resultReceiver);
- void hideSoftInput(in IBinder hideInputToken, int flags, in ResultReceiver resultReceiver);
+ void hideSoftInput(in IBinder hideInputToken, in @nullable ImeTracker.Token statsToken,
+ int flags, in ResultReceiver resultReceiver);
void updateEditorToolType(int toolType);
diff --git a/core/java/com/android/internal/inputmethod/IInputMethodPrivilegedOperations.aidl b/core/java/com/android/internal/inputmethod/IInputMethodPrivilegedOperations.aidl
index 4babb70..f77e962 100644
--- a/core/java/com/android/internal/inputmethod/IInputMethodPrivilegedOperations.aidl
+++ b/core/java/com/android/internal/inputmethod/IInputMethodPrivilegedOperations.aidl
@@ -17,6 +17,7 @@
package com.android.internal.inputmethod;
import android.net.Uri;
+import android.view.inputmethod.ImeTracker;
import android.view.inputmethod.InputMethodSubtype;
import com.android.internal.infra.AndroidFuture;
@@ -41,7 +42,8 @@
void switchToNextInputMethod(boolean onlyCurrentIme, in AndroidFuture future /* T=Boolean */);
void shouldOfferSwitchingToNextInputMethod(in AndroidFuture future /* T=Boolean */);
void notifyUserActionAsync();
- void applyImeVisibilityAsync(IBinder showOrHideInputToken, boolean setVisible);
+ void applyImeVisibilityAsync(IBinder showOrHideInputToken, boolean setVisible,
+ in @nullable ImeTracker.Token statsToken);
void onStylusHandwritingReady(int requestId, int pid);
void resetStylusHandwriting(int requestId);
}
diff --git a/core/java/com/android/internal/inputmethod/InputMethodPrivilegedOperations.java b/core/java/com/android/internal/inputmethod/InputMethodPrivilegedOperations.java
index 67c2103..66e3333 100644
--- a/core/java/com/android/internal/inputmethod/InputMethodPrivilegedOperations.java
+++ b/core/java/com/android/internal/inputmethod/InputMethodPrivilegedOperations.java
@@ -25,6 +25,7 @@
import android.os.RemoteException;
import android.util.Log;
import android.view.View;
+import android.view.inputmethod.ImeTracker;
import android.view.inputmethod.InputMethodSubtype;
import com.android.internal.annotations.GuardedBy;
@@ -100,7 +101,7 @@
}
/**
- * Calls {@link IInputMethodPrivilegedOperations#setImeWindowStatusAsync(int, int}.
+ * Calls {@link IInputMethodPrivilegedOperations#setImeWindowStatusAsync(int, int)}.
*
* @param vis visibility flags
* @param backDisposition disposition flags
@@ -250,7 +251,7 @@
}
/**
- * Calls {@link IInputMethodPrivilegedOperations#hideMySoftInput(int, IVoidResultCallback)}
+ * Calls {@link IInputMethodPrivilegedOperations#hideMySoftInput(int, int, AndroidFuture)}
*
* @param flags additional operating flags
* @param reason the reason to hide soft input
@@ -316,7 +317,7 @@
/**
* Calls {@link IInputMethodPrivilegedOperations#switchToNextInputMethod(boolean,
- * IBooleanResultCallback)}
+ * AndroidFuture)}
*
* @param onlyCurrentIme {@code true} to switch to a {@link InputMethodSubtype} within the same
* IME
@@ -375,22 +376,25 @@
}
/**
- * Calls {@link IInputMethodPrivilegedOperations#applyImeVisibilityAsync(IBinder, boolean)}.
+ * Calls {@link IInputMethodPrivilegedOperations#applyImeVisibilityAsync(IBinder, boolean,
+ * ImeTracker.Token)}.
*
* @param showOrHideInputToken placeholder token that maps to window requesting
* {@link android.view.inputmethod.InputMethodManager#showSoftInput(View, int)} or
- * {@link android.view.inputmethod.InputMethodManager#hideSoftInputFromWindow
- * (IBinder, int)}
+ * {@link android.view.inputmethod.InputMethodManager#hideSoftInputFromWindow(IBinder,
+ * int)}
* @param setVisible {@code true} to set IME visible, else hidden.
+ * @param statsToken the token tracking the current IME request or {@code null} otherwise.
*/
@AnyThread
- public void applyImeVisibilityAsync(IBinder showOrHideInputToken, boolean setVisible) {
+ public void applyImeVisibilityAsync(IBinder showOrHideInputToken, boolean setVisible,
+ @Nullable ImeTracker.Token statsToken) {
final IInputMethodPrivilegedOperations ops = mOps.getAndWarnIfNull();
if (ops == null) {
return;
}
try {
- ops.applyImeVisibilityAsync(showOrHideInputToken, setVisible);
+ ops.applyImeVisibilityAsync(showOrHideInputToken, setVisible, statsToken);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
diff --git a/core/java/com/android/internal/view/BaseIWindow.java b/core/java/com/android/internal/view/BaseIWindow.java
index fe77236..2ac4309 100644
--- a/core/java/com/android/internal/view/BaseIWindow.java
+++ b/core/java/com/android/internal/view/BaseIWindow.java
@@ -16,6 +16,7 @@
package com.android.internal.view;
+import android.annotation.Nullable;
import android.compat.annotation.UnsupportedAppUsage;
import android.hardware.input.InputManager;
import android.os.Bundle;
@@ -31,6 +32,7 @@
import android.view.PointerIcon;
import android.view.ScrollCaptureResponse;
import android.view.WindowInsets.Type.InsetsType;
+import android.view.inputmethod.ImeTracker;
import android.window.ClientWindowFrames;
import com.android.internal.os.IResultReceiver;
@@ -66,11 +68,13 @@
}
@Override
- public void showInsets(@InsetsType int types, boolean fromIme) {
+ public void showInsets(@InsetsType int types, boolean fromIme,
+ @Nullable ImeTracker.Token statsToken) {
}
@Override
- public void hideInsets(@InsetsType int types, boolean fromIme) {
+ public void hideInsets(@InsetsType int types, boolean fromIme,
+ @Nullable ImeTracker.Token statsToken) {
}
@Override
diff --git a/core/java/com/android/internal/view/IInputMethodManager.aidl b/core/java/com/android/internal/view/IInputMethodManager.aidl
index 40c6a05..00bc3f2 100644
--- a/core/java/com/android/internal/view/IInputMethodManager.aidl
+++ b/core/java/com/android/internal/view/IInputMethodManager.aidl
@@ -17,6 +17,7 @@
package com.android.internal.view;
import android.os.ResultReceiver;
+import android.view.inputmethod.ImeTracker;
import android.view.inputmethod.InputMethodInfo;
import android.view.inputmethod.InputMethodSubtype;
import android.view.inputmethod.EditorInfo;
@@ -54,11 +55,12 @@
+ "android.Manifest.permission.INTERACT_ACROSS_USERS_FULL, conditional = true)")
InputMethodSubtype getLastInputMethodSubtype(int userId);
- boolean showSoftInput(in IInputMethodClient client, @nullable IBinder windowToken, int flags,
- int lastClickToolType, in @nullable ResultReceiver resultReceiver, int reason);
- boolean hideSoftInput(in IInputMethodClient client, @nullable IBinder windowToken, int flags,
+ boolean showSoftInput(in IInputMethodClient client, @nullable IBinder windowToken,
+ in @nullable ImeTracker.Token statsToken, int flags, int lastClickToolType,
in @nullable ResultReceiver resultReceiver, int reason);
-
+ boolean hideSoftInput(in IInputMethodClient client, @nullable IBinder windowToken,
+ in @nullable ImeTracker.Token statsToken, int flags,
+ in @nullable ResultReceiver resultReceiver, int reason);
// If windowToken is null, this just does startInput(). Otherwise this reports that a window
// has gained focus, and if 'editorInfo' is non-null then also does startInput.
// @NonNull
diff --git a/core/java/com/android/internal/widget/RemeasuringLinearLayout.java b/core/java/com/android/internal/widget/RemeasuringLinearLayout.java
index 80d1457..7b154a5 100644
--- a/core/java/com/android/internal/widget/RemeasuringLinearLayout.java
+++ b/core/java/com/android/internal/widget/RemeasuringLinearLayout.java
@@ -18,7 +18,6 @@
import android.annotation.Nullable;
import android.content.Context;
-import android.os.Trace;
import android.util.AttributeSet;
import android.view.View;
import android.widget.LinearLayout;
@@ -55,7 +54,6 @@
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
- Trace.beginSection("RemeasuringLinearLayout#onMeasure");
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int count = getChildCount();
int height = 0;
@@ -88,6 +86,5 @@
}
mMatchParentViews.clear();
setMeasuredDimension(getMeasuredWidth(), height);
- Trace.endSection();
}
}
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index 0218215..ac383e6 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -1855,6 +1855,10 @@
-->
<string name="config_defaultCaptivePortalLoginPackageName" translatable="false">com.android.captiveportallogin</string>
+ <!-- The package name of the dock manager app. Must be granted the
+ POST_NOTIFICATIONS permission. -->
+ <string name="config_defaultDockManagerPackageName" translatable="false"></string>
+
<!-- Whether to enable geocoder overlay which allows geocoder to be replaced
by an app at run-time. When disabled, only the
config_geocoderProviderPackageName package will be searched for
@@ -2422,8 +2426,8 @@
<integer name="config_dreamsBatteryLevelDrainCutoff">5</integer>
<!-- Limit of how long the device can remain unlocked due to attention checking. -->
<integer name="config_attentionMaximumExtension">900000</integer> <!-- 15 minutes. -->
- <!-- Is the system user the only user allowed to dream. -->
- <bool name="config_dreamsOnlyEnabledForSystemUser">false</bool>
+ <!-- Whether there is to be a chosen Dock User who is the only user allowed to dream. -->
+ <bool name="config_dreamsOnlyEnabledForDockUser">false</bool>
<!-- Whether dreams are disabled when ambient mode is suppressed. -->
<bool name="config_dreamsDisabledByAmbientModeSuppressionConfig">false</bool>
@@ -2672,9 +2676,9 @@
Should be false for most devices, except automotive vehicle with passenger displays. -->
<bool name="config_multiuserUsersOnSecondaryDisplays">false</bool>
- <!-- Whether to automatically switch a non-primary user back to the primary user after a
- timeout when the device is docked. -->
- <bool name="config_enableTimeoutToUserZeroWhenDocked">false</bool>
+ <!-- Whether to automatically switch to the designated Dock User (the user chosen for
+ displaying dreams, etc.) after a timeout when the device is docked. -->
+ <bool name="config_enableTimeoutToDockUserWhenDocked">false</bool>
<!-- Whether to only install system packages on a user if they're allowlisted for that user
type. These are flags and can be freely combined.
diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml
index 48484c7..216975d 100644
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -6331,6 +6331,8 @@
<string name="vdm_camera_access_denied" product="tablet">Can’t access the tablet’s camera from your <xliff:g id="device" example="Chromebook">%1$s</xliff:g></string>
<!-- Error message indicating the user cannot access secure content when running on a virtual device. [CHAR LIMIT=NONE] -->
<string name="vdm_secure_window">This can’t be accessed while streaming. Try on your phone instead.</string>
+ <!-- Error message indicating the user cannot view picture-in-picture when running on a virtual device. [CHAR LIMIT=NONE] -->
+ <string name="vdm_pip_blocked">Can’t view picture-in-picture while streaming</string>
<!-- Title for preference of the system default locale. [CHAR LIMIT=50]-->
<string name="system_locale_title">System default</string>
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index b5d534f..89ec5ba 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -466,7 +466,7 @@
<java-symbol type="integer" name="config_multiuserMaxRunningUsers" />
<java-symbol type="bool" name="config_multiuserDelayUserDataLocking" />
<java-symbol type="bool" name="config_multiuserUsersOnSecondaryDisplays" />
- <java-symbol type="bool" name="config_enableTimeoutToUserZeroWhenDocked" />
+ <java-symbol type="bool" name="config_enableTimeoutToDockUserWhenDocked" />
<java-symbol type="integer" name="config_userTypePackageWhitelistMode"/>
<java-symbol type="xml" name="config_user_types" />
<java-symbol type="integer" name="config_safe_media_volume_index" />
@@ -2236,7 +2236,7 @@
<java-symbol type="integer" name="config_dreamsBatteryLevelDrainCutoff" />
<java-symbol type="string" name="config_dreamsDefaultComponent" />
<java-symbol type="bool" name="config_dreamsDisabledByAmbientModeSuppressionConfig" />
- <java-symbol type="bool" name="config_dreamsOnlyEnabledForSystemUser" />
+ <java-symbol type="bool" name="config_dreamsOnlyEnabledForDockUser" />
<java-symbol type="integer" name="config_dreamOpenAnimationDuration" />
<java-symbol type="integer" name="config_dreamCloseAnimationDuration" />
<java-symbol type="array" name="config_supportedDreamComplications" />
@@ -3480,6 +3480,9 @@
<!-- Captive Portal Login -->
<java-symbol type="string" name="config_defaultCaptivePortalLoginPackageName" />
+ <!-- Dock Manager -->
+ <java-symbol type="string" name="config_defaultDockManagerPackageName" />
+
<!-- Optional IPsec algorithms -->
<java-symbol type="array" name="config_optionalIpSecAlgorithms" />
@@ -4858,6 +4861,7 @@
<!-- For VirtualDeviceManager -->
<java-symbol type="string" name="vdm_camera_access_denied" />
<java-symbol type="string" name="vdm_secure_window" />
+ <java-symbol type="string" name="vdm_pip_blocked" />
<java-symbol type="color" name="camera_privacy_light_day"/>
<java-symbol type="color" name="camera_privacy_light_night"/>
diff --git a/core/res/res/xml/config_user_types.xml b/core/res/res/xml/config_user_types.xml
index 7663150..df6b7b2 100644
--- a/core/res/res/xml/config_user_types.xml
+++ b/core/res/res/xml/config_user_types.xml
@@ -83,6 +83,8 @@
For profile and full users:
default-restrictions (with values defined in UserRestrictionUtils.USER_RESTRICTIONS)
enabled
+ user-properties
+ max-allowed
For profile users only:
max-allowed-per-parent
icon-badge
diff --git a/core/tests/coretests/src/android/app/activity/LocalReceiver.java b/core/tests/coretests/src/android/app/activity/LocalReceiver.java
index 7f81339..5ac84f8 100644
--- a/core/tests/coretests/src/android/app/activity/LocalReceiver.java
+++ b/core/tests/coretests/src/android/app/activity/LocalReceiver.java
@@ -36,7 +36,8 @@
if (BroadcastTest.BROADCAST_FAIL_REGISTER.equals(intent.getAction())) {
resultString = "Successfully registered, but expected it to fail";
try {
- context.registerReceiver(this, new IntentFilter("foo.bar"));
+ context.registerReceiver(this, new IntentFilter("foo.bar"),
+ Context.RECEIVER_EXPORTED_UNAUDITED);
context.unregisterReceiver(this);
} catch (ReceiverCallNotAllowedException e) {
//resultString = "This is the correct behavior but not yet implemented";
diff --git a/core/tests/coretests/src/android/app/activity/ServiceTest.java b/core/tests/coretests/src/android/app/activity/ServiceTest.java
index c89f37d..3f3d6a3 100644
--- a/core/tests/coretests/src/android/app/activity/ServiceTest.java
+++ b/core/tests/coretests/src/android/app/activity/ServiceTest.java
@@ -172,7 +172,7 @@
pidResult.complete(intent.getIntExtra(EXTRA_PID, NOT_STARTED));
mContext.unregisterReceiver(this);
}
- }, new IntentFilter(ACTION_SERVICE_STARTED));
+ }, new IntentFilter(ACTION_SERVICE_STARTED), Context.RECEIVER_EXPORTED_UNAUDITED);
serviceTrigger.run();
try {
diff --git a/core/tests/coretests/src/android/os/VibratorTest.java b/core/tests/coretests/src/android/os/VibratorTest.java
index 7ebebc9..c59a3f5 100644
--- a/core/tests/coretests/src/android/os/VibratorTest.java
+++ b/core/tests/coretests/src/android/os/VibratorTest.java
@@ -246,10 +246,12 @@
@Test
public void getQFactorAndResonantFrequency_differentValues_returnsNaN() {
VibratorInfo firstVibrator = new VibratorInfo.Builder(/* id= */ 1)
+ .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
.setQFactor(1f)
.setFrequencyProfile(new VibratorInfo.FrequencyProfile(1, 1, 1, null))
.build();
VibratorInfo secondVibrator = new VibratorInfo.Builder(/* id= */ 2)
+ .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
.setQFactor(2f)
.setFrequencyProfile(new VibratorInfo.FrequencyProfile(2, 2, 2, null))
.build();
@@ -258,6 +260,7 @@
assertTrue(Float.isNaN(info.getQFactor()));
assertTrue(Float.isNaN(info.getResonantFrequencyHz()));
+ assertEmptyFrequencyProfileAndControl(info);
// One vibrator with values undefined.
VibratorInfo thirdVibrator = new VibratorInfo.Builder(/* id= */ 3).build();
@@ -266,16 +269,19 @@
assertTrue(Float.isNaN(info.getQFactor()));
assertTrue(Float.isNaN(info.getResonantFrequencyHz()));
+ assertEmptyFrequencyProfileAndControl(info);
}
@Test
public void getQFactorAndResonantFrequency_sameValues_returnsValue() {
VibratorInfo firstVibrator = new VibratorInfo.Builder(/* id= */ 1)
+ .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
.setQFactor(10f)
.setFrequencyProfile(new VibratorInfo.FrequencyProfile(
/* resonantFrequencyHz= */ 11, 10, 0.5f, null))
.build();
VibratorInfo secondVibrator = new VibratorInfo.Builder(/* id= */ 2)
+ .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
.setQFactor(10f)
.setFrequencyProfile(new VibratorInfo.FrequencyProfile(
/* resonantFrequencyHz= */ 11, 5, 1, null))
@@ -285,113 +291,131 @@
assertEquals(10f, info.getQFactor(), TEST_TOLERANCE);
assertEquals(11f, info.getResonantFrequencyHz(), TEST_TOLERANCE);
+
+ // No frequency range defined.
+ assertTrue(info.getFrequencyProfile().isEmpty());
+ assertEquals(false, info.hasCapability(IVibrator.CAP_FREQUENCY_CONTROL));
}
@Test
public void getFrequencyProfile_noVibrator_returnsEmpty() {
VibratorInfo info = new SystemVibrator.NoVibratorInfo();
- assertTrue(info.getFrequencyProfile().isEmpty());
+ assertEmptyFrequencyProfileAndControl(info);
}
@Test
public void getFrequencyProfile_differentResonantFrequencyOrResolutionValues_returnsEmpty() {
VibratorInfo firstVibrator = new VibratorInfo.Builder(/* id= */ 1)
+ .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
.setFrequencyProfile(new VibratorInfo.FrequencyProfile(1, 1, 1,
new float[] { 0, 1 }))
.build();
VibratorInfo differentResonantFrequency = new VibratorInfo.Builder(/* id= */ 2)
+ .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
.setFrequencyProfile(new VibratorInfo.FrequencyProfile(2, 1, 1,
new float[] { 0, 1 }))
.build();
VibratorInfo info = new SystemVibrator.MultiVibratorInfo(
new VibratorInfo[]{firstVibrator, differentResonantFrequency});
- assertTrue(info.getFrequencyProfile().isEmpty());
+ assertEmptyFrequencyProfileAndControl(info);
VibratorInfo differentFrequencyResolution = new VibratorInfo.Builder(/* id= */ 2)
+ .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
.setFrequencyProfile(new VibratorInfo.FrequencyProfile(1, 1, 2,
new float[] { 0, 1 }))
.build();
info = new SystemVibrator.MultiVibratorInfo(
new VibratorInfo[]{firstVibrator, differentFrequencyResolution});
- assertTrue(info.getFrequencyProfile().isEmpty());
+ assertEmptyFrequencyProfileAndControl(info);
}
@Test
public void getFrequencyProfile_missingValues_returnsEmpty() {
VibratorInfo firstVibrator = new VibratorInfo.Builder(/* id= */ 1)
+ .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
.setFrequencyProfile(new VibratorInfo.FrequencyProfile(1, 1, 1,
new float[] { 0, 1 }))
.build();
VibratorInfo missingResonantFrequency = new VibratorInfo.Builder(/* id= */ 2)
+ .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
.setFrequencyProfile(new VibratorInfo.FrequencyProfile(Float.NaN, 1, 1,
new float[] { 0, 1 }))
.build();
VibratorInfo info = new SystemVibrator.MultiVibratorInfo(
new VibratorInfo[]{firstVibrator, missingResonantFrequency});
- assertTrue(info.getFrequencyProfile().isEmpty());
+ assertEmptyFrequencyProfileAndControl(info);
VibratorInfo missingMinFrequency = new VibratorInfo.Builder(/* id= */ 2)
+ .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
.setFrequencyProfile(new VibratorInfo.FrequencyProfile(1, Float.NaN, 1,
new float[] { 0, 1 }))
.build();
info = new SystemVibrator.MultiVibratorInfo(
new VibratorInfo[]{firstVibrator, missingMinFrequency});
- assertTrue(info.getFrequencyProfile().isEmpty());
+ assertEmptyFrequencyProfileAndControl(info);
VibratorInfo missingFrequencyResolution = new VibratorInfo.Builder(/* id= */ 2)
+ .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
.setFrequencyProfile(new VibratorInfo.FrequencyProfile(1, 1, Float.NaN,
new float[] { 0, 1 }))
.build();
info = new SystemVibrator.MultiVibratorInfo(
new VibratorInfo[]{firstVibrator, missingFrequencyResolution});
- assertTrue(info.getFrequencyProfile().isEmpty());
+ assertEmptyFrequencyProfileAndControl(info);
VibratorInfo missingMaxAmplitudes = new VibratorInfo.Builder(/* id= */ 2)
+ .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
.setFrequencyProfile(new VibratorInfo.FrequencyProfile(1, 1, 1, null))
.build();
info = new SystemVibrator.MultiVibratorInfo(
new VibratorInfo[]{firstVibrator, missingMaxAmplitudes});
- assertTrue(info.getFrequencyProfile().isEmpty());
+ assertEmptyFrequencyProfileAndControl(info);
}
@Test
public void getFrequencyProfile_unalignedMaxAmplitudes_returnsEmpty() {
VibratorInfo firstVibrator = new VibratorInfo.Builder(/* id= */ 1)
+ .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
.setFrequencyProfile(new VibratorInfo.FrequencyProfile(11, 10, 0.5f,
new float[] { 0, 1, 1, 0 }))
.build();
VibratorInfo unalignedMinFrequency = new VibratorInfo.Builder(/* id= */ 2)
+ .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
.setFrequencyProfile(new VibratorInfo.FrequencyProfile(11, 10.1f, 0.5f,
new float[] { 0, 1, 1, 0 }))
.build();
VibratorInfo thirdVibrator = new VibratorInfo.Builder(/* id= */ 2)
+ .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
.setFrequencyProfile(new VibratorInfo.FrequencyProfile(11, 10.5f, 0.5f,
new float[] { 0, 1, 1, 0 }))
.build();
VibratorInfo info = new SystemVibrator.MultiVibratorInfo(
new VibratorInfo[]{firstVibrator, unalignedMinFrequency, thirdVibrator});
- assertTrue(info.getFrequencyProfile().isEmpty());
+ assertEmptyFrequencyProfileAndControl(info);
}
@Test
public void getFrequencyProfile_alignedProfiles_returnsIntersection() {
VibratorInfo firstVibrator = new VibratorInfo.Builder(/* id= */ 1)
+ .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
.setFrequencyProfile(new VibratorInfo.FrequencyProfile(11, 10, 0.5f,
new float[] { 0.5f, 1, 1, 0.5f }))
.build();
VibratorInfo secondVibrator = new VibratorInfo.Builder(/* id= */ 2)
+ .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
.setFrequencyProfile(new VibratorInfo.FrequencyProfile(11, 10.5f, 0.5f,
new float[] { 1, 1, 1 }))
.build();
VibratorInfo thirdVibrator = new VibratorInfo.Builder(/* id= */ 3)
+ .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
.setFrequencyProfile(new VibratorInfo.FrequencyProfile(11, 10.5f, 0.5f,
new float[] { 0.8f, 1, 0.8f, 0.5f }))
.build();
@@ -401,6 +425,20 @@
assertEquals(
new VibratorInfo.FrequencyProfile(11, 10.5f, 0.5f, new float[] { 0.8f, 1, 0.5f }),
info.getFrequencyProfile());
+ assertEquals(true, info.hasCapability(IVibrator.CAP_FREQUENCY_CONTROL));
+
+ // Third vibrator without frequency control capability.
+ thirdVibrator = new VibratorInfo.Builder(/* id= */ 3)
+ .setFrequencyProfile(new VibratorInfo.FrequencyProfile(11, 10.5f, 0.5f,
+ new float[] { 0.8f, 1, 0.8f, 0.5f }))
+ .build();
+ info = new SystemVibrator.MultiVibratorInfo(
+ new VibratorInfo[]{firstVibrator, secondVibrator, thirdVibrator});
+
+ assertEquals(
+ new VibratorInfo.FrequencyProfile(11, 10.5f, 0.5f, new float[] { 0.8f, 1, 0.5f }),
+ info.getFrequencyProfile());
+ assertEquals(false, info.hasCapability(IVibrator.CAP_FREQUENCY_CONTROL));
}
@Test
@@ -547,4 +585,12 @@
VibrationAttributes vibrationAttributes = captor.getValue();
assertEquals(new VibrationAttributes.Builder().build(), vibrationAttributes);
}
+
+ /**
+ * Asserts that the frequency profile is empty, and therefore frequency control isn't supported.
+ */
+ void assertEmptyFrequencyProfileAndControl(VibratorInfo info) {
+ assertTrue(info.getFrequencyProfile().isEmpty());
+ assertEquals(false, info.hasCapability(IVibrator.CAP_FREQUENCY_CONTROL));
+ }
}
diff --git a/core/tests/coretests/src/android/view/ImeInsetsSourceConsumerTest.java b/core/tests/coretests/src/android/view/ImeInsetsSourceConsumerTest.java
index ddcb175..0bf133f 100644
--- a/core/tests/coretests/src/android/view/ImeInsetsSourceConsumerTest.java
+++ b/core/tests/coretests/src/android/view/ImeInsetsSourceConsumerTest.java
@@ -24,6 +24,7 @@
import static junit.framework.Assert.assertFalse;
import static junit.framework.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
@@ -98,12 +99,12 @@
InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
// test if setVisibility can show IME
mImeConsumer.onWindowFocusGained(true);
- mController.show(WindowInsets.Type.ime(), true /* fromIme */);
+ mController.show(WindowInsets.Type.ime(), true /* fromIme */, null /* statsToken */);
mController.cancelExistingAnimations();
assertTrue((mController.getRequestedVisibleTypes() & WindowInsets.Type.ime()) != 0);
// test if setVisibility can hide IME
- mController.hide(WindowInsets.Type.ime(), true /* fromIme */);
+ mController.hide(WindowInsets.Type.ime(), true /* fromIme */, null /* statsToken */);
mController.cancelExistingAnimations();
assertFalse((mController.getRequestedVisibleTypes() & WindowInsets.Type.ime()) != 0);
});
@@ -117,7 +118,7 @@
InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
// Request IME visible before control is available.
mImeConsumer.onWindowFocusGained(true);
- mController.show(WindowInsets.Type.ime(), true /* fromIme */);
+ mController.show(WindowInsets.Type.ime(), true /* fromIme */, null /* statsToken */);
// set control and verify visibility is applied.
InsetsSourceControl control =
@@ -125,9 +126,11 @@
mController.onControlsChanged(new InsetsSourceControl[] { control });
// IME show animation should be triggered when control becomes available.
verify(mController).applyAnimation(
- eq(WindowInsets.Type.ime()), eq(true) /* show */, eq(true) /* fromIme */);
+ eq(WindowInsets.Type.ime()), eq(true) /* show */, eq(true) /* fromIme */,
+ any() /* statsToken */);
verify(mController, never()).applyAnimation(
- eq(WindowInsets.Type.ime()), eq(false) /* show */, eq(true) /* fromIme */);
+ eq(WindowInsets.Type.ime()), eq(false) /* show */, eq(true) /* fromIme */,
+ any() /* statsToken */);
});
}
@@ -153,7 +156,8 @@
mImeConsumer.onWindowFocusGained(hasWindowFocus);
final boolean imeVisible = hasWindowFocus && hasViewFocus;
if (imeVisible) {
- mController.show(WindowInsets.Type.ime(), true /* fromIme */);
+ mController.show(WindowInsets.Type.ime(), true /* fromIme */,
+ null /* statsToken */);
}
// set control and verify visibility is applied.
@@ -169,20 +173,21 @@
verify(control).getAndClearSkipAnimationOnce();
verify(mController).applyAnimation(eq(WindowInsets.Type.ime()),
eq(true) /* show */, eq(false) /* fromIme */,
- eq(expectSkipAnim) /* skipAnim */);
+ eq(expectSkipAnim) /* skipAnim */, null /* statsToken */);
}
// If previously hasViewFocus is false, verify when requesting the IME visible next
// time will not skip animation.
if (!hasViewFocus) {
- mController.show(WindowInsets.Type.ime(), true);
+ mController.show(WindowInsets.Type.ime(), true /* fromIme */,
+ null /* statsToken */);
mController.onControlsChanged(new InsetsSourceControl[]{ control });
// Verify IME show animation should be triggered when control becomes available and
// the animation will be skipped by getAndClearSkipAnimationOnce invoked.
verify(control).getAndClearSkipAnimationOnce();
verify(mController).applyAnimation(eq(WindowInsets.Type.ime()),
eq(true) /* show */, eq(true) /* fromIme */,
- eq(false) /* skipAnim */);
+ eq(false) /* skipAnim */, null /* statsToken */);
}
});
}
diff --git a/core/tests/coretests/src/android/view/InsetsAnimationControlImplTest.java b/core/tests/coretests/src/android/view/InsetsAnimationControlImplTest.java
index e9cd8ad..c88255e 100644
--- a/core/tests/coretests/src/android/view/InsetsAnimationControlImplTest.java
+++ b/core/tests/coretests/src/android/view/InsetsAnimationControlImplTest.java
@@ -109,7 +109,8 @@
mController = new InsetsAnimationControlImpl(controls,
new Rect(0, 0, 500, 500), mInsetsState, mMockListener, systemBars(),
mMockController, 10 /* durationMs */, new LinearInterpolator(),
- 0 /* animationType */, 0 /* layoutInsetsDuringAnimation */, null /* translator */);
+ 0 /* animationType */, 0 /* layoutInsetsDuringAnimation */, null /* translator */,
+ null /* statsToken */);
mController.setReadyDispatched(true);
}
diff --git a/core/tests/coretests/src/android/view/InsetsControllerTest.java b/core/tests/coretests/src/android/view/InsetsControllerTest.java
index 409bae8..c6fa778 100644
--- a/core/tests/coretests/src/android/view/InsetsControllerTest.java
+++ b/core/tests/coretests/src/android/view/InsetsControllerTest.java
@@ -248,7 +248,7 @@
mController.setSystemDrivenInsetsAnimationLoggingListener(loggingListener);
mController.getSourceConsumer(ITYPE_IME).onWindowFocusGained(true);
// since there is no focused view, forcefully make IME visible.
- mController.show(ime(), true /* fromIme */);
+ mController.show(WindowInsets.Type.ime(), true /* fromIme */, null /* statsToken */);
verify(loggingListener).onReady(notNull(), anyInt());
});
}
@@ -260,14 +260,14 @@
InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
mController.getSourceConsumer(ITYPE_IME).onWindowFocusGained(true);
// since there is no focused view, forcefully make IME visible.
- mController.show(ime(), true /* fromIme */);
+ mController.show(WindowInsets.Type.ime(), true /* fromIme */, null /* statsToken */);
mController.show(all());
// quickly jump to final state by cancelling it.
mController.cancelExistingAnimations();
final @InsetsType int types = navigationBars() | statusBars() | ime();
assertEquals(types, mController.getRequestedVisibleTypes() & types);
- mController.hide(ime(), true /* fromIme */);
+ mController.hide(ime(), true /* fromIme */, null /* statsToken */);
mController.hide(all());
mController.cancelExistingAnimations();
assertEquals(0, mController.getRequestedVisibleTypes() & types);
@@ -282,10 +282,10 @@
mController.onControlsChanged(new InsetsSourceControl[] { ime });
InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
mController.getSourceConsumer(ITYPE_IME).onWindowFocusGained(true);
- mController.show(ime(), true /* fromIme */);
+ mController.show(WindowInsets.Type.ime(), true /* fromIme */, null /* statsToken */);
mController.cancelExistingAnimations();
assertTrue(isRequestedVisible(mController, ime()));
- mController.hide(ime(), true /* fromIme */);
+ mController.hide(ime(), true /* fromIme */, null /* statsToken */);
mController.cancelExistingAnimations();
assertFalse(isRequestedVisible(mController, ime()));
mController.getSourceConsumer(ITYPE_IME).onWindowFocusLost();
@@ -452,7 +452,7 @@
assertFalse(mController.getState().getSource(ITYPE_IME).isVisible());
// Pretend IME is calling
- mController.show(ime(), true /* fromIme */);
+ mController.show(ime(), true /* fromIme */, null /* statsToken */);
// Gaining control shortly after
mController.onControlsChanged(createSingletonControl(ITYPE_IME));
@@ -476,7 +476,7 @@
mController.onControlsChanged(createSingletonControl(ITYPE_IME));
// Pretend IME is calling
- mController.show(ime(), true /* fromIme */);
+ mController.show(ime(), true /* fromIme */, null /* statsToken */);
assertEquals(ANIMATION_TYPE_SHOW, mController.getAnimationType(ime()));
mController.cancelExistingAnimations();
@@ -554,7 +554,7 @@
verify(listener, never()).onReady(any(), anyInt());
// Pretend that IME is calling.
- mController.show(ime(), true);
+ mController.show(ime(), true /* fromIme */, null /* statsToken */);
// Ready gets deferred until next predraw
mViewRoot.getView().getViewTreeObserver().dispatchOnPreDraw();
@@ -638,7 +638,7 @@
mController.onControlsChanged(createSingletonControl(ITYPE_IME));
// Pretend IME is calling
- mController.show(ime(), true /* fromIme */);
+ mController.show(ime(), true /* fromIme */, null /* statsToken */);
InsetsState copy = new InsetsState(mController.getState(), true /* copySources */);
copy.getSource(ITYPE_IME).setFrame(0, 1, 2, 3);
@@ -845,7 +845,7 @@
// Showing invisible ime should only causes insets change once.
clearInvocations(mTestHost);
- mController.show(ime(), true /* fromIme */);
+ mController.show(ime(), true /* fromIme */, null /* statsToken */);
verify(mTestHost, times(1)).notifyInsetsChanged();
// Sending the same insets state should not cause insets change.
diff --git a/graphics/java/android/graphics/fonts/FontStyle.java b/graphics/java/android/graphics/fonts/FontStyle.java
index 09799fd..48969aa 100644
--- a/graphics/java/android/graphics/fonts/FontStyle.java
+++ b/graphics/java/android/graphics/fonts/FontStyle.java
@@ -48,6 +48,10 @@
private static final String TAG = "FontStyle";
/**
+ * A default value when font weight is unspecified
+ */
+ public static final int FONT_WEIGHT_UNSPECIFIED = -1;
+ /**
* A minimum weight value for the font
*/
public static final int FONT_WEIGHT_MIN = 1;
diff --git a/libs/WindowManager/Shell/res/values/config.xml b/libs/WindowManager/Shell/res/values/config.xml
index df5f921..c6197c8 100644
--- a/libs/WindowManager/Shell/res/values/config.xml
+++ b/libs/WindowManager/Shell/res/values/config.xml
@@ -111,4 +111,8 @@
<!-- Whether to dim a split-screen task when the other is the IME target -->
<bool name="config_dimNonImeAttachedSide">true</bool>
+
+ <!-- Components support to launch multiple instances into split-screen -->
+ <string-array name="config_componentsSupportMultiInstancesSplit">
+ </string-array>
</resources>
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java
index dec1e38..065fd95 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java
@@ -16,14 +16,12 @@
package com.android.wm.shell;
-import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
-import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE;
import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_TASK_ORG;
import static com.android.wm.shell.transition.Transitions.ENABLE_SHELL_TRANSITIONS;
@@ -49,7 +47,6 @@
import android.window.StartingWindowRemovalInfo;
import android.window.TaskAppearedInfo;
import android.window.TaskOrganizer;
-import android.window.WindowContainerTransaction;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.protolog.common.ProtoLog;
@@ -568,6 +565,22 @@
}
}
+ /**
+ * Return list of {@link RunningTaskInfo}s for the given display.
+ *
+ * @return filtered list of tasks or empty list
+ */
+ public ArrayList<RunningTaskInfo> getRunningTasks(int displayId) {
+ ArrayList<RunningTaskInfo> result = new ArrayList<>();
+ for (int i = 0; i < mTasks.size(); i++) {
+ RunningTaskInfo taskInfo = mTasks.valueAt(i).getTaskInfo();
+ if (taskInfo.displayId == displayId) {
+ result.add(taskInfo);
+ }
+ }
+ return result;
+ }
+
/** Gets running task by taskId. Returns {@code null} if no such task observed. */
@Nullable
public RunningTaskInfo getRunningTaskInfo(int taskId) {
@@ -694,57 +707,6 @@
taskListener.reparentChildSurfaceToTask(taskId, sc, t);
}
- /**
- * Create a {@link WindowContainerTransaction} to clear task bounds.
- *
- * Only affects tasks that have {@link RunningTaskInfo#getActivityType()} set to
- * {@link WindowConfiguration#ACTIVITY_TYPE_STANDARD}.
- *
- * @param displayId display id for tasks that will have bounds cleared
- * @return {@link WindowContainerTransaction} with pending operations to clear bounds
- */
- public WindowContainerTransaction prepareClearBoundsForStandardTasks(int displayId) {
- ProtoLog.d(WM_SHELL_DESKTOP_MODE, "prepareClearBoundsForTasks: displayId=%d", displayId);
- WindowContainerTransaction wct = new WindowContainerTransaction();
- for (int i = 0; i < mTasks.size(); i++) {
- RunningTaskInfo taskInfo = mTasks.valueAt(i).getTaskInfo();
- if ((taskInfo.displayId == displayId) && (taskInfo.getActivityType()
- == WindowConfiguration.ACTIVITY_TYPE_STANDARD)) {
- ProtoLog.d(WM_SHELL_DESKTOP_MODE, "clearing bounds for token=%s taskInfo=%s",
- taskInfo.token, taskInfo);
- wct.setBounds(taskInfo.token, null);
- }
- }
- return wct;
- }
-
- /**
- * Create a {@link WindowContainerTransaction} to clear task level freeform setting.
- *
- * Only affects tasks that have {@link RunningTaskInfo#getActivityType()} set to
- * {@link WindowConfiguration#ACTIVITY_TYPE_STANDARD}.
- *
- * @param displayId display id for tasks that will have windowing mode reset to {@link
- * WindowConfiguration#WINDOWING_MODE_UNDEFINED}
- * @return {@link WindowContainerTransaction} with pending operations to clear windowing mode
- */
- public WindowContainerTransaction prepareClearFreeformForStandardTasks(int displayId) {
- ProtoLog.d(WM_SHELL_DESKTOP_MODE, "prepareClearFreeformForTasks: displayId=%d", displayId);
- WindowContainerTransaction wct = new WindowContainerTransaction();
- for (int i = 0; i < mTasks.size(); i++) {
- RunningTaskInfo taskInfo = mTasks.valueAt(i).getTaskInfo();
- if (taskInfo.displayId == displayId
- && taskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM
- && taskInfo.getActivityType() == ACTIVITY_TYPE_STANDARD) {
- ProtoLog.d(WM_SHELL_DESKTOP_MODE,
- "clearing windowing mode for token=%s taskInfo=%s", taskInfo.token,
- taskInfo);
- wct.setWindowingMode(taskInfo.token, WINDOWING_MODE_UNDEFINED);
- }
- }
- return wct;
- }
-
private void logSizeCompatRestartButtonEventReported(@NonNull TaskAppearedInfo info,
int event) {
ActivityInfo topActivityInfo = info.getTaskInfo().topActivityInfo;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java
index b6327e5..d9eaeee 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java
@@ -38,7 +38,6 @@
import android.provider.Settings.Global;
import android.util.Log;
import android.util.SparseArray;
-import android.view.IRemoteAnimationFinishedCallback;
import android.view.IRemoteAnimationRunner;
import android.view.IWindowFocusObserver;
import android.view.InputDevice;
@@ -69,7 +68,7 @@
* Controls the window animation run when a user initiates a back gesture.
*/
public class BackAnimationController implements RemoteCallable<BackAnimationController> {
- private static final String TAG = "BackAnimationController";
+ private static final String TAG = "ShellBackPreview";
private static final int SETTING_VALUE_OFF = 0;
private static final int SETTING_VALUE_ON = 1;
public static final boolean IS_ENABLED =
@@ -82,16 +81,15 @@
/** Predictive back animation developer option */
private final AtomicBoolean mEnableAnimations = new AtomicBoolean(false);
/**
- * Max duration to wait for a transition to finish before accepting another gesture start
- * request.
+ * Max duration to wait for an animation to finish before triggering the real back.
*/
- private static final long MAX_TRANSITION_DURATION = 2000;
+ private static final long MAX_ANIMATION_DURATION = 2000;
/** True when a back gesture is ongoing */
private boolean mBackGestureStarted = false;
- /** Tracks if an uninterruptible transition is in progress */
- private boolean mTransitionInProgress = false;
+ /** Tracks if an uninterruptible animation is in progress */
+ private boolean mPostCommitAnimationInProgress = false;
/** Tracks if we should start the back gesture on the next motion move event */
private boolean mShouldStartOnNextMoveEvent = false;
/** @see #setTriggerBack(boolean) */
@@ -105,9 +103,9 @@
private final ShellController mShellController;
private final ShellExecutor mShellExecutor;
private final Handler mBgHandler;
- private final Runnable mResetTransitionRunnable = () -> {
- ProtoLog.w(WM_SHELL_BACK_PREVIEW, "Transition didn't finish in %d ms. Resetting...",
- MAX_TRANSITION_DURATION);
+ private final Runnable mAnimationTimeoutRunnable = () -> {
+ ProtoLog.w(WM_SHELL_BACK_PREVIEW, "Animation didn't finish in %d ms. Resetting...",
+ MAX_ANIMATION_DURATION);
onBackAnimationFinished();
};
@@ -119,6 +117,8 @@
private final SparseArray<BackAnimationRunner> mAnimationDefinition = new SparseArray<>();
+ private IOnBackInvokedCallback mActiveCallback;
+
@VisibleForTesting
final IWindowFocusObserver mFocusObserver = new IWindowFocusObserver.Stub() {
@Override
@@ -126,9 +126,9 @@
@Override
public void focusLost(IBinder inputToken) {
mShellExecutor.execute(() -> {
- if (!mBackGestureStarted || mTransitionInProgress) {
- // If an uninterruptible transition is already in progress, we should ignore
- // this due to the transition may cause focus lost. (alpha = 0)
+ if (!mBackGestureStarted || mPostCommitAnimationInProgress) {
+ // If an uninterruptible animation is already in progress, we should ignore
+ // this due to it may cause focus lost. (alpha = 0)
return;
}
ProtoLog.i(WM_SHELL_BACK_PREVIEW, "Target window lost focus.");
@@ -180,26 +180,11 @@
}
private void initBackAnimationRunners() {
- final IOnBackInvokedCallback dummyCallback = new IOnBackInvokedCallback.Default();
- final IRemoteAnimationRunner dummyRunner = new IRemoteAnimationRunner.Default() {
- @Override
- public void onAnimationStart(int transit, RemoteAnimationTarget[] apps,
- RemoteAnimationTarget[] wallpapers, RemoteAnimationTarget[] nonApps,
- IRemoteAnimationFinishedCallback finishedCallback) throws RemoteException {
- // Animation missing. Simply finish animation.
- finishedCallback.onAnimationFinished();
- }
- };
-
- final BackAnimationRunner dummyBackRunner =
- new BackAnimationRunner(dummyCallback, dummyRunner);
final CrossTaskBackAnimation crossTaskAnimation = new CrossTaskBackAnimation(mContext);
mAnimationDefinition.set(BackNavigationInfo.TYPE_CROSS_TASK,
new BackAnimationRunner(crossTaskAnimation.mCallback, crossTaskAnimation.mRunner));
// TODO (238474994): register cross activity animation when it's completed.
- mAnimationDefinition.set(BackNavigationInfo.TYPE_CROSS_ACTIVITY, dummyBackRunner);
// TODO (236760237): register dialog close animation when it's completed.
- mAnimationDefinition.set(BackNavigationInfo.TYPE_DIALOG_CLOSE, dummyBackRunner);
}
private void setupAnimationDeveloperSettingsObserver(
@@ -222,10 +207,9 @@
private void updateEnableAnimationFromSetting() {
int settingValue = Global.getInt(mContext.getContentResolver(),
Global.ENABLE_BACK_ANIMATION, SETTING_VALUE_OFF);
- boolean isEnabled = settingValue == SETTING_VALUE_ON;
+ boolean isEnabled = settingValue == SETTING_VALUE_ON && IS_U_ANIMATION_ENABLED;
mEnableAnimations.set(isEnabled);
- ProtoLog.d(WM_SHELL_BACK_PREVIEW, "Back animation enabled=%s",
- isEnabled);
+ ProtoLog.d(WM_SHELL_BACK_PREVIEW, "Back animation enabled=%s", isEnabled);
}
public BackAnimation getBackAnimationImpl() {
@@ -279,7 +263,9 @@
public void setBackToLauncherCallback(IOnBackInvokedCallback callback,
IRemoteAnimationRunner runner) {
executeRemoteCallWithTaskPermission(mController, "setBackToLauncherCallback",
- (controller) -> controller.setBackToLauncherCallback(callback, runner));
+ (controller) -> controller.registerAnimation(
+ BackNavigationInfo.TYPE_RETURN_TO_HOME,
+ new BackAnimationRunner(callback, runner)));
}
@Override
@@ -294,44 +280,22 @@
}
}
- @VisibleForTesting
- void setBackToLauncherCallback(IOnBackInvokedCallback callback, IRemoteAnimationRunner runner) {
- mAnimationDefinition.set(BackNavigationInfo.TYPE_RETURN_TO_HOME,
- new BackAnimationRunner(callback, runner));
+ void registerAnimation(@BackNavigationInfo.BackTargetType int type,
+ @NonNull BackAnimationRunner runner) {
+ mAnimationDefinition.set(type, runner);
}
private void clearBackToLauncherCallback() {
mAnimationDefinition.remove(BackNavigationInfo.TYPE_RETURN_TO_HOME);
}
- @VisibleForTesting
- void onBackAnimationFinished() {
- if (!mTransitionInProgress) {
- return;
- }
-
- ProtoLog.d(WM_SHELL_BACK_PREVIEW, "BackAnimationController: onBackAnimationFinished()");
-
- // Trigger real back.
- if (mBackNavigationInfo != null) {
- IOnBackInvokedCallback callback = mBackNavigationInfo.getOnBackInvokedCallback();
- if (mTriggerBack) {
- dispatchOnBackInvoked(callback);
- } else {
- dispatchOnBackCancelled(callback);
- }
- }
-
- finishBackNavigation();
- }
-
/**
* Called when a new motion event needs to be transferred to this
* {@link BackAnimationController}
*/
public void onMotionEvent(float touchX, float touchY, int keyAction,
@BackEvent.SwipeEdge int swipeEdge) {
- if (mTransitionInProgress) {
+ if (mPostCommitAnimationInProgress) {
return;
}
@@ -348,7 +312,7 @@
onGestureStarted(touchX, touchY, swipeEdge);
mShouldStartOnNextMoveEvent = false;
}
- onMove(touchX, touchY, swipeEdge);
+ onMove();
} else if (keyAction == MotionEvent.ACTION_UP || keyAction == MotionEvent.ACTION_CANCEL) {
ProtoLog.d(WM_SHELL_BACK_PREVIEW,
"Finishing gesture with event action: %d", keyAction);
@@ -386,30 +350,22 @@
return;
}
final int backType = backNavigationInfo.getType();
- final IOnBackInvokedCallback targetCallback;
final boolean shouldDispatchToAnimator = shouldDispatchToAnimator(backType);
if (shouldDispatchToAnimator) {
+ mActiveCallback = mAnimationDefinition.get(backType).getCallback();
mAnimationDefinition.get(backType).startGesture();
} else {
- targetCallback = mBackNavigationInfo.getOnBackInvokedCallback();
- dispatchOnBackStarted(targetCallback, mTouchTracker.createStartEvent(null));
+ mActiveCallback = mBackNavigationInfo.getOnBackInvokedCallback();
+ dispatchOnBackStarted(mActiveCallback, mTouchTracker.createStartEvent(null));
}
}
- private void onMove(float touchX, float touchY, @BackEvent.SwipeEdge int swipeEdge) {
+ private void onMove() {
if (!mBackGestureStarted || mBackNavigationInfo == null || !mEnableAnimations.get()) {
return;
}
final BackEvent backEvent = mTouchTracker.createProgressEvent();
-
- int backType = mBackNavigationInfo.getType();
- IOnBackInvokedCallback targetCallback;
- if (shouldDispatchToAnimator(backType)) {
- targetCallback = mAnimationDefinition.get(backType).getCallback();
- } else {
- targetCallback = mBackNavigationInfo.getOnBackInvokedCallback();
- }
- dispatchOnBackProgressed(targetCallback, backEvent);
+ dispatchOnBackProgressed(mActiveCallback, backEvent);
}
private void injectBackKey() {
@@ -431,57 +387,6 @@
}
}
- private void onGestureFinished(boolean fromTouch) {
- ProtoLog.d(WM_SHELL_BACK_PREVIEW, "onGestureFinished() mTriggerBack == %s", mTriggerBack);
- if (!mBackGestureStarted) {
- finishBackNavigation();
- return;
- }
-
- if (fromTouch) {
- // Let touch reset the flag otherwise it will start a new back navigation and refresh
- // the info when received a new move event.
- mBackGestureStarted = false;
- }
-
- if (mTransitionInProgress) {
- return;
- }
-
- if (mBackNavigationInfo == null) {
- // No focus window found or core are running recents animation, inject back key as
- // legacy behavior.
- if (mTriggerBack) {
- injectBackKey();
- }
- finishBackNavigation();
- return;
- }
-
- int backType = mBackNavigationInfo.getType();
- boolean shouldDispatchToAnimator = shouldDispatchToAnimator(backType);
- final BackAnimationRunner runner = mAnimationDefinition.get(backType);
- IOnBackInvokedCallback targetCallback = shouldDispatchToAnimator
- ? runner.getCallback() : mBackNavigationInfo.getOnBackInvokedCallback();
-
- if (shouldDispatchToAnimator) {
- if (runner.onGestureFinished(mTriggerBack)) {
- Log.w(TAG, "Gesture released, but animation didn't ready.");
- return;
- }
- startTransition();
- }
- if (mTriggerBack) {
- dispatchOnBackInvoked(targetCallback);
- } else {
- dispatchOnBackCancelled(targetCallback);
- }
- if (!shouldDispatchToAnimator) {
- // Animation callback missing. Simply finish animation.
- finishBackNavigation();
- }
- }
-
private boolean shouldDispatchToAnimator(int backType) {
return mEnableAnimations.get()
&& mBackNavigationInfo != null
@@ -495,7 +400,7 @@
return;
}
try {
- if (shouldDispatchAnimation(callback)) {
+ if (mEnableAnimations.get()) {
callback.onBackStarted(backEvent);
}
} catch (RemoteException e) {
@@ -519,7 +424,7 @@
return;
}
try {
- if (shouldDispatchAnimation(callback)) {
+ if (mEnableAnimations.get()) {
callback.onBackCancelled();
}
} catch (RemoteException e) {
@@ -533,7 +438,7 @@
return;
}
try {
- if (shouldDispatchAnimation(callback)) {
+ if (mEnableAnimations.get()) {
callback.onBackProgressed(backEvent);
}
} catch (RemoteException e) {
@@ -541,17 +446,11 @@
}
}
- private boolean shouldDispatchAnimation(IOnBackInvokedCallback callback) {
- return (IS_U_ANIMATION_ENABLED || callback == mAnimationDefinition.get(
- BackNavigationInfo.TYPE_RETURN_TO_HOME).getCallback())
- && mEnableAnimations.get();
- }
-
/**
* Sets to true when the back gesture has passed the triggering threshold, false otherwise.
*/
public void setTriggerBack(boolean triggerBack) {
- if (mTransitionInProgress) {
+ if (mPostCommitAnimationInProgress) {
return;
}
mTriggerBack = triggerBack;
@@ -562,6 +461,109 @@
mTouchTracker.setProgressThreshold(progressThreshold);
}
+ /**
+ * Called when the gesture is released, then it could start the post commit animation.
+ */
+ private void onGestureFinished(boolean fromTouch) {
+ ProtoLog.d(WM_SHELL_BACK_PREVIEW, "onGestureFinished() mTriggerBack == %s", mTriggerBack);
+ if (!mBackGestureStarted) {
+ finishBackNavigation();
+ return;
+ }
+
+ if (fromTouch) {
+ // Let touch reset the flag otherwise it will start a new back navigation and refresh
+ // the info when received a new move event.
+ mBackGestureStarted = false;
+ }
+
+ if (mPostCommitAnimationInProgress) {
+ ProtoLog.w(WM_SHELL_BACK_PREVIEW, "Animation is still running");
+ return;
+ }
+
+ if (mBackNavigationInfo == null) {
+ // No focus window found or core are running recents animation, inject back key as
+ // legacy behavior.
+ if (mTriggerBack) {
+ injectBackKey();
+ }
+ finishBackNavigation();
+ return;
+ }
+
+ final int backType = mBackNavigationInfo.getType();
+ // Directly finish back navigation if no animator defined.
+ if (!shouldDispatchToAnimator(backType)) {
+ if (mTriggerBack) {
+ dispatchOnBackInvoked(mActiveCallback);
+ } else {
+ dispatchOnBackCancelled(mActiveCallback);
+ }
+ // Animation missing. Simply finish back navigation.
+ finishBackNavigation();
+ return;
+ }
+
+ final BackAnimationRunner runner = mAnimationDefinition.get(backType);
+ if (runner.isWaitingAnimation()) {
+ ProtoLog.w(WM_SHELL_BACK_PREVIEW, "Gesture released, but animation didn't ready.");
+ return;
+ }
+ startPostCommitAnimation();
+ }
+
+ /**
+ * Start the phase 2 animation when gesture is released.
+ * Callback to {@link #onBackAnimationFinished} when it is finished or timeout.
+ */
+ private void startPostCommitAnimation() {
+ if (mPostCommitAnimationInProgress) {
+ return;
+ }
+ ProtoLog.d(WM_SHELL_BACK_PREVIEW, "BackAnimationController: startPostCommitAnimation()");
+ mPostCommitAnimationInProgress = true;
+ mShellExecutor.executeDelayed(mAnimationTimeoutRunnable, MAX_ANIMATION_DURATION);
+
+ // The next callback should be {@link #onBackAnimationFinished}.
+ if (mTriggerBack) {
+ dispatchOnBackInvoked(mActiveCallback);
+ } else {
+ dispatchOnBackCancelled(mActiveCallback);
+ }
+ }
+
+ /**
+ * Called when the post commit animation is completed or timeout.
+ * This will trigger the real {@link IOnBackInvokedCallback} behavior.
+ */
+ @VisibleForTesting
+ void onBackAnimationFinished() {
+ if (!mPostCommitAnimationInProgress) {
+ return;
+ }
+ // Stop timeout runner.
+ mShellExecutor.removeCallbacks(mAnimationTimeoutRunnable);
+ mPostCommitAnimationInProgress = false;
+
+ ProtoLog.d(WM_SHELL_BACK_PREVIEW, "BackAnimationController: onBackAnimationFinished()");
+
+ // Trigger the real back.
+ if (mBackNavigationInfo != null) {
+ IOnBackInvokedCallback callback = mBackNavigationInfo.getOnBackInvokedCallback();
+ if (mTriggerBack) {
+ dispatchOnBackInvoked(callback);
+ } else {
+ dispatchOnBackCancelled(callback);
+ }
+ }
+
+ finishBackNavigation();
+ }
+
+ /**
+ * This should be called after the whole back navigation is completed.
+ */
@VisibleForTesting
void finishBackNavigation() {
ProtoLog.d(WM_SHELL_BACK_PREVIEW, "BackAnimationController: finishBackNavigation()");
@@ -571,10 +573,10 @@
mTriggerBack = false;
mShouldStartOnNextMoveEvent = false;
mTouchTracker.reset();
+ mActiveCallback = null;
if (backNavigationInfo == null) {
return;
}
- stopTransition();
if (mBackAnimationFinishedCallback != null) {
try {
mBackAnimationFinishedCallback.onAnimationFinished(triggerBack);
@@ -586,19 +588,6 @@
backNavigationInfo.onBackNavigationFinished(triggerBack);
}
- private void startTransition() {
- if (mTransitionInProgress) {
- return;
- }
- mTransitionInProgress = true;
- mShellExecutor.executeDelayed(mResetTransitionRunnable, MAX_TRANSITION_DURATION);
- }
-
- void stopTransition() {
- mShellExecutor.removeCallbacks(mResetTransitionRunnable);
- mTransitionInProgress = false;
- }
-
private void createAdapter() {
IBackAnimationRunner runner = new IBackAnimationRunner.Stub() {
@Override
@@ -624,20 +613,18 @@
ProtoLog.d(WM_SHELL_BACK_PREVIEW, "BackAnimationController: startAnimation()");
runner.startAnimation(apps, wallpapers, nonApps,
BackAnimationController.this::onBackAnimationFinished);
+
if (apps.length >= 1) {
- final int backType = mBackNavigationInfo.getType();
- IOnBackInvokedCallback targetCallback = mAnimationDefinition.get(backType)
- .getCallback();
dispatchOnBackStarted(
- targetCallback, mTouchTracker.createStartEvent(apps[0]));
+ mActiveCallback, mTouchTracker.createStartEvent(apps[0]));
}
if (!mBackGestureStarted) {
// if the down -> up gesture happened before animation start, we have to
// trigger the uninterruptible transition to finish the back animation.
- final BackEvent backFinish = mTouchTracker.createProgressEvent(1);
- startTransition();
- runner.consumeIfGestureFinished(backFinish);
+ final BackEvent backFinish = mTouchTracker.createProgressEvent();
+ dispatchOnBackProgressed(mActiveCallback, backFinish);
+ startPostCommitAnimation();
}
});
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationRunner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationRunner.java
index c53fcfc..d70b8f5 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationRunner.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationRunner.java
@@ -18,12 +18,12 @@
import static android.view.WindowManager.TRANSIT_OLD_UNSET;
+import android.annotation.NonNull;
import android.os.RemoteException;
import android.util.Log;
import android.view.IRemoteAnimationFinishedCallback;
import android.view.IRemoteAnimationRunner;
import android.view.RemoteAnimationTarget;
-import android.window.BackEvent;
import android.window.IBackAnimationRunner;
import android.window.IOnBackInvokedCallback;
@@ -38,11 +38,11 @@
private final IOnBackInvokedCallback mCallback;
private final IRemoteAnimationRunner mRunner;
- private boolean mTriggerBack;
// Whether we are waiting to receive onAnimationStart
private boolean mWaitingAnimation;
- BackAnimationRunner(IOnBackInvokedCallback callback, IRemoteAnimationRunner runner) {
+ BackAnimationRunner(@NonNull IOnBackInvokedCallback callback,
+ @NonNull IRemoteAnimationRunner runner) {
mCallback = callback;
mRunner = runner;
}
@@ -83,25 +83,7 @@
mWaitingAnimation = true;
}
- boolean onGestureFinished(boolean triggerBack) {
- if (mWaitingAnimation) {
- mTriggerBack = triggerBack;
- return true;
- }
- return false;
- }
-
- void consumeIfGestureFinished(final BackEvent backFinish) {
- Log.d(TAG, "Start transition due to gesture is finished");
- try {
- mCallback.onBackProgressed(backFinish);
- if (mTriggerBack) {
- mCallback.onBackInvoked();
- } else {
- mCallback.onBackCancelled();
- }
- } catch (RemoteException e) {
- Log.e(TAG, "dispatch error: ", e);
- }
+ boolean isWaitingAnimation() {
+ return mWaitingAnimation;
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
index a400555..1fd91de 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
@@ -709,7 +709,7 @@
IntentFilter filter = new IntentFilter();
filter.addAction(Intent.ACTION_CLOSE_SYSTEM_DIALOGS);
filter.addAction(Intent.ACTION_SCREEN_OFF);
- mContext.registerReceiver(mBroadcastReceiver, filter);
+ mContext.registerReceiver(mBroadcastReceiver, filter, Context.RECEIVER_EXPORTED);
}
private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java
index bb7c4134..d9b4f47 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java
@@ -20,6 +20,7 @@
import android.animation.AnimatorListenerAdapter;
import android.animation.ValueAnimator;
import android.annotation.IntDef;
+import android.annotation.Nullable;
import android.content.ComponentName;
import android.content.res.Configuration;
import android.graphics.Point;
@@ -38,6 +39,7 @@
import android.view.WindowInsets.Type.InsetsType;
import android.view.animation.Interpolator;
import android.view.animation.PathInterpolator;
+import android.view.inputmethod.ImeTracker;
import android.view.inputmethod.InputMethodManagerGlobal;
import androidx.annotation.VisibleForTesting;
@@ -112,7 +114,7 @@
}
if (mDisplayController.getDisplayLayout(displayId).rotation()
!= pd.mRotation && isImeShowing(displayId)) {
- pd.startAnimation(true, false /* forceRestart */);
+ pd.startAnimation(true, false /* forceRestart */, null /* statsToken */);
}
}
@@ -244,7 +246,7 @@
mInsetsState.set(insetsState, true /* copySources */);
if (mImeShowing && !newFrame.equals(oldFrame) && newSource.isVisible()) {
if (DEBUG) Slog.d(TAG, "insetsChanged when IME showing, restart animation");
- startAnimation(mImeShowing, true /* forceRestart */);
+ startAnimation(mImeShowing, true /* forceRestart */, null /* statsToken */);
}
}
@@ -280,7 +282,7 @@
!haveSameLeash(mImeSourceControl, imeSourceControl);
if (mAnimation != null) {
if (positionChanged) {
- startAnimation(mImeShowing, true /* forceRestart */);
+ startAnimation(mImeShowing, true /* forceRestart */, null /* statsToken */);
}
} else {
if (leashChanged) {
@@ -312,21 +314,23 @@
}
@Override
- public void showInsets(int types, boolean fromIme) {
+ public void showInsets(@InsetsType int types, boolean fromIme,
+ @Nullable ImeTracker.Token statsToken) {
if ((types & WindowInsets.Type.ime()) == 0) {
return;
}
if (DEBUG) Slog.d(TAG, "Got showInsets for ime");
- startAnimation(true /* show */, false /* forceRestart */);
+ startAnimation(true /* show */, false /* forceRestart */, statsToken);
}
@Override
- public void hideInsets(int types, boolean fromIme) {
+ public void hideInsets(@InsetsType int types, boolean fromIme,
+ @Nullable ImeTracker.Token statsToken) {
if ((types & WindowInsets.Type.ime()) == 0) {
return;
}
if (DEBUG) Slog.d(TAG, "Got hideInsets for ime");
- startAnimation(false /* show */, false /* forceRestart */);
+ startAnimation(false /* show */, false /* forceRestart */, statsToken);
}
@Override
@@ -367,9 +371,11 @@
.navBarFrameHeight();
}
- private void startAnimation(final boolean show, final boolean forceRestart) {
+ private void startAnimation(final boolean show, final boolean forceRestart,
+ @Nullable ImeTracker.Token statsToken) {
final InsetsSource imeSource = mInsetsState.getSource(InsetsState.ITYPE_IME);
if (imeSource == null || mImeSourceControl == null) {
+ ImeTracker.get().onFailed(statsToken, ImeTracker.PHASE_WM_ANIMATION_CREATE);
return;
}
final Rect newFrame = imeSource.getFrame();
@@ -390,8 +396,9 @@
+ (mAnimationDirection == DIRECTION_SHOW ? "SHOW"
: (mAnimationDirection == DIRECTION_HIDE ? "HIDE" : "NONE")));
}
- if (!forceRestart && (mAnimationDirection == DIRECTION_SHOW && show)
+ if ((!forceRestart && (mAnimationDirection == DIRECTION_SHOW && show))
|| (mAnimationDirection == DIRECTION_HIDE && !show)) {
+ ImeTracker.get().onCancelled(statsToken, ImeTracker.PHASE_WM_ANIMATION_CREATE);
return;
}
boolean seek = false;
@@ -435,8 +442,11 @@
mTransactionPool.release(t);
});
mAnimation.setInterpolator(INTERPOLATOR);
+ ImeTracker.get().onProgress(statsToken, ImeTracker.PHASE_WM_ANIMATION_CREATE);
mAnimation.addListener(new AnimatorListenerAdapter() {
private boolean mCancelled = false;
+ @Nullable
+ private final ImeTracker.Token mStatsToken = statsToken;
@Override
public void onAnimationStart(Animator animation) {
@@ -455,6 +465,8 @@
: 1.f;
t.setAlpha(mImeSourceControl.getLeash(), alpha);
if (mAnimationDirection == DIRECTION_SHOW) {
+ ImeTracker.get().onProgress(mStatsToken,
+ ImeTracker.PHASE_WM_ANIMATION_RUNNING);
t.show(mImeSourceControl.getLeash());
}
t.apply();
@@ -476,8 +488,16 @@
}
dispatchEndPositioning(mDisplayId, mCancelled, t);
if (mAnimationDirection == DIRECTION_HIDE && !mCancelled) {
+ ImeTracker.get().onProgress(mStatsToken,
+ ImeTracker.PHASE_WM_ANIMATION_RUNNING);
t.hide(mImeSourceControl.getLeash());
removeImeSurface();
+ ImeTracker.get().onHidden(mStatsToken);
+ } else if (mAnimationDirection == DIRECTION_SHOW && !mCancelled) {
+ ImeTracker.get().onShown(mStatsToken);
+ } else if (mCancelled) {
+ ImeTracker.get().onCancelled(mStatsToken,
+ ImeTracker.PHASE_WM_ANIMATION_RUNNING);
}
t.apply();
mTransactionPool.release(t);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayInsetsController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayInsetsController.java
index 8d4a09d..8759301 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayInsetsController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayInsetsController.java
@@ -16,6 +16,7 @@
package com.android.wm.shell.common;
+import android.annotation.Nullable;
import android.content.ComponentName;
import android.os.RemoteException;
import android.util.Slog;
@@ -25,6 +26,7 @@
import android.view.InsetsSourceControl;
import android.view.InsetsState;
import android.view.WindowInsets.Type.InsetsType;
+import android.view.inputmethod.ImeTracker;
import androidx.annotation.BinderThread;
@@ -156,23 +158,29 @@
}
}
- private void showInsets(int types, boolean fromIme) {
+ private void showInsets(@InsetsType int types, boolean fromIme,
+ @Nullable ImeTracker.Token statsToken) {
CopyOnWriteArrayList<OnInsetsChangedListener> listeners = mListeners.get(mDisplayId);
if (listeners == null) {
+ ImeTracker.get().onFailed(statsToken, ImeTracker.PHASE_WM_REMOTE_INSETS_CONTROLLER);
return;
}
+ ImeTracker.get().onProgress(statsToken, ImeTracker.PHASE_WM_REMOTE_INSETS_CONTROLLER);
for (OnInsetsChangedListener listener : listeners) {
- listener.showInsets(types, fromIme);
+ listener.showInsets(types, fromIme, statsToken);
}
}
- private void hideInsets(int types, boolean fromIme) {
+ private void hideInsets(@InsetsType int types, boolean fromIme,
+ @Nullable ImeTracker.Token statsToken) {
CopyOnWriteArrayList<OnInsetsChangedListener> listeners = mListeners.get(mDisplayId);
if (listeners == null) {
+ ImeTracker.get().onFailed(statsToken, ImeTracker.PHASE_WM_REMOTE_INSETS_CONTROLLER);
return;
}
+ ImeTracker.get().onProgress(statsToken, ImeTracker.PHASE_WM_REMOTE_INSETS_CONTROLLER);
for (OnInsetsChangedListener listener : listeners) {
- listener.hideInsets(types, fromIme);
+ listener.hideInsets(types, fromIme, statsToken);
}
}
@@ -214,16 +222,18 @@
}
@Override
- public void showInsets(int types, boolean fromIme) throws RemoteException {
+ public void showInsets(@InsetsType int types, boolean fromIme,
+ @Nullable ImeTracker.Token statsToken) throws RemoteException {
mMainExecutor.execute(() -> {
- PerDisplay.this.showInsets(types, fromIme);
+ PerDisplay.this.showInsets(types, fromIme, statsToken);
});
}
@Override
- public void hideInsets(int types, boolean fromIme) throws RemoteException {
+ public void hideInsets(@InsetsType int types, boolean fromIme,
+ @Nullable ImeTracker.Token statsToken) throws RemoteException {
mMainExecutor.execute(() -> {
- PerDisplay.this.hideInsets(types, fromIme);
+ PerDisplay.this.hideInsets(types, fromIme, statsToken);
});
}
}
@@ -263,15 +273,21 @@
*
* @param types {@link InsetsType} to show
* @param fromIme true if this request originated from IME (InputMethodService).
+ * @param statsToken the token tracking the current IME show request
+ * or {@code null} otherwise.
*/
- default void showInsets(@InsetsType int types, boolean fromIme) {}
+ default void showInsets(@InsetsType int types, boolean fromIme,
+ @Nullable ImeTracker.Token statsToken) {}
/**
* Called when a set of insets source window should be hidden by policy.
*
* @param types {@link InsetsType} to hide
* @param fromIme true if this request originated from IME (InputMethodService).
+ * @param statsToken the token tracking the current IME hide request
+ * or {@code null} otherwise.
*/
- default void hideInsets(@InsetsType int types, boolean fromIme) {}
+ default void hideInsets(@InsetsType int types, boolean fromIme,
+ @Nullable ImeTracker.Token statsToken) {}
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/SystemWindows.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/SystemWindows.java
index e270edb..af13bf5 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/SystemWindows.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/SystemWindows.java
@@ -19,6 +19,7 @@
import static android.view.WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED;
import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.content.Context;
import android.content.res.Configuration;
import android.graphics.Region;
@@ -46,6 +47,7 @@
import android.view.ViewGroup;
import android.view.WindowManager;
import android.view.WindowlessWindowManager;
+import android.view.inputmethod.ImeTracker;
import android.window.ClientWindowFrames;
import com.android.internal.os.IResultReceiver;
@@ -351,10 +353,10 @@
InsetsSourceControl[] activeControls) {}
@Override
- public void showInsets(int types, boolean fromIme) {}
+ public void showInsets(int types, boolean fromIme, @Nullable ImeTracker.Token statsToken) {}
@Override
- public void hideInsets(int types, boolean fromIme) {}
+ public void hideInsets(int types, boolean fromIme, @Nullable ImeTracker.Token statsToken) {}
@Override
public void moved(int newX, int newY) {}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java
index 839edc8..3de1045 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java
@@ -601,7 +601,7 @@
animator.start();
}
- /** Swich both surface position with animation. */
+ /** Switch both surface position with animation. */
public void splitSwitching(SurfaceControl.Transaction t, SurfaceControl leash1,
SurfaceControl leash2, Consumer<Rect> finishCallback) {
final boolean isLandscape = isLandscape();
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeController.java
index 34ff6d8..abc4024 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeController.java
@@ -16,8 +16,11 @@
package com.android.wm.shell.desktopmode;
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
import static android.view.WindowManager.TRANSIT_CHANGE;
import static android.view.WindowManager.TRANSIT_OPEN;
import static android.view.WindowManager.TRANSIT_TO_FRONT;
@@ -151,21 +154,18 @@
int displayId = mContext.getDisplayId();
+ ArrayList<RunningTaskInfo> runningTasks = mShellTaskOrganizer.getRunningTasks(displayId);
+
WindowContainerTransaction wct = new WindowContainerTransaction();
- // Reset freeform windowing mode that is set per task level (tasks should inherit
- // container value)
- wct.merge(mShellTaskOrganizer.prepareClearFreeformForStandardTasks(displayId),
- true /* transfer */);
- int targetWindowingMode;
+ // Reset freeform windowing mode that is set per task level so tasks inherit it
+ clearFreeformForStandardTasks(runningTasks, wct);
if (active) {
- targetWindowingMode = WINDOWING_MODE_FREEFORM;
+ moveHomeBehindVisibleTasks(runningTasks, wct);
+ setDisplayAreaWindowingMode(displayId, WINDOWING_MODE_FREEFORM, wct);
} else {
- targetWindowingMode = WINDOWING_MODE_FULLSCREEN;
- // Clear any resized bounds
- wct.merge(mShellTaskOrganizer.prepareClearBoundsForStandardTasks(displayId),
- true /* transfer */);
+ clearBoundsForStandardTasks(runningTasks, wct);
+ setDisplayAreaWindowingMode(displayId, WINDOWING_MODE_FULLSCREEN, wct);
}
- prepareWindowingModeChange(wct, displayId, targetWindowingMode);
if (Transitions.ENABLE_SHELL_TRANSITIONS) {
mTransitions.startTransition(TRANSIT_CHANGE, wct, null);
} else {
@@ -173,17 +173,69 @@
}
}
- private void prepareWindowingModeChange(WindowContainerTransaction wct,
- int displayId, @WindowConfiguration.WindowingMode int windowingMode) {
- DisplayAreaInfo displayAreaInfo = mRootTaskDisplayAreaOrganizer
- .getDisplayAreaInfo(displayId);
+ private WindowContainerTransaction clearBoundsForStandardTasks(
+ ArrayList<RunningTaskInfo> runningTasks, WindowContainerTransaction wct) {
+ ProtoLog.v(WM_SHELL_DESKTOP_MODE, "prepareClearBoundsForTasks");
+ for (RunningTaskInfo taskInfo : runningTasks) {
+ if (taskInfo.getActivityType() == ACTIVITY_TYPE_STANDARD) {
+ ProtoLog.v(WM_SHELL_DESKTOP_MODE, "clearing bounds for token=%s taskInfo=%s",
+ taskInfo.token, taskInfo);
+ wct.setBounds(taskInfo.token, null);
+ }
+ }
+ return wct;
+ }
+
+ private void clearFreeformForStandardTasks(ArrayList<RunningTaskInfo> runningTasks,
+ WindowContainerTransaction wct) {
+ ProtoLog.v(WM_SHELL_DESKTOP_MODE, "prepareClearFreeformForTasks");
+ for (RunningTaskInfo taskInfo : runningTasks) {
+ if (taskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM
+ && taskInfo.getActivityType() == ACTIVITY_TYPE_STANDARD) {
+ ProtoLog.v(WM_SHELL_DESKTOP_MODE,
+ "clearing windowing mode for token=%s taskInfo=%s", taskInfo.token,
+ taskInfo);
+ wct.setWindowingMode(taskInfo.token, WINDOWING_MODE_UNDEFINED);
+ }
+ }
+ }
+
+ private void moveHomeBehindVisibleTasks(ArrayList<RunningTaskInfo> runningTasks,
+ WindowContainerTransaction wct) {
+ ProtoLog.v(WM_SHELL_DESKTOP_MODE, "moveHomeBehindVisibleTasks");
+ RunningTaskInfo homeTask = null;
+ ArrayList<RunningTaskInfo> visibleTasks = new ArrayList<>();
+ for (RunningTaskInfo taskInfo : runningTasks) {
+ if (taskInfo.getActivityType() == ACTIVITY_TYPE_HOME) {
+ homeTask = taskInfo;
+ } else if (taskInfo.getActivityType() == ACTIVITY_TYPE_STANDARD
+ && taskInfo.isVisible()) {
+ visibleTasks.add(taskInfo);
+ }
+ }
+ if (homeTask == null) {
+ ProtoLog.w(WM_SHELL_DESKTOP_MODE, "moveHomeBehindVisibleTasks: home task not found");
+ } else {
+ ProtoLog.v(WM_SHELL_DESKTOP_MODE, "moveHomeBehindVisibleTasks: visible tasks %d",
+ visibleTasks.size());
+ wct.reorder(homeTask.getToken(), true /* onTop */);
+ for (RunningTaskInfo task : visibleTasks) {
+ wct.reorder(task.getToken(), true /* onTop */);
+ }
+ }
+ }
+
+ private void setDisplayAreaWindowingMode(int displayId,
+ @WindowConfiguration.WindowingMode int windowingMode, WindowContainerTransaction wct) {
+ DisplayAreaInfo displayAreaInfo = mRootTaskDisplayAreaOrganizer.getDisplayAreaInfo(
+ displayId);
if (displayAreaInfo == null) {
ProtoLog.e(WM_SHELL_DESKTOP_MODE,
"unable to update windowing mode for display %d display not found", displayId);
return;
}
- ProtoLog.d(WM_SHELL_DESKTOP_MODE,
+ ProtoLog.v(WM_SHELL_DESKTOP_MODE,
"setWindowingMode: displayId=%d current wmMode=%d new wmMode=%d", displayId,
displayAreaInfo.configuration.windowConfiguration.getWindowingMode(),
windowingMode);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/ISplitScreen.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/ISplitScreen.aidl
index eb08d0e..5533ad5 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/ISplitScreen.aidl
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/ISplitScreen.aidl
@@ -86,8 +86,8 @@
/**
* Starts a pair of intent and task in one transition.
*/
- oneway void startIntentAndTask(in PendingIntent pendingIntent, in Intent fillInIntent,
- in Bundle options1, int taskId, in Bundle options2, int sidePosition, float splitRatio,
+ oneway void startIntentAndTask(in PendingIntent pendingIntent, in Bundle options1, int taskId,
+ in Bundle options2, int sidePosition, float splitRatio,
in RemoteTransition remoteTransition, in InstanceId instanceId) = 16;
/**
@@ -108,9 +108,8 @@
* Starts a pair of intent and task using legacy transition system.
*/
oneway void startIntentAndTaskWithLegacyTransition(in PendingIntent pendingIntent,
- in Intent fillInIntent, in Bundle options1, int taskId, in Bundle options2,
- int splitPosition, float splitRatio, in RemoteAnimationAdapter adapter,
- in InstanceId instanceId) = 12;
+ in Bundle options1, int taskId, in Bundle options2, int splitPosition, float splitRatio,
+ in RemoteAnimationAdapter adapter, in InstanceId instanceId) = 12;
/**
* Starts a pair of shortcut and task using legacy transition system.
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java
index c6a2b83..cdc8cdd 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java
@@ -18,6 +18,7 @@
import static android.app.ActivityManager.START_SUCCESS;
import static android.app.ActivityManager.START_TASK_TO_FRONT;
+import static android.app.ActivityTaskManager.INVALID_TASK_ID;
import static android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK;
import static android.content.Intent.FLAG_ACTIVITY_NO_USER_ACTION;
import static android.view.Display.DEFAULT_DISPLAY;
@@ -32,6 +33,8 @@
import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_SPLIT_SCREEN;
import static com.android.wm.shell.transition.Transitions.ENABLE_SHELL_TRANSITIONS;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.app.ActivityManager;
import android.app.ActivityOptions;
import android.app.ActivityTaskManager;
@@ -60,13 +63,12 @@
import androidx.annotation.BinderThread;
import androidx.annotation.IntDef;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.logging.InstanceId;
import com.android.internal.protolog.common.ProtoLog;
import com.android.launcher3.icons.IconProvider;
+import com.android.wm.shell.R;
import com.android.wm.shell.RootTaskDisplayAreaOrganizer;
import com.android.wm.shell.ShellTaskOrganizer;
import com.android.wm.shell.common.DisplayController;
@@ -166,8 +168,11 @@
private final IconProvider mIconProvider;
private final Optional<RecentTasksController> mRecentTasksOptional;
private final SplitScreenShellCommandHandler mSplitScreenShellCommandHandler;
+ private final String[] mMultiInstancesComponents;
- private StageCoordinator mStageCoordinator;
+ @VisibleForTesting
+ StageCoordinator mStageCoordinator;
+
// Only used for the legacy recents animation from splitscreen to allow the tasks to be animated
// outside the bounds of the roots by being reparented into a higher level fullscreen container
private SurfaceControl mGoingToRecentsTasksLayer;
@@ -210,6 +215,51 @@
if (ActivityTaskManager.supportsSplitScreenMultiWindow(context)) {
shellInit.addInitCallback(this::onInit, this);
}
+
+ // TODO(255224696): Remove the config once having a way for client apps to opt-in
+ // multi-instances split.
+ mMultiInstancesComponents = mContext.getResources()
+ .getStringArray(R.array.config_componentsSupportMultiInstancesSplit);
+ }
+
+ @VisibleForTesting
+ SplitScreenController(Context context,
+ ShellInit shellInit,
+ ShellCommandHandler shellCommandHandler,
+ ShellController shellController,
+ ShellTaskOrganizer shellTaskOrganizer,
+ SyncTransactionQueue syncQueue,
+ RootTaskDisplayAreaOrganizer rootTDAOrganizer,
+ DisplayController displayController,
+ DisplayImeController displayImeController,
+ DisplayInsetsController displayInsetsController,
+ DragAndDropController dragAndDropController,
+ Transitions transitions,
+ TransactionPool transactionPool,
+ IconProvider iconProvider,
+ RecentTasksController recentTasks,
+ ShellExecutor mainExecutor,
+ StageCoordinator stageCoordinator) {
+ mShellCommandHandler = shellCommandHandler;
+ mShellController = shellController;
+ mTaskOrganizer = shellTaskOrganizer;
+ mSyncQueue = syncQueue;
+ mContext = context;
+ mRootTDAOrganizer = rootTDAOrganizer;
+ mMainExecutor = mainExecutor;
+ mDisplayController = displayController;
+ mDisplayImeController = displayImeController;
+ mDisplayInsetsController = displayInsetsController;
+ mDragAndDropController = dragAndDropController;
+ mTransitions = transitions;
+ mTransactionPool = transactionPool;
+ mIconProvider = iconProvider;
+ mRecentTasksOptional = Optional.of(recentTasks);
+ mStageCoordinator = stageCoordinator;
+ mSplitScreenShellCommandHandler = new SplitScreenShellCommandHandler(this);
+ shellInit.addInitCallback(this::onInit, this);
+ mMultiInstancesComponents = mContext.getResources()
+ .getStringArray(R.array.config_componentsSupportMultiInstancesSplit);
}
public SplitScreen asSplitScreen() {
@@ -471,72 +521,116 @@
startIntent(intent, fillInIntent, position, options);
}
+ private void startIntentAndTaskWithLegacyTransition(PendingIntent pendingIntent,
+ @Nullable Bundle options1, int taskId, @Nullable Bundle options2,
+ @SplitPosition int splitPosition, float splitRatio, RemoteAnimationAdapter adapter,
+ InstanceId instanceId) {
+ Intent fillInIntent = null;
+ if (launchSameComponentAdjacently(pendingIntent, splitPosition, taskId)
+ && supportMultiInstancesSplit(pendingIntent.getIntent().getComponent())) {
+ fillInIntent = new Intent();
+ fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK);
+ }
+ mStageCoordinator.startIntentAndTaskWithLegacyTransition(pendingIntent, fillInIntent,
+ options1, taskId, options2, splitPosition, splitRatio, adapter, instanceId);
+ }
+
+ private void startIntentAndTask(PendingIntent pendingIntent, @Nullable Bundle options1,
+ int taskId, @Nullable Bundle options2, @SplitPosition int splitPosition,
+ float splitRatio, @Nullable RemoteTransition remoteTransition, InstanceId instanceId) {
+ Intent fillInIntent = null;
+ if (launchSameComponentAdjacently(pendingIntent, splitPosition, taskId)
+ && supportMultiInstancesSplit(pendingIntent.getIntent().getComponent())) {
+ fillInIntent = new Intent();
+ fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK);
+ }
+ mStageCoordinator.startIntentAndTask(pendingIntent, fillInIntent, options1, taskId,
+ options2, splitPosition, splitRatio, remoteTransition, instanceId);
+ }
+
@Override
public void startIntent(PendingIntent intent, @Nullable Intent fillInIntent,
@SplitPosition int position, @Nullable Bundle options) {
- if (fillInIntent == null) {
- fillInIntent = new Intent();
- }
- // Flag this as a no-user-action launch to prevent sending user leaving event to the
- // current top activity since it's going to be put into another side of the split. This
- // prevents the current top activity from going into pip mode due to user leaving event.
+ // Flag this as a no-user-action launch to prevent sending user leaving event to the current
+ // top activity since it's going to be put into another side of the split. This prevents the
+ // current top activity from going into pip mode due to user leaving event.
+ if (fillInIntent == null) fillInIntent = new Intent();
fillInIntent.addFlags(FLAG_ACTIVITY_NO_USER_ACTION);
- // Flag with MULTIPLE_TASK if this is launching the same activity into both sides of the
- // split and there is no reusable background task.
- if (shouldAddMultipleTaskFlag(intent.getIntent(), position)) {
- final ActivityManager.RecentTaskInfo taskInfo = mRecentTasksOptional.isPresent()
- ? mRecentTasksOptional.get().findTaskInBackground(
- intent.getIntent().getComponent())
- : null;
- if (taskInfo != null) {
- startTask(taskInfo.taskId, position, options);
+ if (launchSameComponentAdjacently(intent, position, INVALID_TASK_ID)) {
+ final ComponentName launching = intent.getIntent().getComponent();
+ if (supportMultiInstancesSplit(launching)) {
+ // To prevent accumulating large number of instances in the background, reuse task
+ // in the background with priority.
+ final ActivityManager.RecentTaskInfo taskInfo = mRecentTasksOptional
+ .map(recentTasks -> recentTasks.findTaskInBackground(launching))
+ .orElse(null);
+ if (taskInfo != null) {
+ startTask(taskInfo.taskId, position, options);
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN,
+ "Start task in background");
+ return;
+ }
+
+ // Flag with MULTIPLE_TASK if this is launching the same activity into both sides of
+ // the split and there is no reusable background task.
+ fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK);
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, "Adding MULTIPLE_TASK");
+ } else if (isSplitScreenVisible()) {
+ mStageCoordinator.switchSplitPosition("startIntent");
return;
}
- fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK);
- ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, "Adding MULTIPLE_TASK");
}
- if (!ENABLE_SHELL_TRANSITIONS) {
- mStageCoordinator.startIntentLegacy(intent, fillInIntent, position, options);
- return;
- }
mStageCoordinator.startIntent(intent, fillInIntent, position, options);
}
/** Returns {@code true} if it's launching the same component on both sides of the split. */
- @VisibleForTesting
- boolean shouldAddMultipleTaskFlag(@Nullable Intent startIntent, @SplitPosition int position) {
- if (startIntent == null) {
- return false;
- }
+ private boolean launchSameComponentAdjacently(@Nullable PendingIntent pendingIntent,
+ @SplitPosition int position, int taskId) {
+ if (pendingIntent == null || pendingIntent.getIntent() == null) return false;
- final ComponentName launchingActivity = startIntent.getComponent();
- if (launchingActivity == null) {
- return false;
- }
+ final ComponentName launchingActivity = pendingIntent.getIntent().getComponent();
+ if (launchingActivity == null) return false;
- if (isSplitScreenVisible()) {
- // To prevent users from constantly dropping the same app to the same side resulting in
- // a large number of instances in the background.
- final ActivityManager.RunningTaskInfo targetTaskInfo = getTaskInfo(position);
- final ComponentName targetActivity = targetTaskInfo != null
- ? targetTaskInfo.baseIntent.getComponent() : null;
- if (Objects.equals(launchingActivity, targetActivity)) {
- return false;
+ if (taskId != INVALID_TASK_ID) {
+ final ActivityManager.RunningTaskInfo taskInfo =
+ mTaskOrganizer.getRunningTaskInfo(taskId);
+ if (taskInfo != null) {
+ return Objects.equals(taskInfo.baseIntent.getComponent(), launchingActivity);
}
-
- // Allow users to start a new instance the same to adjacent side.
- final ActivityManager.RunningTaskInfo pairedTaskInfo =
- getTaskInfo(SplitLayout.reversePosition(position));
- final ComponentName pairedActivity = pairedTaskInfo != null
- ? pairedTaskInfo.baseIntent.getComponent() : null;
- return Objects.equals(launchingActivity, pairedActivity);
+ return false;
}
- final ActivityManager.RunningTaskInfo taskInfo = getFocusingTaskInfo();
- if (taskInfo != null && isValidToEnterSplitScreen(taskInfo)) {
- return Objects.equals(taskInfo.baseIntent.getComponent(), launchingActivity);
+ if (!isSplitScreenVisible()) {
+ // Split screen is not yet activated, check if the current top running task is valid to
+ // split together.
+ final ActivityManager.RunningTaskInfo taskInfo = getFocusingTaskInfo();
+ if (taskInfo != null && isValidToEnterSplitScreen(taskInfo)) {
+ return Objects.equals(taskInfo.baseIntent.getComponent(), launchingActivity);
+ }
+ return false;
+ }
+
+ // Compare to the adjacent side of the split to determine if this is launching the same
+ // component adjacently.
+ final ActivityManager.RunningTaskInfo pairedTaskInfo =
+ getTaskInfo(SplitLayout.reversePosition(position));
+ final ComponentName pairedActivity = pairedTaskInfo != null
+ ? pairedTaskInfo.baseIntent.getComponent() : null;
+ return Objects.equals(launchingActivity, pairedActivity);
+ }
+
+ @VisibleForTesting
+ /** Returns {@code true} if the component supports multi-instances split. */
+ boolean supportMultiInstancesSplit(@Nullable ComponentName launching) {
+ if (launching == null) return false;
+
+ final String componentName = launching.flattenToString();
+ for (int i = 0; i < mMultiInstancesComponents.length; i++) {
+ if (mMultiInstancesComponents[i].equals(componentName)) {
+ return true;
+ }
}
return false;
@@ -839,14 +933,13 @@
@Override
public void startIntentAndTaskWithLegacyTransition(PendingIntent pendingIntent,
- Intent fillInIntent, Bundle options1, int taskId, Bundle options2,
- int splitPosition, float splitRatio, RemoteAnimationAdapter adapter,
- InstanceId instanceId) {
+ Bundle options1, int taskId, Bundle options2, int splitPosition, float splitRatio,
+ RemoteAnimationAdapter adapter, InstanceId instanceId) {
executeRemoteCallWithTaskPermission(mController,
"startIntentAndTaskWithLegacyTransition", (controller) ->
- controller.mStageCoordinator.startIntentAndTaskWithLegacyTransition(
- pendingIntent, fillInIntent, options1, taskId, options2,
- splitPosition, splitRatio, adapter, instanceId));
+ controller.startIntentAndTaskWithLegacyTransition(pendingIntent,
+ options1, taskId, options2, splitPosition, splitRatio, adapter,
+ instanceId));
}
@Override
@@ -872,14 +965,13 @@
}
@Override
- public void startIntentAndTask(PendingIntent pendingIntent, Intent fillInIntent,
- @Nullable Bundle options1, int taskId, @Nullable Bundle options2,
- @SplitPosition int splitPosition, float splitRatio,
- @Nullable RemoteTransition remoteTransition, InstanceId instanceId) {
+ public void startIntentAndTask(PendingIntent pendingIntent, @Nullable Bundle options1,
+ int taskId, @Nullable Bundle options2, @SplitPosition int splitPosition,
+ float splitRatio, @Nullable RemoteTransition remoteTransition,
+ InstanceId instanceId) {
executeRemoteCallWithTaskPermission(mController, "startIntentAndTask",
- (controller) -> controller.mStageCoordinator.startIntentAndTask(pendingIntent,
- fillInIntent, options1, taskId, options2, splitPosition, splitRatio,
- remoteTransition, instanceId));
+ (controller) -> controller.startIntentAndTask(pendingIntent, options1, taskId,
+ options2, splitPosition, splitRatio, remoteTransition, instanceId));
}
@Override
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
index e888c6f..c2ab7ef 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
@@ -24,7 +24,6 @@
import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS;
import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
-import static android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK;
import static android.content.res.Configuration.SMALLEST_SCREEN_WIDTH_DP_UNDEFINED;
import static android.view.Display.DEFAULT_DISPLAY;
import static android.view.RemoteAnimationTarget.MODE_OPENING;
@@ -428,6 +427,11 @@
/** Launches an activity into split. */
void startIntent(PendingIntent intent, Intent fillInIntent, @SplitPosition int position,
@Nullable Bundle options) {
+ if (!ENABLE_SHELL_TRANSITIONS) {
+ startIntentLegacy(intent, fillInIntent, position, options);
+ return;
+ }
+
final WindowContainerTransaction wct = new WindowContainerTransaction();
final WindowContainerTransaction evictWct = new WindowContainerTransaction();
prepareEvictChildTasks(position, evictWct);
@@ -441,13 +445,7 @@
prepareEnterSplitScreen(wct, null /* taskInfo */, position);
mSplitTransitions.startEnterTransition(transitType, wct, null, this,
- aborted -> {
- // Switch the split position if launching as MULTIPLE_TASK failed.
- if (aborted && (fillInIntent.getFlags() & FLAG_ACTIVITY_MULTIPLE_TASK) != 0) {
- setSideStagePositionAnimated(
- SplitLayout.reversePosition(mSideStagePosition));
- }
- } /* consumedCallback */,
+ null /* consumedCallback */,
(finishWct, finishT) -> {
if (!evictWct.isEmpty()) {
finishWct.merge(evictWct, true);
@@ -457,7 +455,7 @@
/** Launches an activity into split by legacy transition. */
void startIntentLegacy(PendingIntent intent, Intent fillInIntent,
- @SplitPosition int position, @androidx.annotation.Nullable Bundle options) {
+ @SplitPosition int position, @Nullable Bundle options) {
final WindowContainerTransaction evictWct = new WindowContainerTransaction();
prepareEvictChildTasks(position, evictWct);
@@ -473,12 +471,6 @@
exitSplitScreen(mMainStage.getChildCount() == 0
? mSideStage : mMainStage, EXIT_REASON_UNKNOWN));
mSplitUnsupportedToast.show();
- } else {
- // Switch the split position if launching as MULTIPLE_TASK failed.
- if ((fillInIntent.getFlags() & FLAG_ACTIVITY_MULTIPLE_TASK) != 0) {
- setSideStagePosition(SplitLayout.reversePosition(
- getSideStagePosition()), null);
- }
}
// Do nothing when the animation was cancelled.
@@ -771,9 +763,8 @@
mSideStage.evictInvisibleChildren(wct);
}
- Bundle resolveStartStage(@StageType int stage,
- @SplitPosition int position, @androidx.annotation.Nullable Bundle options,
- @androidx.annotation.Nullable WindowContainerTransaction wct) {
+ Bundle resolveStartStage(@StageType int stage, @SplitPosition int position,
+ @Nullable Bundle options, @Nullable WindowContainerTransaction wct) {
switch (stage) {
case STAGE_TYPE_UNDEFINED: {
if (position != SPLIT_POSITION_UNDEFINED) {
@@ -844,9 +835,8 @@
: mMainStage.getTopVisibleChildTaskId();
}
- void setSideStagePositionAnimated(@SplitPosition int sideStagePosition) {
- if (mSideStagePosition == sideStagePosition) return;
- SurfaceControl.Transaction t = mTransactionPool.acquire();
+ void switchSplitPosition(String reason) {
+ final SurfaceControl.Transaction t = mTransactionPool.acquire();
mTempRect1.setEmpty();
final StageTaskListener topLeftStage =
mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT ? mSideStage : mMainStage;
@@ -886,6 +876,11 @@
va.start();
});
});
+
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, "Switch split position: %s", reason);
+ mLogger.logSwap(getMainStagePosition(), mMainStage.getTopChildTaskUid(),
+ getSideStagePosition(), mSideStage.getTopChildTaskUid(),
+ mSplitLayout.isLandscape());
}
void setSideStagePosition(@SplitPosition int sideStagePosition,
@@ -1617,10 +1612,7 @@
@Override
public void onDoubleTappedDivider() {
- setSideStagePositionAnimated(SplitLayout.reversePosition(mSideStagePosition));
- mLogger.logSwap(getMainStagePosition(), mMainStage.getTopChildTaskUid(),
- getSideStagePosition(), mSideStage.getTopChildTaskUid(),
- mSplitLayout.isLandscape());
+ switchSplitPosition("double tap");
}
@Override
diff --git a/libs/WindowManager/Shell/tests/flicker/AndroidTest.xml b/libs/WindowManager/Shell/tests/flicker/AndroidTest.xml
index 2d6e8f5..08913c6 100644
--- a/libs/WindowManager/Shell/tests/flicker/AndroidTest.xml
+++ b/libs/WindowManager/Shell/tests/flicker/AndroidTest.xml
@@ -13,6 +13,8 @@
<option name="run-command" value="cmd window tracing level all" />
<!-- set WM tracing to frame (avoid incomplete states) -->
<option name="run-command" value="cmd window tracing frame" />
+ <!-- disable betterbug as it's log collection dialogues cause flakes in e2e tests -->
+ <option name="run-command" value="pm disable com.google.android.internal.betterbug" />
<!-- ensure lock screen mode is swipe -->
<option name="run-command" value="locksettings set-disabled false" />
<!-- restart launcher to activate TAPL -->
@@ -34,4 +36,4 @@
<option name="collect-on-run-ended-only" value="true" />
<option name="clean-up" value="true" />
</metrics_collector>
-</configuration>
\ No newline at end of file
+</configuration>
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/CopyContentInSplit.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/CopyContentInSplit.kt
index 2bce8e45..9533b91 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/CopyContentInSplit.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/CopyContentInSplit.kt
@@ -24,6 +24,8 @@
import com.android.server.wm.flicker.FlickerTestParameter
import com.android.server.wm.flicker.FlickerTestParameterFactory
import com.android.server.wm.flicker.dsl.FlickerBuilder
+import com.android.server.wm.traces.common.ComponentNameMatcher
+import com.android.server.wm.traces.common.EdgeExtensionComponentMatcher
import com.android.wm.shell.flicker.SPLIT_SCREEN_DIVIDER_COMPONENT
import com.android.wm.shell.flicker.appWindowIsVisibleAtEnd
import com.android.wm.shell.flicker.appWindowIsVisibleAtStart
@@ -49,6 +51,8 @@
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
class CopyContentInSplit(testSpec: FlickerTestParameter) : SplitScreenBase(testSpec) {
private val textEditApp = SplitScreenUtils.getIme(instrumentation)
+ private val MagnifierLayer = ComponentNameMatcher("", "magnifier surface bbq wrapper#")
+ private val PopupWindowLayer = ComponentNameMatcher("", "PopupWindow:")
override val transition: FlickerBuilder.() -> Unit
get() = {
@@ -159,8 +163,18 @@
/** {@inheritDoc} */
@Presubmit
@Test
- override fun visibleLayersShownMoreThanOneConsecutiveEntry() =
- super.visibleLayersShownMoreThanOneConsecutiveEntry()
+ override fun visibleLayersShownMoreThanOneConsecutiveEntry() {
+ testSpec.assertLayers {
+ this.visibleLayersShownMoreThanOneConsecutiveEntry(
+ ignoreLayers = listOf(
+ ComponentNameMatcher.SPLASH_SCREEN,
+ ComponentNameMatcher.SNAPSHOT,
+ ComponentNameMatcher.IME_SNAPSHOT,
+ EdgeExtensionComponentMatcher(),
+ MagnifierLayer,
+ PopupWindowLayer))
+ }
+ }
/** {@inheritDoc} */
@Presubmit
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTaskOrganizerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTaskOrganizerTests.java
index 7cbace5..081c8ae 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTaskOrganizerTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTaskOrganizerTests.java
@@ -16,13 +16,9 @@
package com.android.wm.shell;
-import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
-import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
-import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
-import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
import static android.view.Display.DEFAULT_DISPLAY;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
@@ -34,8 +30,6 @@
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 static org.junit.Assert.fail;
import static org.junit.Assume.assumeFalse;
@@ -44,11 +38,9 @@
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
import android.app.ActivityManager.RunningTaskInfo;
import android.app.TaskInfo;
-import android.app.WindowConfiguration;
import android.content.LocusId;
import android.content.pm.ParceledListSlice;
import android.os.Binder;
@@ -61,8 +53,6 @@
import android.window.ITaskOrganizerController;
import android.window.TaskAppearedInfo;
import android.window.WindowContainerToken;
-import android.window.WindowContainerTransaction;
-import android.window.WindowContainerTransaction.Change;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
@@ -638,130 +628,10 @@
verify(mTaskOrganizerController).restartTaskTopActivityProcessIfVisible(task1.token);
}
- @Test
- public void testPrepareClearBoundsForStandardTasks() {
- MockToken token1 = new MockToken();
- RunningTaskInfo task1 = createTaskInfo(1, WINDOWING_MODE_UNDEFINED, token1);
- mOrganizer.onTaskAppeared(task1, null);
-
- MockToken token2 = new MockToken();
- RunningTaskInfo task2 = createTaskInfo(2, WINDOWING_MODE_UNDEFINED, token2);
- mOrganizer.onTaskAppeared(task2, null);
-
- MockToken otherDisplayToken = new MockToken();
- RunningTaskInfo otherDisplayTask = createTaskInfo(3, WINDOWING_MODE_UNDEFINED,
- otherDisplayToken);
- otherDisplayTask.displayId = 2;
- mOrganizer.onTaskAppeared(otherDisplayTask, null);
-
- WindowContainerTransaction wct = mOrganizer.prepareClearBoundsForStandardTasks(1);
-
- assertEquals(wct.getChanges().size(), 2);
- Change boundsChange1 = wct.getChanges().get(token1.binder());
- assertNotNull(boundsChange1);
- assertNotEquals(
- (boundsChange1.getWindowSetMask() & WindowConfiguration.WINDOW_CONFIG_BOUNDS), 0);
- assertTrue(boundsChange1.getConfiguration().windowConfiguration.getBounds().isEmpty());
-
- Change boundsChange2 = wct.getChanges().get(token2.binder());
- assertNotNull(boundsChange2);
- assertNotEquals(
- (boundsChange2.getWindowSetMask() & WindowConfiguration.WINDOW_CONFIG_BOUNDS), 0);
- assertTrue(boundsChange2.getConfiguration().windowConfiguration.getBounds().isEmpty());
- }
-
- @Test
- public void testPrepareClearBoundsForStandardTasks_onlyClearActivityTypeStandard() {
- MockToken token1 = new MockToken();
- RunningTaskInfo task1 = createTaskInfo(1, WINDOWING_MODE_UNDEFINED, token1);
- mOrganizer.onTaskAppeared(task1, null);
-
- MockToken token2 = new MockToken();
- RunningTaskInfo task2 = createTaskInfo(2, WINDOWING_MODE_UNDEFINED, token2);
- task2.configuration.windowConfiguration.setActivityType(ACTIVITY_TYPE_HOME);
- mOrganizer.onTaskAppeared(task2, null);
-
- WindowContainerTransaction wct = mOrganizer.prepareClearBoundsForStandardTasks(1);
-
- // Only clear bounds for task1
- assertEquals(1, wct.getChanges().size());
- assertNotNull(wct.getChanges().get(token1.binder()));
- }
-
- @Test
- public void testPrepareClearFreeformForStandardTasks() {
- MockToken token1 = new MockToken();
- RunningTaskInfo task1 = createTaskInfo(1, WINDOWING_MODE_FREEFORM, token1);
- mOrganizer.onTaskAppeared(task1, null);
-
- MockToken token2 = new MockToken();
- RunningTaskInfo task2 = createTaskInfo(2, WINDOWING_MODE_MULTI_WINDOW, token2);
- mOrganizer.onTaskAppeared(task2, null);
-
- MockToken otherDisplayToken = new MockToken();
- RunningTaskInfo otherDisplayTask = createTaskInfo(3, WINDOWING_MODE_FREEFORM,
- otherDisplayToken);
- otherDisplayTask.displayId = 2;
- mOrganizer.onTaskAppeared(otherDisplayTask, null);
-
- WindowContainerTransaction wct = mOrganizer.prepareClearFreeformForStandardTasks(1);
-
- // Only task with freeform windowing mode and the right display should be updated
- assertEquals(wct.getChanges().size(), 1);
- Change wmModeChange1 = wct.getChanges().get(token1.binder());
- assertNotNull(wmModeChange1);
- assertEquals(wmModeChange1.getWindowingMode(), WINDOWING_MODE_UNDEFINED);
- }
-
- @Test
- public void testPrepareClearFreeformForStandardTasks_onlyClearActivityTypeStandard() {
- MockToken token1 = new MockToken();
- RunningTaskInfo task1 = createTaskInfo(1, WINDOWING_MODE_FREEFORM, token1);
- mOrganizer.onTaskAppeared(task1, null);
-
- MockToken token2 = new MockToken();
- RunningTaskInfo task2 = createTaskInfo(2, WINDOWING_MODE_FREEFORM, token2);
- task2.configuration.windowConfiguration.setActivityType(ACTIVITY_TYPE_HOME);
- mOrganizer.onTaskAppeared(task2, null);
-
- WindowContainerTransaction wct = mOrganizer.prepareClearFreeformForStandardTasks(1);
-
- // Only clear freeform for task1
- assertEquals(1, wct.getChanges().size());
- assertNotNull(wct.getChanges().get(token1.binder()));
- }
-
private static RunningTaskInfo createTaskInfo(int taskId, int windowingMode) {
RunningTaskInfo taskInfo = new RunningTaskInfo();
taskInfo.taskId = taskId;
taskInfo.configuration.windowConfiguration.setWindowingMode(windowingMode);
return taskInfo;
}
-
- private static RunningTaskInfo createTaskInfo(int taskId, int windowingMode, MockToken token) {
- RunningTaskInfo taskInfo = createTaskInfo(taskId, windowingMode);
- taskInfo.displayId = 1;
- taskInfo.token = token.token();
- taskInfo.configuration.windowConfiguration.setActivityType(ACTIVITY_TYPE_STANDARD);
- return taskInfo;
- }
-
- private static class MockToken {
- private final WindowContainerToken mToken;
- private final IBinder mBinder;
-
- MockToken() {
- mToken = mock(WindowContainerToken.class);
- mBinder = mock(IBinder.class);
- when(mToken.asBinder()).thenReturn(mBinder);
- }
-
- WindowContainerToken token() {
- return mToken;
- }
-
- IBinder binder() {
- return mBinder;
- }
- }
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java
index 7eccbf4..b603e03 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java
@@ -23,6 +23,7 @@
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
@@ -93,7 +94,10 @@
private IActivityTaskManager mActivityTaskManager;
@Mock
- private IOnBackInvokedCallback mIOnBackInvokedCallback;
+ private IOnBackInvokedCallback mAppCallback;
+
+ @Mock
+ private IOnBackInvokedCallback mAnimatorCallback;
@Mock
private IBackAnimationFinishedCallback mBackAnimationFinishedCallback;
@@ -105,8 +109,6 @@
private ShellController mShellController;
private BackAnimationController mController;
-
- private int mEventTime = 0;
private TestableContentResolver mContentResolver;
private TestableLooper mTestableLooper;
@@ -126,16 +128,15 @@
mContentResolver);
mController.setEnableUAnimation(true);
mShellInit.init();
- mEventTime = 0;
mShellExecutor.flushAll();
}
- private void createNavigationInfo(int backType, IOnBackInvokedCallback onBackInvokedCallback) {
+ private void createNavigationInfo(int backType, boolean enableAnimation) {
BackNavigationInfo.Builder builder = new BackNavigationInfo.Builder()
.setType(backType)
.setOnBackNavigationDone(new RemoteCallback((bundle) -> {}))
- .setOnBackInvokedCallback(onBackInvokedCallback)
- .setPrepareRemoteAnimation(true);
+ .setOnBackInvokedCallback(mAppCallback)
+ .setPrepareRemoteAnimation(enableAnimation);
createNavigationInfo(builder);
}
@@ -176,26 +177,47 @@
}
@Test
- public void verifyAnimationFinishes() {
- RemoteAnimationTarget animationTarget = createAnimationTarget();
- boolean[] backNavigationDone = new boolean[]{false};
- boolean[] triggerBack = new boolean[]{false};
- createNavigationInfo(new BackNavigationInfo.Builder()
- .setType(BackNavigationInfo.TYPE_CROSS_ACTIVITY)
- .setOnBackNavigationDone(
- new RemoteCallback(result -> {
- backNavigationDone[0] = true;
- triggerBack[0] = result.getBoolean(KEY_TRIGGER_BACK);
- })));
- triggerBackGesture();
- assertTrue("Navigation Done callback not called", backNavigationDone[0]);
- assertTrue("TriggerBack should have been true", triggerBack[0]);
+ public void verifyNavigationFinishes() throws RemoteException {
+ final int[] testTypes = new int[] {BackNavigationInfo.TYPE_RETURN_TO_HOME,
+ BackNavigationInfo.TYPE_CROSS_TASK,
+ BackNavigationInfo.TYPE_CROSS_ACTIVITY,
+ BackNavigationInfo.TYPE_DIALOG_CLOSE,
+ BackNavigationInfo.TYPE_CALLBACK };
+
+ for (int type: testTypes) {
+ registerAnimation(type);
+ }
+
+ for (int type: testTypes) {
+ boolean[] backNavigationDone = new boolean[]{false};
+ boolean[] triggerBack = new boolean[]{false};
+
+ createNavigationInfo(new BackNavigationInfo.Builder()
+ .setType(type)
+ .setOnBackInvokedCallback(mAppCallback)
+ .setPrepareRemoteAnimation(true)
+ .setOnBackNavigationDone(
+ new RemoteCallback(result -> {
+ backNavigationDone[0] = true;
+ triggerBack[0] = result.getBoolean(KEY_TRIGGER_BACK);
+ })));
+ triggerBackGesture();
+ simulateRemoteAnimationStart(type);
+ simulateRemoteAnimationFinished();
+ mShellExecutor.flushAll();
+
+ assertTrue("Navigation Done callback not called for "
+ + BackNavigationInfo.typeToString(type), backNavigationDone[0]);
+ assertTrue("TriggerBack should have been true", triggerBack[0]);
+ }
}
+
+
@Test
public void backToHome_dispatchesEvents() throws RemoteException {
- mController.setBackToLauncherCallback(mIOnBackInvokedCallback, mBackAnimationRunner);
- createNavigationInfo(BackNavigationInfo.TYPE_RETURN_TO_HOME, mIOnBackInvokedCallback);
+ registerAnimation(BackNavigationInfo.TYPE_RETURN_TO_HOME);
+ createNavigationInfo(BackNavigationInfo.TYPE_RETURN_TO_HOME, true);
doMotionEvent(MotionEvent.ACTION_DOWN, 0);
@@ -203,14 +225,16 @@
doMotionEvent(MotionEvent.ACTION_MOVE, 100);
simulateRemoteAnimationStart(BackNavigationInfo.TYPE_RETURN_TO_HOME);
- verify(mIOnBackInvokedCallback).onBackStarted(any(BackEvent.class));
+
+ verify(mAnimatorCallback).onBackStarted(any(BackEvent.class));
verify(mBackAnimationRunner).onAnimationStart(anyInt(), any(), any(), any(), any());
- verify(mIOnBackInvokedCallback, atLeastOnce()).onBackProgressed(any(BackEvent.class));
+ ArgumentCaptor<BackEvent> backEventCaptor = ArgumentCaptor.forClass(BackEvent.class);
+ verify(mAnimatorCallback, atLeastOnce()).onBackProgressed(backEventCaptor.capture());
// Check that back invocation is dispatched.
mController.setTriggerBack(true); // Fake trigger back
doMotionEvent(MotionEvent.ACTION_UP, 0);
- verify(mIOnBackInvokedCallback).onBackInvoked();
+ verify(mAnimatorCallback).onBackInvoked();
}
@Test
@@ -223,97 +247,94 @@
mActivityTaskManager, mContext,
mContentResolver);
shellInit.init();
- mController.setBackToLauncherCallback(mIOnBackInvokedCallback, mBackAnimationRunner);
+ registerAnimation(BackNavigationInfo.TYPE_RETURN_TO_HOME);
- IOnBackInvokedCallback appCallback = mock(IOnBackInvokedCallback.class);
ArgumentCaptor<BackEvent> backEventCaptor = ArgumentCaptor.forClass(BackEvent.class);
- createNavigationInfo(BackNavigationInfo.TYPE_RETURN_TO_HOME, appCallback);
+ createNavigationInfo(BackNavigationInfo.TYPE_RETURN_TO_HOME, false);
triggerBackGesture();
- verify(appCallback, never()).onBackStarted(any(BackEvent.class));
- verify(appCallback, never()).onBackProgressed(backEventCaptor.capture());
- verify(appCallback, times(1)).onBackInvoked();
+ verify(mAppCallback, never()).onBackStarted(any());
+ verify(mAppCallback, never()).onBackProgressed(backEventCaptor.capture());
+ verify(mAppCallback, times(1)).onBackInvoked();
- verify(mIOnBackInvokedCallback, never()).onBackStarted(any(BackEvent.class));
- verify(mIOnBackInvokedCallback, never()).onBackProgressed(backEventCaptor.capture());
- verify(mIOnBackInvokedCallback, never()).onBackInvoked();
+ verify(mAnimatorCallback, never()).onBackStarted(any());
+ verify(mAnimatorCallback, never()).onBackProgressed(backEventCaptor.capture());
+ verify(mAnimatorCallback, never()).onBackInvoked();
verify(mBackAnimationRunner, never()).onAnimationStart(
anyInt(), any(), any(), any(), any());
}
@Test
public void ignoresGesture_transitionInProgress() throws RemoteException {
- mController.setBackToLauncherCallback(mIOnBackInvokedCallback, mBackAnimationRunner);
- createNavigationInfo(BackNavigationInfo.TYPE_RETURN_TO_HOME, null);
+ registerAnimation(BackNavigationInfo.TYPE_RETURN_TO_HOME);
+ createNavigationInfo(BackNavigationInfo.TYPE_RETURN_TO_HOME, true);
triggerBackGesture();
simulateRemoteAnimationStart(BackNavigationInfo.TYPE_RETURN_TO_HOME);
// Check that back invocation is dispatched.
- verify(mIOnBackInvokedCallback).onBackInvoked();
+ verify(mAnimatorCallback).onBackInvoked();
verify(mBackAnimationRunner).onAnimationStart(anyInt(), any(), any(), any(), any());
- reset(mIOnBackInvokedCallback);
+ reset(mAnimatorCallback);
reset(mBackAnimationRunner);
// Verify that we prevent animation from restarting if another gestures happens before
// the previous transition is finished.
doMotionEvent(MotionEvent.ACTION_DOWN, 0);
- verifyNoMoreInteractions(mIOnBackInvokedCallback);
- mController.onBackAnimationFinished();
- // Pretend the transition handler called finishAnimation.
- mController.finishBackNavigation();
+ verifyNoMoreInteractions(mAnimatorCallback);
+
+ // Finish back navigation.
+ simulateRemoteAnimationFinished();
// Verify that more events from a rejected swipe cannot start animation.
doMotionEvent(MotionEvent.ACTION_MOVE, 100);
doMotionEvent(MotionEvent.ACTION_UP, 0);
- verifyNoMoreInteractions(mIOnBackInvokedCallback);
+ verifyNoMoreInteractions(mAnimatorCallback);
// Verify that we start accepting gestures again once transition finishes.
doMotionEvent(MotionEvent.ACTION_DOWN, 0);
doMotionEvent(MotionEvent.ACTION_MOVE, 100);
simulateRemoteAnimationStart(BackNavigationInfo.TYPE_RETURN_TO_HOME);
- verify(mIOnBackInvokedCallback).onBackStarted(any(BackEvent.class));
+ verify(mAnimatorCallback).onBackStarted(any());
verify(mBackAnimationRunner).onAnimationStart(anyInt(), any(), any(), any(), any());
}
@Test
public void acceptsGesture_transitionTimeout() throws RemoteException {
- mController.setBackToLauncherCallback(mIOnBackInvokedCallback, mBackAnimationRunner);
- createNavigationInfo(BackNavigationInfo.TYPE_RETURN_TO_HOME, null);
+ registerAnimation(BackNavigationInfo.TYPE_RETURN_TO_HOME);
+ createNavigationInfo(BackNavigationInfo.TYPE_RETURN_TO_HOME, true);
+
+ // In case it is still running in animation.
+ doNothing().when(mAnimatorCallback).onBackInvoked();
triggerBackGesture();
simulateRemoteAnimationStart(BackNavigationInfo.TYPE_RETURN_TO_HOME);
- reset(mIOnBackInvokedCallback);
-
// Simulate transition timeout.
mShellExecutor.flushAll();
- mController.onBackAnimationFinished();
- // Pretend the transition handler called finishAnimation.
- mController.finishBackNavigation();
+ reset(mAnimatorCallback);
doMotionEvent(MotionEvent.ACTION_DOWN, 0);
doMotionEvent(MotionEvent.ACTION_MOVE, 100);
simulateRemoteAnimationStart(BackNavigationInfo.TYPE_RETURN_TO_HOME);
- verify(mIOnBackInvokedCallback).onBackStarted(any(BackEvent.class));
+ verify(mAnimatorCallback).onBackStarted(any());
}
-
@Test
public void cancelBackInvokeWhenLostFocus() throws RemoteException {
- mController.setBackToLauncherCallback(mIOnBackInvokedCallback, mBackAnimationRunner);
+ registerAnimation(BackNavigationInfo.TYPE_RETURN_TO_HOME);
- createNavigationInfo(BackNavigationInfo.TYPE_RETURN_TO_HOME, null);
+ createNavigationInfo(BackNavigationInfo.TYPE_RETURN_TO_HOME, true);
doMotionEvent(MotionEvent.ACTION_DOWN, 0);
// Check that back start and progress is dispatched when first move.
doMotionEvent(MotionEvent.ACTION_MOVE, 100);
simulateRemoteAnimationStart(BackNavigationInfo.TYPE_RETURN_TO_HOME);
- verify(mIOnBackInvokedCallback).onBackStarted(any(BackEvent.class));
+ verify(mAnimatorCallback).onBackStarted(any());
verify(mBackAnimationRunner).onAnimationStart(anyInt(), any(), any(), any(), any());
// Check that back invocation is dispatched.
@@ -323,11 +344,11 @@
IBinder token = mock(IBinder.class);
mController.mFocusObserver.focusLost(token);
mShellExecutor.flushAll();
- verify(mIOnBackInvokedCallback).onBackCancelled();
+ verify(mAnimatorCallback).onBackCancelled();
// No more back invoke.
doMotionEvent(MotionEvent.ACTION_UP, 0);
- verify(mIOnBackInvokedCallback, never()).onBackInvoked();
+ verify(mAnimatorCallback, never()).onBackInvoked();
}
private void doMotionEvent(int actionDown, int coordinate) {
@@ -335,7 +356,6 @@
coordinate, coordinate,
actionDown,
BackEvent.EDGE_LEFT);
- mEventTime += 10;
}
private void simulateRemoteAnimationStart(int type) throws RemoteException {
@@ -347,4 +367,14 @@
mShellExecutor.flushAll();
}
}
+
+ private void simulateRemoteAnimationFinished() {
+ mController.onBackAnimationFinished();
+ mController.finishBackNavigation();
+ }
+
+ private void registerAnimation(int type) {
+ mController.registerAnimation(type,
+ new BackAnimationRunner(mAnimatorCallback, mBackAnimationRunner));
+ }
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayImeControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayImeControllerTest.java
index a6f19e7..40f2e88 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayImeControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayImeControllerTest.java
@@ -97,13 +97,13 @@
@Test
public void showInsets_schedulesNoWorkOnExecutor() {
- mPerDisplay.showInsets(ime(), true);
+ mPerDisplay.showInsets(ime(), true /* fromIme */, null /* statsToken */);
verifyZeroInteractions(mExecutor);
}
@Test
public void hideInsets_schedulesNoWorkOnExecutor() {
- mPerDisplay.hideInsets(ime(), true);
+ mPerDisplay.hideInsets(ime(), true /* fromIme */, null /* statsToken */);
verifyZeroInteractions(mExecutor);
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayInsetsControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayInsetsControllerTest.java
index 39db328..956f1cd 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayInsetsControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayInsetsControllerTest.java
@@ -25,6 +25,7 @@
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
+import android.annotation.Nullable;
import android.content.ComponentName;
import android.os.RemoteException;
import android.util.SparseArray;
@@ -33,6 +34,7 @@
import android.view.InsetsSourceControl;
import android.view.InsetsState;
import android.view.WindowInsets;
+import android.view.inputmethod.ImeTracker;
import androidx.test.filters.SmallTest;
@@ -111,8 +113,10 @@
WindowInsets.Type.defaultVisible());
mInsetsControllersByDisplayId.get(DEFAULT_DISPLAY).insetsChanged(null);
mInsetsControllersByDisplayId.get(DEFAULT_DISPLAY).insetsControlChanged(null, null);
- mInsetsControllersByDisplayId.get(DEFAULT_DISPLAY).showInsets(0, false);
- mInsetsControllersByDisplayId.get(DEFAULT_DISPLAY).hideInsets(0, false);
+ mInsetsControllersByDisplayId.get(DEFAULT_DISPLAY).showInsets(0, false,
+ null /* statsToken */);
+ mInsetsControllersByDisplayId.get(DEFAULT_DISPLAY).hideInsets(0, false,
+ null /* statsToken */);
mExecutor.flushAll();
assertTrue(defaultListener.topFocusedWindowChangedCount == 1);
@@ -131,8 +135,10 @@
WindowInsets.Type.defaultVisible());
mInsetsControllersByDisplayId.get(SECOND_DISPLAY).insetsChanged(null);
mInsetsControllersByDisplayId.get(SECOND_DISPLAY).insetsControlChanged(null, null);
- mInsetsControllersByDisplayId.get(SECOND_DISPLAY).showInsets(0, false);
- mInsetsControllersByDisplayId.get(SECOND_DISPLAY).hideInsets(0, false);
+ mInsetsControllersByDisplayId.get(SECOND_DISPLAY).showInsets(0, false,
+ null /* statsToken */);
+ mInsetsControllersByDisplayId.get(SECOND_DISPLAY).hideInsets(0, false,
+ null /* statsToken */);
mExecutor.flushAll();
assertTrue(defaultListener.topFocusedWindowChangedCount == 1);
@@ -191,12 +197,12 @@
}
@Override
- public void showInsets(int types, boolean fromIme) {
+ public void showInsets(int types, boolean fromIme, @Nullable ImeTracker.Token statsToken) {
showInsetsCount++;
}
@Override
- public void hideInsets(int types, boolean fromIme) {
+ public void hideInsets(int types, boolean fromIme, @Nullable ImeTracker.Token statsToken) {
hideInsetsCount++;
}
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeControllerTest.java
index 79b520c..89bafcb 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeControllerTest.java
@@ -16,10 +16,13 @@
package com.android.wm.shell.desktopmode;
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
import static android.app.WindowConfiguration.WINDOW_CONFIG_BOUNDS;
+import static android.view.WindowManager.TRANSIT_CHANGE;
import static android.view.WindowManager.TRANSIT_OPEN;
import static android.view.WindowManager.TRANSIT_TO_FRONT;
import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_REORDER;
@@ -30,13 +33,14 @@
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
-import android.app.ActivityManager;
+import android.app.ActivityManager.RunningTaskInfo;
import android.os.Binder;
import android.os.Handler;
import android.os.IBinder;
@@ -68,6 +72,9 @@
import org.mockito.Mock;
import org.mockito.Mockito;
+import java.util.ArrayList;
+import java.util.Arrays;
+
@SmallTest
@RunWith(AndroidTestingRunner.class)
public class DesktopModeControllerTest extends ShellTestCase {
@@ -83,9 +90,7 @@
@Mock
private Handler mMockHandler;
@Mock
- private Transitions mMockTransitions;
- private TestShellExecutor mExecutor;
-
+ private Transitions mTransitions;
private DesktopModeController mController;
private DesktopModeTaskRepository mDesktopModeTaskRepository;
private ShellInit mShellInit;
@@ -97,20 +102,19 @@
when(DesktopModeStatus.isActive(any())).thenReturn(true);
mShellInit = Mockito.spy(new ShellInit(mTestExecutor));
- mExecutor = new TestShellExecutor();
mDesktopModeTaskRepository = new DesktopModeTaskRepository();
mController = new DesktopModeController(mContext, mShellInit, mShellController,
- mShellTaskOrganizer, mRootTaskDisplayAreaOrganizer, mMockTransitions,
- mDesktopModeTaskRepository, mMockHandler, mExecutor);
+ mShellTaskOrganizer, mRootTaskDisplayAreaOrganizer, mTransitions,
+ mDesktopModeTaskRepository, mMockHandler, new TestShellExecutor());
- when(mShellTaskOrganizer.prepareClearFreeformForStandardTasks(anyInt())).thenReturn(
- new WindowContainerTransaction());
+ when(mShellTaskOrganizer.getRunningTasks(anyInt())).thenReturn(new ArrayList<>());
mShellInit.init();
clearInvocations(mShellTaskOrganizer);
clearInvocations(mRootTaskDisplayAreaOrganizer);
+ clearInvocations(mTransitions);
}
@After
@@ -124,113 +128,133 @@
}
@Test
- public void testDesktopModeEnabled_taskWmClearedDisplaySetToFreeform() {
- // Create a fake WCT to simulate setting task windowing mode to undefined
- WindowContainerTransaction taskWct = new WindowContainerTransaction();
- MockToken taskMockToken = new MockToken();
- taskWct.setWindowingMode(taskMockToken.token(), WINDOWING_MODE_UNDEFINED);
- when(mShellTaskOrganizer.prepareClearFreeformForStandardTasks(
- mContext.getDisplayId())).thenReturn(taskWct);
+ public void testDesktopModeEnabled_rootTdaSetToFreeform() {
+ DisplayAreaInfo displayAreaInfo = createMockDisplayArea();
- // Create a fake DisplayAreaInfo to check if windowing mode change is set correctly
- MockToken displayMockToken = new MockToken();
- DisplayAreaInfo displayAreaInfo = new DisplayAreaInfo(displayMockToken.mToken,
- mContext.getDisplayId(), 0);
- when(mRootTaskDisplayAreaOrganizer.getDisplayAreaInfo(mContext.getDisplayId()))
- .thenReturn(displayAreaInfo);
-
- // The test
mController.updateDesktopModeActive(true);
+ WindowContainerTransaction wct = getDesktopModeSwitchTransaction();
- ArgumentCaptor<WindowContainerTransaction> arg = ArgumentCaptor.forClass(
- WindowContainerTransaction.class);
- verify(mRootTaskDisplayAreaOrganizer).applyTransaction(arg.capture());
-
- // WCT should have 2 changes - clear task wm mode and set display wm mode
- WindowContainerTransaction wct = arg.getValue();
- assertThat(wct.getChanges()).hasSize(2);
-
- // Verify executed WCT has a change for setting task windowing mode to undefined
- Change taskWmModeChange = wct.getChanges().get(taskMockToken.binder());
- assertThat(taskWmModeChange).isNotNull();
- assertThat(taskWmModeChange.getWindowingMode()).isEqualTo(WINDOWING_MODE_UNDEFINED);
-
- // Verify executed WCT has a change for setting display windowing mode to freeform
- Change displayWmModeChange = wct.getChanges().get(displayAreaInfo.token.asBinder());
- assertThat(displayWmModeChange).isNotNull();
- assertThat(displayWmModeChange.getWindowingMode()).isEqualTo(WINDOWING_MODE_FREEFORM);
+ // 1 change: Root TDA windowing mode
+ assertThat(wct.getChanges().size()).isEqualTo(1);
+ // Verify WCT has a change for setting windowing mode to freeform
+ Change change = wct.getChanges().get(displayAreaInfo.token.asBinder());
+ assertThat(change).isNotNull();
+ assertThat(change.getWindowingMode()).isEqualTo(WINDOWING_MODE_FREEFORM);
}
@Test
- public void testDesktopModeDisabled_taskWmAndBoundsClearedDisplaySetToFullscreen() {
- // Create a fake WCT to simulate setting task windowing mode to undefined
- WindowContainerTransaction taskWmWct = new WindowContainerTransaction();
- MockToken taskWmMockToken = new MockToken();
- taskWmWct.setWindowingMode(taskWmMockToken.token(), WINDOWING_MODE_UNDEFINED);
- when(mShellTaskOrganizer.prepareClearFreeformForStandardTasks(
- mContext.getDisplayId())).thenReturn(taskWmWct);
+ public void testDesktopModeDisabled_rootTdaSetToFullscreen() {
+ DisplayAreaInfo displayAreaInfo = createMockDisplayArea();
- // Create a fake WCT to simulate clearing task bounds
- WindowContainerTransaction taskBoundsWct = new WindowContainerTransaction();
- MockToken taskBoundsMockToken = new MockToken();
- taskBoundsWct.setBounds(taskBoundsMockToken.token(), null);
- when(mShellTaskOrganizer.prepareClearBoundsForStandardTasks(
- mContext.getDisplayId())).thenReturn(taskBoundsWct);
-
- // Create a fake DisplayAreaInfo to check if windowing mode change is set correctly
- MockToken displayMockToken = new MockToken();
- DisplayAreaInfo displayAreaInfo = new DisplayAreaInfo(displayMockToken.mToken,
- mContext.getDisplayId(), 0);
- when(mRootTaskDisplayAreaOrganizer.getDisplayAreaInfo(mContext.getDisplayId()))
- .thenReturn(displayAreaInfo);
-
- // The test
mController.updateDesktopModeActive(false);
+ WindowContainerTransaction wct = getDesktopModeSwitchTransaction();
- ArgumentCaptor<WindowContainerTransaction> arg = ArgumentCaptor.forClass(
- WindowContainerTransaction.class);
- verify(mRootTaskDisplayAreaOrganizer).applyTransaction(arg.capture());
+ // 1 change: Root TDA windowing mode
+ assertThat(wct.getChanges().size()).isEqualTo(1);
+ // Verify WCT has a change for setting windowing mode to fullscreen
+ Change change = wct.getChanges().get(displayAreaInfo.token.asBinder());
+ assertThat(change).isNotNull();
+ assertThat(change.getWindowingMode()).isEqualTo(WINDOWING_MODE_FULLSCREEN);
+ }
- // WCT should have 3 changes - clear task wm mode and bounds and set display wm mode
- WindowContainerTransaction wct = arg.getValue();
- assertThat(wct.getChanges()).hasSize(3);
+ @Test
+ public void testDesktopModeEnabled_windowingModeCleared() {
+ createMockDisplayArea();
+ RunningTaskInfo freeformTask = createFreeformTask();
+ RunningTaskInfo fullscreenTask = createFullscreenTask();
+ RunningTaskInfo homeTask = createHomeTask();
+ when(mShellTaskOrganizer.getRunningTasks(anyInt())).thenReturn(new ArrayList<>(
+ Arrays.asList(freeformTask, fullscreenTask, homeTask)));
- // Verify executed WCT has a change for setting task windowing mode to undefined
- Change taskWmMode = wct.getChanges().get(taskWmMockToken.binder());
- assertThat(taskWmMode).isNotNull();
- assertThat(taskWmMode.getWindowingMode()).isEqualTo(WINDOWING_MODE_UNDEFINED);
+ mController.updateDesktopModeActive(true);
+ WindowContainerTransaction wct = getDesktopModeSwitchTransaction();
- // Verify executed WCT has a change for clearing task bounds
- Change bounds = wct.getChanges().get(taskBoundsMockToken.binder());
- assertThat(bounds).isNotNull();
- assertThat(bounds.getWindowSetMask() & WINDOW_CONFIG_BOUNDS).isNotEqualTo(0);
- assertThat(bounds.getConfiguration().windowConfiguration.getBounds().isEmpty()).isTrue();
+ // 2 changes: Root TDA windowing mode and 1 task
+ assertThat(wct.getChanges().size()).isEqualTo(2);
+ // No changes for tasks that are not standard or freeform
+ assertThat(wct.getChanges().get(fullscreenTask.token.asBinder())).isNull();
+ assertThat(wct.getChanges().get(homeTask.token.asBinder())).isNull();
+ // Standard freeform task has windowing mode cleared
+ Change change = wct.getChanges().get(freeformTask.token.asBinder());
+ assertThat(change).isNotNull();
+ assertThat(change.getWindowingMode()).isEqualTo(WINDOWING_MODE_UNDEFINED);
+ }
- // Verify executed WCT has a change for setting display windowing mode to fullscreen
- Change displayWmModeChange = wct.getChanges().get(displayAreaInfo.token.asBinder());
- assertThat(displayWmModeChange).isNotNull();
- assertThat(displayWmModeChange.getWindowingMode()).isEqualTo(WINDOWING_MODE_FULLSCREEN);
+ @Test
+ public void testDesktopModeDisabled_windowingModeAndBoundsCleared() {
+ createMockDisplayArea();
+ RunningTaskInfo freeformTask = createFreeformTask();
+ RunningTaskInfo fullscreenTask = createFullscreenTask();
+ RunningTaskInfo homeTask = createHomeTask();
+ when(mShellTaskOrganizer.getRunningTasks(anyInt())).thenReturn(new ArrayList<>(
+ Arrays.asList(freeformTask, fullscreenTask, homeTask)));
+
+ mController.updateDesktopModeActive(false);
+ WindowContainerTransaction wct = getDesktopModeSwitchTransaction();
+
+ // 3 changes: Root TDA windowing mode and 2 tasks
+ assertThat(wct.getChanges().size()).isEqualTo(3);
+ // No changes to home task
+ assertThat(wct.getChanges().get(homeTask.token.asBinder())).isNull();
+ // Standard tasks have bounds cleared
+ assertThatBoundsCleared(wct.getChanges().get(freeformTask.token.asBinder()));
+ assertThatBoundsCleared(wct.getChanges().get(fullscreenTask.token.asBinder()));
+ // Freeform standard tasks have windowing mode cleared
+ assertThat(wct.getChanges().get(
+ freeformTask.token.asBinder()).getWindowingMode()).isEqualTo(
+ WINDOWING_MODE_UNDEFINED);
+ }
+
+ @Test
+ public void testDesktopModeEnabled_homeTaskBehindVisibleTask() {
+ createMockDisplayArea();
+ RunningTaskInfo fullscreenTask1 = createFullscreenTask();
+ fullscreenTask1.isVisible = true;
+ RunningTaskInfo fullscreenTask2 = createFullscreenTask();
+ fullscreenTask2.isVisible = false;
+ RunningTaskInfo homeTask = createHomeTask();
+ when(mShellTaskOrganizer.getRunningTasks(anyInt())).thenReturn(new ArrayList<>(
+ Arrays.asList(fullscreenTask1, fullscreenTask2, homeTask)));
+
+ mController.updateDesktopModeActive(true);
+ WindowContainerTransaction wct = getDesktopModeSwitchTransaction();
+
+ // Check that there are hierarchy changes for home task and visible task
+ assertThat(wct.getHierarchyOps()).hasSize(2);
+ // First show home task
+ WindowContainerTransaction.HierarchyOp op1 = wct.getHierarchyOps().get(0);
+ assertThat(op1.getType()).isEqualTo(HIERARCHY_OP_TYPE_REORDER);
+ assertThat(op1.getContainer()).isEqualTo(homeTask.token.asBinder());
+
+ // Then visible task on top of it
+ WindowContainerTransaction.HierarchyOp op2 = wct.getHierarchyOps().get(1);
+ assertThat(op2.getType()).isEqualTo(HIERARCHY_OP_TYPE_REORDER);
+ assertThat(op2.getContainer()).isEqualTo(fullscreenTask1.token.asBinder());
}
@Test
public void testShowDesktopApps() {
// Set up two active tasks on desktop
- mDesktopModeTaskRepository.addActiveTask(1);
- mDesktopModeTaskRepository.addActiveTask(2);
- MockToken token1 = new MockToken();
- MockToken token2 = new MockToken();
- ActivityManager.RunningTaskInfo taskInfo1 = new TestRunningTaskInfoBuilder().setToken(
- token1.token()).setLastActiveTime(100).build();
- ActivityManager.RunningTaskInfo taskInfo2 = new TestRunningTaskInfoBuilder().setToken(
- token2.token()).setLastActiveTime(200).build();
- when(mShellTaskOrganizer.getRunningTaskInfo(1)).thenReturn(taskInfo1);
- when(mShellTaskOrganizer.getRunningTaskInfo(2)).thenReturn(taskInfo2);
+ RunningTaskInfo freeformTask1 = createFreeformTask();
+ freeformTask1.lastActiveTime = 100;
+ RunningTaskInfo freeformTask2 = createFreeformTask();
+ freeformTask2.lastActiveTime = 200;
+ mDesktopModeTaskRepository.addActiveTask(freeformTask1.taskId);
+ mDesktopModeTaskRepository.addActiveTask(freeformTask2.taskId);
+ when(mShellTaskOrganizer.getRunningTaskInfo(freeformTask1.taskId)).thenReturn(
+ freeformTask1);
+ when(mShellTaskOrganizer.getRunningTaskInfo(freeformTask2.taskId)).thenReturn(
+ freeformTask2);
// Run show desktop apps logic
mController.showDesktopApps();
ArgumentCaptor<WindowContainerTransaction> wctCaptor = ArgumentCaptor.forClass(
WindowContainerTransaction.class);
- verify(mShellTaskOrganizer).applyTransaction(wctCaptor.capture());
+ if (Transitions.ENABLE_SHELL_TRANSITIONS) {
+ verify(mTransitions).startTransition(eq(TRANSIT_TO_FRONT), wctCaptor.capture(), any());
+ } else {
+ verify(mShellTaskOrganizer).applyTransaction(wctCaptor.capture());
+ }
WindowContainerTransaction wct = wctCaptor.getValue();
// Check wct has reorder calls
@@ -239,12 +263,12 @@
// Task 2 has activity later, must be first
WindowContainerTransaction.HierarchyOp op1 = wct.getHierarchyOps().get(0);
assertThat(op1.getType()).isEqualTo(HIERARCHY_OP_TYPE_REORDER);
- assertThat(op1.getContainer()).isEqualTo(token2.binder());
+ assertThat(op1.getContainer()).isEqualTo(freeformTask2.token.asBinder());
// Task 1 should be second
- WindowContainerTransaction.HierarchyOp op2 = wct.getHierarchyOps().get(0);
+ WindowContainerTransaction.HierarchyOp op2 = wct.getHierarchyOps().get(1);
assertThat(op2.getType()).isEqualTo(HIERARCHY_OP_TYPE_REORDER);
- assertThat(op2.getContainer()).isEqualTo(token2.binder());
+ assertThat(op2.getContainer()).isEqualTo(freeformTask1.token.asBinder());
}
@Test
@@ -266,7 +290,7 @@
@Test
public void testHandleTransitionRequest_notFreeform_returnsNull() {
- ActivityManager.RunningTaskInfo trigger = new ActivityManager.RunningTaskInfo();
+ RunningTaskInfo trigger = new RunningTaskInfo();
trigger.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN);
WindowContainerTransaction wct = mController.handleRequest(
new Binder(),
@@ -276,7 +300,7 @@
@Test
public void testHandleTransitionRequest_returnsWct() {
- ActivityManager.RunningTaskInfo trigger = new ActivityManager.RunningTaskInfo();
+ RunningTaskInfo trigger = new RunningTaskInfo();
trigger.token = new MockToken().mToken;
trigger.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FREEFORM);
WindowContainerTransaction wct = mController.handleRequest(
@@ -285,6 +309,57 @@
assertThat(wct).isNotNull();
}
+ private DisplayAreaInfo createMockDisplayArea() {
+ DisplayAreaInfo displayAreaInfo = new DisplayAreaInfo(new MockToken().mToken,
+ mContext.getDisplayId(), 0);
+ when(mRootTaskDisplayAreaOrganizer.getDisplayAreaInfo(mContext.getDisplayId()))
+ .thenReturn(displayAreaInfo);
+ return displayAreaInfo;
+ }
+
+ private RunningTaskInfo createFreeformTask() {
+ return new TestRunningTaskInfoBuilder()
+ .setToken(new MockToken().token())
+ .setActivityType(ACTIVITY_TYPE_STANDARD)
+ .setWindowingMode(WINDOWING_MODE_FREEFORM)
+ .setLastActiveTime(100)
+ .build();
+ }
+
+ private RunningTaskInfo createFullscreenTask() {
+ return new TestRunningTaskInfoBuilder()
+ .setToken(new MockToken().token())
+ .setActivityType(ACTIVITY_TYPE_STANDARD)
+ .setWindowingMode(WINDOWING_MODE_FULLSCREEN)
+ .setLastActiveTime(100)
+ .build();
+ }
+
+ private RunningTaskInfo createHomeTask() {
+ return new TestRunningTaskInfoBuilder()
+ .setToken(new MockToken().token())
+ .setActivityType(ACTIVITY_TYPE_HOME)
+ .setWindowingMode(WINDOWING_MODE_FULLSCREEN)
+ .setLastActiveTime(100)
+ .build();
+ }
+
+ private WindowContainerTransaction getDesktopModeSwitchTransaction() {
+ ArgumentCaptor<WindowContainerTransaction> arg = ArgumentCaptor.forClass(
+ WindowContainerTransaction.class);
+ if (Transitions.ENABLE_SHELL_TRANSITIONS) {
+ verify(mTransitions).startTransition(eq(TRANSIT_CHANGE), arg.capture(), any());
+ } else {
+ verify(mRootTaskDisplayAreaOrganizer).applyTransaction(arg.capture());
+ }
+ return arg.getValue();
+ }
+
+ private void assertThatBoundsCleared(Change change) {
+ assertThat((change.getWindowSetMask() & WINDOW_CONFIG_BOUNDS) != 0).isTrue();
+ assertThat(change.getConfiguration().windowConfiguration.getBounds().isEmpty()).isTrue();
+ }
+
private static class MockToken {
private final WindowContainerToken mToken;
private final IBinder mBinder;
@@ -298,9 +373,5 @@
WindowContainerToken token() {
return mToken;
}
-
- IBinder binder() {
- return mBinder;
- }
}
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java
index d01f3d3..38b75f8 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java
@@ -16,18 +16,24 @@
package com.android.wm.shell.splitscreen;
+import static android.app.PendingIntent.FLAG_IMMUTABLE;
import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
+import static android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK;
+import static android.content.Intent.FLAG_ACTIVITY_NO_USER_ACTION;
import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT;
import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assume.assumeTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.isNull;
+import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
@@ -35,6 +41,8 @@
import static org.mockito.Mockito.when;
import android.app.ActivityManager;
+import android.app.ActivityTaskManager;
+import android.app.PendingIntent;
import android.content.ComponentName;
import android.content.Intent;
import android.content.pm.ActivityInfo;
@@ -65,11 +73,11 @@
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
-import java.util.Optional;
-
/**
* Tests for {@link SplitScreenController}
*/
@@ -91,18 +99,21 @@
@Mock Transitions mTransitions;
@Mock TransactionPool mTransactionPool;
@Mock IconProvider mIconProvider;
- @Mock Optional<RecentTasksController> mRecentTasks;
+ @Mock StageCoordinator mStageCoordinator;
+ @Mock RecentTasksController mRecentTasks;
+ @Captor ArgumentCaptor<Intent> mIntentCaptor;
private SplitScreenController mSplitScreenController;
@Before
public void setup() {
+ assumeTrue(ActivityTaskManager.supportsSplitScreenMultiWindow(mContext));
MockitoAnnotations.initMocks(this);
mSplitScreenController = spy(new SplitScreenController(mContext, mShellInit,
mShellCommandHandler, mShellController, mTaskOrganizer, mSyncQueue,
mRootTDAOrganizer, mDisplayController, mDisplayImeController,
mDisplayInsetsController, mDragAndDropController, mTransitions, mTransactionPool,
- mIconProvider, mRecentTasks, mMainExecutor));
+ mIconProvider, mRecentTasks, mMainExecutor, mStageCoordinator));
}
@Test
@@ -148,58 +159,100 @@
}
@Test
- public void testShouldAddMultipleTaskFlag_notInSplitScreen() {
- doReturn(false).when(mSplitScreenController).isSplitScreenVisible();
- doReturn(true).when(mSplitScreenController).isValidToEnterSplitScreen(any());
-
- // Verify launching the same activity returns true.
+ public void testStartIntent_appendsNoUserActionFlag() {
Intent startIntent = createStartIntent("startActivity");
- ActivityManager.RunningTaskInfo focusTaskInfo =
- createTaskInfo(WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD, startIntent);
- doReturn(focusTaskInfo).when(mSplitScreenController).getFocusingTaskInfo();
- assertTrue(mSplitScreenController.shouldAddMultipleTaskFlag(
- startIntent, SPLIT_POSITION_TOP_OR_LEFT));
+ PendingIntent pendingIntent =
+ PendingIntent.getActivity(mContext, 0, startIntent, FLAG_IMMUTABLE);
- // Verify launching different activity returns false.
- Intent diffIntent = createStartIntent("diffActivity");
- focusTaskInfo =
- createTaskInfo(WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD, diffIntent);
- doReturn(focusTaskInfo).when(mSplitScreenController).getFocusingTaskInfo();
- assertFalse(mSplitScreenController.shouldAddMultipleTaskFlag(
- startIntent, SPLIT_POSITION_TOP_OR_LEFT));
+ mSplitScreenController.startIntent(pendingIntent, null, SPLIT_POSITION_TOP_OR_LEFT, null);
+
+ verify(mStageCoordinator).startIntent(eq(pendingIntent), mIntentCaptor.capture(),
+ eq(SPLIT_POSITION_TOP_OR_LEFT), isNull());
+ assertEquals(FLAG_ACTIVITY_NO_USER_ACTION,
+ mIntentCaptor.getValue().getFlags() & FLAG_ACTIVITY_NO_USER_ACTION);
}
@Test
- public void testShouldAddMultipleTaskFlag_inSplitScreen() {
- doReturn(true).when(mSplitScreenController).isSplitScreenVisible();
+ public void startIntent_multiInstancesSupported_appendsMultipleTaskFag() {
+ doReturn(true).when(mSplitScreenController).supportMultiInstancesSplit(any());
Intent startIntent = createStartIntent("startActivity");
+ PendingIntent pendingIntent =
+ PendingIntent.getActivity(mContext, 0, startIntent, FLAG_IMMUTABLE);
+ // Put the same component into focus task
+ ActivityManager.RunningTaskInfo focusTaskInfo =
+ createTaskInfo(WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD, startIntent);
+ doReturn(focusTaskInfo).when(mStageCoordinator).getFocusingTaskInfo();
+ doReturn(true).when(mStageCoordinator).isValidToEnterSplitScreen(any());
+
+ mSplitScreenController.startIntent(pendingIntent, null, SPLIT_POSITION_TOP_OR_LEFT, null);
+
+ verify(mStageCoordinator).startIntent(eq(pendingIntent), mIntentCaptor.capture(),
+ eq(SPLIT_POSITION_TOP_OR_LEFT), isNull());
+ assertEquals(FLAG_ACTIVITY_MULTIPLE_TASK,
+ mIntentCaptor.getValue().getFlags() & FLAG_ACTIVITY_MULTIPLE_TASK);
+ }
+
+ @Test
+ public void startIntent_multiInstancesSupported_startTaskInBackgroundBeforeSplitActivated() {
+ doReturn(true).when(mSplitScreenController).supportMultiInstancesSplit(any());
+ doNothing().when(mSplitScreenController).startTask(anyInt(), anyInt(), any());
+ Intent startIntent = createStartIntent("startActivity");
+ PendingIntent pendingIntent =
+ PendingIntent.getActivity(mContext, 0, startIntent, FLAG_IMMUTABLE);
+ // Put the same component into focus task
+ ActivityManager.RunningTaskInfo focusTaskInfo =
+ createTaskInfo(WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD, startIntent);
+ doReturn(focusTaskInfo).when(mStageCoordinator).getFocusingTaskInfo();
+ doReturn(true).when(mStageCoordinator).isValidToEnterSplitScreen(any());
+ // Put the same component into a task in the background
+ ActivityManager.RecentTaskInfo sameTaskInfo = new ActivityManager.RecentTaskInfo();
+ doReturn(sameTaskInfo).when(mRecentTasks).findTaskInBackground(any());
+
+ mSplitScreenController.startIntent(pendingIntent, null, SPLIT_POSITION_TOP_OR_LEFT, null);
+
+ verify(mSplitScreenController).startTask(anyInt(), eq(SPLIT_POSITION_TOP_OR_LEFT),
+ isNull());
+ }
+
+ @Test
+ public void startIntent_multiInstancesSupported_startTaskInBackgroundAfterSplitActivated() {
+ doReturn(true).when(mSplitScreenController).supportMultiInstancesSplit(any());
+ doNothing().when(mSplitScreenController).startTask(anyInt(), anyInt(), any());
+ Intent startIntent = createStartIntent("startActivity");
+ PendingIntent pendingIntent =
+ PendingIntent.getActivity(mContext, 0, startIntent, FLAG_IMMUTABLE);
+ // Put the same component into another side of the split
+ doReturn(true).when(mSplitScreenController).isSplitScreenVisible();
ActivityManager.RunningTaskInfo sameTaskInfo =
createTaskInfo(WINDOWING_MODE_MULTI_WINDOW, ACTIVITY_TYPE_STANDARD, startIntent);
- Intent diffIntent = createStartIntent("diffActivity");
- ActivityManager.RunningTaskInfo differentTaskInfo =
- createTaskInfo(WINDOWING_MODE_MULTI_WINDOW, ACTIVITY_TYPE_STANDARD, diffIntent);
+ doReturn(sameTaskInfo).when(mSplitScreenController).getTaskInfo(
+ SPLIT_POSITION_BOTTOM_OR_RIGHT);
+ // Put the same component into a task in the background
+ doReturn(new ActivityManager.RecentTaskInfo()).when(mRecentTasks)
+ .findTaskInBackground(any());
- // Verify launching the same activity return false.
- doReturn(sameTaskInfo).when(mSplitScreenController)
- .getTaskInfo(SPLIT_POSITION_TOP_OR_LEFT);
- assertFalse(mSplitScreenController.shouldAddMultipleTaskFlag(
- startIntent, SPLIT_POSITION_TOP_OR_LEFT));
+ mSplitScreenController.startIntent(pendingIntent, null, SPLIT_POSITION_TOP_OR_LEFT, null);
- // Verify launching the same activity as adjacent returns true.
- doReturn(differentTaskInfo).when(mSplitScreenController)
- .getTaskInfo(SPLIT_POSITION_TOP_OR_LEFT);
- doReturn(sameTaskInfo).when(mSplitScreenController)
- .getTaskInfo(SPLIT_POSITION_BOTTOM_OR_RIGHT);
- assertTrue(mSplitScreenController.shouldAddMultipleTaskFlag(
- startIntent, SPLIT_POSITION_TOP_OR_LEFT));
+ verify(mSplitScreenController).startTask(anyInt(), eq(SPLIT_POSITION_TOP_OR_LEFT),
+ isNull());
+ }
- // Verify launching different activity from adjacent returns false.
- doReturn(differentTaskInfo).when(mSplitScreenController)
- .getTaskInfo(SPLIT_POSITION_TOP_OR_LEFT);
- doReturn(differentTaskInfo).when(mSplitScreenController)
- .getTaskInfo(SPLIT_POSITION_BOTTOM_OR_RIGHT);
- assertFalse(mSplitScreenController.shouldAddMultipleTaskFlag(
- startIntent, SPLIT_POSITION_TOP_OR_LEFT));
+ @Test
+ public void startIntent_multiInstancesNotSupported_switchesPositionAfterSplitActivated() {
+ doReturn(false).when(mSplitScreenController).supportMultiInstancesSplit(any());
+ Intent startIntent = createStartIntent("startActivity");
+ PendingIntent pendingIntent =
+ PendingIntent.getActivity(mContext, 0, startIntent, FLAG_IMMUTABLE);
+ // Put the same component into another side of the split
+ doReturn(true).when(mSplitScreenController).isSplitScreenVisible();
+ ActivityManager.RunningTaskInfo sameTaskInfo =
+ createTaskInfo(WINDOWING_MODE_MULTI_WINDOW, ACTIVITY_TYPE_STANDARD, startIntent);
+ doReturn(sameTaskInfo).when(mSplitScreenController).getTaskInfo(
+ SPLIT_POSITION_BOTTOM_OR_RIGHT);
+
+ mSplitScreenController.startIntent(pendingIntent, null, SPLIT_POSITION_TOP_OR_LEFT, null);
+
+ verify(mStageCoordinator).switchSplitPosition(anyString());
}
private Intent createStartIntent(String activityName) {
diff --git a/libs/androidfw/Android.bp b/libs/androidfw/Android.bp
index eb8d26a..b1f327c 100644
--- a/libs/androidfw/Android.bp
+++ b/libs/androidfw/Android.bp
@@ -211,6 +211,8 @@
"tests/data/**/*.apk",
"tests/data/**/*.arsc",
"tests/data/**/*.idmap",
+ ":FrameworkResourcesSparseTestApp",
+ ":FrameworkResourcesNotSparseTestApp",
],
test_suites: ["device-tests"],
}
diff --git a/libs/androidfw/tests/LoadedArsc_test.cpp b/libs/androidfw/tests/LoadedArsc_test.cpp
index d214e2d..c90ec19 100644
--- a/libs/androidfw/tests/LoadedArsc_test.cpp
+++ b/libs/androidfw/tests/LoadedArsc_test.cpp
@@ -71,62 +71,6 @@
ASSERT_TRUE(LoadedPackage::GetEntry(type.type, entry_index).has_value());
}
-TEST(LoadedArscTest, LoadSparseEntryApp) {
- std::string contents;
- ASSERT_TRUE(ReadFileFromZipToString(GetTestDataPath() + "/sparse/sparse.apk", "resources.arsc",
- &contents));
-
- std::unique_ptr<const LoadedArsc> loaded_arsc = LoadedArsc::Load(contents.data(),
- contents.length());
- ASSERT_THAT(loaded_arsc, NotNull());
-
- const LoadedPackage* package =
- loaded_arsc->GetPackageById(get_package_id(sparse::R::integer::foo_9));
- ASSERT_THAT(package, NotNull());
-
- const uint8_t type_index = get_type_id(sparse::R::integer::foo_9) - 1;
- const uint16_t entry_index = get_entry_id(sparse::R::integer::foo_9);
-
- const TypeSpec* type_spec = package->GetTypeSpecByTypeIndex(type_index);
- ASSERT_THAT(type_spec, NotNull());
- ASSERT_THAT(type_spec->type_entries.size(), Ge(1u));
-
- auto type = type_spec->type_entries[0];
- ASSERT_TRUE(LoadedPackage::GetEntry(type.type, entry_index).has_value());
-}
-
-TEST(LoadedArscTest, FindSparseEntryApp) {
- std::string contents;
- ASSERT_TRUE(ReadFileFromZipToString(GetTestDataPath() + "/sparse/sparse.apk", "resources.arsc",
- &contents));
-
- std::unique_ptr<const LoadedArsc> loaded_arsc = LoadedArsc::Load(contents.data(),
- contents.length());
- ASSERT_THAT(loaded_arsc, NotNull());
-
- const LoadedPackage* package =
- loaded_arsc->GetPackageById(get_package_id(sparse::R::string::only_v26));
- ASSERT_THAT(package, NotNull());
-
- const uint8_t type_index = get_type_id(sparse::R::string::only_v26) - 1;
- const uint16_t entry_index = get_entry_id(sparse::R::string::only_v26);
-
- const TypeSpec* type_spec = package->GetTypeSpecByTypeIndex(type_index);
- ASSERT_THAT(type_spec, NotNull());
- ASSERT_THAT(type_spec->type_entries.size(), Ge(1u));
-
- // Ensure that AAPT2 sparsely encoded the v26 config as expected.
- auto type_entry = std::find_if(
- type_spec->type_entries.begin(), type_spec->type_entries.end(),
- [](const TypeSpec::TypeEntry& x) { return x.config.sdkVersion == 26; });
- ASSERT_NE(type_entry, type_spec->type_entries.end());
- ASSERT_NE(type_entry->type->flags & ResTable_type::FLAG_SPARSE, 0);
-
- // Test fetching a resource with only sparsely encoded configs by name.
- auto id = package->FindEntryByName(u"string", u"only_v26");
- ASSERT_EQ(id.value(), fix_package_id(sparse::R::string::only_v26, 0));
-}
-
TEST(LoadedArscTest, LoadSharedLibrary) {
std::string contents;
ASSERT_TRUE(ReadFileFromZipToString(GetTestDataPath() + "/lib_one/lib_one.apk", "resources.arsc",
@@ -404,4 +348,84 @@
// sizeof(Res_value) might not be backwards compatible.
// TEST(LoadedArscTest, LoadingShouldBeForwardsAndBackwardsCompatible) { ASSERT_TRUE(false); }
+class LoadedArscParameterizedTest :
+ public testing::TestWithParam<std::string> {
+};
+
+TEST_P(LoadedArscParameterizedTest, LoadSparseEntryApp) {
+ std::string contents;
+ ASSERT_TRUE(ReadFileFromZipToString(GetParam(), "resources.arsc", &contents));
+
+ std::unique_ptr<const LoadedArsc> loaded_arsc = LoadedArsc::Load(contents.data(),
+ contents.length());
+ ASSERT_THAT(loaded_arsc, NotNull());
+
+ const LoadedPackage* package =
+ loaded_arsc->GetPackageById(get_package_id(sparse::R::integer::foo_9));
+ ASSERT_THAT(package, NotNull());
+
+ const uint8_t type_index = get_type_id(sparse::R::integer::foo_9) - 1;
+ const uint16_t entry_index = get_entry_id(sparse::R::integer::foo_9);
+
+ const TypeSpec* type_spec = package->GetTypeSpecByTypeIndex(type_index);
+ ASSERT_THAT(type_spec, NotNull());
+ ASSERT_THAT(type_spec->type_entries.size(), Ge(1u));
+
+ auto type = type_spec->type_entries[0];
+ ASSERT_TRUE(LoadedPackage::GetEntry(type.type, entry_index).has_value());
+}
+
+TEST_P(LoadedArscParameterizedTest, FindSparseEntryApp) {
+ std::string contents;
+ ASSERT_TRUE(ReadFileFromZipToString(GetParam(), "resources.arsc", &contents));
+
+ std::unique_ptr<const LoadedArsc> loaded_arsc = LoadedArsc::Load(contents.data(),
+ contents.length());
+ ASSERT_THAT(loaded_arsc, NotNull());
+
+ const LoadedPackage* package =
+ loaded_arsc->GetPackageById(get_package_id(sparse::R::string::only_land));
+ ASSERT_THAT(package, NotNull());
+
+ const uint8_t type_index = get_type_id(sparse::R::string::only_land) - 1;
+
+ const TypeSpec* type_spec = package->GetTypeSpecByTypeIndex(type_index);
+ ASSERT_THAT(type_spec, NotNull());
+ ASSERT_THAT(type_spec->type_entries.size(), Ge(1u));
+
+ // Type Entry with default orientation is not sparse encoded because the ratio of
+ // populated entries to total entries is above threshold.
+ // Only find out default locale because Soong build system will introduce pseudo
+ // locales for the apk generated at runtime.
+ auto type_entry_default = std::find_if(
+ type_spec->type_entries.begin(), type_spec->type_entries.end(),
+ [] (const TypeSpec::TypeEntry& x) { return x.config.orientation == 0 &&
+ x.config.locale == 0; });
+ ASSERT_NE(type_entry_default, type_spec->type_entries.end());
+ ASSERT_EQ(type_entry_default->type->flags & ResTable_type::FLAG_SPARSE, 0);
+
+ // Type Entry with land orientation is sparse encoded as expected.
+ // Only find out default locale because Soong build system will introduce pseudo
+ // locales for the apk generated at runtime.
+ auto type_entry_land = std::find_if(
+ type_spec->type_entries.begin(), type_spec->type_entries.end(),
+ [](const TypeSpec::TypeEntry& x) { return x.config.orientation ==
+ ResTable_config::ORIENTATION_LAND &&
+ x.config.locale == 0; });
+ ASSERT_NE(type_entry_land, type_spec->type_entries.end());
+ ASSERT_NE(type_entry_land->type->flags & ResTable_type::FLAG_SPARSE, 0);
+
+ // Test fetching a resource with only sparsely encoded configs by name.
+ auto id = package->FindEntryByName(u"string", u"only_land");
+ ASSERT_EQ(id.value(), fix_package_id(sparse::R::string::only_land, 0));
+}
+
+INSTANTIATE_TEST_SUITE_P(
+ FrameWorkResourcesLoadedArscTests,
+ LoadedArscParameterizedTest,
+ ::testing::Values(
+ base::GetExecutableDirectory() + "/tests/data/sparse/sparse.apk",
+ base::GetExecutableDirectory() + "/FrameworkResourcesSparseTestApp.apk"
+ ));
+
} // namespace android
diff --git a/libs/androidfw/tests/ResTable_test.cpp b/libs/androidfw/tests/ResTable_test.cpp
index 9aeb00c..fbf7098 100644
--- a/libs/androidfw/tests/ResTable_test.cpp
+++ b/libs/androidfw/tests/ResTable_test.cpp
@@ -15,6 +15,7 @@
*/
#include "androidfw/ResourceTypes.h"
+#include "android-base/file.h"
#include <codecvt>
#include <locale>
@@ -41,34 +42,6 @@
ASSERT_EQ(NO_ERROR, table.add(contents.data(), contents.size()));
}
-TEST(ResTableTest, ShouldLoadSparseEntriesSuccessfully) {
- std::string contents;
- ASSERT_TRUE(ReadFileFromZipToString(GetTestDataPath() + "/sparse/sparse.apk", "resources.arsc",
- &contents));
-
- ResTable table;
- ASSERT_EQ(NO_ERROR, table.add(contents.data(), contents.size()));
-
- ResTable_config config;
- memset(&config, 0, sizeof(config));
- config.sdkVersion = 26;
- table.setParameters(&config);
-
- String16 name(u"com.android.sparse:integer/foo_9");
- uint32_t flags;
- uint32_t resid =
- table.identifierForName(name.string(), name.size(), nullptr, 0, nullptr, 0, &flags);
- ASSERT_NE(0u, resid);
-
- Res_value val;
- ResTable_config selected_config;
- ASSERT_GE(
- table.getResource(resid, &val, false /*mayBeBag*/, 0u /*density*/, &flags, &selected_config),
- 0);
- EXPECT_EQ(Res_value::TYPE_INT_DEC, val.dataType);
- EXPECT_EQ(900u, val.data);
-}
-
TEST(ResTableTest, SimpleTypeIsRetrievedCorrectly) {
std::string contents;
ASSERT_TRUE(ReadFileFromZipToString(GetTestDataPath() + "/basic/basic.apk",
@@ -476,4 +449,43 @@
ASSERT_FALSE(invalid_pool->stringAt(invalid_val.data).has_value());
}
+class ResTableParameterizedTest :
+ public testing::TestWithParam<std::string> {
+};
+
+TEST_P(ResTableParameterizedTest, ShouldLoadSparseEntriesSuccessfully) {
+ std::string contents;
+ ASSERT_TRUE(ReadFileFromZipToString(GetParam(), "resources.arsc", &contents));
+
+ ResTable table;
+ ASSERT_EQ(NO_ERROR, table.add(contents.data(), contents.size()));
+
+ ResTable_config config;
+ memset(&config, 0, sizeof(config));
+ config.orientation = ResTable_config::ORIENTATION_LAND;
+ table.setParameters(&config);
+
+ String16 name(u"com.android.sparse:integer/foo_9");
+ uint32_t flags;
+ uint32_t resid =
+ table.identifierForName(name.string(), name.size(), nullptr, 0, nullptr, 0, &flags);
+ ASSERT_NE(0u, resid);
+
+ Res_value val;
+ ResTable_config selected_config;
+ ASSERT_GE(
+ table.getResource(resid, &val, false /*mayBeBag*/, 0u /*density*/, &flags, &selected_config),
+ 0);
+ EXPECT_EQ(Res_value::TYPE_INT_DEC, val.dataType);
+ EXPECT_EQ(900u, val.data);
+}
+
+INSTANTIATE_TEST_SUITE_P(
+ FrameWorkResourcesResTableTests,
+ ResTableParameterizedTest,
+ ::testing::Values(
+ base::GetExecutableDirectory() + "/tests/data/sparse/sparse.apk",
+ base::GetExecutableDirectory() + "/FrameworkResourcesSparseTestApp.apk"
+ ));
+
} // namespace android
diff --git a/libs/androidfw/tests/SparseEntry_bench.cpp b/libs/androidfw/tests/SparseEntry_bench.cpp
index c9b4ad8..fffeeb8 100644
--- a/libs/androidfw/tests/SparseEntry_bench.cpp
+++ b/libs/androidfw/tests/SparseEntry_bench.cpp
@@ -16,6 +16,7 @@
#include "androidfw/AssetManager.h"
#include "androidfw/ResourceTypes.h"
+#include "android-base/file.h"
#include "BenchmarkHelpers.h"
#include "data/sparse/R.h"
@@ -24,40 +25,74 @@
namespace android {
+static void BM_SparseEntryGetResourceHelper(const std::vector<std::string>& paths,
+ uint32_t resid, benchmark::State& state, void (*GetResourceBenchmarkFunc)(
+ const std::vector<std::string>&, const ResTable_config*,
+ uint32_t, benchmark::State&)){
+ ResTable_config config;
+ memset(&config, 0, sizeof(config));
+ config.orientation = ResTable_config::ORIENTATION_LAND;
+ GetResourceBenchmarkFunc(paths, &config, resid, state);
+}
+
static void BM_SparseEntryGetResourceOldSparse(benchmark::State& state, uint32_t resid) {
- ResTable_config config;
- memset(&config, 0, sizeof(config));
- config.sdkVersion = 26;
- GetResourceBenchmarkOld({GetTestDataPath() + "/sparse/sparse.apk"}, &config, resid, state);
+ BM_SparseEntryGetResourceHelper({GetTestDataPath() + "/sparse/sparse.apk"}, resid,
+ state, &GetResourceBenchmarkOld);
}
BENCHMARK_CAPTURE(BM_SparseEntryGetResourceOldSparse, Small, sparse::R::integer::foo_9);
BENCHMARK_CAPTURE(BM_SparseEntryGetResourceOldSparse, Large, sparse::R::string::foo_999);
static void BM_SparseEntryGetResourceOldNotSparse(benchmark::State& state, uint32_t resid) {
- ResTable_config config;
- memset(&config, 0, sizeof(config));
- config.sdkVersion = 26;
- GetResourceBenchmarkOld({GetTestDataPath() + "/sparse/not_sparse.apk"}, &config, resid, state);
+ BM_SparseEntryGetResourceHelper({GetTestDataPath() + "/sparse/not_sparse.apk"}, resid,
+ state, &GetResourceBenchmarkOld);
}
BENCHMARK_CAPTURE(BM_SparseEntryGetResourceOldNotSparse, Small, sparse::R::integer::foo_9);
BENCHMARK_CAPTURE(BM_SparseEntryGetResourceOldNotSparse, Large, sparse::R::string::foo_999);
static void BM_SparseEntryGetResourceSparse(benchmark::State& state, uint32_t resid) {
- ResTable_config config;
- memset(&config, 0, sizeof(config));
- config.sdkVersion = 26;
- GetResourceBenchmark({GetTestDataPath() + "/sparse/sparse.apk"}, &config, resid, state);
+ BM_SparseEntryGetResourceHelper({GetTestDataPath() + "/sparse/sparse.apk"}, resid,
+ state, &GetResourceBenchmark);
}
BENCHMARK_CAPTURE(BM_SparseEntryGetResourceSparse, Small, sparse::R::integer::foo_9);
BENCHMARK_CAPTURE(BM_SparseEntryGetResourceSparse, Large, sparse::R::string::foo_999);
static void BM_SparseEntryGetResourceNotSparse(benchmark::State& state, uint32_t resid) {
- ResTable_config config;
- memset(&config, 0, sizeof(config));
- config.sdkVersion = 26;
- GetResourceBenchmark({GetTestDataPath() + "/sparse/not_sparse.apk"}, &config, resid, state);
+ BM_SparseEntryGetResourceHelper({GetTestDataPath() + "/sparse/not_sparse.apk"}, resid,
+ state, &GetResourceBenchmark);
}
BENCHMARK_CAPTURE(BM_SparseEntryGetResourceNotSparse, Small, sparse::R::integer::foo_9);
BENCHMARK_CAPTURE(BM_SparseEntryGetResourceNotSparse, Large, sparse::R::string::foo_999);
+static void BM_SparseEntryGetResourceOldSparseRuntime(benchmark::State& state, uint32_t resid) {
+ BM_SparseEntryGetResourceHelper({base::GetExecutableDirectory() +
+ "/FrameworkResourcesSparseTestApp.apk"}, resid, state,
+ &GetResourceBenchmarkOld);
+}
+BENCHMARK_CAPTURE(BM_SparseEntryGetResourceOldSparseRuntime, Small, sparse::R::integer::foo_9);
+BENCHMARK_CAPTURE(BM_SparseEntryGetResourceOldSparseRuntime, Large, sparse::R::string::foo_999);
+
+static void BM_SparseEntryGetResourceOldNotSparseRuntime(benchmark::State& state, uint32_t resid) {
+ BM_SparseEntryGetResourceHelper({base::GetExecutableDirectory() +
+ "/FrameworkResourcesNotSparseTestApp.apk"}, resid, state,
+ &GetResourceBenchmarkOld);
+}
+BENCHMARK_CAPTURE(BM_SparseEntryGetResourceOldNotSparseRuntime, Small, sparse::R::integer::foo_9);
+BENCHMARK_CAPTURE(BM_SparseEntryGetResourceOldNotSparseRuntime, Large, sparse::R::string::foo_999);
+
+static void BM_SparseEntryGetResourceSparseRuntime(benchmark::State& state, uint32_t resid) {
+ BM_SparseEntryGetResourceHelper({base::GetExecutableDirectory() +
+ "/FrameworkResourcesSparseTestApp.apk"}, resid, state,
+ &GetResourceBenchmark);
+}
+BENCHMARK_CAPTURE(BM_SparseEntryGetResourceSparseRuntime, Small, sparse::R::integer::foo_9);
+BENCHMARK_CAPTURE(BM_SparseEntryGetResourceSparseRuntime, Large, sparse::R::string::foo_999);
+
+static void BM_SparseEntryGetResourceNotSparseRuntime(benchmark::State& state, uint32_t resid) {
+ BM_SparseEntryGetResourceHelper({base::GetExecutableDirectory() +
+ "/FrameworkResourcesNotSparseTestApp.apk"}, resid, state,
+ &GetResourceBenchmark);
+}
+BENCHMARK_CAPTURE(BM_SparseEntryGetResourceNotSparseRuntime, Small, sparse::R::integer::foo_9);
+BENCHMARK_CAPTURE(BM_SparseEntryGetResourceNotSparseRuntime, Large, sparse::R::string::foo_999);
+
} // namespace android
diff --git a/libs/androidfw/tests/data/sparse/Android.bp b/libs/androidfw/tests/data/sparse/Android.bp
new file mode 100644
index 0000000..0fed79e
--- /dev/null
+++ b/libs/androidfw/tests/data/sparse/Android.bp
@@ -0,0 +1,14 @@
+android_test_helper_app {
+ name: "FrameworkResourcesSparseTestApp",
+ sdk_version: "current",
+ min_sdk_version: "32",
+ aaptflags: [
+ "--enable-sparse-encoding",
+ ],
+}
+
+android_test_helper_app {
+ name: "FrameworkResourcesNotSparseTestApp",
+ sdk_version: "current",
+ min_sdk_version: "32",
+}
diff --git a/libs/androidfw/tests/data/sparse/AndroidManifest.xml b/libs/androidfw/tests/data/sparse/AndroidManifest.xml
index 27911b6..9c23a72 100644
--- a/libs/androidfw/tests/data/sparse/AndroidManifest.xml
+++ b/libs/androidfw/tests/data/sparse/AndroidManifest.xml
@@ -17,4 +17,5 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.android.sparse">
<application />
+ <uses-sdk android:minSdkVersion="32" />
</manifest>
diff --git a/libs/androidfw/tests/data/sparse/R.h b/libs/androidfw/tests/data/sparse/R.h
index 2492dbf..a66e1af 100644
--- a/libs/androidfw/tests/data/sparse/R.h
+++ b/libs/androidfw/tests/data/sparse/R.h
@@ -42,7 +42,7 @@
struct string {
enum : uint32_t {
foo_999 = 0x7f0203e7,
- only_v26 = 0x7f0203e8
+ only_land = 0x7f0203e8
};
};
};
diff --git a/libs/androidfw/tests/data/sparse/gen_strings.sh b/libs/androidfw/tests/data/sparse/gen_strings.sh
index 4ea5468..114ecbb 100755
--- a/libs/androidfw/tests/data/sparse/gen_strings.sh
+++ b/libs/androidfw/tests/data/sparse/gen_strings.sh
@@ -1,20 +1,20 @@
#!/bin/bash
OUTPUT_default=res/values/strings.xml
-OUTPUT_v26=res/values-v26/strings.xml
+OUTPUT_land=res/values-land/strings.xml
echo "<resources>" > $OUTPUT_default
-echo "<resources>" > $OUTPUT_v26
+echo "<resources>" > $OUTPUT_land
for i in {0..999}
do
echo " <string name=\"foo_$i\">$i</string>" >> $OUTPUT_default
if [ "$(($i % 3))" -eq "0" ]
then
- echo " <string name=\"foo_$i\">$(($i * 10))</string>" >> $OUTPUT_v26
+ echo " <string name=\"foo_$i\">$(($i * 10))</string>" >> $OUTPUT_land
fi
done
echo "</resources>" >> $OUTPUT_default
-echo " <string name=\"only_v26\">only v26</string>" >> $OUTPUT_v26
-echo "</resources>" >> $OUTPUT_v26
+echo " <string name=\"only_land\">only land</string>" >> $OUTPUT_land
+echo "</resources>" >> $OUTPUT_land
diff --git a/libs/androidfw/tests/data/sparse/not_sparse.apk b/libs/androidfw/tests/data/sparse/not_sparse.apk
index b08a621..4d4d4a8 100644
--- a/libs/androidfw/tests/data/sparse/not_sparse.apk
+++ b/libs/androidfw/tests/data/sparse/not_sparse.apk
Binary files differ
diff --git a/libs/androidfw/tests/data/sparse/res/values-v26/strings.xml b/libs/androidfw/tests/data/sparse/res/values-land/strings.xml
similarity index 99%
rename from libs/androidfw/tests/data/sparse/res/values-v26/strings.xml
rename to libs/androidfw/tests/data/sparse/res/values-land/strings.xml
index d116087e..66222c3 100644
--- a/libs/androidfw/tests/data/sparse/res/values-v26/strings.xml
+++ b/libs/androidfw/tests/data/sparse/res/values-land/strings.xml
@@ -333,5 +333,5 @@
<string name="foo_993">9930</string>
<string name="foo_996">9960</string>
<string name="foo_999">9990</string>
- <string name="only_v26">only v26</string>
+ <string name="only_land">only land</string>
</resources>
diff --git a/libs/androidfw/tests/data/sparse/res/values-v26/values.xml b/libs/androidfw/tests/data/sparse/res/values-land/values.xml
similarity index 100%
rename from libs/androidfw/tests/data/sparse/res/values-v26/values.xml
rename to libs/androidfw/tests/data/sparse/res/values-land/values.xml
diff --git a/libs/androidfw/tests/data/sparse/sparse.apk b/libs/androidfw/tests/data/sparse/sparse.apk
index 9fd01fb..0f2d75a 100644
--- a/libs/androidfw/tests/data/sparse/sparse.apk
+++ b/libs/androidfw/tests/data/sparse/sparse.apk
Binary files differ
diff --git a/media/java/android/media/session/MediaSession.java b/media/java/android/media/session/MediaSession.java
index 1bd12af..7e1bbe3 100644
--- a/media/java/android/media/session/MediaSession.java
+++ b/media/java/android/media/session/MediaSession.java
@@ -244,12 +244,9 @@
mCallback = null;
return;
}
- if (handler == null) {
- handler = new Handler();
- }
+ Looper looper = handler != null ? handler.getLooper() : Looper.myLooper();
callback.mSession = this;
- CallbackMessageHandler msgHandler = new CallbackMessageHandler(handler.getLooper(),
- callback);
+ CallbackMessageHandler msgHandler = new CallbackMessageHandler(looper, callback);
mCallback = msgHandler;
}
}
diff --git a/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/SlicePurchaseActivity.java b/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/SlicePurchaseActivity.java
index e67ea7e..5d1e164 100644
--- a/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/SlicePurchaseActivity.java
+++ b/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/SlicePurchaseActivity.java
@@ -105,7 +105,7 @@
loge("Unable to start the slice purchase application on the non-default data "
+ "subscription: " + mSubId);
SlicePurchaseBroadcastReceiver.sendSlicePurchaseAppResponse(
- intent, SlicePurchaseController.EXTRA_INTENT_NOT_DEFAULT_DATA_SUB);
+ intent, SlicePurchaseController.EXTRA_INTENT_NOT_DEFAULT_DATA_SUBSCRIPTION);
finishAndRemoveTask();
return;
}
diff --git a/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/SlicePurchaseBroadcastReceiver.java b/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/SlicePurchaseBroadcastReceiver.java
index 5761b3c..c2ad2c560 100644
--- a/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/SlicePurchaseBroadcastReceiver.java
+++ b/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/SlicePurchaseBroadcastReceiver.java
@@ -173,7 +173,7 @@
&& isPendingIntentValid(intent, SlicePurchaseController.EXTRA_INTENT_CARRIER_ERROR)
&& isPendingIntentValid(intent, SlicePurchaseController.EXTRA_INTENT_REQUEST_FAILED)
&& isPendingIntentValid(intent,
- SlicePurchaseController.EXTRA_INTENT_NOT_DEFAULT_DATA_SUB)
+ SlicePurchaseController.EXTRA_INTENT_NOT_DEFAULT_DATA_SUBSCRIPTION)
&& isPendingIntentValid(intent, SlicePurchaseController.EXTRA_INTENT_SUCCESS);
}
@@ -204,8 +204,8 @@
case SlicePurchaseController.EXTRA_INTENT_CANCELED: return "canceled";
case SlicePurchaseController.EXTRA_INTENT_CARRIER_ERROR: return "carrier error";
case SlicePurchaseController.EXTRA_INTENT_REQUEST_FAILED: return "request failed";
- case SlicePurchaseController.EXTRA_INTENT_NOT_DEFAULT_DATA_SUB:
- return "not default data sub";
+ case SlicePurchaseController.EXTRA_INTENT_NOT_DEFAULT_DATA_SUBSCRIPTION:
+ return "not default data subscription";
case SlicePurchaseController.EXTRA_INTENT_SUCCESS: return "success";
default: {
loge("Unknown pending intent extra: " + extra);
diff --git a/packages/SettingsLib/Spa/build.gradle b/packages/SettingsLib/Spa/build.gradle
index 4fb77d7..b8fd579 100644
--- a/packages/SettingsLib/Spa/build.gradle
+++ b/packages/SettingsLib/Spa/build.gradle
@@ -16,9 +16,10 @@
buildscript {
ext {
- spa_min_sdk = 21
- spa_target_sdk = 33
- jetpack_compose_version = '1.3.0'
+ BUILD_TOOLS_VERSION = "30.0.3"
+ MIN_SDK = 21
+ TARGET_SDK = 33
+ jetpack_compose_version = '1.4.0-alpha01'
jetpack_compose_compiler_version = '1.3.2'
}
}
diff --git a/packages/SettingsLib/Spa/gallery/build.gradle b/packages/SettingsLib/Spa/gallery/build.gradle
index e04a9be..7868aff 100644
--- a/packages/SettingsLib/Spa/gallery/build.gradle
+++ b/packages/SettingsLib/Spa/gallery/build.gradle
@@ -21,12 +21,13 @@
android {
namespace 'com.android.settingslib.spa.gallery'
- compileSdk spa_target_sdk
+ compileSdk TARGET_SDK
+ buildToolsVersion = BUILD_TOOLS_VERSION
defaultConfig {
applicationId "com.android.settingslib.spa.gallery"
- minSdk spa_min_sdk
- targetSdk spa_target_sdk
+ minSdk MIN_SDK
+ targetSdk TARGET_SDK
versionCode 1
versionName "1.0"
}
diff --git a/packages/SettingsLib/Spa/spa/Android.bp b/packages/SettingsLib/Spa/spa/Android.bp
index 037b45e..2eaa73e 100644
--- a/packages/SettingsLib/Spa/spa/Android.bp
+++ b/packages/SettingsLib/Spa/spa/Android.bp
@@ -24,6 +24,9 @@
srcs: ["src/**/*.kt"],
static_libs: [
+ "androidx.slice_slice-builders",
+ "androidx.slice_slice-core",
+ "androidx.slice_slice-view",
"androidx.compose.material3_material3",
"androidx.compose.material_material-icons-extended",
"androidx.compose.runtime_runtime",
diff --git a/packages/SettingsLib/Spa/spa/build.gradle b/packages/SettingsLib/Spa/spa/build.gradle
index 7a20c747..4944784 100644
--- a/packages/SettingsLib/Spa/spa/build.gradle
+++ b/packages/SettingsLib/Spa/spa/build.gradle
@@ -21,11 +21,12 @@
android {
namespace 'com.android.settingslib.spa'
- compileSdk spa_target_sdk
+ compileSdk TARGET_SDK
+ buildToolsVersion = BUILD_TOOLS_VERSION
defaultConfig {
- minSdk spa_min_sdk
- targetSdk spa_target_sdk
+ minSdk MIN_SDK
+ targetSdk TARGET_SDK
}
sourceSets {
@@ -55,6 +56,9 @@
dependencies {
api "androidx.appcompat:appcompat:1.7.0-alpha01"
+ api "androidx.slice:slice-builders:1.1.0-alpha02"
+ api "androidx.slice:slice-core:1.1.0-alpha02"
+ api "androidx.slice:slice-view:1.1.0-alpha02"
api "androidx.compose.material3:material3:1.1.0-alpha01"
api "androidx.compose.material:material-icons-extended:$jetpack_compose_version"
api "androidx.compose.runtime:runtime-livedata:$jetpack_compose_version"
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/SpaSearchProvider.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/SpaSearchProvider.kt
index 35b9c0f..3689e4e 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/SpaSearchProvider.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/SpaSearchProvider.kt
@@ -119,7 +119,7 @@
val entryRepository by spaEnvironment.entryRepository
val cursor = MatrixCursor(QueryEnum.SEARCH_IMMUTABLE_STATUS_DATA_QUERY.getColumns())
for (entry in entryRepository.getAllEntries()) {
- if (!entry.isAllowSearch || entry.mutableStatus) continue
+ if (!entry.isAllowSearch || entry.hasMutableStatus) continue
fetchStatusData(entry, cursor)
}
return cursor
@@ -129,7 +129,7 @@
val entryRepository by spaEnvironment.entryRepository
val cursor = MatrixCursor(QueryEnum.SEARCH_MUTABLE_STATUS_DATA_QUERY.getColumns())
for (entry in entryRepository.getAllEntries()) {
- if (!entry.isAllowSearch || !entry.mutableStatus) continue
+ if (!entry.isAllowSearch || !entry.hasMutableStatus) continue
fetchStatusData(entry, cursor)
}
return cursor
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/SpaSliceBroadcastReceiver.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/SpaSliceBroadcastReceiver.kt
new file mode 100644
index 0000000..8c038c8
--- /dev/null
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/SpaSliceBroadcastReceiver.kt
@@ -0,0 +1,31 @@
+/*
+ * 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.settingslib.spa.framework
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import com.android.settingslib.spa.framework.common.SpaEnvironmentFactory
+
+class SpaSliceBroadcastReceiver : BroadcastReceiver() {
+ override fun onReceive(context: Context?, intent: Intent?) {
+ val sliceRepository = SpaEnvironmentFactory.instance.sliceDataRepository
+ val sliceUri = intent?.data ?: return
+ val sliceData = sliceRepository.getActiveSliceData(sliceUri) ?: return
+ sliceData.doAction()
+ }
+}
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/SpaSliceProvider.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/SpaSliceProvider.kt
new file mode 100644
index 0000000..d800e50
--- /dev/null
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/SpaSliceProvider.kt
@@ -0,0 +1,76 @@
+/*
+ * 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.settingslib.spa.framework
+
+import android.net.Uri
+import android.util.Log
+import androidx.lifecycle.Observer
+import androidx.slice.Slice
+import androidx.slice.SliceProvider
+import com.android.settingslib.spa.framework.common.EntrySliceData
+import com.android.settingslib.spa.framework.common.SpaEnvironmentFactory
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.withContext
+
+private const val TAG = "SpaSliceProvider"
+
+class SpaSliceProvider : SliceProvider(), Observer<Slice?> {
+ private fun getOrPutSliceData(sliceUri: Uri): EntrySliceData? {
+ if (!SpaEnvironmentFactory.isReady()) return null
+ return SpaEnvironmentFactory.instance.sliceDataRepository.getOrBuildSliceData(sliceUri)
+ }
+
+ override fun onBindSlice(sliceUri: Uri): Slice? {
+ if (context == null) return null
+ Log.d(TAG, "onBindSlice: $sliceUri")
+ return getOrPutSliceData(sliceUri)?.value
+ }
+
+ override fun onSlicePinned(sliceUri: Uri) {
+ Log.d(TAG, "onSlicePinned: $sliceUri")
+ super.onSlicePinned(sliceUri)
+ val sliceLiveData = getOrPutSliceData(sliceUri) ?: return
+ runBlocking {
+ withContext(Dispatchers.Main) {
+ sliceLiveData.observeForever(this@SpaSliceProvider)
+ }
+ }
+ }
+
+ override fun onSliceUnpinned(sliceUri: Uri) {
+ Log.d(TAG, "onSliceUnpinned: $sliceUri")
+ super.onSliceUnpinned(sliceUri)
+ val sliceLiveData = getOrPutSliceData(sliceUri) ?: return
+ runBlocking {
+ withContext(Dispatchers.Main) {
+ sliceLiveData.removeObserver(this@SpaSliceProvider)
+ }
+ }
+ }
+
+ override fun onChanged(slice: Slice?) {
+ val uri = slice?.uri ?: return
+ Log.d(TAG, "onChanged: $uri")
+ context?.contentResolver?.notifyChange(uri, null)
+ }
+
+ override fun onCreateSliceProvider(): Boolean {
+ Log.d(TAG, "onCreateSliceProvider")
+ return true
+ }
+}
\ No newline at end of file
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/EntrySliceData.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/EntrySliceData.kt
new file mode 100644
index 0000000..fc551a8
--- /dev/null
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/EntrySliceData.kt
@@ -0,0 +1,58 @@
+/*
+ * 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.settingslib.spa.framework.common
+
+import androidx.lifecycle.LiveData
+import androidx.slice.Slice
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.launch
+
+open class EntrySliceData : LiveData<Slice?>() {
+ private val asyncRunnerScope = CoroutineScope(Dispatchers.IO)
+ private var asyncRunnerJob: Job? = null
+ private var asyncActionJob: Job? = null
+ private var isActive = false
+
+ open suspend fun asyncRunner() {}
+
+ open suspend fun asyncAction() {}
+
+ override fun onActive() {
+ asyncRunnerJob?.cancel()
+ asyncRunnerJob = asyncRunnerScope.launch { asyncRunner() }
+ isActive = true
+ }
+
+ override fun onInactive() {
+ asyncRunnerJob?.cancel()
+ asyncRunnerJob = null
+ asyncActionJob?.cancel()
+ asyncActionJob = null
+ isActive = false
+ }
+
+ fun isActive(): Boolean {
+ return isActive
+ }
+
+ fun doAction() {
+ asyncActionJob?.cancel()
+ asyncActionJob = asyncRunnerScope.launch { asyncAction() }
+ }
+}
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsEntry.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsEntry.kt
index 224fe1d..9ee7f9e 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsEntry.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsEntry.kt
@@ -16,6 +16,7 @@
package com.android.settingslib.spa.framework.common
+import android.net.Uri
import android.os.Bundle
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
@@ -39,6 +40,11 @@
val LocalEntryDataProvider =
compositionLocalOf<EntryData> { object : EntryData {} }
+typealias UiLayerRenderer = @Composable (arguments: Bundle?) -> Unit
+typealias StatusDataGetter = (arguments: Bundle?) -> EntryStatusData?
+typealias SearchDataGetter = (arguments: Bundle?) -> EntrySearchData?
+typealias SliceDataGetter = (sliceUri: Uri, arguments: Bundle?) -> EntrySliceData?
+
/**
* Defines data of a Settings entry.
*/
@@ -71,7 +77,10 @@
// Indicate whether the status of entry is mutable.
// If so, for instance, we'll reindex its status for search.
- val mutableStatus: Boolean = false,
+ val hasMutableStatus: Boolean = false,
+
+ // Indicate whether the entry has SliceProvider support.
+ val hasSliceSupport: Boolean = false,
/**
* ========================================
@@ -83,13 +92,19 @@
* API to get the status data of the entry, such as isDisabled / isSwitchOff.
* Returns null if this entry do NOT have any status.
*/
- private val statusDataImpl: (arguments: Bundle?) -> EntryStatusData? = { null },
+ private val statusDataImpl: StatusDataGetter = { null },
/**
* API to get Search indexing data for this entry, such as title / keyword.
* Returns null if this entry do NOT support search.
*/
- private val searchDataImpl: (arguments: Bundle?) -> EntrySearchData? = { null },
+ private val searchDataImpl: SearchDataGetter = { null },
+
+ /**
+ * API to get Slice data of this entry. The Slice data is implemented as a LiveData,
+ * and is associated with the Slice's lifecycle (pin / unpin) by the framework.
+ */
+ private val sliceDataImpl: SliceDataGetter = { _: Uri, _: Bundle? -> null },
/**
* API to Render UI of this entry directly. For now, we use it in the internal injection, to
@@ -97,7 +112,7 @@
* injected entry. In the long term, we may deprecate the @Composable Page() API in SPP, and
* use each entries' UI rendering function in the page instead.
*/
- private val uiLayoutImpl: (@Composable (arguments: Bundle?) -> Unit) = {},
+ private val uiLayoutImpl: UiLayerRenderer = {},
) {
fun containerPage(): SettingsPage {
// The Container page of the entry, which is the from-page or
@@ -121,6 +136,10 @@
return searchDataImpl(fullArgument(runtimeArguments))
}
+ fun getSliceData(sliceUri: Uri, runtimeArguments: Bundle? = null): EntrySliceData? {
+ return sliceDataImpl(sliceUri, fullArgument(runtimeArguments))
+ }
+
@Composable
fun UiLayout(runtimeArguments: Bundle? = null) {
CompositionLocalProvider(provideLocalEntryData()) {
@@ -152,12 +171,14 @@
// Attributes
private var isAllowSearch: Boolean = false
private var isSearchDataDynamic: Boolean = false
- private var mutableStatus: Boolean = false
+ private var hasMutableStatus: Boolean = false
+ private var hasSliceSupport: Boolean = false
// Functions
- private var statusDataFn: (arguments: Bundle?) -> EntryStatusData? = { null }
- private var searchDataFn: (arguments: Bundle?) -> EntrySearchData? = { null }
- private var uiLayoutFn: (@Composable (arguments: Bundle?) -> Unit) = { }
+ private var uiLayoutFn: UiLayerRenderer = { }
+ private var statusDataFn: StatusDataGetter = { null }
+ private var searchDataFn: SearchDataGetter = { null }
+ private var sliceDataFn: SliceDataGetter = { _: Uri, _: Bundle? -> null }
fun build(): SettingsEntry {
return SettingsEntry(
@@ -173,11 +194,13 @@
// attributes
isAllowSearch = isAllowSearch,
isSearchDataDynamic = isSearchDataDynamic,
- mutableStatus = mutableStatus,
+ hasMutableStatus = hasMutableStatus,
+ hasSliceSupport = hasSliceSupport,
// functions
statusDataImpl = statusDataFn,
searchDataImpl = searchDataFn,
+ sliceDataImpl = sliceDataFn,
uiLayoutImpl = uiLayoutFn,
)
}
@@ -207,7 +230,7 @@
}
fun setHasMutableStatus(hasMutableStatus: Boolean): SettingsEntryBuilder {
- this.mutableStatus = hasMutableStatus
+ this.hasMutableStatus = hasMutableStatus
return this
}
@@ -221,17 +244,23 @@
return this
}
- fun setStatusDataFn(fn: (arguments: Bundle?) -> EntryStatusData?): SettingsEntryBuilder {
+ fun setStatusDataFn(fn: StatusDataGetter): SettingsEntryBuilder {
this.statusDataFn = fn
return this
}
- fun setSearchDataFn(fn: (arguments: Bundle?) -> EntrySearchData?): SettingsEntryBuilder {
+ fun setSearchDataFn(fn: SearchDataGetter): SettingsEntryBuilder {
this.searchDataFn = fn
return this
}
- fun setUiLayoutFn(fn: @Composable (arguments: Bundle?) -> Unit): SettingsEntryBuilder {
+ fun setSliceDataFn(fn: SliceDataGetter): SettingsEntryBuilder {
+ this.sliceDataFn = fn
+ this.hasSliceSupport = true
+ return this
+ }
+
+ fun setUiLayoutFn(fn: UiLayerRenderer): SettingsEntryBuilder {
this.uiLayoutFn = fn
return this
}
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsPage.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsPage.kt
index bb287d1..a372bbd 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsPage.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsPage.kt
@@ -113,6 +113,12 @@
)
}
+ fun createBrowseIntent(entryId: String? = null): Intent? {
+ val context = SpaEnvironmentFactory.instance.appContext
+ val browseActivityClass = SpaEnvironmentFactory.instance.browseActivityClass
+ return createBrowseIntent(context, browseActivityClass, entryId)
+ }
+
fun createBrowseIntent(
context: Context?,
browseActivityClass: Class<out Activity>?,
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsPageProvider.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsPageProvider.kt
index 151b50cd..60599d4 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsPageProvider.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsPageProvider.kt
@@ -30,14 +30,14 @@
val name: String
/** The display name of this page provider, for better readability. */
- val displayName: String?
- get() = null
+ val displayName: String
+ get() = name
/** The page parameters, default is no parameters. */
val parameter: List<NamedNavArgument>
get() = emptyList()
- fun getTitle(arguments: Bundle?): String = displayName ?: name
+ fun getTitle(arguments: Bundle?): String = displayName
fun buildEntry(arguments: Bundle?): List<SettingsEntry> = emptyList()
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SpaEnvironment.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SpaEnvironment.kt
index a9cb041..9bb06f0 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SpaEnvironment.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SpaEnvironment.kt
@@ -17,10 +17,12 @@
package com.android.settingslib.spa.framework.common
import android.app.Activity
+import android.content.BroadcastReceiver
import android.content.Context
import android.util.Log
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
+import com.android.settingslib.spa.slice.SettingsSliceDataRepository
private const val TAG = "SpaEnvironment"
@@ -46,6 +48,10 @@
Log.d(TAG, "resetForPreview")
}
+ fun isReady(): Boolean {
+ return spaEnvironment != null
+ }
+
val instance: SpaEnvironment
get() {
if (spaEnvironment == null)
@@ -59,13 +65,15 @@
val entryRepository = lazy { SettingsEntryRepository(pageProviderRepository.value) }
+ val sliceDataRepository = SettingsSliceDataRepository()
+
// In Robolectric test, applicationContext is not available. Use context as fallback.
val appContext: Context = context.applicationContext ?: context
open val browseActivityClass: Class<out Activity>? = null
-
+ open val sliceBroadcastReceiverClass: Class<out BroadcastReceiver>? = null
open val searchProviderAuthorities: String? = null
-
+ open val sliceProviderAuthorities: String? = null
open val logger: SpaLogger = object : SpaLogger {}
// TODO: add other environment setup here.
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/debug/DebugActivity.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/debug/DebugActivity.kt
index 26491d5..760064a 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/debug/DebugActivity.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/debug/DebugActivity.kt
@@ -16,6 +16,7 @@
package com.android.settingslib.spa.framework.debug
+import android.net.Uri
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
@@ -38,6 +39,8 @@
import com.android.settingslib.spa.framework.compose.navigator
import com.android.settingslib.spa.framework.compose.toState
import com.android.settingslib.spa.framework.theme.SettingsTheme
+import com.android.settingslib.spa.slice.appendSliceParams
+import com.android.settingslib.spa.slice.presenter.SliceDemo
import com.android.settingslib.spa.widget.preference.Preference
import com.android.settingslib.spa.widget.preference.PreferenceModel
import com.android.settingslib.spa.widget.scaffold.HomeScaffold
@@ -47,6 +50,7 @@
private const val ROUTE_ROOT = "root"
private const val ROUTE_All_PAGES = "pages"
private const val ROUTE_All_ENTRIES = "entries"
+private const val ROUTE_All_SLICES = "slices"
private const val ROUTE_PAGE = "page"
private const val ROUTE_ENTRY = "entry"
private const val PARAM_NAME_PAGE_ID = "pid"
@@ -81,6 +85,7 @@
composable(route = ROUTE_ROOT) { RootPage() }
composable(route = ROUTE_All_PAGES) { AllPages() }
composable(route = ROUTE_All_ENTRIES) { AllEntries() }
+ composable(route = ROUTE_All_SLICES) { AllSlices() }
composable(
route = "$ROUTE_PAGE/{$PARAM_NAME_PAGE_ID}",
arguments = listOf(
@@ -102,6 +107,8 @@
val entryRepository by spaEnvironment.entryRepository
val allPageWithEntry = remember { entryRepository.getAllPageWithEntry() }
val allEntry = remember { entryRepository.getAllEntries() }
+ val allSliceEntry =
+ remember { entryRepository.getAllEntries().filter { it.hasSliceSupport } }
HomeScaffold(title = "Settings Debug") {
Preference(object : PreferenceModel {
override val title = "List All Pages (${allPageWithEntry.size})"
@@ -111,6 +118,10 @@
override val title = "List All Entries (${allEntry.size})"
override val onClick = navigator(route = ROUTE_All_ENTRIES)
})
+ Preference(object : PreferenceModel {
+ override val title = "List All Slices (${allSliceEntry.size})"
+ override val onClick = navigator(route = ROUTE_All_SLICES)
+ })
}
}
@@ -140,6 +151,19 @@
}
@Composable
+ fun AllSlices() {
+ val entryRepository by spaEnvironment.entryRepository
+ val authority = spaEnvironment.sliceProviderAuthorities
+ val allSliceEntry =
+ remember { entryRepository.getAllEntries().filter { it.hasSliceSupport } }
+ RegularScaffold(title = "All Slices (${allSliceEntry.size})") {
+ for (entry in allSliceEntry) {
+ SliceDemo(sliceUri = entry.createSliceUri(authority))
+ }
+ }
+ }
+
+ @Composable
fun OnePage(arguments: Bundle?) {
val context = LocalContext.current
val entryRepository by spaEnvironment.entryRepository
@@ -221,6 +245,18 @@
}
}
+private fun SettingsEntry.createSliceUri(
+ authority: String?,
+ runtimeArguments: Bundle? = null
+): Uri {
+ if (authority == null) return Uri.EMPTY
+ return Uri.Builder().scheme("content").authority(authority).appendSliceParams(
+ route = this.containerPage().buildRoute(),
+ entryId = this.id,
+ runtimeArguments = runtimeArguments,
+ ).build()
+}
+
/**
* A blank activity without any page.
*/
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/slice/SettingsSliceDataRepository.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/slice/SettingsSliceDataRepository.kt
new file mode 100644
index 0000000..d8d2378
--- /dev/null
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/slice/SettingsSliceDataRepository.kt
@@ -0,0 +1,59 @@
+/*
+ * 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.settingslib.spa.slice
+
+import android.net.Uri
+import android.util.Log
+import com.android.settingslib.spa.framework.common.EntrySliceData
+import com.android.settingslib.spa.framework.common.SpaEnvironmentFactory
+
+private const val TAG = "SliceDataRepository"
+
+class SettingsSliceDataRepository {
+ // The map of slice uri to its EntrySliceData, a.k.a. LiveData<Slice?>
+ private val sliceDataMap: MutableMap<String, EntrySliceData> = mutableMapOf()
+
+ // Note: mark this function synchronized, so that we can get the same livedata during the
+ // whole lifecycle of a Slice.
+ @Synchronized
+ fun getOrBuildSliceData(sliceUri: Uri): EntrySliceData? {
+ val sliceString = sliceUri.getSliceId() ?: return null
+ return sliceDataMap[sliceString] ?: buildLiveDataImpl(sliceUri)?.let {
+ sliceDataMap[sliceString] = it
+ it
+ }
+ }
+
+ fun getActiveSliceData(sliceUri: Uri): EntrySliceData? {
+ val sliceString = sliceUri.getSliceId() ?: return null
+ val sliceData = sliceDataMap[sliceString] ?: return null
+ return if (sliceData.isActive()) sliceData else null
+ }
+
+ private fun buildLiveDataImpl(sliceUri: Uri): EntrySliceData? {
+ Log.d(TAG, "buildLiveData: $sliceUri")
+ if (!SpaEnvironmentFactory.isReady()) return null
+
+ val entryRepository by SpaEnvironmentFactory.instance.entryRepository
+ val entryId = sliceUri.getEntryId() ?: return null
+ val entry = entryRepository.getEntry(entryId) ?: return null
+ if (!entry.hasSliceSupport) return null
+
+ val arguments = sliceUri.getRuntimeArguments()
+ return entry.getSliceData(runtimeArguments = arguments, sliceUri = sliceUri)
+ }
+}
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/slice/SliceUtil.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/slice/SliceUtil.kt
new file mode 100644
index 0000000..ff143ed
--- /dev/null
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/slice/SliceUtil.kt
@@ -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.
+ */
+
+package com.android.settingslib.spa.slice
+
+import android.app.Activity
+import android.app.PendingIntent
+import android.content.BroadcastReceiver
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.net.Uri
+import android.os.Bundle
+import com.android.settingslib.spa.framework.BrowseActivity.Companion.KEY_DESTINATION
+import com.android.settingslib.spa.framework.BrowseActivity.Companion.KEY_HIGHLIGHT_ENTRY
+import com.android.settingslib.spa.framework.common.SpaEnvironmentFactory
+
+// Defines SliceUri, which contains special query parameters:
+// -- KEY_DESTINATION: The route that this slice is navigated to.
+// -- KEY_HIGHLIGHT_ENTRY: The entry id of this slice
+// Other parameters can considered as runtime parameters.
+// Use {entryId, runtimeParams} as the unique Id of this Slice.
+typealias SliceUri = Uri
+
+val RESERVED_KEYS = listOf(
+ KEY_DESTINATION,
+ KEY_HIGHLIGHT_ENTRY
+)
+
+fun SliceUri.getEntryId(): String? {
+ return getQueryParameter(KEY_HIGHLIGHT_ENTRY)
+}
+
+fun SliceUri.getDestination(): String? {
+ return getQueryParameter(KEY_DESTINATION)
+}
+
+fun SliceUri.getRuntimeArguments(): Bundle {
+ val params = Bundle()
+ for (queryName in queryParameterNames) {
+ if (RESERVED_KEYS.contains(queryName)) continue
+ params.putString(queryName, getQueryParameter(queryName))
+ }
+ return params
+}
+
+fun SliceUri.getSliceId(): String? {
+ val entryId = getEntryId() ?: return null
+ val params = getRuntimeArguments()
+ return "${entryId}_$params"
+}
+
+fun Uri.Builder.appendSliceParams(
+ route: String? = null,
+ entryId: String? = null,
+ runtimeArguments: Bundle? = null
+): Uri.Builder {
+ if (route != null) appendQueryParameter(KEY_DESTINATION, route)
+ if (entryId != null) appendQueryParameter(KEY_HIGHLIGHT_ENTRY, entryId)
+ if (runtimeArguments != null) {
+ for (key in runtimeArguments.keySet()) {
+ appendQueryParameter(key, runtimeArguments.getString(key, ""))
+ }
+ }
+ return this
+}
+
+fun SliceUri.createBroadcastPendingIntent(): PendingIntent? {
+ val context = SpaEnvironmentFactory.instance.appContext
+ val sliceBroadcastClass =
+ SpaEnvironmentFactory.instance.sliceBroadcastReceiverClass ?: return null
+ val entryId = getEntryId() ?: return null
+ return createBroadcastPendingIntent(context, sliceBroadcastClass, entryId)
+}
+
+fun SliceUri.createBrowsePendingIntent(): PendingIntent? {
+ val context = SpaEnvironmentFactory.instance.appContext
+ val browseActivityClass = SpaEnvironmentFactory.instance.browseActivityClass ?: return null
+ val destination = getDestination() ?: return null
+ val entryId = getEntryId()
+ return createBrowsePendingIntent(context, browseActivityClass, destination, entryId)
+}
+
+fun Intent.createBrowsePendingIntent(): PendingIntent? {
+ val context = SpaEnvironmentFactory.instance.appContext
+ val browseActivityClass = SpaEnvironmentFactory.instance.browseActivityClass ?: return null
+ val destination = getStringExtra(KEY_DESTINATION) ?: return null
+ val entryId = getStringExtra(KEY_HIGHLIGHT_ENTRY)
+ return createBrowsePendingIntent(context, browseActivityClass, destination, entryId)
+}
+
+private fun createBrowsePendingIntent(
+ context: Context,
+ browseActivityClass: Class<out Activity>,
+ destination: String,
+ entryId: String?
+): PendingIntent {
+ val intent = Intent().setComponent(ComponentName(context, browseActivityClass))
+ .apply {
+ // Set both extra and data (which is a Uri) in Slice Intent:
+ // 1) extra is used in SPA navigation framework
+ // 2) data is used in Slice framework
+ putExtra(KEY_DESTINATION, destination)
+ if (entryId != null) {
+ putExtra(KEY_HIGHLIGHT_ENTRY, entryId)
+ }
+ data = Uri.Builder().appendSliceParams(destination, entryId).build()
+ flags = Intent.FLAG_ACTIVITY_NEW_TASK
+ }
+
+ return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_IMMUTABLE)
+}
+
+private fun createBroadcastPendingIntent(
+ context: Context,
+ sliceBroadcastClass: Class<out BroadcastReceiver>,
+ entryId: String
+): PendingIntent {
+ val intent = Intent().setComponent(ComponentName(context, sliceBroadcastClass))
+ .apply { data = Uri.Builder().appendSliceParams(entryId = entryId).build() }
+ return PendingIntent.getBroadcast(
+ context, 0 /* requestCode */, intent,
+ PendingIntent.FLAG_CANCEL_CURRENT or PendingIntent.FLAG_MUTABLE
+ )
+}
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/slice/presenter/Demo.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/slice/presenter/Demo.kt
new file mode 100644
index 0000000..cff1c0c
--- /dev/null
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/slice/presenter/Demo.kt
@@ -0,0 +1,47 @@
+/*
+ * 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.settingslib.spa.slice.presenter
+
+import android.net.Uri
+import androidx.compose.material3.Divider
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalLifecycleOwner
+import androidx.compose.ui.viewinterop.AndroidView
+import androidx.slice.widget.SliceLiveData
+import androidx.slice.widget.SliceView
+
+@Composable
+fun SliceDemo(sliceUri: Uri) {
+ val context = LocalContext.current
+ val lifecycleOwner = LocalLifecycleOwner.current
+ val sliceData = remember {
+ SliceLiveData.fromUri(context, sliceUri)
+ }
+
+ Divider()
+ AndroidView(
+ factory = { localContext ->
+ val view = SliceView(localContext)
+ view.setShowTitleItems(true)
+ view.isScrollable = false
+ view
+ },
+ update = { view -> sliceData.observe(lifecycleOwner, view) }
+ )
+}
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/slice/provider/Demo.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/slice/provider/Demo.kt
new file mode 100644
index 0000000..b65b91f
--- /dev/null
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/slice/provider/Demo.kt
@@ -0,0 +1,62 @@
+/*
+ * 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.settingslib.spa.slice.provider
+
+import android.app.PendingIntent
+import android.content.Context
+import android.net.Uri
+import androidx.core.graphics.drawable.IconCompat
+import androidx.slice.Slice
+import androidx.slice.SliceManager
+import androidx.slice.builders.ListBuilder
+import androidx.slice.builders.SliceAction
+import com.android.settingslib.spa.framework.common.SpaEnvironmentFactory
+import com.android.settingslib.spa.slice.createBroadcastPendingIntent
+import com.android.settingslib.spa.slice.createBrowsePendingIntent
+
+fun createDemoBrowseSlice(sliceUri: Uri, title: String, summary: String): Slice? {
+ val intent = sliceUri.createBrowsePendingIntent() ?: return null
+ return createDemoSlice(sliceUri, title, summary, intent)
+}
+
+fun createDemoActionSlice(sliceUri: Uri, title: String, summary: String): Slice? {
+ val intent = sliceUri.createBroadcastPendingIntent() ?: return null
+ return createDemoSlice(sliceUri, title, summary, intent)
+}
+
+fun createDemoSlice(sliceUri: Uri, title: String, summary: String, intent: PendingIntent): Slice? {
+ val context = SpaEnvironmentFactory.instance.appContext
+ if (!SliceManager.getInstance(context).pinnedSlices.contains(sliceUri)) return null
+ return ListBuilder(context, sliceUri, ListBuilder.INFINITY)
+ .addRow(ListBuilder.RowBuilder().apply {
+ setPrimaryAction(createSliceAction(context, intent))
+ setTitle(title)
+ setSubtitle(summary)
+ }).build()
+}
+
+private fun createSliceAction(context: Context, intent: PendingIntent): SliceAction {
+ return SliceAction.create(
+ intent,
+ IconCompat.createWithResource(
+ context,
+ com.google.android.material.R.drawable.navigation_empty_icon
+ ),
+ ListBuilder.ICON_IMAGE,
+ "Enter app"
+ )
+}
diff --git a/packages/SettingsLib/Spa/tests/build.gradle b/packages/SettingsLib/Spa/tests/build.gradle
index 529a201..2d501fc 100644
--- a/packages/SettingsLib/Spa/tests/build.gradle
+++ b/packages/SettingsLib/Spa/tests/build.gradle
@@ -21,11 +21,12 @@
android {
namespace 'com.android.settingslib.spa.tests'
- compileSdk spa_target_sdk
+ compileSdk TARGET_SDK
+ buildToolsVersion = BUILD_TOOLS_VERSION
defaultConfig {
- minSdk spa_min_sdk
- targetSdk spa_target_sdk
+ minSdk MIN_SDK
+ targetSdk TARGET_SDK
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
@@ -60,7 +61,6 @@
dependencies {
androidTestImplementation project(":spa")
androidTestImplementation project(":testutils")
- androidTestImplementation "androidx.test.ext:junit-ktx:1.1.3"
androidTestImplementation "androidx.compose.ui:ui-test-junit4:$jetpack_compose_version"
androidTestImplementation "com.google.truth:truth:1.1.3"
androidTestImplementation "org.mockito:mockito-android:3.4.6"
diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/common/SettingsEntryTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/common/SettingsEntryTest.kt
index 31d2ae4..2017d53 100644
--- a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/common/SettingsEntryTest.kt
+++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/common/SettingsEntryTest.kt
@@ -63,7 +63,7 @@
assertThat(entry.toPage).isNull()
assertThat(entry.isAllowSearch).isFalse()
assertThat(entry.isSearchDataDynamic).isFalse()
- assertThat(entry.mutableStatus).isFalse()
+ assertThat(entry.hasMutableStatus).isFalse()
}
@Test
@@ -133,7 +133,7 @@
assertThat(entry.toPage).isNull()
assertThat(entry.isAllowSearch).isTrue()
assertThat(entry.isSearchDataDynamic).isFalse()
- assertThat(entry.mutableStatus).isTrue()
+ assertThat(entry.hasMutableStatus).isTrue()
}
@Test
diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/common/SettingsPageTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/common/SettingsPageTest.kt
index 539e56b..7097a5d 100644
--- a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/common/SettingsPageTest.kt
+++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/common/SettingsPageTest.kt
@@ -118,6 +118,7 @@
page.enterPage()
page.leavePage()
page.enterPage()
+ assertThat(page.createBrowseIntent()).isNotNull()
assertThat(spaLogger.getEventCount(page.id, LogEvent.PAGE_ENTER, LogCategory.FRAMEWORK))
.isEqualTo(2)
assertThat(spaLogger.getEventCount(page.id, LogEvent.PAGE_LEAVE, LogCategory.FRAMEWORK))
diff --git a/packages/SettingsLib/Spa/testutils/build.gradle b/packages/SettingsLib/Spa/testutils/build.gradle
index 71d7d8a..cbfbb9c 100644
--- a/packages/SettingsLib/Spa/testutils/build.gradle
+++ b/packages/SettingsLib/Spa/testutils/build.gradle
@@ -20,11 +20,12 @@
}
android {
- compileSdk spa_target_sdk
+ compileSdk TARGET_SDK
+ buildToolsVersion = BUILD_TOOLS_VERSION
defaultConfig {
- minSdk spa_min_sdk
- targetSdk spa_target_sdk
+ minSdk MIN_SDK
+ targetSdk TARGET_SDK
}
sourceSets {
diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppStorageSizeTest.kt b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppStorageSizeTest.kt
index cec6d7d..b3638c2 100644
--- a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppStorageSizeTest.kt
+++ b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppStorageSizeTest.kt
@@ -28,7 +28,6 @@
import com.android.settingslib.spa.framework.compose.stateOf
import com.android.settingslib.spaprivileged.framework.common.storageStatsManager
import com.android.settingslib.spaprivileged.model.app.userHandle
-import com.google.common.truth.Truth.assertThat
import java.util.UUID
import org.junit.Before
import org.junit.Rule
@@ -77,7 +76,7 @@
}
}
- assertThat(storageSize.value).isEqualTo("123 B")
+ composeTestRule.waitUntil { storageSize.value == "123 B" }
}
companion object {
diff --git a/packages/SettingsLib/src/com/android/settingslib/graph/ThemedBatteryDrawable.kt b/packages/SettingsLib/src/com/android/settingslib/graph/ThemedBatteryDrawable.kt
index 5fa04f9..faea5b2 100644
--- a/packages/SettingsLib/src/com/android/settingslib/graph/ThemedBatteryDrawable.kt
+++ b/packages/SettingsLib/src/com/android/settingslib/graph/ThemedBatteryDrawable.kt
@@ -412,14 +412,13 @@
}
companion object {
- private const val TAG = "ThemedBatteryDrawable"
- private const val WIDTH = 12f
- private const val HEIGHT = 20f
+ const val WIDTH = 12f
+ const val HEIGHT = 20f
private const val CRITICAL_LEVEL = 15
// On a 12x20 grid, how wide to make the fill protection stroke.
// Scales when our size changes
private const val PROTECTION_STROKE_WIDTH = 3f
// Arbitrarily chosen for visibility at small sizes
- private const val PROTECTION_MIN_STROKE_WIDTH = 6f
+ const val PROTECTION_MIN_STROKE_WIDTH = 6f
}
}
diff --git a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
index aea2f52..f5c9bcd 100644
--- a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
+++ b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
@@ -884,7 +884,7 @@
Settings.Secure.SHOW_QR_CODE_SCANNER_SETTING,
Settings.Secure.SKIP_ACCESSIBILITY_SHORTCUT_DIALOG_TIMEOUT_RESTRICTION,
Settings.Secure.SPATIAL_AUDIO_ENABLED,
- Settings.Secure.TIMEOUT_TO_USER_ZERO,
+ Settings.Secure.TIMEOUT_TO_DOCK_USER,
Settings.Secure.UI_NIGHT_MODE_LAST_COMPUTED,
Settings.Secure.UI_NIGHT_MODE_OVERRIDE_OFF,
Settings.Secure.UI_NIGHT_MODE_OVERRIDE_ON);
diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml
index ce9829b..55d6379 100644
--- a/packages/SystemUI/res/values/config.xml
+++ b/packages/SystemUI/res/values/config.xml
@@ -485,6 +485,12 @@
<!-- Whether to show a severe low battery dialog. -->
<bool name="config_severe_battery_dialog">false</bool>
+ <!-- A path representing a shield. Will sometimes be displayed with the battery icon when
+ needed. This path is a 10px wide and 13px tall. -->
+ <string name="config_batterymeterShieldPath" translatable="false">
+ M5 0L0 1.88V6.19C0 9.35 2.13 12.29 5 13.01C7.87 12.29 10 9.35 10 6.19V1.88L5 0Z
+ </string>
+
<!-- A path similar to frameworks/base/core/res/res/values/config.xml
config_mainBuiltInDisplayCutout that describes a path larger than the exact path of a display
cutout. If present as well as config_enableDisplayCutoutProtection is set to true, then
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index f9f2195..6577b07 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -105,6 +105,12 @@
so the width of the icon should be 13.0dp * (12.0 / 20.0) -->
<dimen name="status_bar_battery_icon_width">7.8dp</dimen>
+ <!-- The battery icon is 13dp tall, but the other system icons are 15dp tall (see
+ @*android:dimen/status_bar_system_icon_size) with some top and bottom padding embedded in
+ the drawables themselves. So, the battery icon may need an extra 1dp of spacing so that its
+ bottom still aligns with the bottom of all the other system icons. See b/258672854. -->
+ <dimen name="status_bar_battery_extra_vertical_spacing">1dp</dimen>
+
<!-- The font size for the clock in the status bar. -->
<dimen name="status_bar_clock_size">14sp</dimen>
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 6824d7f..3270e45 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -316,7 +316,7 @@
<!-- Content description of the QR Code scanner for accessibility (not shown on the screen). [CHAR LIMIT=NONE] -->
<string name="accessibility_qr_code_scanner_button">QR Code Scanner</string>
<!-- Content description of the unlock button for accessibility (not shown on the screen). [CHAR LIMIT=NONE] -->
- <string name="accessibility_unlock_button">Unlock</string>
+ <string name="accessibility_unlock_button">Unlocked</string>
<!-- Content description of the lock icon for accessibility (not shown on the screen). [CHAR LIMIT=NONE] -->
<string name="accessibility_lock_icon">Device locked</string>
<!-- Content description hint of the unlock button when fingerprint is on (not shown on the screen). [CHAR LIMIT=NONE] -->
@@ -439,11 +439,17 @@
<string name="accessibility_battery_level">Battery <xliff:g id="number">%d</xliff:g> percent.</string>
<!-- Content description of the battery level icon for accessibility, including the estimated time remaining before the phone runs out of battery (not shown on the screen). [CHAR LIMIT=NONE] -->
- <string name="accessibility_battery_level_with_estimate">Battery <xliff:g id="percentage" example="95%">%1$s</xliff:g> percent, about <xliff:g id="time" example="Until 3:15pm">%2$s</xliff:g> left based on your usage</string>
+ <string name="accessibility_battery_level_with_estimate">Battery <xliff:g id="percentage" example="95%">%1$d</xliff:g> percent, about <xliff:g id="time" example="Until 3:15pm">%2$s</xliff:g> left based on your usage</string>
<!-- Content description of the battery level icon for accessibility while the device is charging (not shown on the screen). [CHAR LIMIT=NONE] -->
<string name="accessibility_battery_level_charging">Battery charging, <xliff:g id="battery_percentage">%d</xliff:g> percent.</string>
+ <!-- Content description of the battery level icon for accessibility, with information that the device charging is paused in order to protect the lifetime of the battery (not shown on screen). [CHAR LIMIT=NONE] -->
+ <string name="accessibility_battery_level_charging_paused">Battery <xliff:g id="percentage" example="90%">%d</xliff:g> percent. Charging paused for battery protection.</string>
+
+ <!-- Content description of the battery level icon for accessibility, including the estimated time remaining before the phone runs out of battery *and* information that the device charging is paused in order to protect the lifetime of the battery (not shown on screen). [CHAR LIMIT=NONE] -->
+ <string name="accessibility_battery_level_charging_paused_with_estimate">Battery <xliff:g id="percentage" example="90%">%1$d</xliff:g> percent, about <xliff:g id="time" example="Until 3:15pm">%2$s</xliff:g> left based on your usage. Charging paused for battery protection.</string>
+
<!-- Content description of overflow icon container of the notifications for accessibility (not shown on the screen)[CHAR LIMIT=NONE] -->
<string name="accessibility_overflow_action">See all notifications</string>
diff --git a/packages/SystemUI/res/xml/qqs_header.xml b/packages/SystemUI/res/xml/qqs_header.xml
index af4be1a..e07a6c1 100644
--- a/packages/SystemUI/res/xml/qqs_header.xml
+++ b/packages/SystemUI/res/xml/qqs_header.xml
@@ -25,7 +25,7 @@
android:id="@+id/clock">
<Layout
android:layout_width="wrap_content"
- android:layout_height="0dp"
+ android:layout_height="@dimen/large_screen_shade_header_min_height"
app:layout_constraintStart_toStartOf="@id/begin_guide"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
@@ -42,7 +42,7 @@
<Constraint
android:id="@+id/date">
<Layout
- android:layout_width="0dp"
+ android:layout_width="wrap_content"
android:layout_height="@dimen/new_qs_header_non_clickable_element_height"
android:layout_marginStart="8dp"
app:layout_constrainedWidth="true"
@@ -57,7 +57,7 @@
<Constraint
android:id="@+id/statusIcons">
<Layout
- android:layout_width="0dp"
+ android:layout_width="wrap_content"
android:layout_height="@dimen/new_qs_header_non_clickable_element_height"
app:layout_constraintHeight_min="@dimen/new_qs_header_non_clickable_element_height"
app:layout_constraintStart_toEndOf="@id/date"
@@ -65,6 +65,7 @@
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintHorizontal_bias="1"
+ app:layout_constraintHorizontal_chainStyle="packed"
/>
</Constraint>
@@ -80,12 +81,16 @@
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintHorizontal_bias="1"
+ app:layout_constraintHorizontal_chainStyle="packed"
/>
</Constraint>
<Constraint
android:id="@+id/carrier_group">
<Layout
+ app:layout_constraintWidth_min="48dp"
+ android:layout_width="wrap_content"
+ android:layout_height="@dimen/large_screen_shade_header_min_height"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
/>
diff --git a/packages/SystemUI/res/xml/qs_header_new.xml b/packages/SystemUI/res/xml/qs_header_new.xml
index d8a4e77..982c422 100644
--- a/packages/SystemUI/res/xml/qs_header_new.xml
+++ b/packages/SystemUI/res/xml/qs_header_new.xml
@@ -43,6 +43,7 @@
app:layout_constraintBottom_toBottomOf="@id/carrier_group"
app:layout_constraintEnd_toStartOf="@id/carrier_group"
app:layout_constraintHorizontal_bias="0"
+ app:layout_constraintHorizontal_chainStyle="spread_inside"
/>
<Transform
android:scaleX="2.57"
@@ -53,7 +54,7 @@
<Constraint
android:id="@+id/date">
<Layout
- android:layout_width="0dp"
+ android:layout_width="wrap_content"
android:layout_height="@dimen/new_qs_header_non_clickable_element_height"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@id/space"
@@ -67,16 +68,15 @@
<Constraint
android:id="@+id/carrier_group">
<Layout
- app:layout_constraintHeight_min="@dimen/large_screen_shade_header_min_height"
- android:minHeight="@dimen/large_screen_shade_header_min_height"
app:layout_constraintWidth_min="48dp"
- android:layout_width="0dp"
- android:layout_height="0dp"
+ android:layout_width="wrap_content"
+ android:layout_height="@dimen/large_screen_shade_header_min_height"
app:layout_constraintStart_toEndOf="@id/clock"
app:layout_constraintTop_toBottomOf="@id/privacy_container"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="1"
app:layout_constraintBottom_toTopOf="@id/batteryRemainingIcon"
+ app:layout_constraintHorizontal_chainStyle="spread_inside"
/>
<PropertySet
android:alpha="1"
@@ -86,7 +86,7 @@
<Constraint
android:id="@+id/statusIcons">
<Layout
- android:layout_width="0dp"
+ android:layout_width="wrap_content"
android:layout_height="@dimen/new_qs_header_non_clickable_element_height"
app:layout_constrainedWidth="true"
app:layout_constraintStart_toEndOf="@id/space"
@@ -108,6 +108,7 @@
app:layout_constraintTop_toTopOf="@id/date"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintHorizontal_bias="1"
+ app:layout_constraintHorizontal_chainStyle="spread_inside"
/>
</Constraint>
diff --git a/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/ViewScreenshotTestRule.kt b/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/ViewScreenshotTestRule.kt
index 0b0595f..36ac1ff 100644
--- a/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/ViewScreenshotTestRule.kt
+++ b/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/ViewScreenshotTestRule.kt
@@ -34,6 +34,7 @@
import platform.test.screenshot.DeviceEmulationRule
import platform.test.screenshot.DeviceEmulationSpec
import platform.test.screenshot.MaterialYouColorsRule
+import platform.test.screenshot.PathConfig
import platform.test.screenshot.ScreenshotTestRule
import platform.test.screenshot.getEmulatedDevicePathConfig
import platform.test.screenshot.matchers.BitmapMatcher
@@ -41,13 +42,19 @@
/** A rule for View screenshot diff unit tests. */
class ViewScreenshotTestRule(
emulationSpec: DeviceEmulationSpec,
- private val matcher: BitmapMatcher = UnitTestBitmapMatcher
+ private val matcher: BitmapMatcher = UnitTestBitmapMatcher,
+ pathConfig: PathConfig = getEmulatedDevicePathConfig(emulationSpec),
+ assetsPathRelativeToRepo: String = ""
) : TestRule {
private val colorsRule = MaterialYouColorsRule()
private val deviceEmulationRule = DeviceEmulationRule(emulationSpec)
private val screenshotRule =
ScreenshotTestRule(
- SystemUIGoldenImagePathManager(getEmulatedDevicePathConfig(emulationSpec))
+ if (assetsPathRelativeToRepo.isBlank()) {
+ SystemUIGoldenImagePathManager(pathConfig)
+ } else {
+ SystemUIGoldenImagePathManager(pathConfig, assetsPathRelativeToRepo)
+ }
)
private val activityRule = ActivityScenarioRule(ScreenshotActivity::class.java)
private val delegateRule =
diff --git a/packages/SystemUI/shared/Android.bp b/packages/SystemUI/shared/Android.bp
index 485a0d3..8a0fca0 100644
--- a/packages/SystemUI/shared/Android.bp
+++ b/packages/SystemUI/shared/Android.bp
@@ -63,9 +63,6 @@
resource_dirs: [
"res",
],
- optimize: {
- proguard_flags_files: ["proguard.flags"],
- },
min_sdk_version: "current",
plugins: ["dagger2-compiler"],
kotlincflags: ["-Xjvm-default=enable"],
diff --git a/packages/SystemUI/shared/proguard.flags b/packages/SystemUI/shared/proguard.flags
deleted file mode 100644
index 5eda045..0000000
--- a/packages/SystemUI/shared/proguard.flags
+++ /dev/null
@@ -1,4 +0,0 @@
-# Retain signatures of TypeToken and its subclasses for gson usage in ClockRegistry
--keepattributes Signature
--keep,allowobfuscation,allowshrinking class com.google.gson.reflect.TypeToken
--keep,allowobfuscation,allowshrinking class * extends com.google.gson.reflect.TypeToken
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/keyguard/logging/KeyguardLogger.kt b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardLogger.kt
index 32ce537..9e58500 100644
--- a/packages/SystemUI/src/com/android/keyguard/logging/KeyguardLogger.kt
+++ b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardLogger.kt
@@ -18,14 +18,11 @@
import com.android.systemui.log.dagger.KeyguardLog
import com.android.systemui.plugins.log.LogBuffer
-import com.android.systemui.plugins.log.LogLevel
import com.android.systemui.plugins.log.LogLevel.DEBUG
import com.android.systemui.plugins.log.LogLevel.ERROR
import com.android.systemui.plugins.log.LogLevel.INFO
import com.android.systemui.plugins.log.LogLevel.VERBOSE
import com.android.systemui.plugins.log.LogLevel.WARNING
-import com.android.systemui.plugins.log.MessageInitializer
-import com.android.systemui.plugins.log.MessagePrinter
import com.google.errorprone.annotations.CompileTimeConstant
import javax.inject.Inject
@@ -37,18 +34,16 @@
* an overkill.
*/
class KeyguardLogger @Inject constructor(@KeyguardLog private val buffer: LogBuffer) {
- fun d(@CompileTimeConstant msg: String) = log(msg, DEBUG)
+ fun d(@CompileTimeConstant msg: String) = buffer.log(TAG, DEBUG, msg)
- fun e(@CompileTimeConstant msg: String) = log(msg, ERROR)
+ fun e(@CompileTimeConstant msg: String) = buffer.log(TAG, ERROR, msg)
- fun v(@CompileTimeConstant msg: String) = log(msg, VERBOSE)
+ fun v(@CompileTimeConstant msg: String) = buffer.log(TAG, VERBOSE, msg)
- fun w(@CompileTimeConstant msg: String) = log(msg, WARNING)
+ fun w(@CompileTimeConstant msg: String) = buffer.log(TAG, WARNING, msg)
- fun log(msg: String, level: LogLevel) = buffer.log(TAG, level, msg)
-
- private fun debugLog(messageInitializer: MessageInitializer, messagePrinter: MessagePrinter) {
- buffer.log(TAG, DEBUG, messageInitializer, messagePrinter)
+ fun logException(ex: Exception, @CompileTimeConstant logMsg: String) {
+ buffer.log(TAG, ERROR, {}, { logMsg }, exception = ex)
}
fun v(msg: String, arg: Any) {
@@ -61,17 +56,24 @@
// TODO: remove after b/237743330 is fixed
fun logStatusBarCalculatedAlpha(alpha: Float) {
- debugLog({ double1 = alpha.toDouble() }, { "Calculated new alpha: $double1" })
+ buffer.log(TAG, DEBUG, { 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" })
+ buffer.log(
+ TAG,
+ DEBUG,
+ { double1 = alpha.toDouble() },
+ { "new mExplicitAlpha value: $double1" }
+ )
}
// TODO: remove after b/237743330 is fixed
fun logStatusBarAlphaVisibility(visibility: Int, alpha: Float, state: String) {
- debugLog(
+ buffer.log(
+ TAG,
+ DEBUG,
{
int1 = visibility
double1 = alpha.toDouble()
@@ -80,4 +82,22 @@
{ "changing visibility to $int1 with alpha $double1 in state: $str1" }
)
}
+
+ @JvmOverloads
+ fun logBiometricMessage(
+ @CompileTimeConstant context: String,
+ msgId: Int? = null,
+ msg: String? = null
+ ) {
+ buffer.log(
+ TAG,
+ DEBUG,
+ {
+ str1 = context
+ str2 = "$msgId"
+ str3 = msg
+ },
+ { "$str1 msgId: $str2 msg: $str3" }
+ )
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/battery/AccessorizedBatteryDrawable.kt b/packages/SystemUI/src/com/android/systemui/battery/AccessorizedBatteryDrawable.kt
new file mode 100644
index 0000000..b52ddc1
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/battery/AccessorizedBatteryDrawable.kt
@@ -0,0 +1,207 @@
+/*
+ * 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.battery
+
+import android.content.Context
+import android.graphics.Canvas
+import android.graphics.Color
+import android.graphics.ColorFilter
+import android.graphics.Matrix
+import android.graphics.Paint
+import android.graphics.Path
+import android.graphics.PixelFormat
+import android.graphics.PorterDuff
+import android.graphics.PorterDuffXfermode
+import android.graphics.Rect
+import android.graphics.drawable.DrawableWrapper
+import android.util.PathParser
+import com.android.settingslib.graph.ThemedBatteryDrawable
+import com.android.systemui.R
+import com.android.systemui.battery.BatterySpecs.BATTERY_HEIGHT
+import com.android.systemui.battery.BatterySpecs.BATTERY_HEIGHT_WITH_SHIELD
+import com.android.systemui.battery.BatterySpecs.BATTERY_WIDTH
+import com.android.systemui.battery.BatterySpecs.BATTERY_WIDTH_WITH_SHIELD
+import com.android.systemui.battery.BatterySpecs.SHIELD_LEFT_OFFSET
+import com.android.systemui.battery.BatterySpecs.SHIELD_STROKE
+import com.android.systemui.battery.BatterySpecs.SHIELD_TOP_OFFSET
+
+/**
+ * A battery drawable that accessorizes [ThemedBatteryDrawable] with additional information if
+ * necessary.
+ *
+ * For now, it adds a shield in the bottom-right corner when [displayShield] is true.
+ */
+class AccessorizedBatteryDrawable(
+ private val context: Context,
+ frameColor: Int,
+) : DrawableWrapper(ThemedBatteryDrawable(context, frameColor)) {
+ private val mainBatteryDrawable: ThemedBatteryDrawable
+ get() = drawable as ThemedBatteryDrawable
+
+ private val shieldPath = Path()
+ private val scaledShield = Path()
+ private val scaleMatrix = Matrix()
+
+ private var shieldLeftOffsetScaled = SHIELD_LEFT_OFFSET
+ private var shieldTopOffsetScaled = SHIELD_TOP_OFFSET
+
+ private var density = context.resources.displayMetrics.density
+
+ private val dualTone =
+ context.resources.getBoolean(com.android.internal.R.bool.config_batterymeterDualTone)
+
+ private val shieldTransparentOutlinePaint =
+ Paint(Paint.ANTI_ALIAS_FLAG).also { p ->
+ p.color = Color.TRANSPARENT
+ p.strokeWidth = ThemedBatteryDrawable.PROTECTION_MIN_STROKE_WIDTH
+ p.xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC_IN)
+ p.style = Paint.Style.FILL_AND_STROKE
+ }
+
+ private val shieldPaint =
+ Paint(Paint.ANTI_ALIAS_FLAG).also { p ->
+ p.color = Color.MAGENTA
+ p.style = Paint.Style.FILL
+ p.isDither = true
+ }
+
+ init {
+ loadPaths()
+ }
+
+ override fun onBoundsChange(bounds: Rect) {
+ super.onBoundsChange(bounds)
+ updateSizes()
+ }
+
+ var displayShield: Boolean = false
+
+ private fun updateSizes() {
+ val b = bounds
+ if (b.isEmpty) {
+ return
+ }
+
+ val mainWidth = BatterySpecs.getMainBatteryWidth(b.width().toFloat(), displayShield)
+ val mainHeight = BatterySpecs.getMainBatteryHeight(b.height().toFloat(), displayShield)
+
+ drawable?.setBounds(
+ b.left,
+ b.top,
+ /* right= */ b.left + mainWidth.toInt(),
+ /* bottom= */ b.top + mainHeight.toInt()
+ )
+
+ if (displayShield) {
+ val sx = b.right / BATTERY_WIDTH_WITH_SHIELD
+ val sy = b.bottom / BATTERY_HEIGHT_WITH_SHIELD
+ scaleMatrix.setScale(sx, sy)
+ shieldPath.transform(scaleMatrix, scaledShield)
+
+ shieldLeftOffsetScaled = sx * SHIELD_LEFT_OFFSET
+ shieldTopOffsetScaled = sy * SHIELD_TOP_OFFSET
+
+ val scaledStrokeWidth =
+ (sx * SHIELD_STROKE).coerceAtLeast(
+ ThemedBatteryDrawable.PROTECTION_MIN_STROKE_WIDTH
+ )
+ shieldTransparentOutlinePaint.strokeWidth = scaledStrokeWidth
+ }
+ }
+
+ override fun getIntrinsicHeight(): Int {
+ val height =
+ if (displayShield) {
+ BATTERY_HEIGHT_WITH_SHIELD
+ } else {
+ BATTERY_HEIGHT
+ }
+ return (height * density).toInt()
+ }
+
+ override fun getIntrinsicWidth(): Int {
+ val width =
+ if (displayShield) {
+ BATTERY_WIDTH_WITH_SHIELD
+ } else {
+ BATTERY_WIDTH
+ }
+ return (width * density).toInt()
+ }
+
+ override fun draw(c: Canvas) {
+ c.saveLayer(null, null)
+ // Draw the main battery icon
+ super.draw(c)
+
+ if (displayShield) {
+ c.translate(shieldLeftOffsetScaled, shieldTopOffsetScaled)
+ // We need a transparent outline around the shield, so first draw the transparent-ness
+ // then draw the shield
+ c.drawPath(scaledShield, shieldTransparentOutlinePaint)
+ c.drawPath(scaledShield, shieldPaint)
+ }
+ c.restore()
+ }
+
+ override fun getOpacity(): Int {
+ return PixelFormat.OPAQUE
+ }
+
+ override fun setAlpha(p0: Int) {
+ // Unused internally -- see [ThemedBatteryDrawable.setAlpha].
+ }
+
+ override fun setColorFilter(colorfilter: ColorFilter?) {
+ super.setColorFilter(colorFilter)
+ shieldPaint.colorFilter = colorFilter
+ }
+
+ /** Sets whether the battery is currently charging. */
+ fun setCharging(charging: Boolean) {
+ mainBatteryDrawable.charging = charging
+ }
+
+ /** Sets the current level (out of 100) of the battery. */
+ fun setBatteryLevel(level: Int) {
+ mainBatteryDrawable.setBatteryLevel(level)
+ }
+
+ /** Sets whether power save is enabled. */
+ fun setPowerSaveEnabled(powerSaveEnabled: Boolean) {
+ mainBatteryDrawable.powerSaveEnabled = powerSaveEnabled
+ }
+
+ /** Returns whether power save is currently enabled. */
+ fun getPowerSaveEnabled(): Boolean {
+ return mainBatteryDrawable.powerSaveEnabled
+ }
+
+ /** Sets the colors to use for the icon. */
+ fun setColors(fgColor: Int, bgColor: Int, singleToneColor: Int) {
+ shieldPaint.color = if (dualTone) fgColor else singleToneColor
+ mainBatteryDrawable.setColors(fgColor, bgColor, singleToneColor)
+ }
+
+ /** Notifies this drawable that the density might have changed. */
+ fun notifyDensityChanged() {
+ density = context.resources.displayMetrics.density
+ }
+
+ private fun loadPaths() {
+ val shieldPathString = context.resources.getString(R.string.config_batterymeterShieldPath)
+ shieldPath.set(PathParser.createPathFromPathData(shieldPathString))
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterView.java b/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterView.java
index 6a10d4a..03d999f 100644
--- a/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterView.java
+++ b/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterView.java
@@ -45,7 +45,6 @@
import androidx.annotation.StyleRes;
import androidx.annotation.VisibleForTesting;
-import com.android.settingslib.graph.ThemedBatteryDrawable;
import com.android.systemui.DualToneHandler;
import com.android.systemui.R;
import com.android.systemui.animation.Interpolators;
@@ -68,7 +67,7 @@
public static final int MODE_OFF = 2;
public static final int MODE_ESTIMATE = 3;
- private final ThemedBatteryDrawable mDrawable;
+ private final AccessorizedBatteryDrawable mDrawable;
private final ImageView mBatteryIconView;
private TextView mBatteryPercentView;
@@ -77,7 +76,10 @@
private int mLevel;
private int mShowPercentMode = MODE_DEFAULT;
private boolean mShowPercentAvailable;
+ private String mEstimateText = null;
private boolean mCharging;
+ private boolean mIsOverheated;
+ private boolean mDisplayShieldEnabled;
// Error state where we know nothing about the current battery state
private boolean mBatteryStateUnknown;
// Lazily-loaded since this is expected to be a rare-if-ever state
@@ -106,7 +108,7 @@
final int frameColor = atts.getColor(R.styleable.BatteryMeterView_frameColor,
context.getColor(R.color.meter_background_color));
mPercentageStyleId = atts.getResourceId(R.styleable.BatteryMeterView_textAppearance, 0);
- mDrawable = new ThemedBatteryDrawable(context, frameColor);
+ mDrawable = new AccessorizedBatteryDrawable(context, frameColor);
atts.recycle();
mShowPercentAvailable = context.getResources().getBoolean(
@@ -170,12 +172,14 @@
if (mode == mShowPercentMode) return;
mShowPercentMode = mode;
updateShowPercent();
+ updatePercentText();
}
@Override
protected void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
updatePercentView();
+ mDrawable.notifyDensityChanged();
}
public void setColorsFromContext(Context context) {
@@ -203,6 +207,17 @@
mDrawable.setPowerSaveEnabled(isPowerSave);
}
+ void onIsOverheatedChanged(boolean isOverheated) {
+ boolean valueChanged = mIsOverheated != isOverheated;
+ mIsOverheated = isOverheated;
+ if (valueChanged) {
+ updateContentDescription();
+ // The battery drawable is a different size depending on whether it's currently
+ // overheated or not, so we need to re-scale the view when overheated changes.
+ scaleBatteryMeterViews();
+ }
+ }
+
private TextView loadPercentView() {
return (TextView) LayoutInflater.from(getContext())
.inflate(R.layout.battery_percentage_view, null);
@@ -227,13 +242,17 @@
mBatteryEstimateFetcher = fetcher;
}
+ void setDisplayShieldEnabled(boolean displayShieldEnabled) {
+ mDisplayShieldEnabled = displayShieldEnabled;
+ }
+
void updatePercentText() {
if (mBatteryStateUnknown) {
- setContentDescription(getContext().getString(R.string.accessibility_battery_unknown));
return;
}
if (mBatteryEstimateFetcher == null) {
+ setPercentTextAtCurrentLevel();
return;
}
@@ -245,10 +264,9 @@
return;
}
if (estimate != null && mShowPercentMode == MODE_ESTIMATE) {
+ mEstimateText = estimate;
mBatteryPercentView.setText(estimate);
- setContentDescription(getContext().getString(
- R.string.accessibility_battery_level_with_estimate,
- mLevel, estimate));
+ updateContentDescription();
} else {
setPercentTextAtCurrentLevel();
}
@@ -257,28 +275,49 @@
setPercentTextAtCurrentLevel();
}
} else {
- setContentDescription(
- getContext().getString(mCharging ? R.string.accessibility_battery_level_charging
- : R.string.accessibility_battery_level, mLevel));
+ updateContentDescription();
}
}
private void setPercentTextAtCurrentLevel() {
- if (mBatteryPercentView == null) {
- return;
+ if (mBatteryPercentView != null) {
+ mEstimateText = null;
+ String percentText = NumberFormat.getPercentInstance().format(mLevel / 100f);
+ // Setting text actually triggers a layout pass (because the text view is set to
+ // wrap_content width and TextView always relayouts for this). Avoid needless
+ // relayout if the text didn't actually change.
+ if (!TextUtils.equals(mBatteryPercentView.getText(), percentText)) {
+ mBatteryPercentView.setText(percentText);
+ }
}
- String percentText = NumberFormat.getPercentInstance().format(mLevel / 100f);
- // Setting text actually triggers a layout pass (because the text view is set to
- // wrap_content width and TextView always relayouts for this). Avoid needless
- // relayout if the text didn't actually change.
- if (!TextUtils.equals(mBatteryPercentView.getText(), percentText)) {
- mBatteryPercentView.setText(percentText);
+ updateContentDescription();
+ }
+
+ private void updateContentDescription() {
+ Context context = getContext();
+
+ String contentDescription;
+ if (mBatteryStateUnknown) {
+ contentDescription = context.getString(R.string.accessibility_battery_unknown);
+ } else if (mShowPercentMode == MODE_ESTIMATE && !TextUtils.isEmpty(mEstimateText)) {
+ contentDescription = context.getString(
+ mIsOverheated
+ ? R.string.accessibility_battery_level_charging_paused_with_estimate
+ : R.string.accessibility_battery_level_with_estimate,
+ mLevel,
+ mEstimateText);
+ } else if (mIsOverheated) {
+ contentDescription =
+ context.getString(R.string.accessibility_battery_level_charging_paused, mLevel);
+ } else if (mCharging) {
+ contentDescription =
+ context.getString(R.string.accessibility_battery_level_charging, mLevel);
+ } else {
+ contentDescription = context.getString(R.string.accessibility_battery_level, mLevel);
}
- setContentDescription(
- getContext().getString(mCharging ? R.string.accessibility_battery_level_charging
- : R.string.accessibility_battery_level, mLevel));
+ setContentDescription(contentDescription);
}
void updateShowPercent() {
@@ -329,6 +368,7 @@
}
mBatteryStateUnknown = isUnknown;
+ updateContentDescription();
if (mBatteryStateUnknown) {
mBatteryIconView.setImageDrawable(getUnknownStateDrawable());
@@ -349,15 +389,43 @@
res.getValue(R.dimen.status_bar_icon_scale_factor, typedValue, true);
float iconScaleFactor = typedValue.getFloat();
- int batteryHeight = res.getDimensionPixelSize(R.dimen.status_bar_battery_icon_height);
- int batteryWidth = res.getDimensionPixelSize(R.dimen.status_bar_battery_icon_width);
+ float mainBatteryHeight =
+ res.getDimensionPixelSize(R.dimen.status_bar_battery_icon_height) * iconScaleFactor;
+ float mainBatteryWidth =
+ res.getDimensionPixelSize(R.dimen.status_bar_battery_icon_width) * iconScaleFactor;
+
+ // If the battery is marked as overheated, we should display a shield indicating that the
+ // battery is being "defended".
+ boolean displayShield = mDisplayShieldEnabled && mIsOverheated;
+ float fullBatteryIconHeight =
+ BatterySpecs.getFullBatteryHeight(mainBatteryHeight, displayShield);
+ float fullBatteryIconWidth =
+ BatterySpecs.getFullBatteryWidth(mainBatteryWidth, displayShield);
+
+ int marginTop;
+ if (displayShield) {
+ // If the shield is displayed, we need some extra marginTop so that the bottom of the
+ // main icon is still aligned with the bottom of all the other system icons.
+ int shieldHeightAddition = Math.round(fullBatteryIconHeight - mainBatteryHeight);
+ // However, the other system icons have some embedded bottom padding that the battery
+ // doesn't have, so we shouldn't move the battery icon down by the full amount.
+ // See b/258672854.
+ marginTop = shieldHeightAddition
+ - res.getDimensionPixelSize(R.dimen.status_bar_battery_extra_vertical_spacing);
+ } else {
+ marginTop = 0;
+ }
+
int marginBottom = res.getDimensionPixelSize(R.dimen.battery_margin_bottom);
LinearLayout.LayoutParams scaledLayoutParams = new LinearLayout.LayoutParams(
- (int) (batteryWidth * iconScaleFactor), (int) (batteryHeight * iconScaleFactor));
- scaledLayoutParams.setMargins(0, 0, 0, marginBottom);
+ Math.round(fullBatteryIconWidth),
+ Math.round(fullBatteryIconHeight));
+ scaledLayoutParams.setMargins(0, marginTop, 0, marginBottom);
+ mDrawable.setDisplayShield(displayShield);
mBatteryIconView.setLayoutParams(scaledLayoutParams);
+ mBatteryIconView.invalidateDrawable(mDrawable);
}
@Override
diff --git a/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterViewController.java b/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterViewController.java
index ae9a323..77cb9d1 100644
--- a/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterViewController.java
@@ -29,6 +29,8 @@
import com.android.systemui.broadcast.BroadcastDispatcher;
import com.android.systemui.dagger.qualifiers.Main;
+import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.flags.Flags;
import com.android.systemui.settings.CurrentUserTracker;
import com.android.systemui.statusbar.phone.StatusBarIconController;
import com.android.systemui.statusbar.policy.BatteryController;
@@ -84,6 +86,11 @@
public void onBatteryUnknownStateChanged(boolean isUnknown) {
mView.onBatteryUnknownStateChanged(isUnknown);
}
+
+ @Override
+ public void onIsOverheatedChanged(boolean isOverheated) {
+ mView.onIsOverheatedChanged(isOverheated);
+ }
};
// Some places may need to show the battery conditionally, and not obey the tuner
@@ -98,6 +105,7 @@
BroadcastDispatcher broadcastDispatcher,
@Main Handler mainHandler,
ContentResolver contentResolver,
+ FeatureFlags featureFlags,
BatteryController batteryController) {
super(view);
mConfigurationController = configurationController;
@@ -106,6 +114,7 @@
mBatteryController = batteryController;
mView.setBatteryEstimateFetcher(mBatteryController::getEstimatedTimeRemainingString);
+ mView.setDisplayShieldEnabled(featureFlags.isEnabled(Flags.BATTERY_SHIELD_ICON));
mSlotBattery = getResources().getString(com.android.internal.R.string.status_bar_battery);
mSettingObserver = new SettingObserver(mainHandler);
diff --git a/packages/SystemUI/src/com/android/systemui/battery/BatterySpecs.kt b/packages/SystemUI/src/com/android/systemui/battery/BatterySpecs.kt
new file mode 100644
index 0000000..6455a96
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/battery/BatterySpecs.kt
@@ -0,0 +1,109 @@
+/*
+ * 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.battery
+
+import com.android.settingslib.graph.ThemedBatteryDrawable
+
+/** An object storing specs related to the battery icon in the status bar. */
+object BatterySpecs {
+
+ /** Width of the main battery icon, not including the shield. */
+ const val BATTERY_WIDTH = ThemedBatteryDrawable.WIDTH
+ /** Height of the main battery icon, not including the shield. */
+ const val BATTERY_HEIGHT = ThemedBatteryDrawable.HEIGHT
+
+ private const val SHIELD_WIDTH = 10f
+ private const val SHIELD_HEIGHT = 13f
+
+ /**
+ * Amount that the left side of the shield should be offset from the left side of the battery.
+ */
+ const val SHIELD_LEFT_OFFSET = 8f
+ /** Amount that the top of the shield should be offset from the top of the battery. */
+ const val SHIELD_TOP_OFFSET = 10f
+
+ const val SHIELD_STROKE = 4f
+
+ /** The full width of the battery icon, including the main battery icon *and* the shield. */
+ const val BATTERY_WIDTH_WITH_SHIELD = SHIELD_LEFT_OFFSET + SHIELD_WIDTH
+ /** The full height of the battery icon, including the main battery icon *and* the shield. */
+ const val BATTERY_HEIGHT_WITH_SHIELD = SHIELD_TOP_OFFSET + SHIELD_HEIGHT
+
+ /**
+ * Given the desired height of the main battery icon in pixels, returns the height that the full
+ * battery icon will take up in pixels.
+ *
+ * If there's no shield, this will just return [mainBatteryHeight]. Otherwise, the shield
+ * extends slightly below the bottom of the main battery icon so we need some extra height.
+ */
+ @JvmStatic
+ fun getFullBatteryHeight(mainBatteryHeight: Float, displayShield: Boolean): Float {
+ return if (!displayShield) {
+ mainBatteryHeight
+ } else {
+ val verticalScaleFactor = mainBatteryHeight / BATTERY_HEIGHT
+ verticalScaleFactor * BATTERY_HEIGHT_WITH_SHIELD
+ }
+ }
+
+ /**
+ * Given the desired width of the main battery icon in pixels, returns the width that the full
+ * battery icon will take up in pixels.
+ *
+ * If there's no shield, this will just return [mainBatteryWidth]. Otherwise, the shield extends
+ * past the right side of the main battery icon so we need some extra width.
+ */
+ @JvmStatic
+ fun getFullBatteryWidth(mainBatteryWidth: Float, displayShield: Boolean): Float {
+ return if (!displayShield) {
+ mainBatteryWidth
+ } else {
+ val horizontalScaleFactor = mainBatteryWidth / BATTERY_WIDTH
+ horizontalScaleFactor * BATTERY_WIDTH_WITH_SHIELD
+ }
+ }
+
+ /**
+ * Given the height of the full battery icon, return how tall the main battery icon should be.
+ *
+ * If there's no shield, this will just return [fullBatteryHeight]. Otherwise, the shield takes
+ * up some of the view's height so the main battery width will be just a portion of
+ * [fullBatteryHeight].
+ */
+ @JvmStatic
+ fun getMainBatteryHeight(fullBatteryHeight: Float, displayShield: Boolean): Float {
+ return if (!displayShield) {
+ fullBatteryHeight
+ } else {
+ return (BATTERY_HEIGHT / BATTERY_HEIGHT_WITH_SHIELD) * fullBatteryHeight
+ }
+ }
+
+ /**
+ * Given the width of the full battery icon, return how wide the main battery icon should be.
+ *
+ * If there's no shield, this will just return [fullBatteryWidth]. Otherwise, the shield takes
+ * up some of the view's width so the main battery width will be just a portion of
+ * [fullBatteryWidth].
+ */
+ @JvmStatic
+ fun getMainBatteryWidth(fullBatteryWidth: Float, displayShield: Boolean): Float {
+ return if (!displayShield) {
+ fullBatteryWidth
+ } else {
+ return (BATTERY_WIDTH / BATTERY_WIDTH_WITH_SHIELD) * fullBatteryWidth
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/TEST_MAPPING b/packages/SystemUI/src/com/android/systemui/biometrics/TEST_MAPPING
new file mode 100644
index 0000000..794eba4
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/TEST_MAPPING
@@ -0,0 +1,16 @@
+{
+ "presubmit": [
+ {
+ // TODO(b/251476085): Consider merging with SystemUIGoogleScreenshotTests (in U+)
+ "name": "SystemUIGoogleBiometricsScreenshotTests",
+ "options": [
+ {
+ "exclude-annotation": "org.junit.Ignore"
+ },
+ {
+ "exclude-annotation": "androidx.test.filters.FlakyTest"
+ }
+ ]
+ }
+ ]
+}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/CredentialPasswordViewBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/CredentialPasswordViewBinder.kt
index c619648..7dde947 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/CredentialPasswordViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/CredentialPasswordViewBinder.kt
@@ -13,7 +13,6 @@
import com.android.systemui.biometrics.ui.CredentialView
import com.android.systemui.biometrics.ui.viewmodel.CredentialViewModel
import com.android.systemui.lifecycle.repeatWhenAttached
-import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
/** Sub-binder for the [CredentialPasswordView]. */
@@ -24,14 +23,17 @@
view: CredentialPasswordView,
host: CredentialView.Host,
viewModel: CredentialViewModel,
+ requestFocusForInput: Boolean,
) {
val imeManager = view.context.getSystemService(InputMethodManager::class.java)!!
val passwordField: ImeAwareEditText = view.requireViewById(R.id.lockPassword)
view.repeatWhenAttached {
- passwordField.requestFocus()
- passwordField.scheduleShowSoftInput()
+ if (requestFocusForInput) {
+ passwordField.requestFocus()
+ passwordField.scheduleShowSoftInput()
+ }
repeatOnLifecycle(Lifecycle.State.STARTED) {
// observe credential validation attempts and submit/cancel buttons
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/CredentialPatternViewBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/CredentialPatternViewBinder.kt
index 4765551..b692ad3 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/CredentialPatternViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/CredentialPatternViewBinder.kt
@@ -9,7 +9,6 @@
import com.android.systemui.biometrics.ui.CredentialView
import com.android.systemui.biometrics.ui.viewmodel.CredentialViewModel
import com.android.systemui.lifecycle.repeatWhenAttached
-import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
/** Sub-binder for the [CredentialPatternView]. */
@@ -30,7 +29,7 @@
viewModel.header.collect { header ->
lockPatternView.setOnPatternListener(
OnPatternDetectedListener { pattern ->
- if (pattern.isPatternLongEnough()) {
+ if (pattern.isPatternTooShort()) {
// Pattern size is less than the minimum
// do not count it as a failed attempt
viewModel.showPatternTooShortError()
@@ -71,5 +70,5 @@
}
}
-private fun List<LockPatternView.Cell>.isPatternLongEnough(): Boolean =
+private fun List<LockPatternView.Cell>.isPatternTooShort(): Boolean =
size < LockPatternUtils.MIN_PATTERN_REGISTER_FAIL
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/CredentialViewBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/CredentialViewBinder.kt
index fcc9487..e2d36dc 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/CredentialViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/CredentialViewBinder.kt
@@ -17,7 +17,6 @@
import com.android.systemui.lifecycle.repeatWhenAttached
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
-import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
@@ -40,6 +39,7 @@
panelViewController: AuthPanelController,
animatePanel: Boolean,
maxErrorDuration: Long = 3_000L,
+ requestFocusForInput: Boolean = true,
) {
val titleView: TextView = view.requireViewById(R.id.title)
val subtitleView: TextView = view.requireViewById(R.id.subtitle)
@@ -110,7 +110,8 @@
// bind the auth widget
when (view) {
- is CredentialPasswordView -> CredentialPasswordViewBinder.bind(view, host, viewModel)
+ is CredentialPasswordView ->
+ CredentialPasswordViewBinder.bind(view, host, viewModel, requestFocusForInput)
is CredentialPatternView -> CredentialPatternViewBinder.bind(view, host, viewModel)
else -> throw IllegalStateException("unexpected view type: ${view.javaClass.name}")
}
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/GlobalRootComponent.java b/packages/SystemUI/src/com/android/systemui/dagger/GlobalRootComponent.java
index fe89c9a..9e8c0ec 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/GlobalRootComponent.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/GlobalRootComponent.java
@@ -21,24 +21,21 @@
import com.android.systemui.dagger.qualifiers.InstrumentationTest;
import com.android.systemui.util.InitializationChecker;
-import javax.inject.Singleton;
-
import dagger.BindsInstance;
-import dagger.Component;
/**
* Base root component for Dagger injection.
*
+ * This class is not actually annotated as a Dagger component, since it is not used directly as one.
+ * Doing so generates unnecessary code bloat.
+ *
* See {@link ReferenceGlobalRootComponent} for the one actually used by AOSP.
*/
-@Singleton
-@Component(modules = {GlobalModule.class})
public interface GlobalRootComponent {
/**
* Builder for a GlobalRootComponent.
*/
- @Component.Builder
interface Builder {
@BindsInstance
Builder context(Context context);
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/complication/DreamHomeControlsComplication.java b/packages/SystemUI/src/com/android/systemui/dreams/complication/DreamHomeControlsComplication.java
index cedd850a..c01cf43 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/complication/DreamHomeControlsComplication.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/complication/DreamHomeControlsComplication.java
@@ -33,6 +33,7 @@
import com.android.internal.logging.UiEventLogger;
import com.android.systemui.CoreStartable;
import com.android.systemui.animation.ActivityLaunchAnimator;
+import com.android.systemui.controls.ControlsServiceInfo;
import com.android.systemui.controls.dagger.ControlsComponent;
import com.android.systemui.controls.management.ControlsListingController;
import com.android.systemui.controls.ui.ControlsActivity;
@@ -42,6 +43,8 @@
import com.android.systemui.plugins.ActivityStarter;
import com.android.systemui.util.ViewController;
+import java.util.List;
+
import javax.inject.Inject;
import javax.inject.Named;
@@ -76,16 +79,25 @@
private final DreamOverlayStateController mDreamOverlayStateController;
private final ControlsComponent mControlsComponent;
- private boolean mControlServicesAvailable = false;
+ private boolean mOverlayActive = false;
// Callback for when the home controls service availability changes.
private final ControlsListingController.ControlsListingCallback mControlsCallback =
- serviceInfos -> {
- boolean available = !serviceInfos.isEmpty();
+ services -> updateHomeControlsComplication();
- if (available != mControlServicesAvailable) {
- mControlServicesAvailable = available;
- updateComplicationAvailability();
+ private final DreamOverlayStateController.Callback mOverlayStateCallback =
+ new DreamOverlayStateController.Callback() {
+ @Override
+ public void onStateChanged() {
+ if (mOverlayActive == mDreamOverlayStateController.isOverlayActive()) {
+ return;
+ }
+
+ mOverlayActive = !mOverlayActive;
+
+ if (mOverlayActive) {
+ updateHomeControlsComplication();
+ }
}
};
@@ -102,18 +114,29 @@
public void start() {
mControlsComponent.getControlsListingController().ifPresent(
c -> c.addCallback(mControlsCallback));
+ mDreamOverlayStateController.addCallback(mOverlayStateCallback);
}
- private void updateComplicationAvailability() {
+ private void updateHomeControlsComplication() {
+ mControlsComponent.getControlsListingController().ifPresent(c -> {
+ if (isHomeControlsAvailable(c.getCurrentServices())) {
+ mDreamOverlayStateController.addComplication(mComplication);
+ } else {
+ mDreamOverlayStateController.removeComplication(mComplication);
+ }
+ });
+ }
+
+ private boolean isHomeControlsAvailable(List<ControlsServiceInfo> controlsServices) {
+ if (controlsServices.isEmpty()) {
+ return false;
+ }
+
final boolean hasFavorites = mControlsComponent.getControlsController()
.map(c -> !c.getFavorites().isEmpty())
.orElse(false);
- if (!hasFavorites || !mControlServicesAvailable
- || mControlsComponent.getVisibility() == UNAVAILABLE) {
- mDreamOverlayStateController.removeComplication(mComplication);
- } else {
- mDreamOverlayStateController.addComplication(mComplication);
- }
+ final ControlsComponent.Visibility visibility = mControlsComponent.getVisibility();
+ return hasFavorites && visibility != UNAVAILABLE;
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamModule.java b/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamModule.java
index f9dca08..101f4a4 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamModule.java
@@ -44,7 +44,7 @@
DreamOverlayComponent.class,
})
public interface DreamModule {
- String DREAM_ONLY_ENABLED_FOR_SYSTEM_USER = "dream_only_enabled_for_system_user";
+ String DREAM_ONLY_ENABLED_FOR_DOCK_USER = "dream_only_enabled_for_dock_user";
String DREAM_SUPPORTED = "dream_supported";
@@ -70,10 +70,10 @@
/** */
@Provides
- @Named(DREAM_ONLY_ENABLED_FOR_SYSTEM_USER)
- static boolean providesDreamOnlyEnabledForSystemUser(@Main Resources resources) {
+ @Named(DREAM_ONLY_ENABLED_FOR_DOCK_USER)
+ static boolean providesDreamOnlyEnabledForDockUser(@Main Resources resources) {
return resources.getBoolean(
- com.android.internal.R.bool.config_dreamsOnlyEnabledForSystemUser);
+ com.android.internal.R.bool.config_dreamsOnlyEnabledForDockUser);
}
/** */
diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
index c4cc338..076e705 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
@@ -82,7 +82,10 @@
// TODO(b/257506350): Tracking Bug
val FSI_CHROME = unreleasedFlag(117, "fsi_chrome")
- // next id: 118
+ // TODO(b/257315550): Tracking Bug
+ val NO_HUN_FOR_OLD_WHEN = unreleasedFlag(118, "no_hun_for_old_when")
+
+ // next id: 119
// 200 - keyguard/lockscreen
// ** Flag retired **
@@ -239,6 +242,9 @@
// TODO(b/256613548): Tracking Bug
val NEW_STATUS_BAR_WIFI_ICON_BACKEND = unreleasedFlag(609, "new_status_bar_wifi_icon_backend")
+ // TODO(b/256623670): Tracking Bug
+ @JvmField val BATTERY_SHIELD_ICON = unreleasedFlag(610, "battery_shield_icon")
+
// 700 - dialer/calls
// TODO(b/254512734): Tracking Bug
val ONGOING_CALL_STATUS_BAR_CHIP = releasedFlag(700, "ongoing_call_status_bar_chip")
@@ -380,7 +386,8 @@
// TODO(b/254513155): Tracking Bug
@JvmField
- val SCREENSHOT_WORK_PROFILE_POLICY = unreleasedFlag(1301, "screenshot_work_profile_policy")
+ val SCREENSHOT_WORK_PROFILE_POLICY =
+ unreleasedFlag(1301, "screenshot_work_profile_policy", teamfood = true)
// 1400 - columbus
// TODO(b/254512756): Tracking Bug
diff --git a/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java b/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java
index 0697133..f92bbf7 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java
@@ -364,13 +364,18 @@
private void distributeTiles() {
emptyAndInflateOrRemovePages();
- final int tileCount = mPages.get(0).maxTiles();
- if (DEBUG) Log.d(TAG, "Distributing tiles");
+ final int tilesPerPageCount = mPages.get(0).maxTiles();
int index = 0;
- final int NT = mTiles.size();
- for (int i = 0; i < NT; i++) {
+ final int totalTilesCount = mTiles.size();
+ if (DEBUG) {
+ Log.d(TAG, "Distributing tiles: "
+ + "[tilesPerPageCount=" + tilesPerPageCount + "]"
+ + "[totalTilesCount=" + totalTilesCount + "]"
+ );
+ }
+ for (int i = 0; i < totalTilesCount; i++) {
TileRecord tile = mTiles.get(i);
- if (mPages.get(index).mRecords.size() == tileCount) index++;
+ if (mPages.get(index).mRecords.size() == tilesPerPageCount) index++;
if (DEBUG) {
Log.d(TAG, "Adding " + tile.tile.getClass().getSimpleName() + " to "
+ index);
@@ -577,8 +582,8 @@
});
setOffscreenPageLimit(lastPageNumber); // Ensure the page to reveal has been inflated.
int dx = getWidth() * lastPageNumber;
- mScroller.startScroll(getScrollX(), getScrollY(), isLayoutRtl() ? -dx : dx, 0,
- REVEAL_SCROLL_DURATION_MILLIS);
+ mScroller.startScroll(getScrollX(), getScrollY(), isLayoutRtl() ? -dx : dx, 0,
+ REVEAL_SCROLL_DURATION_MILLIS);
postInvalidateOnAnimation();
}
@@ -738,6 +743,7 @@
public interface PageListener {
int INVALID_PAGE = -1;
+
void onPageChanged(boolean isFirst, int pageNumber);
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/TileLayout.java b/packages/SystemUI/src/com/android/systemui/qs/TileLayout.java
index 3d00dd4..7ee4047 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/TileLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/TileLayout.java
@@ -123,7 +123,6 @@
public boolean updateResources() {
final Resources res = mContext.getResources();
mResourceColumns = Math.max(1, res.getInteger(R.integer.quick_settings_num_columns));
- updateColumns();
mMaxCellHeight = mContext.getResources().getDimensionPixelSize(mCellHeightResId);
mCellMarginHorizontal = res.getDimensionPixelSize(R.dimen.qs_tile_margin_horizontal);
mSidePadding = useSidePadding() ? mCellMarginHorizontal / 2 : 0;
diff --git a/packages/SystemUI/src/com/android/systemui/qs/customize/QSCustomizer.java b/packages/SystemUI/src/com/android/systemui/qs/customize/QSCustomizer.java
index cf10c79..79fcc7d 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/customize/QSCustomizer.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/customize/QSCustomizer.java
@@ -82,12 +82,12 @@
DefaultItemAnimator animator = new DefaultItemAnimator();
animator.setMoveDuration(TileAdapter.MOVE_DURATION);
mRecyclerView.setItemAnimator(animator);
+
+ updateTransparentViewHeight();
}
void updateResources() {
- LayoutParams lp = (LayoutParams) mTransparentView.getLayoutParams();
- lp.height = QSUtils.getQsHeaderSystemIconsAreaHeight(mContext);
- mTransparentView.setLayoutParams(lp);
+ updateTransparentViewHeight();
mRecyclerView.getAdapter().notifyItemChanged(0);
}
@@ -236,4 +236,10 @@
public boolean isOpening() {
return mOpening;
}
+
+ private void updateTransparentViewHeight() {
+ LayoutParams lp = (LayoutParams) mTransparentView.getLayoutParams();
+ lp.height = QSUtils.getQsHeaderSystemIconsAreaHeight(mContext);
+ mTransparentView.setLayoutParams(lp);
+ }
}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/DreamTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/DreamTile.java
index bebd580..4abe309 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/DreamTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/DreamTile.java
@@ -70,7 +70,7 @@
private final SettingObserver mDreamSettingObserver;
private final UserTracker mUserTracker;
private final boolean mDreamSupported;
- private final boolean mDreamOnlyEnabledForSystemUser;
+ private final boolean mDreamOnlyEnabledForDockUser;
private boolean mIsDocked = false;
@@ -100,8 +100,8 @@
BroadcastDispatcher broadcastDispatcher,
UserTracker userTracker,
@Named(DreamModule.DREAM_SUPPORTED) boolean dreamSupported,
- @Named(DreamModule.DREAM_ONLY_ENABLED_FOR_SYSTEM_USER)
- boolean dreamOnlyEnabledForSystemUser
+ @Named(DreamModule.DREAM_ONLY_ENABLED_FOR_DOCK_USER)
+ boolean dreamOnlyEnabledForDockUser
) {
super(host, backgroundLooper, mainHandler, falsingManager, metricsLogger,
statusBarStateController, activityStarter, qsLogger);
@@ -123,7 +123,7 @@
};
mUserTracker = userTracker;
mDreamSupported = dreamSupported;
- mDreamOnlyEnabledForSystemUser = dreamOnlyEnabledForSystemUser;
+ mDreamOnlyEnabledForDockUser = dreamOnlyEnabledForDockUser;
}
@Override
@@ -203,7 +203,8 @@
// For now, restrict to debug users.
return Build.isDebuggable()
&& mDreamSupported
- && (!mDreamOnlyEnabledForSystemUser || mUserTracker.getUserHandle().isSystem());
+ // TODO(b/257333623): Allow the Dock User to be non-SystemUser user in HSUM.
+ && (!mDreamOnlyEnabledForDockUser || mUserTracker.getUserHandle().isSystem());
}
@VisibleForTesting
diff --git a/packages/SystemUI/src/com/android/systemui/shade/CombinedShadeHeadersConstraintManagerImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/CombinedShadeHeadersConstraintManagerImpl.kt
index 4063af3..954534d 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/CombinedShadeHeadersConstraintManagerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/CombinedShadeHeadersConstraintManagerImpl.kt
@@ -92,7 +92,6 @@
centerEnd,
ConstraintSet.END
)
- constrainWidth(R.id.statusIcons, 0)
},
qsConstraintsChanges = {
setGuidelineBegin(centerStart, offsetFromEdge)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java
index 15f4b12..2101efb 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java
@@ -55,7 +55,6 @@
import android.hardware.face.FaceManager;
import android.hardware.fingerprint.FingerprintManager;
import android.os.BatteryManager;
-import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
@@ -64,7 +63,6 @@
import android.os.UserManager;
import android.text.TextUtils;
import android.text.format.Formatter;
-import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.view.accessibility.AccessibilityManager;
@@ -76,6 +74,7 @@
import com.android.internal.widget.LockPatternUtils;
import com.android.keyguard.KeyguardUpdateMonitor;
import com.android.keyguard.KeyguardUpdateMonitorCallback;
+import com.android.keyguard.logging.KeyguardLogger;
import com.android.settingslib.Utils;
import com.android.settingslib.fuelgauge.BatteryStatus;
import com.android.systemui.R;
@@ -123,7 +122,6 @@
private static final String TAG = "KeyguardIndication";
private static final boolean DEBUG_CHARGING_SPEED = false;
- private static final boolean DEBUG = Build.IS_DEBUGGABLE;
private static final int MSG_HIDE_TRANSIENT = 1;
private static final int MSG_SHOW_ACTION_TO_UNLOCK = 2;
@@ -139,6 +137,7 @@
protected final StatusBarStateController mStatusBarStateController;
private final KeyguardUpdateMonitor mKeyguardUpdateMonitor;
private final AuthController mAuthController;
+ private final KeyguardLogger mKeyguardLogger;
private ViewGroup mIndicationArea;
private KeyguardIndicationTextView mTopIndicationView;
private KeyguardIndicationTextView mLockScreenIndicationView;
@@ -229,7 +228,8 @@
ScreenLifecycle screenLifecycle,
KeyguardBypassController keyguardBypassController,
AccessibilityManager accessibilityManager,
- FaceHelpMessageDeferral faceHelpMessageDeferral) {
+ FaceHelpMessageDeferral faceHelpMessageDeferral,
+ KeyguardLogger keyguardLogger) {
mContext = context;
mBroadcastDispatcher = broadcastDispatcher;
mDevicePolicyManager = devicePolicyManager;
@@ -249,6 +249,7 @@
mKeyguardBypassController = keyguardBypassController;
mAccessibilityManager = accessibilityManager;
mScreenLifecycle = screenLifecycle;
+ mKeyguardLogger = keyguardLogger;
mScreenLifecycle.addObserver(mScreenObserver);
mFaceAcquiredMessageDeferral = faceHelpMessageDeferral;
@@ -1024,7 +1025,7 @@
mChargingTimeRemaining = mPowerPluggedIn
? mBatteryInfo.computeChargeTimeRemaining() : -1;
} catch (RemoteException e) {
- Log.e(TAG, "Error calling IBatteryStats: ", e);
+ mKeyguardLogger.logException(e, "Error calling IBatteryStats");
mChargingTimeRemaining = -1;
}
updateDeviceEntryIndication(!wasPluggedIn && mPowerPluggedInWired);
@@ -1072,8 +1073,10 @@
final boolean isCoExFaceAcquisitionMessage =
faceAuthSoftError && isUnlockWithFingerprintPossible;
if (isCoExFaceAcquisitionMessage && !mCoExFaceAcquisitionMsgIdsToShow.contains(msgId)) {
- debugLog("skip showing msgId=" + msgId + " helpString=" + helpString
- + ", due to co-ex logic");
+ mKeyguardLogger.logBiometricMessage(
+ "skipped showing help message due to co-ex logic",
+ msgId,
+ helpString);
} else if (mStatusBarKeyguardViewManager.isBouncerShowing()) {
mStatusBarKeyguardViewManager.setKeyguardMessage(helpString,
mInitialTextColorState);
@@ -1131,7 +1134,7 @@
CharSequence deferredFaceMessage = mFaceAcquiredMessageDeferral.getDeferredMessage();
mFaceAcquiredMessageDeferral.reset();
if (shouldSuppressFaceError(msgId, mKeyguardUpdateMonitor)) {
- debugLog("suppressingFaceError msgId=" + msgId + " errString= " + errString);
+ mKeyguardLogger.logBiometricMessage("suppressingFaceError", msgId, errString);
return;
}
if (msgId == FaceManager.FACE_ERROR_TIMEOUT) {
@@ -1145,8 +1148,9 @@
private void onFingerprintAuthError(int msgId, String errString) {
if (shouldSuppressFingerprintError(msgId, mKeyguardUpdateMonitor)) {
- debugLog("suppressingFingerprintError msgId=" + msgId
- + " errString= " + errString);
+ mKeyguardLogger.logBiometricMessage("suppressingFingerprintError",
+ msgId,
+ errString);
} else {
showErrorMessageNowOrLater(errString, null);
}
@@ -1282,7 +1286,8 @@
}
private void handleFaceAuthTimeoutError(@Nullable CharSequence deferredFaceMessage) {
- debugLog("showDeferredFaceMessage msgId=" + deferredFaceMessage);
+ mKeyguardLogger.logBiometricMessage("deferred message after face auth timeout",
+ null, String.valueOf(deferredFaceMessage));
if (canUnlockWithFingerprint()) {
// Co-ex: show deferred message OR nothing
// if we're on the lock screen (bouncer isn't showing), show the deferred msg
@@ -1294,7 +1299,8 @@
);
} else {
// otherwise, don't show any message
- debugLog("skip showing FACE_ERROR_TIMEOUT due to co-ex logic");
+ mKeyguardLogger.logBiometricMessage(
+ "skip showing FACE_ERROR_TIMEOUT due to co-ex logic");
}
} else if (deferredFaceMessage != null) {
// Face-only: The face timeout message is not very actionable, let's ask the
@@ -1315,12 +1321,6 @@
KeyguardUpdateMonitor.getCurrentUser());
}
- private void debugLog(String logMsg) {
- if (DEBUG) {
- Log.d(TAG, logMsg);
- }
- }
-
private void showErrorMessageNowOrLater(String errString, @Nullable String followUpMsg) {
if (mStatusBarKeyguardViewManager.isBouncerShowing()) {
mStatusBarKeyguardViewManager.setKeyguardMessage(errString, mInitialTextColorState);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarViewModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarViewModule.java
index 0369845..2d580ab 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarViewModule.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarViewModule.java
@@ -225,6 +225,7 @@
BroadcastDispatcher broadcastDispatcher,
@Main Handler mainHandler,
ContentResolver contentResolver,
+ FeatureFlags featureFlags,
BatteryController batteryController
) {
return new BatteryMeterViewController(
@@ -234,6 +235,7 @@
broadcastDispatcher,
mainHandler,
contentResolver,
+ featureFlags,
batteryController);
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryController.java
index 149ed0a..d10d7cf 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryController.java
@@ -155,6 +155,9 @@
default void onWirelessChargingChanged(boolean isWirlessCharging) {
}
+
+ default void onIsOverheatedChanged(boolean isOverheated) {
+ }
}
/**
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryControllerImpl.java
index c7ad767..3c2ac7b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryControllerImpl.java
@@ -16,6 +16,9 @@
package com.android.systemui.statusbar.policy;
+import static android.os.BatteryManager.BATTERY_HEALTH_OVERHEAT;
+import static android.os.BatteryManager.BATTERY_HEALTH_UNKNOWN;
+import static android.os.BatteryManager.EXTRA_HEALTH;
import static android.os.BatteryManager.EXTRA_PRESENT;
import android.annotation.WorkerThread;
@@ -87,6 +90,7 @@
protected boolean mPowerSave;
private boolean mAodPowerSave;
private boolean mWirelessCharging;
+ private boolean mIsOverheated = false;
private boolean mTestMode = false;
@VisibleForTesting
boolean mHasReceivedBattery = false;
@@ -184,6 +188,7 @@
cb.onPowerSaveChanged(mPowerSave);
cb.onBatteryUnknownStateChanged(mStateUnknown);
cb.onWirelessChargingChanged(mWirelessCharging);
+ cb.onIsOverheatedChanged(mIsOverheated);
}
@Override
@@ -222,6 +227,13 @@
fireBatteryUnknownStateChanged();
}
+ int batteryHealth = intent.getIntExtra(EXTRA_HEALTH, BATTERY_HEALTH_UNKNOWN);
+ boolean isOverheated = batteryHealth == BATTERY_HEALTH_OVERHEAT;
+ if (isOverheated != mIsOverheated) {
+ mIsOverheated = isOverheated;
+ fireIsOverheatedChanged();
+ }
+
fireBatteryLevelChanged();
} else if (action.equals(PowerManager.ACTION_POWER_SAVE_MODE_CHANGED)) {
updatePowerSave();
@@ -292,6 +304,10 @@
return mPluggedChargingSource == BatteryManager.BATTERY_PLUGGED_WIRELESS;
}
+ public boolean isOverheated() {
+ return mIsOverheated;
+ }
+
@Override
public void getEstimatedTimeRemainingString(EstimateFetchCompletion completion) {
// Need to fetch or refresh the estimate, but it may involve binder calls so offload the
@@ -402,6 +418,15 @@
}
}
+ private void fireIsOverheatedChanged() {
+ synchronized (mChangeCallbacks) {
+ final int n = mChangeCallbacks.size();
+ for (int i = 0; i < n; i++) {
+ mChangeCallbacks.get(i).onIsOverheatedChanged(mIsOverheated);
+ }
+ }
+ }
+
@Override
public void dispatchDemoCommand(String command, Bundle args) {
if (!mDemoModeController.isInDemoMode()) {
@@ -412,6 +437,7 @@
String plugged = args.getString("plugged");
String powerSave = args.getString("powersave");
String present = args.getString("present");
+ String overheated = args.getString("overheated");
if (level != null) {
mLevel = Math.min(Math.max(Integer.parseInt(level), 0), 100);
}
@@ -426,6 +452,10 @@
mStateUnknown = !present.equals("true");
fireBatteryUnknownStateChanged();
}
+ if (overheated != null) {
+ mIsOverheated = overheated.equals("true");
+ fireIsOverheatedChanged();
+ }
fireBatteryLevelChanged();
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/battery/AccessorizedBatteryDrawableTest.kt b/packages/SystemUI/tests/src/com/android/systemui/battery/AccessorizedBatteryDrawableTest.kt
new file mode 100644
index 0000000..982f033
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/battery/AccessorizedBatteryDrawableTest.kt
@@ -0,0 +1,52 @@
+/*
+ * 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.battery
+
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.battery.BatterySpecs.BATTERY_HEIGHT
+import com.android.systemui.battery.BatterySpecs.BATTERY_HEIGHT_WITH_SHIELD
+import com.android.systemui.battery.BatterySpecs.BATTERY_WIDTH
+import com.android.systemui.battery.BatterySpecs.BATTERY_WIDTH_WITH_SHIELD
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+
+@SmallTest
+class AccessorizedBatteryDrawableTest : SysuiTestCase() {
+ @Test
+ fun intrinsicSize_shieldFalse_isBatterySize() {
+ val drawable = AccessorizedBatteryDrawable(context, frameColor = 0)
+ drawable.displayShield = false
+
+ val density = context.resources.displayMetrics.density
+ assertThat(drawable.intrinsicHeight).isEqualTo((BATTERY_HEIGHT * density).toInt())
+ assertThat(drawable.intrinsicWidth).isEqualTo((BATTERY_WIDTH * density).toInt())
+ }
+
+ @Test
+ fun intrinsicSize_shieldTrue_isBatteryPlusShieldSize() {
+ val drawable = AccessorizedBatteryDrawable(context, frameColor = 0)
+ drawable.displayShield = true
+
+ val density = context.resources.displayMetrics.density
+ assertThat(drawable.intrinsicHeight)
+ .isEqualTo((BATTERY_HEIGHT_WITH_SHIELD * density).toInt())
+ assertThat(drawable.intrinsicWidth).isEqualTo((BATTERY_WIDTH_WITH_SHIELD * density).toInt())
+ }
+
+ // TODO(b/255625888): Screenshot tests for this drawable would be amazing!
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/battery/BatteryMeterViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/battery/BatteryMeterViewControllerTest.java
index 1d038a4..bc8f961 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/battery/BatteryMeterViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/battery/BatteryMeterViewControllerTest.java
@@ -35,6 +35,8 @@
import com.android.systemui.SysuiTestCase;
import com.android.systemui.broadcast.BroadcastDispatcher;
+import com.android.systemui.flags.FakeFeatureFlags;
+import com.android.systemui.flags.Flags;
import com.android.systemui.statusbar.policy.BatteryController;
import com.android.systemui.statusbar.policy.ConfigurationController;
import com.android.systemui.tuner.TunerService;
@@ -59,6 +61,7 @@
private Handler mHandler;
@Mock
private ContentResolver mContentResolver;
+ private FakeFeatureFlags mFeatureFlags;
@Mock
private BatteryController mBatteryController;
@@ -71,19 +74,13 @@
when(mBatteryMeterView.getContext()).thenReturn(mContext);
when(mBatteryMeterView.getResources()).thenReturn(mContext.getResources());
- mController = new BatteryMeterViewController(
- mBatteryMeterView,
- mConfigurationController,
- mTunerService,
- mBroadcastDispatcher,
- mHandler,
- mContentResolver,
- mBatteryController
- );
+ mFeatureFlags = new FakeFeatureFlags();
+ mFeatureFlags.set(Flags.BATTERY_SHIELD_ICON, false);
}
@Test
public void onViewAttached_callbacksRegistered() {
+ initController();
mController.onViewAttached();
verify(mConfigurationController).addCallback(any());
@@ -101,6 +98,7 @@
@Test
public void onViewDetached_callbacksUnregistered() {
+ initController();
// Set everything up first.
mController.onViewAttached();
@@ -114,6 +112,7 @@
@Test
public void ignoreTunerUpdates_afterOnViewAttached_callbackUnregistered() {
+ initController();
// Start out receiving tuner updates
mController.onViewAttached();
@@ -124,10 +123,43 @@
@Test
public void ignoreTunerUpdates_beforeOnViewAttached_callbackNeverRegistered() {
+ initController();
+
mController.ignoreTunerUpdates();
mController.onViewAttached();
verify(mTunerService, never()).addTunable(any(), any());
}
+
+ @Test
+ public void shieldFlagDisabled_viewNotified() {
+ mFeatureFlags.set(Flags.BATTERY_SHIELD_ICON, false);
+
+ initController();
+
+ verify(mBatteryMeterView).setDisplayShieldEnabled(false);
+ }
+
+ @Test
+ public void shieldFlagEnabled_viewNotified() {
+ mFeatureFlags.set(Flags.BATTERY_SHIELD_ICON, true);
+
+ initController();
+
+ verify(mBatteryMeterView).setDisplayShieldEnabled(true);
+ }
+
+ private void initController() {
+ mController = new BatteryMeterViewController(
+ mBatteryMeterView,
+ mConfigurationController,
+ mTunerService,
+ mBroadcastDispatcher,
+ mHandler,
+ mContentResolver,
+ mFeatureFlags,
+ mBatteryController
+ );
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/battery/BatteryMeterViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/battery/BatteryMeterViewTest.kt
index b4ff2a5..eb7d9c3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/battery/BatteryMeterViewTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/battery/BatteryMeterViewTest.kt
@@ -17,7 +17,9 @@
import android.testing.AndroidTestingRunner
import android.testing.TestableLooper.RunWithLooper
+import android.widget.ImageView
import androidx.test.filters.SmallTest
+import com.android.systemui.R
import com.android.systemui.SysuiTestCase
import com.android.systemui.battery.BatteryMeterView.BatteryEstimateFetcher
import com.android.systemui.statusbar.policy.BatteryController.EstimateFetchCompletion
@@ -58,6 +60,182 @@
// No assert needed
}
+ @Test
+ fun contentDescription_unknown() {
+ mBatteryMeterView.onBatteryUnknownStateChanged(true)
+
+ assertThat(mBatteryMeterView.contentDescription).isEqualTo(
+ context.getString(R.string.accessibility_battery_unknown)
+ )
+ }
+
+ @Test
+ fun contentDescription_estimate() {
+ mBatteryMeterView.onBatteryLevelChanged(15, false)
+ mBatteryMeterView.setPercentShowMode(BatteryMeterView.MODE_ESTIMATE)
+ mBatteryMeterView.setBatteryEstimateFetcher(Fetcher())
+
+ mBatteryMeterView.updatePercentText()
+
+ assertThat(mBatteryMeterView.contentDescription).isEqualTo(
+ context.getString(
+ R.string.accessibility_battery_level_with_estimate, 15, ESTIMATE
+ )
+ )
+ }
+
+ @Test
+ fun contentDescription_estimateAndOverheated() {
+ mBatteryMeterView.onBatteryLevelChanged(17, false)
+ mBatteryMeterView.onIsOverheatedChanged(true)
+ mBatteryMeterView.setPercentShowMode(BatteryMeterView.MODE_ESTIMATE)
+ mBatteryMeterView.setBatteryEstimateFetcher(Fetcher())
+
+ mBatteryMeterView.updatePercentText()
+
+ assertThat(mBatteryMeterView.contentDescription).isEqualTo(
+ context.getString(
+ R.string.accessibility_battery_level_charging_paused_with_estimate,
+ 17,
+ ESTIMATE,
+ )
+ )
+ }
+
+ @Test
+ fun contentDescription_overheated() {
+ mBatteryMeterView.onBatteryLevelChanged(90, false)
+ mBatteryMeterView.onIsOverheatedChanged(true)
+
+ assertThat(mBatteryMeterView.contentDescription).isEqualTo(
+ context.getString(R.string.accessibility_battery_level_charging_paused, 90)
+ )
+ }
+
+ @Test
+ fun contentDescription_charging() {
+ mBatteryMeterView.onBatteryLevelChanged(45, true)
+
+ assertThat(mBatteryMeterView.contentDescription).isEqualTo(
+ context.getString(R.string.accessibility_battery_level_charging, 45)
+ )
+ }
+
+ @Test
+ fun contentDescription_notCharging() {
+ mBatteryMeterView.onBatteryLevelChanged(45, false)
+
+ assertThat(mBatteryMeterView.contentDescription).isEqualTo(
+ context.getString(R.string.accessibility_battery_level, 45)
+ )
+ }
+
+ @Test
+ fun changesFromEstimateToPercent_textAndContentDescriptionChanges() {
+ mBatteryMeterView.onBatteryLevelChanged(15, false)
+ mBatteryMeterView.setPercentShowMode(BatteryMeterView.MODE_ESTIMATE)
+ mBatteryMeterView.setBatteryEstimateFetcher(Fetcher())
+
+ mBatteryMeterView.updatePercentText()
+
+ assertThat(mBatteryMeterView.contentDescription).isEqualTo(
+ context.getString(
+ R.string.accessibility_battery_level_with_estimate, 15, ESTIMATE
+ )
+ )
+
+ // Update the show mode from estimate to percent
+ mBatteryMeterView.setPercentShowMode(BatteryMeterView.MODE_ON)
+
+ assertThat(mBatteryMeterView.batteryPercentViewText).isEqualTo("15%")
+ assertThat(mBatteryMeterView.contentDescription).isEqualTo(
+ context.getString(R.string.accessibility_battery_level, 15)
+ )
+ }
+
+ @Test
+ fun contentDescription_manyUpdates_alwaysUpdated() {
+ // Overheated
+ mBatteryMeterView.onBatteryLevelChanged(90, false)
+ mBatteryMeterView.onIsOverheatedChanged(true)
+ assertThat(mBatteryMeterView.contentDescription).isEqualTo(
+ context.getString(R.string.accessibility_battery_level_charging_paused, 90)
+ )
+
+ // Overheated & estimate
+ mBatteryMeterView.setPercentShowMode(BatteryMeterView.MODE_ESTIMATE)
+ mBatteryMeterView.setBatteryEstimateFetcher(Fetcher())
+ mBatteryMeterView.updatePercentText()
+ assertThat(mBatteryMeterView.contentDescription).isEqualTo(
+ context.getString(
+ R.string.accessibility_battery_level_charging_paused_with_estimate,
+ 90,
+ ESTIMATE,
+ )
+ )
+
+ // Just estimate
+ mBatteryMeterView.onIsOverheatedChanged(false)
+ assertThat(mBatteryMeterView.contentDescription).isEqualTo(
+ context.getString(
+ R.string.accessibility_battery_level_with_estimate,
+ 90,
+ ESTIMATE,
+ )
+ )
+
+ // Just percent
+ mBatteryMeterView.setPercentShowMode(BatteryMeterView.MODE_ON)
+ assertThat(mBatteryMeterView.contentDescription).isEqualTo(
+ context.getString(R.string.accessibility_battery_level, 90)
+ )
+
+ // Charging
+ mBatteryMeterView.onBatteryLevelChanged(90, true)
+ assertThat(mBatteryMeterView.contentDescription).isEqualTo(
+ context.getString(R.string.accessibility_battery_level_charging, 90)
+ )
+ }
+
+ @Test
+ fun isOverheatedChanged_true_drawableGetsTrue() {
+ mBatteryMeterView.setDisplayShieldEnabled(true)
+ val drawable = getBatteryDrawable()
+
+ mBatteryMeterView.onIsOverheatedChanged(true)
+
+ assertThat(drawable.displayShield).isTrue()
+ }
+
+ @Test
+ fun isOverheatedChanged_false_drawableGetsFalse() {
+ mBatteryMeterView.setDisplayShieldEnabled(true)
+ val drawable = getBatteryDrawable()
+
+ // Start as true
+ mBatteryMeterView.onIsOverheatedChanged(true)
+
+ // Update to false
+ mBatteryMeterView.onIsOverheatedChanged(false)
+
+ assertThat(drawable.displayShield).isFalse()
+ }
+
+ @Test
+ fun isOverheatedChanged_true_featureflagOff_drawableGetsFalse() {
+ mBatteryMeterView.setDisplayShieldEnabled(false)
+ val drawable = getBatteryDrawable()
+
+ mBatteryMeterView.onIsOverheatedChanged(true)
+
+ assertThat(drawable.displayShield).isFalse()
+ }
+
+ private fun getBatteryDrawable(): AccessorizedBatteryDrawable {
+ return (mBatteryMeterView.getChildAt(0) as ImageView)
+ .drawable as AccessorizedBatteryDrawable
+ }
+
private class Fetcher : BatteryEstimateFetcher {
override fun fetchBatteryTimeRemainingEstimate(
completion: EstimateFetchCompletion) {
@@ -68,4 +246,4 @@
private companion object {
const val ESTIMATE = "2 hours 2 minutes"
}
-}
\ No newline at end of file
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/battery/BatterySpecsTest.kt b/packages/SystemUI/tests/src/com/android/systemui/battery/BatterySpecsTest.kt
new file mode 100644
index 0000000..39cb047
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/battery/BatterySpecsTest.kt
@@ -0,0 +1,101 @@
+/*
+ * 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.battery
+
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.battery.BatterySpecs.BATTERY_HEIGHT
+import com.android.systemui.battery.BatterySpecs.BATTERY_HEIGHT_WITH_SHIELD
+import com.android.systemui.battery.BatterySpecs.BATTERY_WIDTH
+import com.android.systemui.battery.BatterySpecs.BATTERY_WIDTH_WITH_SHIELD
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+
+@SmallTest
+class BatterySpecsTest : SysuiTestCase() {
+ @Test
+ fun getFullBatteryHeight_shieldFalse_returnsMainHeight() {
+ val fullHeight = BatterySpecs.getFullBatteryHeight(56f, displayShield = false)
+
+ assertThat(fullHeight).isEqualTo(56f)
+ }
+
+ @Test
+ fun getFullBatteryHeight_shieldTrue_returnsMainHeightPlusShield() {
+ val mainHeight = BATTERY_HEIGHT * 5
+ val fullHeight = BatterySpecs.getFullBatteryHeight(mainHeight, displayShield = true)
+
+ // Since the main battery was scaled 5x, the output height should also be scaled 5x
+ val expectedFullHeight = BATTERY_HEIGHT_WITH_SHIELD * 5
+
+ assertThat(fullHeight).isWithin(.0001f).of(expectedFullHeight)
+ }
+
+ @Test
+ fun getFullBatteryWidth_shieldFalse_returnsMainWidth() {
+ val fullWidth = BatterySpecs.getFullBatteryWidth(33f, displayShield = false)
+
+ assertThat(fullWidth).isEqualTo(33f)
+ }
+
+ @Test
+ fun getFullBatteryWidth_shieldTrue_returnsMainWidthPlusShield() {
+ val mainWidth = BATTERY_WIDTH * 3.3f
+
+ val fullWidth = BatterySpecs.getFullBatteryWidth(mainWidth, displayShield = true)
+
+ // Since the main battery was scaled 3.3x, the output width should also be scaled 5x
+ val expectedFullWidth = BATTERY_WIDTH_WITH_SHIELD * 3.3f
+ assertThat(fullWidth).isWithin(.0001f).of(expectedFullWidth)
+ }
+
+ @Test
+ fun getMainBatteryHeight_shieldFalse_returnsFullHeight() {
+ val mainHeight = BatterySpecs.getMainBatteryHeight(89f, displayShield = false)
+
+ assertThat(mainHeight).isEqualTo(89f)
+ }
+
+ @Test
+ fun getMainBatteryHeight_shieldTrue_returnsNotFullHeight() {
+ val fullHeight = BATTERY_HEIGHT_WITH_SHIELD * 7.7f
+
+ val mainHeight = BatterySpecs.getMainBatteryHeight(fullHeight, displayShield = true)
+
+ // Since the full height was scaled 7.7x, the main height should also be scaled 7.7x.
+ val expectedHeight = BATTERY_HEIGHT * 7.7f
+ assertThat(mainHeight).isWithin(.0001f).of(expectedHeight)
+ }
+
+ @Test
+ fun getMainBatteryWidth_shieldFalse_returnsFullWidth() {
+ val mainWidth = BatterySpecs.getMainBatteryWidth(2345f, displayShield = false)
+
+ assertThat(mainWidth).isEqualTo(2345f)
+ }
+
+ @Test
+ fun getMainBatteryWidth_shieldTrue_returnsNotFullWidth() {
+ val fullWidth = BATTERY_WIDTH_WITH_SHIELD * 0.6f
+
+ val mainWidth = BatterySpecs.getMainBatteryWidth(fullWidth, displayShield = true)
+
+ // Since the full width was scaled 0.6x, the main height should also be scaled 0.6x.
+ val expectedWidth = BATTERY_WIDTH * 0.6f
+ assertThat(mainWidth).isWithin(.0001f).of(expectedWidth)
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/DreamHomeControlsComplicationTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/DreamHomeControlsComplicationTest.java
index aa8c93e..30ad485 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/DreamHomeControlsComplicationTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/DreamHomeControlsComplicationTest.java
@@ -90,7 +90,10 @@
private ActivityStarter mActivityStarter;
@Mock
- UiEventLogger mUiEventLogger;
+ private UiEventLogger mUiEventLogger;
+
+ @Captor
+ private ArgumentCaptor<DreamOverlayStateController.Callback> mStateCallbackCaptor;
@Before
public void setup() {
@@ -164,6 +167,29 @@
verify(mDreamOverlayStateController).addComplication(mComplication);
}
+ @Test
+ public void complicationAvailability_checkAvailabilityWhenDreamOverlayBecomesActive() {
+ final DreamHomeControlsComplication.Registrant registrant =
+ new DreamHomeControlsComplication.Registrant(mComplication,
+ mDreamOverlayStateController, mControlsComponent);
+ registrant.start();
+
+ setServiceAvailable(true);
+ setHaveFavorites(false);
+
+ // Complication not available on start.
+ verify(mDreamOverlayStateController, never()).addComplication(mComplication);
+
+ // Favorite controls added, complication should be available now.
+ setHaveFavorites(true);
+
+ // Dream overlay becomes active.
+ setDreamOverlayActive(true);
+
+ // Verify complication is added.
+ verify(mDreamOverlayStateController).addComplication(mComplication);
+ }
+
/**
* Ensures clicking home controls chip logs UiEvent.
*/
@@ -196,10 +222,17 @@
private void setServiceAvailable(boolean value) {
final List<ControlsServiceInfo> serviceInfos = mock(List.class);
+ when(mControlsListingController.getCurrentServices()).thenReturn(serviceInfos);
when(serviceInfos.isEmpty()).thenReturn(!value);
triggerControlsListingCallback(serviceInfos);
}
+ private void setDreamOverlayActive(boolean value) {
+ when(mDreamOverlayStateController.isOverlayActive()).thenReturn(value);
+ verify(mDreamOverlayStateController).addCallback(mStateCallbackCaptor.capture());
+ mStateCallbackCaptor.getValue().onStateChanged();
+ }
+
private void triggerControlsListingCallback(List<ControlsServiceInfo> serviceInfos) {
verify(mControlsListingController).addCallback(mCallbackCaptor.capture());
mCallbackCaptor.getValue().onServicesUpdated(serviceInfos);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/TileLayoutTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/TileLayoutTest.java
index 5abc0e1..35c8cc7 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/TileLayoutTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/TileLayoutTest.java
@@ -27,6 +27,8 @@
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
+import android.content.Context;
+import android.content.res.Resources;
import android.test.suitebuilder.annotation.SmallTest;
import android.view.accessibility.AccessibilityNodeInfo;
@@ -42,16 +44,22 @@
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
+import org.mockito.Mockito;
@SmallTest
@RunWith(AndroidJUnit4.class)
public class TileLayoutTest extends SysuiTestCase {
- private TileLayout mTileLayout;
+ private Resources mResources;
private int mLayoutSizeForOneTile;
+ private TileLayout mTileLayout; // under test
@Before
public void setUp() throws Exception {
- mTileLayout = new TileLayout(mContext);
+ Context context = Mockito.spy(mContext);
+ mResources = Mockito.spy(context.getResources());
+ Mockito.when(mContext.getResources()).thenReturn(mResources);
+
+ mTileLayout = new TileLayout(context);
// Layout needs to leave space for the tile margins. Three times the margin size is
// sufficient for any number of columns.
mLayoutSizeForOneTile =
@@ -203,4 +211,21 @@
verify(tileRecord1.tileView).setPosition(0);
verify(tileRecord2.tileView).setPosition(1);
}
+
+ @Test
+ public void resourcesChanged_updateResources_returnsTrue() {
+ Mockito.when(mResources.getInteger(R.integer.quick_settings_num_columns)).thenReturn(1);
+ mTileLayout.updateResources(); // setup with 1
+ Mockito.when(mResources.getInteger(R.integer.quick_settings_num_columns)).thenReturn(2);
+
+ assertEquals(true, mTileLayout.updateResources());
+ }
+
+ @Test
+ public void resourcesSame_updateResources_returnsFalse() {
+ Mockito.when(mResources.getInteger(R.integer.quick_settings_num_columns)).thenReturn(1);
+ mTileLayout.updateResources(); // setup with 1
+
+ assertEquals(false, mTileLayout.updateResources());
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/CombinedShadeHeaderConstraintsTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/CombinedShadeHeaderConstraintsTest.kt
index 0ce9056..d7eb337 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/CombinedShadeHeaderConstraintsTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/CombinedShadeHeaderConstraintsTest.kt
@@ -24,6 +24,7 @@
import com.android.systemui.R
import com.android.systemui.SysuiTestCase
import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.Truth.assertWithMessage
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@@ -320,6 +321,48 @@
assertThat(changes.largeScreenConstraintsChanges).isNull()
}
+ @Test
+ fun testRelevantViewsAreNotMatchConstraints() {
+ val views = mapOf(
+ R.id.clock to "clock",
+ R.id.date to "date",
+ R.id.statusIcons to "icons",
+ R.id.privacy_container to "privacy",
+ R.id.carrier_group to "carriers",
+ R.id.batteryRemainingIcon to "battery",
+ )
+ views.forEach { (id, name) ->
+ assertWithMessage("$name has 0 height in qqs")
+ .that(qqsConstraint.getConstraint(id).layout.mHeight).isNotEqualTo(0)
+ assertWithMessage("$name has 0 width in qqs")
+ .that(qqsConstraint.getConstraint(id).layout.mWidth).isNotEqualTo(0)
+ assertWithMessage("$name has 0 height in qs")
+ .that(qsConstraint.getConstraint(id).layout.mHeight).isNotEqualTo(0)
+ assertWithMessage("$name has 0 width in qs")
+ .that(qsConstraint.getConstraint(id).layout.mWidth).isNotEqualTo(0)
+ }
+ }
+
+ @Test
+ fun testCheckViewsDontChangeSizeBetweenAnimationConstraints() {
+ val views = mapOf(
+ R.id.clock to "clock",
+ R.id.date to "date",
+ R.id.statusIcons to "icons",
+ R.id.privacy_container to "privacy",
+ R.id.carrier_group to "carriers",
+ R.id.batteryRemainingIcon to "battery",
+ )
+ views.forEach { (id, name) ->
+ assertWithMessage("$name changes height")
+ .that(qqsConstraint.getConstraint(id).layout.mHeight)
+ .isEqualTo(qsConstraint.getConstraint(id).layout.mHeight)
+ assertWithMessage("$name changes width")
+ .that(qqsConstraint.getConstraint(id).layout.mWidth)
+ .isEqualTo(qsConstraint.getConstraint(id).layout.mWidth)
+ }
+ }
+
private operator fun ConstraintsChanges.invoke() {
qqsConstraintsChanges?.invoke(qqsConstraint)
qsConstraintsChanges?.invoke(qsConstraint)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyguardIndicationControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyguardIndicationControllerTest.java
index fe4da47..e8a7ec8 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyguardIndicationControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyguardIndicationControllerTest.java
@@ -87,6 +87,7 @@
import com.android.internal.widget.LockPatternUtils;
import com.android.keyguard.KeyguardUpdateMonitor;
import com.android.keyguard.KeyguardUpdateMonitorCallback;
+import com.android.keyguard.logging.KeyguardLogger;
import com.android.settingslib.fuelgauge.BatteryStatus;
import com.android.systemui.R;
import com.android.systemui.SysuiTestCase;
@@ -271,7 +272,7 @@
mUserManager, mExecutor, mExecutor, mFalsingManager,
mAuthController, mLockPatternUtils, mScreenLifecycle,
mKeyguardBypassController, mAccessibilityManager,
- mFaceHelpMessageDeferral);
+ mFaceHelpMessageDeferral, mock(KeyguardLogger.class));
mController.init();
mController.setIndicationArea(mIndicationArea);
verify(mStatusBarStateController).addCallback(mStatusBarStateListenerCaptor.capture());
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/BatteryControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/BatteryControllerTest.java
index 43d0fe9..1eee08c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/BatteryControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/BatteryControllerTest.java
@@ -221,4 +221,33 @@
Assert.assertFalse(mBatteryController.isChargingSourceDock());
}
+
+ @Test
+ public void batteryStateChanged_healthNotOverheated_outputsFalse() {
+ Intent intent = new Intent(Intent.ACTION_BATTERY_CHANGED);
+ intent.putExtra(BatteryManager.EXTRA_HEALTH, BatteryManager.BATTERY_HEALTH_GOOD);
+
+ mBatteryController.onReceive(getContext(), intent);
+
+ Assert.assertFalse(mBatteryController.isOverheated());
+ }
+
+ @Test
+ public void batteryStateChanged_healthOverheated_outputsTrue() {
+ Intent intent = new Intent(Intent.ACTION_BATTERY_CHANGED);
+ intent.putExtra(BatteryManager.EXTRA_HEALTH, BatteryManager.BATTERY_HEALTH_OVERHEAT);
+
+ mBatteryController.onReceive(getContext(), intent);
+
+ Assert.assertTrue(mBatteryController.isOverheated());
+ }
+
+ @Test
+ public void batteryStateChanged_noHealthGiven_outputsFalse() {
+ Intent intent = new Intent(Intent.ACTION_BATTERY_CHANGED);
+
+ mBatteryController.onReceive(getContext(), intent);
+
+ Assert.assertFalse(mBatteryController.isOverheated());
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
index d42f757..db3b9b5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
@@ -1331,7 +1331,7 @@
spyOn(mContext);
mBubbleController.updateBubble(mBubbleEntry);
verify(mContext).registerReceiver(mBroadcastReceiverArgumentCaptor.capture(),
- mFilterArgumentCaptor.capture());
+ mFilterArgumentCaptor.capture(), eq(Context.RECEIVER_EXPORTED));
assertThat(mFilterArgumentCaptor.getValue().getAction(0)).isEqualTo(
Intent.ACTION_CLOSE_SYSTEM_DIALOGS);
assertThat(mFilterArgumentCaptor.getValue().getAction(1)).isEqualTo(
@@ -1351,7 +1351,7 @@
mBubbleController.updateBubble(mBubbleEntry);
mBubbleData.setExpanded(true);
verify(mContext).registerReceiver(mBroadcastReceiverArgumentCaptor.capture(),
- mFilterArgumentCaptor.capture());
+ mFilterArgumentCaptor.capture(), eq(Context.RECEIVER_EXPORTED));
Intent i = new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS);
mBroadcastReceiverArgumentCaptor.getValue().onReceive(mContext, i);
@@ -1365,7 +1365,7 @@
mBubbleData.setExpanded(true);
verify(mContext).registerReceiver(mBroadcastReceiverArgumentCaptor.capture(),
- mFilterArgumentCaptor.capture());
+ mFilterArgumentCaptor.capture(), eq(Context.RECEIVER_EXPORTED));
Intent i = new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS);
i.putExtra("reason", "gestureNav");
mBroadcastReceiverArgumentCaptor.getValue().onReceive(mContext, i);
@@ -1379,7 +1379,7 @@
mBubbleData.setExpanded(true);
verify(mContext).registerReceiver(mBroadcastReceiverArgumentCaptor.capture(),
- mFilterArgumentCaptor.capture());
+ mFilterArgumentCaptor.capture(), eq(Context.RECEIVER_EXPORTED));
Intent i = new Intent(Intent.ACTION_SCREEN_OFF);
mBroadcastReceiverArgumentCaptor.getValue().onReceive(mContext, i);
diff --git a/packages/VpnDialogs/Android.bp b/packages/VpnDialogs/Android.bp
index 05135b2..e4f80e2 100644
--- a/packages/VpnDialogs/Android.bp
+++ b/packages/VpnDialogs/Android.bp
@@ -23,10 +23,15 @@
default_applicable_licenses: ["frameworks_base_license"],
}
+android_library {
+ name: "VpnDialogsLib",
+ srcs: ["src/**/*.java"],
+}
+
android_app {
name: "VpnDialogs",
certificate: "platform",
privileged: true,
- srcs: ["src/**/*.java"],
+ static_libs: ["VpnDialogsLib"],
platform_apis: true,
}
diff --git a/packages/VpnDialogs/res/values/strings.xml b/packages/VpnDialogs/res/values/strings.xml
index f971a09..28e7272 100644
--- a/packages/VpnDialogs/res/values/strings.xml
+++ b/packages/VpnDialogs/res/values/strings.xml
@@ -100,4 +100,33 @@
without any consequences. [CHAR LIMIT=20] -->
<string name="dismiss">Dismiss</string>
+ <!-- Malicious VPN apps may provide very long labels or cunning HTML to trick the system dialogs
+ into displaying what they want. The system will attempt to sanitize the label, and if the
+ label is deemed dangerous, then this string is used instead. The first argument is the
+ first 30 characters of the label, and the second argument is the package name of the app.
+ Example : Normally a VPN app may be called "My VPN app" in which case the dialog will read
+ "My VPN app wants to set up a VPN connection...". If the label is very long, then, this
+ will be used to show "VerylongVPNlabel… (com.my.vpn.app) wants to set up a VPN
+ connection...". For this case, the code will refer to sanitized_vpn_label_with_ellipsis.
+ -->
+ <string name="sanitized_vpn_label_with_ellipsis">
+ <xliff:g id="sanitized_vpn_label_with_ellipsis" example="My VPN app">%1$s</xliff:g>… (
+ <xliff:g id="sanitized_vpn_label_with_ellipsis" example="com.my.vpn.app">%2$s</xliff:g>)
+ </string>
+
+ <!-- Malicious VPN apps may provide very long labels or cunning HTML to trick the system dialogs
+ into displaying what they want. The system will attempt to sanitize the label, and if the
+ label is deemed dangerous, then this string is used instead. The first argument is the
+ label, and the second argument is the package name of the app.
+ Example : Normally a VPN app may be called "My VPN app" in which case the dialog will read
+ "My VPN app wants to set up a VPN connection...". If the VPN label contains HTML tag but
+ the length is not very long, the dialog will show "VpnLabelWith<br>HtmlTag
+ (com.my.vpn.app) wants to set up a VPN connection...". For this case, the code will refer
+ to sanitized_vpn_label.
+ -->
+ <string name="sanitized_vpn_label">
+ <xliff:g id="sanitized_vpn_label" example="My VPN app">%1$s</xliff:g> (
+ <xliff:g id="sanitized_vpn_label" example="com.my.vpn.app">%2$s</xliff:g>)
+ </string>
+
</resources>
diff --git a/packages/VpnDialogs/src/com/android/vpndialogs/ConfirmDialog.java b/packages/VpnDialogs/src/com/android/vpndialogs/ConfirmDialog.java
index fb23678..a98d6d8 100644
--- a/packages/VpnDialogs/src/com/android/vpndialogs/ConfirmDialog.java
+++ b/packages/VpnDialogs/src/com/android/vpndialogs/ConfirmDialog.java
@@ -33,6 +33,7 @@
import android.widget.Button;
import android.widget.TextView;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.app.AlertActivity;
import com.android.internal.net.VpnConfig;
@@ -40,12 +41,19 @@
implements DialogInterface.OnClickListener, ImageGetter {
private static final String TAG = "VpnConfirm";
+ // Usually the label represents the app name, 150 code points might be enough to display the app
+ // name, and 150 code points won't cover the warning message from VpnDialog.
+ @VisibleForTesting
+ static final int MAX_VPN_LABEL_LENGTH = 150;
+
@VpnManager.VpnType private final int mVpnType;
private String mPackage;
private VpnManager mVm;
+ private View mView;
+
public ConfirmDialog() {
this(VpnManager.TYPE_VPN_SERVICE);
}
@@ -54,6 +62,43 @@
mVpnType = vpnType;
}
+ /**
+ * This function will use the string resource to combine the VPN label and the package name.
+ *
+ * If the VPN label violates the length restriction, the first 30 code points of VPN label and
+ * the package name will be returned. Or return the VPN label and the package name directly if
+ * the VPN label doesn't violate the length restriction.
+ *
+ * The result will be something like,
+ * - ThisIsAVeryLongVpnAppNameWhich... (com.vpn.app)
+ * if the VPN label violates the length restriction.
+ * or
+ * - VpnLabelWith<br>HtmlTag (com.vpn.app)
+ * if the VPN label doesn't violate the length restriction.
+ *
+ */
+ private String getSimplifiedLabel(String vpnLabel, String packageName) {
+ if (vpnLabel.codePointCount(0, vpnLabel.length()) > 30) {
+ return getString(R.string.sanitized_vpn_label_with_ellipsis,
+ vpnLabel.substring(0, vpnLabel.offsetByCodePoints(0, 30)),
+ packageName);
+ }
+
+ return getString(R.string.sanitized_vpn_label, vpnLabel, packageName);
+ }
+
+ @VisibleForTesting
+ protected String getSanitizedVpnLabel(String vpnLabel, String packageName) {
+ final String sanitizedVpnLabel = Html.escapeHtml(vpnLabel);
+ final boolean exceedMaxVpnLabelLength = sanitizedVpnLabel.codePointCount(0,
+ sanitizedVpnLabel.length()) > MAX_VPN_LABEL_LENGTH;
+ if (exceedMaxVpnLabelLength || !vpnLabel.equals(sanitizedVpnLabel)) {
+ return getSimplifiedLabel(sanitizedVpnLabel, packageName);
+ }
+
+ return sanitizedVpnLabel;
+ }
+
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@@ -75,15 +120,16 @@
finish();
return;
}
- View view = View.inflate(this, R.layout.confirm, null);
- ((TextView) view.findViewById(R.id.warning)).setText(
- Html.fromHtml(getString(R.string.warning, getVpnLabel()),
- this, null /* tagHandler */));
+ mView = View.inflate(this, R.layout.confirm, null);
+ ((TextView) mView.findViewById(R.id.warning)).setText(
+ Html.fromHtml(getString(R.string.warning, getSanitizedVpnLabel(
+ getVpnLabel().toString(), mPackage)),
+ this /* imageGetter */, null /* tagHandler */));
mAlertParams.mTitle = getText(R.string.prompt);
mAlertParams.mPositiveButtonText = getText(android.R.string.ok);
mAlertParams.mPositiveButtonListener = this;
mAlertParams.mNegativeButtonText = getText(android.R.string.cancel);
- mAlertParams.mView = view;
+ mAlertParams.mView = mView;
setupAlert();
getWindow().setCloseOnTouchOutside(false);
@@ -92,6 +138,11 @@
button.setFilterTouchesWhenObscured(true);
}
+ @VisibleForTesting
+ public CharSequence getWarningText() {
+ return ((TextView) mView.findViewById(R.id.warning)).getText();
+ }
+
private CharSequence getVpnLabel() {
try {
return VpnConfig.getVpnLabel(this, mPackage);
diff --git a/packages/VpnDialogs/tests/Android.bp b/packages/VpnDialogs/tests/Android.bp
new file mode 100644
index 0000000..68639bd
--- /dev/null
+++ b/packages/VpnDialogs/tests/Android.bp
@@ -0,0 +1,36 @@
+// Copyright 2022, The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package {
+ default_applicable_licenses: ["frameworks_base_license"],
+}
+
+android_test {
+ name: "VpnDialogsTests",
+ // Use platform certificate because the test will invoke a hidden API.
+ // (e.g. VpnManager#prepareVpn()).
+ certificate: "platform",
+ libs: [
+ "android.test.runner",
+ "android.test.base",
+ ],
+ static_libs: [
+ "androidx.test.core",
+ "androidx.test.rules",
+ "androidx.test.ext.junit",
+ "mockito-target-minus-junit4",
+ "VpnDialogsLib",
+ ],
+ srcs: ["src/**/*.java"],
+}
diff --git a/packages/VpnDialogs/tests/AndroidManifest.xml b/packages/VpnDialogs/tests/AndroidManifest.xml
new file mode 100644
index 0000000..f26c1fe
--- /dev/null
+++ b/packages/VpnDialogs/tests/AndroidManifest.xml
@@ -0,0 +1,31 @@
+<?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.
+ */
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ package="com.android.vpndialogs.tests">
+
+ <application android:debuggable="true">
+ <uses-library android:name="android.test.runner" />
+ <activity android:name="com.android.vpndialogs.VpnDialogTest$InstrumentedConfirmDialog"/>
+ </application>
+ <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+ android:targetPackage="com.android.vpndialogs.tests"
+ android:label="Vpn dialog tests">
+ </instrumentation>
+</manifest>
diff --git a/packages/VpnDialogs/tests/src/com/android/vpndialogs/VpnDialogTest.java b/packages/VpnDialogs/tests/src/com/android/vpndialogs/VpnDialogTest.java
new file mode 100644
index 0000000..7cfa466
--- /dev/null
+++ b/packages/VpnDialogs/tests/src/com/android/vpndialogs/VpnDialogTest.java
@@ -0,0 +1,154 @@
+/*
+ * 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.vpndialogs;
+
+import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
+
+import static junit.framework.Assert.assertFalse;
+import static junit.framework.Assert.assertTrue;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.doReturn;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.net.VpnManager;
+
+import androidx.test.core.app.ActivityScenario;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+@RunWith(AndroidJUnit4.class)
+public class VpnDialogTest {
+ private ActivityScenario<ConfirmDialog> mActivityScenario;
+
+ @SuppressWarnings("StaticMockMember")
+ @Mock
+ private static PackageManager sPm;
+
+ @SuppressWarnings("StaticMockMember")
+ @Mock
+ private static VpnManager sVm;
+
+ @Mock
+ private ApplicationInfo mAi;
+
+ private static final String VPN_APP_NAME = "VpnApp";
+ private static final String VPN_APP_PACKAGE_NAME = "com.android.vpndialogs.VpnDialogTest";
+ private static final String VPN_LABEL_CONTAINS_HTML_TAG =
+ "<b><a href=\"https://www.malicious.vpn.app.com\">Google Play</a>";
+ private static final String VPN_LABEL_CONTAINS_HTML_TAG_AND_VIOLATE_LENGTH_RESTRICTION =
+ "<b><a href=\"https://www.malicious.vpn.app.com\">Google Play</a></b>"
+ + " Wants to connect the network. <br></br><br></br><br></br><br></br><br></br>"
+ + " <br></br><br></br><br></br><br></br><br></br><br></br><br></br><br></br> Deny it?";
+ private static final String VPN_LABEL_VIOLATES_LENGTH_RESTRICTION = "This is a VPN label"
+ + " which violates the length restriction. The length restriction here are 150 code"
+ + " points. So the VPN label should be sanitized, and shows the package name to the"
+ + " user.";
+
+ public static class InstrumentedConfirmDialog extends ConfirmDialog {
+ @Override
+ public PackageManager getPackageManager() {
+ return sPm;
+ }
+
+ @Override
+ public @Nullable Object getSystemService(@ServiceName @NonNull String name) {
+ switch (name) {
+ case Context.VPN_MANAGEMENT_SERVICE:
+ return sVm;
+ default:
+ return super.getSystemService(name);
+ }
+ }
+
+ @Override
+ public String getCallingPackage() {
+ return VPN_APP_PACKAGE_NAME;
+ }
+ }
+
+ private void launchActivity() {
+ final Context context = getInstrumentation().getContext();
+ mActivityScenario = ActivityScenario.launch(
+ new Intent(context, InstrumentedConfirmDialog.class));
+ }
+
+ @Test
+ public void testGetSanitizedVpnLabel_withNormalCase() throws Exception {
+ // Test the normal case that the VPN label showed in the VpnDialog is the app name.
+ doReturn(VPN_APP_NAME).when(mAi).loadLabel(sPm);
+ launchActivity();
+ mActivityScenario.onActivity(activity -> {
+ assertTrue(activity.getWarningText().toString().contains(VPN_APP_NAME));
+ });
+ }
+
+ private void verifySanitizedVpnLabel(String originalLabel) {
+ doReturn(originalLabel).when(mAi).loadLabel(sPm);
+ launchActivity();
+ mActivityScenario.onActivity(activity -> {
+ // The VPN label was sanitized because violating length restriction or having a html
+ // tag, so the warning message will contain the package name.
+ assertTrue(activity.getWarningText().toString().contains(activity.getCallingPackage()));
+ // Also, the length of sanitized VPN label shouldn't longer than MAX_VPN_LABEL_LENGTH
+ // and it shouldn't contain html tag.
+ final String sanitizedVpnLabel =
+ activity.getSanitizedVpnLabel(originalLabel, VPN_APP_PACKAGE_NAME);
+ assertTrue(sanitizedVpnLabel.codePointCount(0, sanitizedVpnLabel.length())
+ < ConfirmDialog.MAX_VPN_LABEL_LENGTH);
+ assertFalse(sanitizedVpnLabel.contains("<b>"));
+ });
+ }
+
+ @Test
+ public void testGetSanitizedVpnLabel_withHtmlTag() throws Exception {
+ // Test the case that the VPN label was sanitized because there is a html tag.
+ verifySanitizedVpnLabel(VPN_LABEL_CONTAINS_HTML_TAG);
+ }
+
+ @Test
+ public void testGetSanitizedVpnLabel_withHtmlTagAndViolateLengthRestriction() throws Exception {
+ // Test the case that the VPN label was sanitized because there is a html tag.
+ verifySanitizedVpnLabel(VPN_LABEL_CONTAINS_HTML_TAG_AND_VIOLATE_LENGTH_RESTRICTION);
+ }
+
+ @Test
+ public void testGetSanitizedVpnLabel_withLengthRestriction() throws Exception {
+ // Test the case that the VPN label was sanitized because hitting the length restriction.
+ verifySanitizedVpnLabel(VPN_LABEL_VIOLATES_LENGTH_RESTRICTION);
+ }
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+ doReturn(false).when(sVm).prepareVpn(anyString(), anyString(), anyInt());
+ doReturn(null).when(sPm).queryIntentServices(any(), anyInt());
+ doReturn(mAi).when(sPm).getApplicationInfo(anyString(), anyInt());
+ }
+}
diff --git a/services/companion/java/com/android/server/companion/virtual/GenericWindowPolicyController.java b/services/companion/java/com/android/server/companion/virtual/GenericWindowPolicyController.java
index 000bafe..ce7854d 100644
--- a/services/companion/java/com/android/server/companion/virtual/GenericWindowPolicyController.java
+++ b/services/companion/java/com/android/server/companion/virtual/GenericWindowPolicyController.java
@@ -86,6 +86,15 @@
}
/**
+ * For communicating when activities are blocked from entering PIP on the display by this
+ * policy controller.
+ */
+ public interface PipBlockedCallback {
+ /** Called when an activity is blocked from entering PIP. */
+ void onEnteringPipBlocked(int uid);
+ }
+
+ /**
* If required, allow the secure activity to display on remote device since
* {@link android.os.Build.VERSION_CODES#TIRAMISU}.
*/
@@ -112,6 +121,7 @@
@GuardedBy("mGenericWindowPolicyControllerLock")
final ArraySet<Integer> mRunningUids = new ArraySet<>();
@Nullable private final ActivityListener mActivityListener;
+ @Nullable private final PipBlockedCallback mPipBlockedCallback;
private final Handler mHandler = new Handler(Looper.getMainLooper());
@NonNull
@GuardedBy("mGenericWindowPolicyControllerLock")
@@ -155,6 +165,7 @@
@NonNull Set<ComponentName> blockedActivities,
@ActivityPolicy int defaultActivityPolicy,
@NonNull ActivityListener activityListener,
+ @NonNull PipBlockedCallback pipBlockedCallback,
@NonNull ActivityBlockedCallback activityBlockedCallback,
@NonNull SecureWindowCallback secureWindowCallback,
@AssociationRequest.DeviceProfile String deviceProfile) {
@@ -169,6 +180,7 @@
setInterestedWindowFlags(windowFlags, systemWindowFlags);
mActivityListener = activityListener;
mDeviceProfile = deviceProfile;
+ mPipBlockedCallback = pipBlockedCallback;
mSecureWindowCallback = secureWindowCallback;
}
@@ -317,6 +329,17 @@
}
}
+ @Override
+ public boolean isEnteringPipAllowed(int uid) {
+ if (super.isEnteringPipAllowed(uid)) {
+ return true;
+ }
+ mHandler.post(() -> {
+ mPipBlockedCallback.onEnteringPipBlocked(uid);
+ });
+ return false;
+ }
+
/**
* Returns true if an app with the given UID has an activity running on the virtual display for
* this controller.
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 5ebbf07..be21075 100644
--- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
+++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
@@ -624,6 +624,7 @@
mParams.getBlockedActivities(),
mParams.getDefaultActivityPolicy(),
createListenerAdapter(),
+ this::onEnteringPipBlocked,
this::onActivityBlocked,
this::onSecureWindowShown,
mAssociationInfo.getDeviceProfile());
@@ -779,6 +780,11 @@
return mVirtualDisplayIds.contains(displayId);
}
+ void onEnteringPipBlocked(int uid) {
+ showToastWhereUidIsRunning(uid, com.android.internal.R.string.vdm_pip_blocked,
+ Toast.LENGTH_LONG, mContext.getMainLooper());
+ }
+
interface OnDeviceCloseListener {
void onClose(int associationId);
}
diff --git a/services/core/java/com/android/server/BinaryTransparencyService.java b/services/core/java/com/android/server/BinaryTransparencyService.java
index 8055afc..2662e03 100644
--- a/services/core/java/com/android/server/BinaryTransparencyService.java
+++ b/services/core/java/com/android/server/BinaryTransparencyService.java
@@ -685,7 +685,9 @@
FrameworkStatsLog.write(FrameworkStatsLog.APEX_INFO_GATHERED,
packageInfo.packageName,
packageInfo.getLongVersionCode(),
- mBinaryHashes.get(packageInfo.packageName));
+ mBinaryHashes.get(packageInfo.packageName),
+ 4, // indicating that the digest is SHA256
+ null); // TODO: This is to comform to the extended schema.
}
}
} catch (PackageManager.NameNotFoundException e) {
diff --git a/services/core/java/com/android/server/appop/AppOpsUidStateTrackerImpl.java b/services/core/java/com/android/server/appop/AppOpsUidStateTrackerImpl.java
index 3c281d1..5114bd5 100644
--- a/services/core/java/com/android/server/appop/AppOpsUidStateTrackerImpl.java
+++ b/services/core/java/com/android/server/appop/AppOpsUidStateTrackerImpl.java
@@ -26,6 +26,7 @@
import static android.app.AppOpsManager.MODE_ALLOWED;
import static android.app.AppOpsManager.MODE_IGNORED;
import static android.app.AppOpsManager.OP_CAMERA;
+import static android.app.AppOpsManager.OP_RECEIVE_EXPLICIT_USER_INTERACTION_AUDIO;
import static android.app.AppOpsManager.OP_RECORD_AUDIO;
import static android.app.AppOpsManager.UID_STATE_FOREGROUND_SERVICE;
import static android.app.AppOpsManager.UID_STATE_MAX_LAST_NON_RESTRICTED;
@@ -171,6 +172,7 @@
return MODE_ALLOWED;
}
case OP_RECORD_AUDIO:
+ case OP_RECEIVE_EXPLICIT_USER_INTERACTION_AUDIO:
if ((capability & PROCESS_CAPABILITY_FOREGROUND_MICROPHONE) == 0) {
return MODE_IGNORED;
} else {
diff --git a/services/core/java/com/android/server/display/DisplayPowerController2.java b/services/core/java/com/android/server/display/DisplayPowerController2.java
index c06101f..bf0b388 100644
--- a/services/core/java/com/android/server/display/DisplayPowerController2.java
+++ b/services/core/java/com/android/server/display/DisplayPowerController2.java
@@ -2511,19 +2511,22 @@
float appliedThermalCapNits =
event.getThermalMax() == PowerManager.BRIGHTNESS_MAX
? -1f : convertToNits(event.getThermalMax());
-
- FrameworkStatsLog.write(FrameworkStatsLog.DISPLAY_BRIGHTNESS_CHANGED,
- convertToNits(event.getInitialBrightness()),
- convertToNits(event.getBrightness()),
- event.getSlowAmbientLux(),
- event.getPhysicalDisplayId(),
- event.isShortTermModelActive(),
- appliedLowPowerMode,
- appliedRbcStrength,
- appliedHbmMaxNits,
- appliedThermalCapNits,
- event.isAutomaticBrightnessEnabled(),
- FrameworkStatsLog.DISPLAY_BRIGHTNESS_CHANGED__REASON__REASON_MANUAL);
+ if (mLogicalDisplay.getPrimaryDisplayDeviceLocked() != null
+ && mLogicalDisplay.getPrimaryDisplayDeviceLocked()
+ .getDisplayDeviceInfoLocked().type == Display.TYPE_INTERNAL) {
+ FrameworkStatsLog.write(FrameworkStatsLog.DISPLAY_BRIGHTNESS_CHANGED,
+ convertToNits(event.getInitialBrightness()),
+ convertToNits(event.getBrightness()),
+ event.getSlowAmbientLux(),
+ event.getPhysicalDisplayId(),
+ event.isShortTermModelActive(),
+ appliedLowPowerMode,
+ appliedRbcStrength,
+ appliedHbmMaxNits,
+ appliedThermalCapNits,
+ event.isAutomaticBrightnessEnabled(),
+ FrameworkStatsLog.DISPLAY_BRIGHTNESS_CHANGED__REASON__REASON_MANUAL);
+ }
}
private final class DisplayControllerHandler extends Handler {
diff --git a/services/core/java/com/android/server/dreams/DreamManagerService.java b/services/core/java/com/android/server/dreams/DreamManagerService.java
index facc6b2..4a0ba22 100644
--- a/services/core/java/com/android/server/dreams/DreamManagerService.java
+++ b/services/core/java/com/android/server/dreams/DreamManagerService.java
@@ -116,7 +116,7 @@
private final DreamUiEventLogger mDreamUiEventLogger;
private final ComponentName mAmbientDisplayComponent;
private final boolean mDismissDreamOnActivityStart;
- private final boolean mDreamsOnlyEnabledForSystemUser;
+ private final boolean mDreamsOnlyEnabledForDockUser;
private final boolean mDreamsEnabledByDefaultConfig;
private final boolean mDreamsActivatedOnChargeByDefault;
private final boolean mDreamsActivatedOnDockByDefault;
@@ -214,8 +214,8 @@
mContext.getResources().getStringArray(R.array.config_loggable_dream_prefixes));
AmbientDisplayConfiguration adc = new AmbientDisplayConfiguration(mContext);
mAmbientDisplayComponent = ComponentName.unflattenFromString(adc.ambientDisplayComponent());
- mDreamsOnlyEnabledForSystemUser =
- mContext.getResources().getBoolean(R.bool.config_dreamsOnlyEnabledForSystemUser);
+ mDreamsOnlyEnabledForDockUser =
+ mContext.getResources().getBoolean(R.bool.config_dreamsOnlyEnabledForDockUser);
mDismissDreamOnActivityStart = mContext.getResources().getBoolean(
R.bool.config_dismissDreamOnActivityStart);
@@ -292,10 +292,9 @@
pw.println();
pw.println("mCurrentDream=" + mCurrentDream);
pw.println("mForceAmbientDisplayEnabled=" + mForceAmbientDisplayEnabled);
- pw.println("mDreamsOnlyEnabledForSystemUser=" + mDreamsOnlyEnabledForSystemUser);
+ pw.println("mDreamsOnlyEnabledForDockUser=" + mDreamsOnlyEnabledForDockUser);
pw.println("mDreamsEnabledSetting=" + mDreamsEnabledSetting);
pw.println("mForceAmbientDisplayEnabled=" + mForceAmbientDisplayEnabled);
- pw.println("mDreamsOnlyEnabledForSystemUser=" + mDreamsOnlyEnabledForSystemUser);
pw.println("mDreamsActivatedOnDockByDefault=" + mDreamsActivatedOnDockByDefault);
pw.println("mDreamsActivatedOnChargeByDefault=" + mDreamsActivatedOnChargeByDefault);
pw.println("mIsDocked=" + mIsDocked);
@@ -602,7 +601,8 @@
}
private boolean dreamsEnabledForUser(int userId) {
- return !mDreamsOnlyEnabledForSystemUser || (userId == UserHandle.USER_SYSTEM);
+ // TODO(b/257333623): Support non-system Dock Users in HSUM.
+ return !mDreamsOnlyEnabledForDockUser || (userId == UserHandle.USER_SYSTEM);
}
private ServiceInfo getServiceInfo(ComponentName name) {
diff --git a/services/core/java/com/android/server/input/InputManagerInternal.java b/services/core/java/com/android/server/input/InputManagerInternal.java
index 6234421..298098a 100644
--- a/services/core/java/com/android/server/input/InputManagerInternal.java
+++ b/services/core/java/com/android/server/input/InputManagerInternal.java
@@ -17,10 +17,15 @@
package com.android.server.input;
import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.UserIdInt;
import android.graphics.PointF;
import android.hardware.display.DisplayViewport;
import android.os.IBinder;
import android.view.InputChannel;
+import android.view.inputmethod.InputMethodSubtype;
+
+import com.android.internal.inputmethod.InputMethodSubtypeHandle;
import java.util.List;
@@ -136,6 +141,26 @@
public abstract InputChannel createInputChannel(String inputChannelName);
/**
+ * Pilfer pointers from the input channel with the given token so that ongoing gestures are
+ * canceled for all other channels.
+ */
+ public abstract void pilferPointers(IBinder token);
+
+ /**
+ * Called when the current input method and/or {@link InputMethodSubtype} is updated.
+ *
+ * @param userId User ID to be notified about.
+ * @param subtypeHandle A {@link InputMethodSubtypeHandle} corresponds to {@code subtype}.
+ * @param subtype A {@link InputMethodSubtype} object, or {@code null} when the current
+ * {@link InputMethodSubtype} is not suitable for the physical keyboard layout
+ * mapping.
+ * @see InputMethodSubtype#isSuitableForPhysicalKeyboardLayoutMapping()
+ */
+ public abstract void onInputMethodSubtypeChangedForKeyboardLayoutMapping(@UserIdInt int userId,
+ @Nullable InputMethodSubtypeHandle subtypeHandle,
+ @Nullable InputMethodSubtype subtype);
+
+ /**
* Increments keyboard backlight level if the device has an associated keyboard backlight
* {@see Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT}
*/
diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java
index c9a7646..cb615a9 100644
--- a/services/core/java/com/android/server/input/InputManagerService.java
+++ b/services/core/java/com/android/server/input/InputManagerService.java
@@ -23,6 +23,7 @@
import android.annotation.EnforcePermission;
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.annotation.UserIdInt;
import android.app.ActivityManagerInternal;
import android.app.Notification;
import android.app.NotificationManager;
@@ -111,11 +112,13 @@
import android.view.SurfaceControl;
import android.view.VerifiedInputEvent;
import android.view.ViewConfiguration;
+import android.view.inputmethod.InputMethodSubtype;
import android.widget.Toast;
import com.android.internal.R;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.inputmethod.InputMethodSubtypeHandle;
import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
import com.android.internal.notification.SystemNotificationChannels;
import com.android.internal.os.SomeArgs;
@@ -3791,6 +3794,21 @@
}
@Override
+ public void pilferPointers(IBinder token) {
+ mNative.pilferPointers(token);
+ }
+
+ @Override
+ public void onInputMethodSubtypeChangedForKeyboardLayoutMapping(@UserIdInt int userId,
+ @Nullable InputMethodSubtypeHandle subtypeHandle,
+ @Nullable InputMethodSubtype subtype) {
+ if (DEBUG) {
+ Slog.i(TAG, "InputMethodSubtype changed: userId=" + userId
+ + " subtypeHandle=" + subtypeHandle);
+ }
+ }
+
+ @Override
public void incrementKeyboardBacklight(int deviceId) {
mKeyboardBacklightController.incrementKeyboardBacklight(deviceId);
}
diff --git a/services/core/java/com/android/server/inputmethod/IInputMethodInvoker.java b/services/core/java/com/android/server/inputmethod/IInputMethodInvoker.java
index 1a0f6f7..015e576 100644
--- a/services/core/java/com/android/server/inputmethod/IInputMethodInvoker.java
+++ b/services/core/java/com/android/server/inputmethod/IInputMethodInvoker.java
@@ -28,6 +28,7 @@
import android.view.InputChannel;
import android.view.MotionEvent;
import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.ImeTracker;
import android.view.inputmethod.InputBinding;
import android.view.inputmethod.InputMethodSubtype;
import android.window.ImeOnBackInvokedDispatcher;
@@ -198,9 +199,10 @@
// TODO(b/192412909): Convert this back to void method
@AnyThread
- boolean showSoftInput(IBinder showInputToken, int flags, ResultReceiver resultReceiver) {
+ boolean showSoftInput(IBinder showInputToken, @Nullable ImeTracker.Token statsToken, int flags,
+ ResultReceiver resultReceiver) {
try {
- mTarget.showSoftInput(showInputToken, flags, resultReceiver);
+ mTarget.showSoftInput(showInputToken, statsToken, flags, resultReceiver);
} catch (RemoteException e) {
logRemoteException(e);
return false;
@@ -210,9 +212,10 @@
// TODO(b/192412909): Convert this back to void method
@AnyThread
- boolean hideSoftInput(IBinder hideInputToken, int flags, ResultReceiver resultReceiver) {
+ boolean hideSoftInput(IBinder hideInputToken, @Nullable ImeTracker.Token statsToken, int flags,
+ ResultReceiver resultReceiver) {
try {
- mTarget.hideSoftInput(hideInputToken, flags, resultReceiver);
+ mTarget.hideSoftInput(hideInputToken, statsToken, flags, resultReceiver);
} catch (RemoteException e) {
logRemoteException(e);
return false;
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
index 4d1c5ae..8b083bd 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
@@ -129,6 +129,7 @@
import android.view.WindowManager.LayoutParams;
import android.view.WindowManager.LayoutParams.SoftInputModeFlags;
import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.ImeTracker;
import android.view.inputmethod.InputBinding;
import android.view.inputmethod.InputConnection;
import android.view.inputmethod.InputMethod;
@@ -151,6 +152,7 @@
import com.android.internal.inputmethod.IAccessibilityInputMethodSession;
import com.android.internal.inputmethod.IInlineSuggestionsRequestCallback;
import com.android.internal.inputmethod.IInputContentUriToken;
+import com.android.internal.inputmethod.IInputMethod;
import com.android.internal.inputmethod.IInputMethodClient;
import com.android.internal.inputmethod.IInputMethodPrivilegedOperations;
import com.android.internal.inputmethod.IInputMethodSession;
@@ -162,6 +164,7 @@
import com.android.internal.inputmethod.InputBindResult;
import com.android.internal.inputmethod.InputMethodDebug;
import com.android.internal.inputmethod.InputMethodNavButtonFlags;
+import com.android.internal.inputmethod.InputMethodSubtypeHandle;
import com.android.internal.inputmethod.SoftInputShowHideReason;
import com.android.internal.inputmethod.StartInputFlags;
import com.android.internal.inputmethod.StartInputReason;
@@ -641,6 +644,10 @@
*/
private boolean mInputShown;
+ /** The token tracking the current IME request or {@code null} otherwise. */
+ @Nullable
+ private ImeTracker.Token mCurStatsToken;
+
/**
* {@code true} if the current input method is in fullscreen mode.
*/
@@ -760,7 +767,7 @@
* <dd>
* If this bit is ON, some of IME view, e.g. software input, candidate view, is visible.
* </dd>
- * dt>{@link InputMethodService#IME_INVISIBLE}</dt>
+ * <dt>{@link InputMethodService#IME_INVISIBLE}</dt>
* <dd> If this bit is ON, IME is ready with views from last EditorInfo but is
* currently invisible.
* </dd>
@@ -784,8 +791,7 @@
/**
* Internal state snapshot when
- * {@link com.android.internal.view.IInputMethod#startInput(IBinder, IRemoteInputConnection, EditorInfo,
- * boolean)} is about to be called.
+ * {@link IInputMethod#startInput(IInputMethod.StartInputParams)} is about to be called.
*
* <p>Calling that IPC endpoint basically means that
* {@link InputMethodService#doStartInput(InputConnection, EditorInfo, boolean)} will be called
@@ -1070,7 +1076,7 @@
/**
* Add a new entry and discard the oldest entry as needed.
- * @param info {@lin StartInputInfo} to be added.
+ * @param info {@link StartInputInfo} to be added.
*/
void addEntry(@NonNull StartInputInfo info) {
final int index = mNextIndex;
@@ -1188,18 +1194,18 @@
} else if (accessibilityRequestingNoImeUri.equals(uri)) {
final int accessibilitySoftKeyboardSetting = Settings.Secure.getIntForUser(
mContext.getContentResolver(),
- Settings.Secure.ACCESSIBILITY_SOFT_KEYBOARD_MODE, 0, mUserId);
+ Settings.Secure.ACCESSIBILITY_SOFT_KEYBOARD_MODE, 0 /* def */, mUserId);
mAccessibilityRequestingNoSoftKeyboard =
(accessibilitySoftKeyboardSetting & AccessibilityService.SHOW_MODE_MASK)
== AccessibilityService.SHOW_MODE_HIDDEN;
if (mAccessibilityRequestingNoSoftKeyboard) {
final boolean showRequested = mShowRequested;
- hideCurrentInputLocked(mCurFocusedWindow, 0, null,
+ hideCurrentInputLocked(mCurFocusedWindow, null /* statsToken */,
+ 0 /* flags */, null /* resultReceiver */,
SoftInputShowHideReason.HIDE_SETTINGS_ON_CHANGE);
mShowRequested = showRequested;
} else if (mShowRequested) {
- showCurrentInputLocked(mCurFocusedWindow,
- InputMethodManager.SHOW_IMPLICIT, null,
+ showCurrentInputImplicitLocked(mCurFocusedWindow,
SoftInputShowHideReason.SHOW_SETTINGS_ON_CHANGE);
}
} else {
@@ -1665,8 +1671,8 @@
}
// Hide soft input before user switch task since switch task may block main handler a while
// and delayed the hideCurrentInputLocked().
- hideCurrentInputLocked(
- mCurFocusedWindow, 0, null, SoftInputShowHideReason.HIDE_SWITCH_USER);
+ hideCurrentInputLocked(mCurFocusedWindow, null /* statsToken */, 0 /* flags */,
+ null /* resultReceiver */, SoftInputShowHideReason.HIDE_SWITCH_USER);
final UserSwitchHandlerTask task = new UserSwitchHandlerTask(this, userId,
clientToBeReset);
mUserSwitchHandlerTask = task;
@@ -2221,7 +2227,7 @@
}
final ClientDeathRecipient deathRecipient = new ClientDeathRecipient(this, client);
try {
- client.asBinder().linkToDeath(deathRecipient, 0);
+ client.asBinder().linkToDeath(deathRecipient, 0 /* flags */);
} catch (RemoteException e) {
throw new IllegalStateException(e);
}
@@ -2246,7 +2252,7 @@
synchronized (ImfLock.class) {
ClientState cs = mClients.remove(client.asBinder());
if (cs != null) {
- client.asBinder().unlinkToDeath(cs.mClientDeathRecipient, 0);
+ client.asBinder().unlinkToDeath(cs.mClientDeathRecipient, 0 /* flags */);
clearClientSessionLocked(cs);
clearClientSessionForAccessibilityLocked(cs);
@@ -2259,8 +2265,8 @@
}
if (mCurClient == cs) {
- hideCurrentInputLocked(
- mCurFocusedWindow, 0, null, SoftInputShowHideReason.HIDE_REMOVE_CLIENT);
+ hideCurrentInputLocked(mCurFocusedWindow, null /* statsToken */, 0 /* flags */,
+ null /* resultReceiver */, SoftInputShowHideReason.HIDE_REMOVE_CLIENT);
if (mBoundToMethod) {
mBoundToMethod = false;
IInputMethodInvoker curMethod = getCurMethodLocked();
@@ -2307,6 +2313,8 @@
mCurClient.mSessionRequestedForAccessibility = false;
mCurClient = null;
mCurVirtualDisplayToScreenMatrix = null;
+ ImeTracker.get().onFailed(mCurStatsToken, ImeTracker.PHASE_SERVER_WAIT_IME);
+ mCurStatsToken = null;
mMenuController.hideInputMethodMenuLocked();
}
@@ -2379,8 +2387,11 @@
navButtonFlags, mCurImeDispatcher);
if (mShowRequested) {
if (DEBUG) Slog.v(TAG, "Attach new input asks to show input");
- showCurrentInputLocked(mCurFocusedWindow, getAppShowFlagsLocked(), null,
- SoftInputShowHideReason.ATTACH_NEW_INPUT);
+ // Re-use current statsToken, if it exists.
+ final ImeTracker.Token statsToken = mCurStatsToken;
+ mCurStatsToken = null;
+ showCurrentInputLocked(mCurFocusedWindow, statsToken, getAppShowFlagsLocked(),
+ null /* resultReceiver */, SoftInputShowHideReason.ATTACH_NEW_INPUT);
}
String curId = getCurIdLocked();
@@ -2499,7 +2510,8 @@
if (mDisplayIdToShowIme == INVALID_DISPLAY) {
mImeHiddenByDisplayPolicy = true;
- hideCurrentInputLocked(mCurFocusedWindow, 0, null,
+ hideCurrentInputLocked(mCurFocusedWindow, null /* statsToken */, 0 /* flags */,
+ null /* resultReceiver */,
SoftInputShowHideReason.HIDE_DISPLAY_IME_POLICY_HIDE);
return InputBindResult.NO_IME;
}
@@ -3201,6 +3213,18 @@
}
@GuardedBy("ImfLock.class")
+ private void notifyInputMethodSubtypeChangedLocked(@UserIdInt int userId,
+ @NonNull InputMethodInfo imi, @Nullable InputMethodSubtype subtype) {
+ final InputMethodSubtype normalizedSubtype =
+ subtype != null && subtype.isSuitableForPhysicalKeyboardLayoutMapping()
+ ? subtype : null;
+ final InputMethodSubtypeHandle newSubtypeHandle = normalizedSubtype != null
+ ? InputMethodSubtypeHandle.of(imi, normalizedSubtype) : null;
+ mInputManagerInternal.onInputMethodSubtypeChangedForKeyboardLayoutMapping(
+ userId, newSubtypeHandle, normalizedSubtype);
+ }
+
+ @GuardedBy("ImfLock.class")
void setInputMethodLocked(String id, int subtypeId) {
InputMethodInfo info = mMethodMap.get(id);
if (info == null) {
@@ -3209,8 +3233,10 @@
// See if we need to notify a subtype change within the same IME.
if (id.equals(getSelectedMethodIdLocked())) {
+ final int userId = mSettings.getCurrentUserId();
final int subtypeCount = info.getSubtypeCount();
if (subtypeCount <= 0) {
+ notifyInputMethodSubtypeChangedLocked(userId, info, null);
return;
}
final InputMethodSubtype oldSubtype = mCurrentSubtype;
@@ -3225,6 +3251,7 @@
if (newSubtype == null || oldSubtype == null) {
Slog.w(TAG, "Illegal subtype state: old subtype = " + oldSubtype
+ ", new subtype = " + newSubtype);
+ notifyInputMethodSubtypeChangedLocked(userId, info, null);
return;
}
if (newSubtype != oldSubtype) {
@@ -3262,22 +3289,23 @@
}
@Override
- public boolean showSoftInput(IInputMethodClient client, IBinder windowToken, int flags,
- int lastClickTooType, ResultReceiver resultReceiver,
- @SoftInputShowHideReason int reason) {
+ public boolean showSoftInput(IInputMethodClient client, IBinder windowToken,
+ @Nullable ImeTracker.Token statsToken, int flags, int lastClickTooType,
+ ResultReceiver resultReceiver, @SoftInputShowHideReason int reason) {
Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "IMMS.showSoftInput");
int uid = Binder.getCallingUid();
ImeTracing.getInstance().triggerManagerServiceDump(
"InputMethodManagerService#showSoftInput");
synchronized (ImfLock.class) {
- if (!canInteractWithImeLocked(uid, client, "showSoftInput")) {
+ if (!canInteractWithImeLocked(uid, client, "showSoftInput", statsToken)) {
+ ImeTracker.get().onFailed(statsToken, ImeTracker.PHASE_SERVER_CLIENT_FOCUSED);
return false;
}
final long ident = Binder.clearCallingIdentity();
try {
if (DEBUG) Slog.v(TAG, "Client requesting input be shown");
- return showCurrentInputLocked(
- windowToken, flags, lastClickTooType, resultReceiver, reason);
+ return showCurrentInputLocked(windowToken, statsToken, flags, lastClickTooType,
+ resultReceiver, reason);
} finally {
Binder.restoreCallingIdentity(ident);
Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
@@ -3294,7 +3322,8 @@
"InputMethodManagerService#startStylusHandwriting");
int uid = Binder.getCallingUid();
synchronized (ImfLock.class) {
- if (!canInteractWithImeLocked(uid, client, "startStylusHandwriting")) {
+ if (!canInteractWithImeLocked(uid, client, "startStylusHandwriting",
+ null /* statsToken */)) {
return;
}
if (!hasSupportedStylusLocked()) {
@@ -3349,19 +3378,33 @@
}
@GuardedBy("ImfLock.class")
- boolean showCurrentInputLocked(IBinder windowToken, int flags,
- ResultReceiver resultReceiver, @SoftInputShowHideReason int reason) {
- return showCurrentInputLocked(
- windowToken, flags, MotionEvent.TOOL_TYPE_UNKNOWN, resultReceiver, reason);
+ boolean showCurrentInputLocked(IBinder windowToken, @Nullable ImeTracker.Token statsToken,
+ int flags, ResultReceiver resultReceiver, @SoftInputShowHideReason int reason) {
+ return showCurrentInputLocked(windowToken, statsToken, flags,
+ MotionEvent.TOOL_TYPE_UNKNOWN, resultReceiver, reason);
}
@GuardedBy("ImfLock.class")
- private boolean showCurrentInputLocked(IBinder windowToken, int flags, int lastClickToolType,
+ private boolean showCurrentInputLocked(IBinder windowToken,
+ @Nullable ImeTracker.Token statsToken, int flags, int lastClickToolType,
ResultReceiver resultReceiver, @SoftInputShowHideReason int reason) {
+ // Create statsToken is none exists.
+ if (statsToken == null) {
+ String packageName = null;
+ if (mCurEditorInfo != null) {
+ packageName = mCurEditorInfo.packageName;
+ }
+ statsToken = new ImeTracker.Token(packageName);
+ ImeTracker.get().onRequestShow(statsToken, ImeTracker.ORIGIN_SERVER_START_INPUT,
+ reason);
+ }
+
mShowRequested = true;
if (mAccessibilityRequestingNoSoftKeyboard || mImeHiddenByDisplayPolicy) {
+ ImeTracker.get().onFailed(statsToken, ImeTracker.PHASE_SERVER_ACCESSIBILITY);
return false;
}
+ ImeTracker.get().onProgress(statsToken, ImeTracker.PHASE_SERVER_ACCESSIBILITY);
if ((flags & InputMethodManager.SHOW_FORCED) != 0) {
mShowExplicitlyRequested = true;
@@ -3371,8 +3414,10 @@
}
if (!mSystemReady) {
+ ImeTracker.get().onFailed(statsToken, ImeTracker.PHASE_SERVER_SYSTEM_READY);
return false;
}
+ ImeTracker.get().onProgress(statsToken, ImeTracker.PHASE_SERVER_SYSTEM_READY);
mBindingController.setCurrentMethodVisible();
final IInputMethodInvoker curMethod = getCurMethodLocked();
@@ -3380,6 +3425,9 @@
// create a placeholder token for IMS so that IMS cannot inject windows into client app.
Binder showInputToken = new Binder();
mShowRequestWindowMap.put(showInputToken, windowToken);
+ ImeTracker.get().onCancelled(mCurStatsToken, ImeTracker.PHASE_SERVER_WAIT_IME);
+ ImeTracker.get().onProgress(statsToken, ImeTracker.PHASE_SERVER_HAS_IME);
+ mCurStatsToken = null;
final int showFlags = getImeShowFlagsLocked();
if (DEBUG) {
Slog.v(TAG, "Calling " + curMethod + ".showSoftInput(" + showInputToken
@@ -3391,23 +3439,34 @@
curMethod.updateEditorToolType(lastClickToolType);
}
// TODO(b/192412909): Check if we can always call onShowHideSoftInputRequested() or not.
- if (curMethod.showSoftInput(showInputToken, showFlags, resultReceiver)) {
+ if (curMethod.showSoftInput(showInputToken, statsToken, showFlags, resultReceiver)) {
onShowHideSoftInputRequested(true /* show */, windowToken, reason);
}
mInputShown = true;
return true;
+ } else {
+ ImeTracker.get().onCancelled(mCurStatsToken, ImeTracker.PHASE_SERVER_WAIT_IME);
+ ImeTracker.get().onProgress(statsToken, ImeTracker.PHASE_SERVER_WAIT_IME);
+ mCurStatsToken = statsToken;
}
return false;
}
@Override
- public boolean hideSoftInput(IInputMethodClient client, IBinder windowToken, int flags,
- ResultReceiver resultReceiver, @SoftInputShowHideReason int reason) {
+ public boolean hideSoftInput(IInputMethodClient client, IBinder windowToken,
+ @Nullable ImeTracker.Token statsToken, int flags, ResultReceiver resultReceiver,
+ @SoftInputShowHideReason int reason) {
int uid = Binder.getCallingUid();
ImeTracing.getInstance().triggerManagerServiceDump(
"InputMethodManagerService#hideSoftInput");
synchronized (ImfLock.class) {
- if (!canInteractWithImeLocked(uid, client, "hideSoftInput")) {
+ if (!canInteractWithImeLocked(uid, client, "hideSoftInput", statsToken)) {
+ if (mInputShown) {
+ ImeTracker.get().onFailed(statsToken, ImeTracker.PHASE_SERVER_CLIENT_FOCUSED);
+ } else {
+ ImeTracker.get().onCancelled(statsToken,
+ ImeTracker.PHASE_SERVER_CLIENT_FOCUSED);
+ }
return false;
}
final long ident = Binder.clearCallingIdentity();
@@ -3415,7 +3474,7 @@
Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "IMMS.hideSoftInput");
if (DEBUG) Slog.v(TAG, "Client requesting input be hidden");
return InputMethodManagerService.this.hideCurrentInputLocked(windowToken,
- flags, resultReceiver, reason);
+ statsToken, flags, resultReceiver, reason);
} finally {
Binder.restoreCallingIdentity(ident);
Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
@@ -3424,17 +3483,32 @@
}
@GuardedBy("ImfLock.class")
- boolean hideCurrentInputLocked(IBinder windowToken, int flags, ResultReceiver resultReceiver,
- @SoftInputShowHideReason int reason) {
+ boolean hideCurrentInputLocked(IBinder windowToken, @Nullable ImeTracker.Token statsToken,
+ int flags, ResultReceiver resultReceiver, @SoftInputShowHideReason int reason) {
+ // Create statsToken is none exists.
+ if (statsToken == null) {
+ String packageName = null;
+ if (mCurEditorInfo != null) {
+ packageName = mCurEditorInfo.packageName;
+ }
+ statsToken = new ImeTracker.Token(packageName);
+ ImeTracker.get().onRequestHide(statsToken, ImeTracker.ORIGIN_SERVER_HIDE_INPUT, reason);
+ }
+
if ((flags & InputMethodManager.HIDE_IMPLICIT_ONLY) != 0
&& (mShowExplicitlyRequested || mShowForced)) {
if (DEBUG) Slog.v(TAG, "Not hiding: explicit show not cancelled by non-explicit hide");
+ ImeTracker.get().onFailed(statsToken, ImeTracker.PHASE_SERVER_HIDE_IMPLICIT);
return false;
}
+ ImeTracker.get().onProgress(statsToken, ImeTracker.PHASE_SERVER_HIDE_IMPLICIT);
+
if (mShowForced && (flags & InputMethodManager.HIDE_NOT_ALWAYS) != 0) {
if (DEBUG) Slog.v(TAG, "Not hiding: forced show not cancelled by not-always hide");
+ ImeTracker.get().onFailed(statsToken, ImeTracker.PHASE_SERVER_HIDE_NOT_ALWAYS);
return false;
}
+ ImeTracker.get().onProgress(statsToken, ImeTracker.PHASE_SERVER_HIDE_NOT_ALWAYS);
// There is a chance that IMM#hideSoftInput() is called in a transient state where
// IMMS#InputShown is already updated to be true whereas IMMS#mImeWindowVis is still waiting
@@ -3445,8 +3519,8 @@
// IMMS#InputShown indicates that the software keyboard is shown.
// TODO: Clean up, IMMS#mInputShown, IMMS#mImeWindowVis and mShowRequested.
IInputMethodInvoker curMethod = getCurMethodLocked();
- final boolean shouldHideSoftInput = (curMethod != null) && (mInputShown
- || (mImeWindowVis & InputMethodService.IME_ACTIVE) != 0);
+ final boolean shouldHideSoftInput = (curMethod != null)
+ && (mInputShown || (mImeWindowVis & InputMethodService.IME_ACTIVE) != 0);
boolean res;
if (shouldHideSoftInput) {
final Binder hideInputToken = new Binder();
@@ -3455,17 +3529,20 @@
// delivered to the IME process as an IPC. Hence the inconsistency between
// IMMS#mInputShown and IMMS#mImeWindowVis should be resolved spontaneously in
// the final state.
+ ImeTracker.get().onProgress(statsToken, ImeTracker.PHASE_SERVER_SHOULD_HIDE);
if (DEBUG) {
Slog.v(TAG, "Calling " + curMethod + ".hideSoftInput(0, " + hideInputToken
+ ", " + resultReceiver + ") for reason: "
+ InputMethodDebug.softInputDisplayReasonToString(reason));
}
// TODO(b/192412909): Check if we can always call onShowHideSoftInputRequested() or not.
- if (curMethod.hideSoftInput(hideInputToken, 0 /* flags */, resultReceiver)) {
+ if (curMethod.hideSoftInput(hideInputToken, statsToken, 0 /* flags */,
+ resultReceiver)) {
onShowHideSoftInputRequested(false /* show */, windowToken, reason);
}
res = true;
} else {
+ ImeTracker.get().onCancelled(statsToken, ImeTracker.PHASE_SERVER_SHOULD_HIDE);
res = false;
}
mBindingController.setCurrentMethodNotVisible();
@@ -3473,6 +3550,9 @@
mShowRequested = false;
mShowExplicitlyRequested = false;
mShowForced = false;
+ // Cancel existing statsToken for show IME as we got a hide request.
+ ImeTracker.get().onCancelled(mCurStatsToken, ImeTracker.PHASE_SERVER_WAIT_IME);
+ mCurStatsToken = null;
return res;
}
@@ -3630,8 +3710,8 @@
Slog.w(TAG, "If you need to impersonate a foreground user/profile from"
+ " a background user, use EditorInfo.targetInputMethodUser with"
+ " INTERACT_ACROSS_USERS_FULL permission.");
- hideCurrentInputLocked(
- mCurFocusedWindow, 0, null, SoftInputShowHideReason.HIDE_INVALID_USER);
+ hideCurrentInputLocked(mCurFocusedWindow, null /* statsToken */, 0 /* flags */,
+ null /* resultReceiver */, SoftInputShowHideReason.HIDE_INVALID_USER);
return InputBindResult.INVALID_USER;
}
@@ -3687,7 +3767,7 @@
boolean didStart = false;
InputBindResult res = null;
- // We shows the IME when the system allows the IME focused target window to restore the
+ // We show the IME when the system allows the IME focused target window to restore the
// IME visibility (e.g. switching to the app task when last time the IME is visible).
// Note that we don't restore IME visibility for some cases (e.g. when the soft input
// state is ALWAYS_HIDDEN or STATE_HIDDEN with forward navigation).
@@ -3699,7 +3779,7 @@
res = startInputUncheckedLocked(cs, inputContext, remoteAccessibilityInputConnection,
editorInfo, startInputFlags, startInputReason, unverifiedTargetSdkVersion,
imeDispatcher);
- showCurrentInputLocked(windowToken, InputMethodManager.SHOW_IMPLICIT, null,
+ showCurrentInputImplicitLocked(windowToken,
SoftInputShowHideReason.SHOW_RESTORE_IME_VISIBILITY);
return res;
}
@@ -3712,8 +3792,8 @@
// be behind any soft input window, so hide the
// soft input window if it is shown.
if (DEBUG) Slog.v(TAG, "Unspecified window will hide input");
- hideCurrentInputLocked(
- mCurFocusedWindow, InputMethodManager.HIDE_NOT_ALWAYS, null,
+ hideCurrentInputLocked(mCurFocusedWindow, null /* statsToken */,
+ InputMethodManager.HIDE_NOT_ALWAYS, null /* resultReceiver */,
SoftInputShowHideReason.HIDE_UNSPECIFIED_WINDOW);
// If focused display changed, we should unbind current method
@@ -3742,10 +3822,7 @@
imeDispatcher);
didStart = true;
}
- showCurrentInputLocked(
- windowToken,
- InputMethodManager.SHOW_IMPLICIT,
- null,
+ showCurrentInputImplicitLocked(windowToken,
SoftInputShowHideReason.SHOW_AUTO_EDITOR_FORWARD_NAV);
}
break;
@@ -3758,14 +3835,16 @@
case LayoutParams.SOFT_INPUT_STATE_HIDDEN:
if ((softInputMode & LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION) != 0) {
if (DEBUG) Slog.v(TAG, "Window asks to hide input going forward");
- hideCurrentInputLocked(mCurFocusedWindow, 0, null,
+ hideCurrentInputLocked(mCurFocusedWindow, null /* statsToken */, 0 /* flags */,
+ null /* resultReceiver */,
SoftInputShowHideReason.HIDE_STATE_HIDDEN_FORWARD_NAV);
}
break;
case LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN:
if (!sameWindowFocused) {
if (DEBUG) Slog.v(TAG, "Window asks to hide input");
- hideCurrentInputLocked(mCurFocusedWindow, 0, null,
+ hideCurrentInputLocked(mCurFocusedWindow, null /* statsToken */, 0 /* flags */,
+ null /* resultReceiver */,
SoftInputShowHideReason.HIDE_ALWAYS_HIDDEN_STATE);
}
break;
@@ -3781,7 +3860,7 @@
imeDispatcher);
didStart = true;
}
- showCurrentInputLocked(windowToken, InputMethodManager.SHOW_IMPLICIT, null,
+ showCurrentInputImplicitLocked(windowToken,
SoftInputShowHideReason.SHOW_STATE_VISIBLE_FORWARD_NAV);
} else {
Slog.e(TAG, "SOFT_INPUT_STATE_VISIBLE is ignored because"
@@ -3802,7 +3881,7 @@
imeDispatcher);
didStart = true;
}
- showCurrentInputLocked(windowToken, InputMethodManager.SHOW_IMPLICIT, null,
+ showCurrentInputImplicitLocked(windowToken,
SoftInputShowHideReason.SHOW_STATE_ALWAYS_VISIBLE);
}
} else {
@@ -3824,7 +3903,8 @@
// an editor upon refocusing a window.
if (startInputByWinGainedFocus) {
if (DEBUG) Slog.v(TAG, "Same window without editor will hide input");
- hideCurrentInputLocked(mCurFocusedWindow, 0, null,
+ hideCurrentInputLocked(mCurFocusedWindow, null /* statsToken */,
+ 0 /* flags */, null /* resultReceiver */,
SoftInputShowHideReason.HIDE_SAME_WINDOW_FOCUSED_WITHOUT_EDITOR);
}
}
@@ -3838,7 +3918,8 @@
// 2) SOFT_INPUT_STATE_VISIBLE state without an editor
// 3) SOFT_INPUT_STATE_ALWAYS_VISIBLE state without an editor
if (DEBUG) Slog.v(TAG, "Window without editor will hide input");
- hideCurrentInputLocked(mCurFocusedWindow, 0, null,
+ hideCurrentInputLocked(mCurFocusedWindow, null /* statsToken */, 0 /* flags */,
+ null /* resultReceiver */,
SoftInputShowHideReason.HIDE_WINDOW_GAINED_FOCUS_WITHOUT_EDITOR);
}
res = startInputUncheckedLocked(cs, inputContext,
@@ -3853,8 +3934,15 @@
}
@GuardedBy("ImfLock.class")
- private boolean canInteractWithImeLocked(
- int uid, IInputMethodClient client, String methodName) {
+ private void showCurrentInputImplicitLocked(@NonNull IBinder windowToken,
+ @SoftInputShowHideReason int reason) {
+ showCurrentInputLocked(windowToken, null /* statsToken */, InputMethodManager.SHOW_IMPLICIT,
+ null /* resultReceiver */, reason);
+ }
+
+ @GuardedBy("ImfLock.class")
+ private boolean canInteractWithImeLocked(int uid, IInputMethodClient client, String methodName,
+ @Nullable ImeTracker.Token statsToken) {
if (mCurClient == null || client == null
|| mCurClient.mClient.asBinder() != client.asBinder()) {
// We need to check if this is the current client with
@@ -3862,13 +3950,16 @@
// be made before input is started in it.
final ClientState cs = mClients.get(client.asBinder());
if (cs == null) {
+ ImeTracker.get().onFailed(statsToken, ImeTracker.PHASE_SERVER_CLIENT_KNOWN);
throw new IllegalArgumentException("unknown client " + client.asBinder());
}
+ ImeTracker.get().onProgress(statsToken, ImeTracker.PHASE_SERVER_CLIENT_KNOWN);
if (!isImeClientFocused(mCurFocusedWindow, cs)) {
Slog.w(TAG, String.format("Ignoring %s of uid %d : %s", methodName, uid, client));
return false;
}
}
+ ImeTracker.get().onProgress(statsToken, ImeTracker.PHASE_SERVER_CLIENT_FOCUSED);
return true;
}
@@ -4221,7 +4312,7 @@
final int curTokenDisplayId;
synchronized (ImfLock.class) {
if (!canInteractWithImeLocked(callingUid, client,
- "getInputMethodWindowVisibleHeight")) {
+ "getInputMethodWindowVisibleHeight", null /* statsToken */)) {
if (!mLoggedDeniedGetInputMethodWindowVisibleHeightForUid.get(callingUid)) {
EventLog.writeEvent(0x534e4554, "204906124", callingUid, "");
mLoggedDeniedGetInputMethodWindowVisibleHeightForUid.put(callingUid, true);
@@ -4444,7 +4535,8 @@
int uid = Binder.getCallingUid();
synchronized (ImfLock.class) {
- if (!canInteractWithImeLocked(uid, client, "addVirtualStylusIdForTestSession")) {
+ if (!canInteractWithImeLocked(uid, client, "addVirtualStylusIdForTestSession",
+ null /* statsToken */)) {
return;
}
final long ident = Binder.clearCallingIdentity();
@@ -4471,7 +4563,8 @@
int uid = Binder.getCallingUid();
synchronized (ImfLock.class) {
- if (!canInteractWithImeLocked(uid, client, "setStylusWindowIdleTimeoutForTest")) {
+ if (!canInteractWithImeLocked(uid, client, "setStylusWindowIdleTimeoutForTest",
+ null /* statsToken */)) {
return;
}
final long ident = Binder.clearCallingIdentity();
@@ -4656,7 +4749,8 @@
}
@BinderThread
- private void applyImeVisibility(IBinder token, IBinder windowToken, boolean setVisible) {
+ private void applyImeVisibility(IBinder token, IBinder windowToken, boolean setVisible,
+ @Nullable ImeTracker.Token statsToken) {
Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "IMMS.applyImeVisibility");
synchronized (ImfLock.class) {
if (!calledWithValidTokenLocked(token)) {
@@ -4664,13 +4758,22 @@
}
if (!setVisible) {
if (mCurClient != null) {
+ ImeTracker.get().onProgress(statsToken,
+ ImeTracker.PHASE_SERVER_APPLY_IME_VISIBILITY);
+
mWindowManagerInternal.hideIme(
mHideRequestWindowMap.get(windowToken),
- mCurClient.mSelfReportedDisplayId);
+ mCurClient.mSelfReportedDisplayId, statsToken);
+ } else {
+ ImeTracker.get().onFailed(statsToken,
+ ImeTracker.PHASE_SERVER_APPLY_IME_VISIBILITY);
}
} else {
+ ImeTracker.get().onProgress(statsToken,
+ ImeTracker.PHASE_SERVER_APPLY_IME_VISIBILITY);
// Send to window manager to show IME after IME layout finishes.
- mWindowManagerInternal.showImePostLayout(mShowRequestWindowMap.get(windowToken));
+ mWindowManagerInternal.showImePostLayout(mShowRequestWindowMap.get(windowToken),
+ statsToken);
}
}
Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
@@ -4737,7 +4840,8 @@
}
final long ident = Binder.clearCallingIdentity();
try {
- hideCurrentInputLocked(mLastImeTargetWindow, flags, null, reason);
+ hideCurrentInputLocked(mLastImeTargetWindow, null /* statsToken */, flags,
+ null /* resultReceiver */, reason);
} finally {
Binder.restoreCallingIdentity(ident);
}
@@ -4754,7 +4858,8 @@
}
final long ident = Binder.clearCallingIdentity();
try {
- showCurrentInputLocked(mLastImeTargetWindow, flags, null,
+ showCurrentInputLocked(mLastImeTargetWindow, null /* statsToken */, flags,
+ null /* resultReceiver */,
SoftInputShowHideReason.SHOW_SOFT_INPUT_FROM_IME);
} finally {
Binder.restoreCallingIdentity(ident);
@@ -4845,7 +4950,8 @@
case MSG_HIDE_CURRENT_INPUT_METHOD:
synchronized (ImfLock.class) {
final @SoftInputShowHideReason int reason = (int) msg.obj;
- hideCurrentInputLocked(mCurFocusedWindow, 0, null, reason);
+ hideCurrentInputLocked(mCurFocusedWindow, null /* statsToken */, 0 /* flags */,
+ null /* resultReceiver */, reason);
}
return true;
@@ -5297,6 +5403,7 @@
mCurrentSubtype = getCurrentInputMethodSubtypeLocked();
}
}
+ notifyInputMethodSubtypeChangedLocked(mSettings.getCurrentUserId(), imi, mCurrentSubtype);
if (!setSubtypeOnly) {
// Set InputMethod here
@@ -6332,7 +6439,8 @@
final String nextIme;
final List<InputMethodInfo> nextEnabledImes;
if (userId == mSettings.getCurrentUserId()) {
- hideCurrentInputLocked(mCurFocusedWindow, 0, null,
+ hideCurrentInputLocked(mCurFocusedWindow, null /* statsToken */,
+ 0 /* flags */, null /* resultReceiver */,
SoftInputShowHideReason.HIDE_RESET_SHELL_COMMAND);
mBindingController.unbindCurrentMethod();
// Reset the current IME
@@ -6597,8 +6705,9 @@
@BinderThread
@Override
- public void applyImeVisibilityAsync(IBinder windowToken, boolean setVisible) {
- mImms.applyImeVisibility(mToken, windowToken, setVisible);
+ public void applyImeVisibilityAsync(IBinder windowToken, boolean setVisible,
+ @Nullable ImeTracker.Token statsToken) {
+ mImms.applyImeVisibility(mToken, windowToken, setVisible, statsToken);
}
@BinderThread
diff --git a/services/core/java/com/android/server/location/gnss/GnssConfiguration.java b/services/core/java/com/android/server/location/gnss/GnssConfiguration.java
index 1435016..b8abd98 100644
--- a/services/core/java/com/android/server/location/gnss/GnssConfiguration.java
+++ b/services/core/java/com/android/server/location/gnss/GnssConfiguration.java
@@ -76,6 +76,8 @@
"ENABLE_PSDS_PERIODIC_DOWNLOAD";
private static final String CONFIG_ENABLE_ACTIVE_SIM_EMERGENCY_SUPL =
"ENABLE_ACTIVE_SIM_EMERGENCY_SUPL";
+ private static final String CONFIG_ENABLE_NI_SUPL_MESSAGE_INJECTION =
+ "ENABLE_NI_SUPL_MESSAGE_INJECTION";
static final String CONFIG_LONGTERM_PSDS_SERVER_1 = "LONGTERM_PSDS_SERVER_1";
static final String CONFIG_LONGTERM_PSDS_SERVER_2 = "LONGTERM_PSDS_SERVER_2";
static final String CONFIG_LONGTERM_PSDS_SERVER_3 = "LONGTERM_PSDS_SERVER_3";
@@ -218,6 +220,14 @@
}
/**
+ * Returns true if NI SUPL message injection is enabled; Returns false otherwise.
+ * Default false if not set.
+ */
+ boolean isNiSuplMessageInjectionEnabled() {
+ return getBooleanConfig(CONFIG_ENABLE_NI_SUPL_MESSAGE_INJECTION, false);
+ }
+
+ /**
* Returns true if a long-term PSDS server is configured.
*/
boolean isLongTermPsdsServerConfigured() {
diff --git a/services/core/java/com/android/server/location/gnss/GnssLocationProvider.java b/services/core/java/com/android/server/location/gnss/GnssLocationProvider.java
index 6f637b8..6f6b1c9 100644
--- a/services/core/java/com/android/server/location/gnss/GnssLocationProvider.java
+++ b/services/core/java/com/android/server/location/gnss/GnssLocationProvider.java
@@ -84,6 +84,7 @@
import android.os.WorkSource;
import android.os.WorkSource.WorkChain;
import android.provider.Settings;
+import android.provider.Telephony.Sms.Intents;
import android.telephony.CarrierConfigManager;
import android.telephony.CellIdentity;
import android.telephony.CellIdentityGsm;
@@ -95,6 +96,7 @@
import android.telephony.CellInfoLte;
import android.telephony.CellInfoNr;
import android.telephony.CellInfoWcdma;
+import android.telephony.SmsMessage;
import android.telephony.SubscriptionManager;
import android.telephony.TelephonyManager;
import android.text.TextUtils;
@@ -107,6 +109,7 @@
import com.android.internal.location.GpsNetInitiatedHandler;
import com.android.internal.location.GpsNetInitiatedHandler.GpsNiNotification;
import com.android.internal.util.FrameworkStatsLog;
+import com.android.internal.util.HexDump;
import com.android.server.FgThread;
import com.android.server.location.gnss.GnssSatelliteBlocklistHelper.GnssSatelliteBlocklistCallback;
import com.android.server.location.gnss.NtpTimeHelper.InjectNtpTimeCallback;
@@ -523,23 +526,31 @@
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED);
intentFilter.addAction(TelephonyManager.ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED);
- mContext.registerReceiver(new BroadcastReceiver() {
- @Override
- public void onReceive(Context context, Intent intent) {
- String action = intent.getAction();
- if (DEBUG) Log.d(TAG, "receive broadcast intent, action: " + action);
- if (action == null) {
- return;
- }
+ mContext.registerReceiver(mIntentReceiver, intentFilter, null, mHandler);
- switch (action) {
- case CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED:
- case TelephonyManager.ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED:
- subscriptionOrCarrierConfigChanged();
- break;
- }
+ if (mNetworkConnectivityHandler.isNativeAgpsRilSupported()
+ && mGnssConfiguration.isNiSuplMessageInjectionEnabled()) {
+ // Listen to WAP PUSH NI SUPL message.
+ // See User Plane Location Protocol Candidate Version 3.0,
+ // OMA-TS-ULP-V3_0-20110920-C, Section 8.3 OMA Push.
+ intentFilter = new IntentFilter();
+ intentFilter.addAction(Intents.WAP_PUSH_RECEIVED_ACTION);
+ try {
+ intentFilter.addDataType("application/vnd.omaloc-supl-init");
+ } catch (IntentFilter.MalformedMimeTypeException e) {
+ Log.w(TAG, "Malformed SUPL init mime type");
}
- }, intentFilter, null, mHandler);
+ mContext.registerReceiver(mIntentReceiver, intentFilter, null, mHandler);
+
+ // Listen to MT SMS NI SUPL message.
+ // See User Plane Location Protocol Candidate Version 3.0,
+ // OMA-TS-ULP-V3_0-20110920-C, Section 8.4 MT SMS.
+ intentFilter = new IntentFilter();
+ intentFilter.addAction(Intents.DATA_SMS_RECEIVED_ACTION);
+ intentFilter.addDataScheme("sms");
+ intentFilter.addDataAuthority("localhost", "7275");
+ mContext.registerReceiver(mIntentReceiver, intentFilter, null, mHandler);
+ }
mNetworkConnectivityHandler.registerNetworkCallbacks();
@@ -560,6 +571,80 @@
updateEnabled();
}
+ private BroadcastReceiver mIntentReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ String action = intent.getAction();
+ if (DEBUG) Log.d(TAG, "receive broadcast intent, action: " + action);
+ if (action == null) {
+ return;
+ }
+
+ switch (action) {
+ case CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED:
+ case TelephonyManager.ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED:
+ subscriptionOrCarrierConfigChanged();
+ break;
+ case Intents.WAP_PUSH_RECEIVED_ACTION:
+ case Intents.DATA_SMS_RECEIVED_ACTION:
+ injectSuplInit(intent);
+ break;
+ }
+ }
+ };
+
+ private void injectSuplInit(Intent intent) {
+ if (!isNfwLocationAccessAllowed()) {
+ Log.w(TAG, "Reject SUPL INIT as no NFW location access");
+ return;
+ }
+
+ int slotIndex = intent.getIntExtra(SubscriptionManager.EXTRA_SLOT_INDEX,
+ SubscriptionManager.INVALID_SIM_SLOT_INDEX);
+ if (slotIndex == SubscriptionManager.INVALID_SIM_SLOT_INDEX) {
+ Log.e(TAG, "Invalid slot index");
+ return;
+ }
+
+ byte[] suplInit = null;
+ String action = intent.getAction();
+ if (action.equals(Intents.DATA_SMS_RECEIVED_ACTION)) {
+ SmsMessage[] messages = Intents.getMessagesFromIntent(intent);
+ if (messages == null) {
+ Log.e(TAG, "Message does not exist in the intent");
+ return;
+ }
+ for (SmsMessage message : messages) {
+ suplInit = message.getUserData();
+ injectSuplInit(suplInit, slotIndex);
+ }
+ } else if (action.equals(Intents.WAP_PUSH_RECEIVED_ACTION)) {
+ suplInit = intent.getByteArrayExtra("data");
+ injectSuplInit(suplInit, slotIndex);
+ }
+ }
+
+ private void injectSuplInit(byte[] suplInit, int slotIndex) {
+ if (suplInit != null) {
+ if (DEBUG) {
+ Log.d(TAG, "suplInit = "
+ + HexDump.toHexString(suplInit) + " slotIndex = " + slotIndex);
+ }
+ mGnssNative.injectNiSuplMessageData(suplInit, suplInit.length , slotIndex);
+ }
+ }
+
+ private boolean isNfwLocationAccessAllowed() {
+ if (mGnssNative.isInEmergencySession()) {
+ return true;
+ }
+ if (mGnssVisibilityControl != null
+ && mGnssVisibilityControl.hasLocationPermissionEnabledProxyApps()) {
+ return true;
+ }
+ return false;
+ }
+
/**
* Implements {@link InjectNtpTimeCallback#injectTime}
*/
diff --git a/services/core/java/com/android/server/location/gnss/GnssNetworkConnectivityHandler.java b/services/core/java/com/android/server/location/gnss/GnssNetworkConnectivityHandler.java
index 02bdfd5..a7fffe2 100644
--- a/services/core/java/com/android/server/location/gnss/GnssNetworkConnectivityHandler.java
+++ b/services/core/java/com/android/server/location/gnss/GnssNetworkConnectivityHandler.java
@@ -762,6 +762,10 @@
return APN_INVALID;
}
+ protected boolean isNativeAgpsRilSupported() {
+ return native_is_agps_ril_supported();
+ }
+
// AGPS support
private native void native_agps_data_conn_open(long networkHandle, String apn, int apnIpType);
diff --git a/services/core/java/com/android/server/location/gnss/GnssVisibilityControl.java b/services/core/java/com/android/server/location/gnss/GnssVisibilityControl.java
index 631dbbf..4e5e5f8 100644
--- a/services/core/java/com/android/server/location/gnss/GnssVisibilityControl.java
+++ b/services/core/java/com/android/server/location/gnss/GnssVisibilityControl.java
@@ -437,6 +437,10 @@
return locationPermissionEnabledProxyApps;
}
+ public boolean hasLocationPermissionEnabledProxyApps() {
+ return getLocationPermissionEnabledProxyApps().length > 0;
+ }
+
private void handleNfwNotification(NfwNotification nfwNotification) {
if (DEBUG) Log.d(TAG, "Non-framework location access notification: " + nfwNotification);
diff --git a/services/core/java/com/android/server/location/gnss/hal/GnssNative.java b/services/core/java/com/android/server/location/gnss/hal/GnssNative.java
index 2d015a5d..edb2e5b 100644
--- a/services/core/java/com/android/server/location/gnss/hal/GnssNative.java
+++ b/services/core/java/com/android/server/location/gnss/hal/GnssNative.java
@@ -989,6 +989,14 @@
mGnssHal.injectPsdsData(data, length, psdsType);
}
+ /**
+ * Injects NI SUPL message data into the GNSS HAL.
+ */
+ public void injectNiSuplMessageData(byte[] data, int length, int slotIndex) {
+ Preconditions.checkState(mRegistered);
+ mGnssHal.injectNiSuplMessageData(data, length, slotIndex);
+ }
+
@NativeEntryPoint
void reportGnssServiceDied() {
// Not necessary to clear (and restore) binder identity since it runs on another thread.
@@ -1278,7 +1286,7 @@
}
@NativeEntryPoint
- boolean isInEmergencySession() {
+ public boolean isInEmergencySession() {
return Binder.withCleanCallingIdentity(
() -> mEmergencyHelper.isInEmergency(
TimeUnit.SECONDS.toMillis(mConfiguration.getEsExtensionSec())));
@@ -1507,6 +1515,10 @@
protected void injectPsdsData(byte[] data, int length, int psdsType) {
native_inject_psds_data(data, length, psdsType);
}
+
+ protected void injectNiSuplMessageData(byte[] data, int length, int slotIndex) {
+ native_inject_ni_supl_message_data(data, length, slotIndex);
+ }
}
// basic APIs
@@ -1650,6 +1662,9 @@
private static native void native_agps_set_ref_location_cellid(int type, int mcc, int mnc,
int lac, long cid, int tac, int pcid, int arfcn);
+ private static native void native_inject_ni_supl_message_data(byte[] data, int length,
+ int slotIndex);
+
// PSDS APIs
private static native boolean native_supports_psds();
diff --git a/services/core/java/com/android/server/locksettings/LockSettingsStorage.java b/services/core/java/com/android/server/locksettings/LockSettingsStorage.java
index d836df5..807ba3c 100644
--- a/services/core/java/com/android/server/locksettings/LockSettingsStorage.java
+++ b/services/core/java/com/android/server/locksettings/LockSettingsStorage.java
@@ -309,6 +309,10 @@
}
private void writeFile(File path, byte[] data) {
+ writeFile(path, data, /* syncParentDir= */ true);
+ }
+
+ private void writeFile(File path, byte[] data, boolean syncParentDir) {
synchronized (mFileWriteLock) {
// Use AtomicFile to guarantee atomicity of the file write, including when an existing
// file is replaced with a new one. This method is usually used to create new files,
@@ -326,9 +330,11 @@
file.failWrite(out);
}
// For performance reasons, AtomicFile only syncs the file itself, not also the parent
- // directory. The latter must be done explicitly here, as some callers need a guarantee
- // that the file really exists on-disk when this returns.
- fsyncDirectory(path.getParentFile());
+ // directory. The latter must be done explicitly when requested here, as some callers
+ // need a guarantee that the file really exists on-disk when this returns.
+ if (syncParentDir) {
+ fsyncDirectory(path.getParentFile());
+ }
mCache.putFile(path, data);
}
}
@@ -378,10 +384,20 @@
}
}
+ /**
+ * Writes the synthetic password state file for the given user ID, protector ID, and state name.
+ * If the file already exists, then it is atomically replaced.
+ * <p>
+ * This doesn't sync the parent directory, and a result the new state file may be lost if the
+ * system crashes. The caller must call {@link syncSyntheticPasswordState()} afterwards to sync
+ * the parent directory if needed, preferably after batching up other state file creations for
+ * the same user. We do it this way because directory syncs are expensive on some filesystems.
+ */
public void writeSyntheticPasswordState(int userId, long protectorId, String name,
byte[] data) {
ensureSyntheticPasswordDirectoryForUser(userId);
- writeFile(getSyntheticPasswordStateFileForUser(userId, protectorId, name), data);
+ writeFile(getSyntheticPasswordStateFileForUser(userId, protectorId, name), data,
+ /* syncParentDir= */ false);
}
public byte[] readSyntheticPasswordState(int userId, long protectorId, String name) {
@@ -392,6 +408,13 @@
deleteFile(getSyntheticPasswordStateFileForUser(userId, protectorId, name));
}
+ /**
+ * Ensures that all synthetic password state files for the user have really been saved to disk.
+ */
+ public void syncSyntheticPasswordState(int userId) {
+ fsyncDirectory(getSyntheticPasswordDirectoryForUser(userId));
+ }
+
public Map<Integer, List<Long>> listSyntheticPasswordProtectorsForAllUsers(String stateName) {
Map<Integer, List<Long>> result = new ArrayMap<>();
final UserManager um = UserManager.get(mContext);
diff --git a/services/core/java/com/android/server/locksettings/SyntheticPasswordManager.java b/services/core/java/com/android/server/locksettings/SyntheticPasswordManager.java
index 3fd488e..73a16fd 100644
--- a/services/core/java/com/android/server/locksettings/SyntheticPasswordManager.java
+++ b/services/core/java/com/android/server/locksettings/SyntheticPasswordManager.java
@@ -619,12 +619,16 @@
/**
* Creates a new synthetic password (SP) for the given user.
- *
+ * <p>
* Any existing SID for the user is cleared.
- *
+ * <p>
* Also saves the escrow information necessary to re-generate the synthetic password under
* an escrow scheme. This information can be removed with {@link #destroyEscrowData} if
* password escrow should be disabled completely on the given user.
+ * <p>
+ * {@link syncState()} is not called yet; the caller should create a protector afterwards, which
+ * handles this. This makes it so that all the user's initial SP state files, including the
+ * initial LSKF-based protector, are efficiently created with only a single {@link syncState()}.
*/
SyntheticPassword newSyntheticPassword(int userId) {
clearSidForUser(userId);
@@ -668,6 +672,7 @@
private void saveSyntheticPasswordHandle(byte[] spHandle, int userId) {
saveState(SP_HANDLE_NAME, spHandle, NULL_PROTECTOR_ID, userId);
+ syncState(userId);
}
private boolean loadEscrowData(SyntheticPassword sp, int userId) {
@@ -677,6 +682,11 @@
return e0 != null && p1 != null;
}
+ /**
+ * Saves the escrow data for the synthetic password. The caller is responsible for calling
+ * {@link syncState()} afterwards, once the user's other initial synthetic password state files
+ * have been created.
+ */
private void saveEscrowData(SyntheticPassword sp, int userId) {
saveState(SP_E0_NAME, sp.mEncryptedEscrowSplit0, NULL_PROTECTOR_ID, userId);
saveState(SP_P1_NAME, sp.mEscrowSplit1, NULL_PROTECTOR_ID, userId);
@@ -708,6 +718,10 @@
return buffer.getInt();
}
+ /**
+ * Creates a file that stores the Weaver slot the protector is using. The caller is responsible
+ * for calling {@link syncState()} afterwards, once all the protector's files have been created.
+ */
private void saveWeaverSlot(int slot, long protectorId, int userId) {
ByteBuffer buffer = ByteBuffer.allocate(Byte.BYTES + Integer.BYTES);
buffer.put(WEAVER_VERSION);
@@ -837,6 +851,7 @@
}
createSyntheticPasswordBlob(protectorId, PROTECTOR_TYPE_LSKF_BASED, sp, protectorSecret,
sid, userId);
+ syncState(userId); // ensure the new files are really saved to disk
return protectorId;
}
@@ -996,6 +1011,7 @@
saveSecdiscardable(tokenHandle, tokenData.secdiscardableOnDisk, userId);
createSyntheticPasswordBlob(tokenHandle, getTokenBasedProtectorType(tokenData.mType), sp,
tokenData.aggregatedSecret, 0L, userId);
+ syncState(userId); // ensure the new files are really saved to disk
tokenMap.get(userId).remove(tokenHandle);
if (tokenData.mCallback != null) {
tokenData.mCallback.onEscrowTokenActivated(tokenHandle, userId);
@@ -1003,6 +1019,11 @@
return true;
}
+ /**
+ * Creates a synthetic password blob, i.e. the file that stores the encrypted synthetic password
+ * (or encrypted escrow secret) for a protector. The caller is responsible for calling
+ * {@link syncState()} afterwards, once all the protector's files have been created.
+ */
private void createSyntheticPasswordBlob(long protectorId, byte protectorType,
SyntheticPassword sp, byte[] protectorSecret, long sid, int userId) {
final byte[] spSecret;
@@ -1118,6 +1139,7 @@
// (getting rid of CREDENTIAL_TYPE_PASSWORD_OR_PIN)
pwd.credentialType = credential.getType();
saveState(PASSWORD_DATA_NAME, pwd.toBytes(), protectorId, userId);
+ syncState(userId);
synchronizeFrpPassword(pwd, 0, userId);
} else {
Slog.w(TAG, "Fail to re-enroll user password for user " + userId);
@@ -1156,6 +1178,7 @@
if (result.syntheticPassword != null && !credential.isNone() &&
!hasPasswordMetrics(protectorId, userId)) {
savePasswordMetrics(credential, result.syntheticPassword, protectorId, userId);
+ syncState(userId); // Not strictly needed as the upgrade can be re-done, but be safe.
}
return result;
}
@@ -1275,6 +1298,7 @@
+ blob.mProtectorType);
createSyntheticPasswordBlob(protectorId, blob.mProtectorType, result, protectorSecret,
sid, userId);
+ syncState(userId); // Not strictly needed as the upgrade can be re-done, but be safe.
}
return result;
}
@@ -1396,12 +1420,21 @@
return ArrayUtils.concat(data, secdiscardable);
}
+ /**
+ * Generates and writes the secdiscardable file for the given protector. The caller is
+ * responsible for calling {@link syncState()} afterwards, once all the protector's files have
+ * been created.
+ */
private byte[] createSecdiscardable(long protectorId, int userId) {
byte[] data = secureRandom(SECDISCARDABLE_LENGTH);
saveSecdiscardable(protectorId, data, userId);
return data;
}
+ /**
+ * Writes the secdiscardable file for the given protector. The caller is responsible for
+ * calling {@link syncState()} afterwards, once all the protector's files have been created.
+ */
private void saveSecdiscardable(long protectorId, byte[] secdiscardable, int userId) {
saveState(SECDISCARDABLE_NAME, secdiscardable, protectorId, userId);
}
@@ -1445,6 +1478,11 @@
return VersionedPasswordMetrics.deserialize(decrypted).getMetrics();
}
+ /**
+ * Creates the password metrics file: the file associated with the LSKF-based protector that
+ * contains the encrypted metrics about the LSKF. The caller is responsible for calling
+ * {@link syncState()} afterwards if needed.
+ */
private void savePasswordMetrics(LockscreenCredential credential, SyntheticPassword sp,
long protectorId, int userId) {
final byte[] encrypted = SyntheticPasswordCrypto.encrypt(sp.deriveMetricsKey(),
@@ -1466,10 +1504,21 @@
return mStorage.readSyntheticPasswordState(userId, protectorId, stateName);
}
+ /**
+ * Persists the given synthetic password state for the given user ID and protector ID.
+ * <p>
+ * For performance reasons, this doesn't sync the user's synthetic password state directory. As
+ * a result, it doesn't guarantee that the file will really be present after a crash. If that
+ * is needed, call {@link syncState()} afterwards, preferably after batching up related updates.
+ */
private void saveState(String stateName, byte[] data, long protectorId, int userId) {
mStorage.writeSyntheticPasswordState(userId, protectorId, stateName, data);
}
+ private void syncState(int userId) {
+ mStorage.syncSyntheticPasswordState(userId);
+ }
+
private void destroyState(String stateName, long protectorId, int userId) {
mStorage.deleteSyntheticPasswordState(userId, protectorId, stateName);
}
diff --git a/services/core/java/com/android/server/pm/Computer.java b/services/core/java/com/android/server/pm/Computer.java
index bf00a33..5b8ee2b 100644
--- a/services/core/java/com/android/server/pm/Computer.java
+++ b/services/core/java/com/android/server/pm/Computer.java
@@ -125,6 +125,14 @@
ActivityInfo getActivityInfo(ComponentName component, long flags, int userId);
/**
+ * Similar to {@link Computer#getActivityInfo(android.content.ComponentName, long, int)} but
+ * only visible as internal service. This method bypass INTERACT_ACROSS_USERS or
+ * INTERACT_ACROSS_USERS_FULL permission checks and only to be used for intent resolution across
+ * chained cross profiles
+ */
+ ActivityInfo getActivityInfoCrossProfile(ComponentName component, long flags, int userId);
+
+ /**
* Important: The provided filterCallingUid is used exclusively to filter out activities
* that can be seen based on user state. It's typically the original caller uid prior
* to clearing. Because it can only be provided by trusted code, its value can be
diff --git a/services/core/java/com/android/server/pm/ComputerEngine.java b/services/core/java/com/android/server/pm/ComputerEngine.java
index b285136..a8534b0 100644
--- a/services/core/java/com/android/server/pm/ComputerEngine.java
+++ b/services/core/java/com/android/server/pm/ComputerEngine.java
@@ -835,6 +835,24 @@
}
/**
+ * Similar to {@link Computer#getActivityInfo(android.content.ComponentName, long, int)} but
+ * only visible as internal service. This method bypass INTERACT_ACROSS_USERS or
+ * INTERACT_ACROSS_USERS_FULL permission checks and only to be used for intent resolution across
+ * chained cross profiles
+ * @param component application's component
+ * @param flags resolve info flags
+ * @param userId user id where activity resides
+ * @return ActivityInfo corresponding to requested component.
+ */
+ public final ActivityInfo getActivityInfoCrossProfile(ComponentName component,
+ @PackageManager.ResolveInfoFlagsBits long flags, int userId) {
+ if (!mUserManager.exists(userId)) return null;
+ flags = updateFlagsForComponent(flags, userId);
+
+ return getActivityInfoInternalBody(component, flags, Binder.getCallingUid(), userId);
+ }
+
+ /**
* Important: The provided filterCallingUid is used exclusively to filter out activities
* that can be seen based on user state. It's typically the original caller uid prior
* to clearing. Because it can only be provided by trusted code, its value can be
@@ -1711,7 +1729,7 @@
ComponentName forwardingActivityComponentName = new ComponentName(
androidApplication().packageName, className);
ActivityInfo forwardingActivityInfo =
- getActivityInfo(forwardingActivityComponentName, 0,
+ getActivityInfoCrossProfile(forwardingActivityComponentName, 0,
sourceUserId);
if (!targetIsProfile) {
forwardingActivityInfo.showUserIcon = targetUserId;
diff --git a/services/core/java/com/android/server/pm/CrossProfileIntentFilter.java b/services/core/java/com/android/server/pm/CrossProfileIntentFilter.java
index 798217f..04bd135 100644
--- a/services/core/java/com/android/server/pm/CrossProfileIntentFilter.java
+++ b/services/core/java/com/android/server/pm/CrossProfileIntentFilter.java
@@ -49,6 +49,15 @@
//flag to decide if intent needs to be resolved cross profile if pkgName is already defined
public static final int FLAG_IS_PACKAGE_FOR_FILTER = 0x00000008;
+ /*
+ This flag, denotes if further cross profile resolution is allowed, e.g. if profile#0 is linked
+ to profile#1 and profile#2 . When intent resolution from profile#1 is started we resolve it in
+ profile#1 and profile#0. The profile#0 is also linked to profile#2, we will only resolve in
+ profile#2 if CrossProfileIntentFilter between profile#1 and profile#0 have set flag
+ FLAG_ALLOW_CHAINED_RESOLUTION.
+ */
+ public static final int FLAG_ALLOW_CHAINED_RESOLUTION = 0x00000010;
+
private static final String TAG = "CrossProfileIntentFilter";
/**
diff --git a/services/core/java/com/android/server/pm/CrossProfileIntentResolverEngine.java b/services/core/java/com/android/server/pm/CrossProfileIntentResolverEngine.java
index 5ae4cab..4362956 100644
--- a/services/core/java/com/android/server/pm/CrossProfileIntentResolverEngine.java
+++ b/services/core/java/com/android/server/pm/CrossProfileIntentResolverEngine.java
@@ -36,14 +36,19 @@
import android.util.Pair;
import android.util.Slog;
import android.util.SparseArray;
+import android.util.SparseBooleanArray;
import com.android.server.LocalServices;
import com.android.server.pm.pkg.PackageStateInternal;
import com.android.server.pm.verify.domain.DomainVerificationManagerInternal;
import com.android.server.pm.verify.domain.DomainVerificationUtils;
+import java.util.ArrayDeque;
import java.util.ArrayList;
+import java.util.HashSet;
import java.util.List;
+import java.util.Queue;
+import java.util.Set;
import java.util.function.Function;
/**
@@ -115,73 +120,111 @@
Intent intent, String resolvedType, int userId, long flags, String pkgName,
boolean hasNonNegativePriorityResult,
Function<String, PackageStateInternal> pkgSettingFunction) {
-
+ Queue<Integer> pendingUsers = new ArrayDeque<>();
+ Set<Integer> visitedUserIds = new HashSet<>();
+ SparseBooleanArray hasNonNegativePriorityResultFromParent = new SparseBooleanArray();
+ visitedUserIds.add(userId);
+ pendingUsers.add(userId);
+ hasNonNegativePriorityResultFromParent.put(userId, hasNonNegativePriorityResult);
+ UserManagerInternal umInternal = LocalServices.getService(UserManagerInternal.class);
List<CrossProfileDomainInfo> crossProfileDomainInfos = new ArrayList<>();
+ while (!pendingUsers.isEmpty()) {
+ int currentUserId = pendingUsers.poll();
+ List<CrossProfileIntentFilter> matchingFilters =
+ computer.getMatchingCrossProfileIntentFilters(intent, resolvedType,
+ currentUserId);
- List<CrossProfileIntentFilter> matchingFilters =
- computer.getMatchingCrossProfileIntentFilters(intent, resolvedType, userId);
-
- if (matchingFilters == null || matchingFilters.isEmpty()) {
- /** if intent is web intent, checking if parent profile should handle the intent even
- if there is no matching filter. The configuration is based on user profile
- restriction android.os.UserManager#ALLOW_PARENT_PROFILE_APP_LINKING **/
- if (intent.hasWebURI()) {
- UserInfo parent = computer.getProfileParent(userId);
- if (parent != null) {
- CrossProfileDomainInfo generalizedCrossProfileDomainInfo = computer
- .getCrossProfileDomainPreferredLpr(intent, resolvedType, flags, userId,
- parent.id);
- if (generalizedCrossProfileDomainInfo != null) {
- crossProfileDomainInfos.add(generalizedCrossProfileDomainInfo);
+ if (matchingFilters == null || matchingFilters.isEmpty()) {
+ /** if intent is web intent, checking if parent profile should handle the intent
+ * even if there is no matching filter. The configuration is based on user profile
+ * restriction android.os.UserManager#ALLOW_PARENT_PROFILE_APP_LINKING **/
+ if (currentUserId == userId && intent.hasWebURI()) {
+ UserInfo parent = computer.getProfileParent(currentUserId);
+ if (parent != null) {
+ CrossProfileDomainInfo generalizedCrossProfileDomainInfo = computer
+ .getCrossProfileDomainPreferredLpr(intent, resolvedType, flags,
+ currentUserId, parent.id);
+ if (generalizedCrossProfileDomainInfo != null) {
+ crossProfileDomainInfos.add(generalizedCrossProfileDomainInfo);
+ }
}
}
+ continue;
}
- return crossProfileDomainInfos;
- }
- UserManagerInternal umInternal = LocalServices.getService(UserManagerInternal.class);
- UserInfo sourceUserInfo = umInternal.getUserInfo(userId);
+ UserInfo sourceUserInfo = umInternal.getUserInfo(currentUserId);
- // Grouping the CrossProfileIntentFilters based on targerId
- SparseArray<List<CrossProfileIntentFilter>> crossProfileIntentFiltersByUser =
- new SparseArray<>();
+ // Grouping the CrossProfileIntentFilters based on targerId
+ SparseArray<List<CrossProfileIntentFilter>> crossProfileIntentFiltersByUser =
+ new SparseArray<>();
- for (int index = 0; index < matchingFilters.size(); index++) {
- CrossProfileIntentFilter crossProfileIntentFilter = matchingFilters.get(index);
+ for (int index = 0; index < matchingFilters.size(); index++) {
+ CrossProfileIntentFilter crossProfileIntentFilter = matchingFilters.get(index);
- if (!crossProfileIntentFiltersByUser
- .contains(crossProfileIntentFilter.mTargetUserId)) {
- crossProfileIntentFiltersByUser.put(crossProfileIntentFilter.mTargetUserId,
- new ArrayList<>());
+ if (!crossProfileIntentFiltersByUser
+ .contains(crossProfileIntentFilter.mTargetUserId)) {
+ crossProfileIntentFiltersByUser.put(crossProfileIntentFilter.mTargetUserId,
+ new ArrayList<>());
+ }
+ crossProfileIntentFiltersByUser.get(crossProfileIntentFilter.mTargetUserId)
+ .add(crossProfileIntentFilter);
}
- crossProfileIntentFiltersByUser.get(crossProfileIntentFilter.mTargetUserId)
- .add(crossProfileIntentFilter);
- }
- /*
- For each target user, we would call their corresponding strategy
- {@link CrossProfileResolver} to resolve intent in corresponding user
- */
- for (int index = 0; index < crossProfileIntentFiltersByUser.size(); index++) {
+ /*
+ For each target user, we would call their corresponding strategy
+ {@link CrossProfileResolver} to resolve intent in corresponding user
+ */
+ for (int index = 0; index < crossProfileIntentFiltersByUser.size(); index++) {
- UserInfo targetUserInfo = umInternal.getUserInfo(crossProfileIntentFiltersByUser
- .keyAt(index));
+ int targetUserId = crossProfileIntentFiltersByUser.keyAt(index);
- // Choosing strategy based on source and target user
- CrossProfileResolver crossProfileResolver =
- chooseCrossProfileResolver(computer, sourceUserInfo, targetUserInfo);
+ //if user is already visited then skip resolution for particular user.
+ if (visitedUserIds.contains(targetUserId)) {
+ continue;
+ }
+
+ UserInfo targetUserInfo = umInternal.getUserInfo(targetUserId);
+
+ // Choosing strategy based on source and target user
+ CrossProfileResolver crossProfileResolver =
+ chooseCrossProfileResolver(computer, sourceUserInfo, targetUserInfo);
/*
If {@link CrossProfileResolver} is available for source,target pair we will call it to
get {@link CrossProfileDomainInfo}s from that user.
*/
- if (crossProfileResolver != null) {
- List<CrossProfileDomainInfo> crossProfileInfos = crossProfileResolver
- .resolveIntent(computer, intent, resolvedType, userId,
- crossProfileIntentFiltersByUser.keyAt(index), flags, pkgName,
- crossProfileIntentFiltersByUser.valueAt(index),
- hasNonNegativePriorityResult, pkgSettingFunction);
- crossProfileDomainInfos.addAll(crossProfileInfos);
+ if (crossProfileResolver != null) {
+ List<CrossProfileDomainInfo> crossProfileInfos = crossProfileResolver
+ .resolveIntent(computer, intent, resolvedType, currentUserId,
+ targetUserId, flags, pkgName,
+ crossProfileIntentFiltersByUser.valueAt(index),
+ hasNonNegativePriorityResultFromParent.get(currentUserId),
+ pkgSettingFunction);
+ crossProfileDomainInfos.addAll(crossProfileInfos);
+
+ hasNonNegativePriorityResultFromParent.put(targetUserId,
+ hasNonNegativePriority(crossProfileInfos));
+
+ /*
+ Adding target user to queue if flag
+ {@link CrossProfileIntentFilter#FLAG_ALLOW_CHAINED_RESOLUTION} is set for any
+ {@link CrossProfileIntentFilter}
+ */
+ boolean allowChainedResolution = false;
+ for (int filterIndex = 0; filterIndex < crossProfileIntentFiltersByUser
+ .valueAt(index).size(); filterIndex++) {
+ if ((CrossProfileIntentFilter
+ .FLAG_ALLOW_CHAINED_RESOLUTION & crossProfileIntentFiltersByUser
+ .valueAt(index).get(filterIndex).mFlags) != 0) {
+ allowChainedResolution = true;
+ break;
+ }
+ }
+ if (allowChainedResolution) {
+ pendingUsers.add(targetUserId);
+ }
+ visitedUserIds.add(targetUserId);
+ }
}
}
@@ -237,7 +280,7 @@
/**
* Returns true if we source user can reach target user for given intent. The source can
- * directly or indirectly reach to target. This will perform depth first search to check if
+ * directly or indirectly reach to target. This will perform breadth first search to check if
* source can reach target.
* @param computer {@link Computer} instance used for resolution by {@link ComponentResolverApi}
* @param intent request
@@ -251,13 +294,38 @@
@UserIdInt int targetUserId) {
if (sourceUserId == targetUserId) return true;
- List<CrossProfileIntentFilter> matches =
- computer.getMatchingCrossProfileIntentFilters(intent, resolvedType, sourceUserId);
- if (matches != null) {
- for (int index = 0; index < matches.size(); index++) {
- CrossProfileIntentFilter crossProfileIntentFilter = matches.get(index);
- if (crossProfileIntentFilter.mTargetUserId == targetUserId) {
- return true;
+ Queue<Integer> pendingUsers = new ArrayDeque<>();
+ Set<Integer> visitedUserIds = new HashSet<>();
+ visitedUserIds.add(sourceUserId);
+ pendingUsers.add(sourceUserId);
+
+ while (!pendingUsers.isEmpty()) {
+ int currentUserId = pendingUsers.poll();
+
+ List<CrossProfileIntentFilter> matches =
+ computer.getMatchingCrossProfileIntentFilters(intent, resolvedType,
+ currentUserId);
+ if (matches != null) {
+ for (int index = 0; index < matches.size(); index++) {
+ CrossProfileIntentFilter crossProfileIntentFilter = matches.get(index);
+ if (crossProfileIntentFilter.mTargetUserId == targetUserId) {
+ return true;
+ }
+ if (visitedUserIds.contains(crossProfileIntentFilter.mTargetUserId)) {
+ continue;
+ }
+
+ /*
+ If source cannot directly reach to target, we will add
+ CrossProfileIntentFilter.mTargetUserId user to queue to check if target user
+ can be reached via CrossProfileIntentFilter.mTargetUserId i.e. it can be
+ indirectly reached through chained/linked profiles.
+ */
+ if ((CrossProfileIntentFilter.FLAG_ALLOW_CHAINED_RESOLUTION
+ & crossProfileIntentFilter.mFlags) != 0) {
+ pendingUsers.add(crossProfileIntentFilter.mTargetUserId);
+ visitedUserIds.add(crossProfileIntentFilter.mTargetUserId);
+ }
}
}
}
@@ -605,4 +673,14 @@
return resolveInfoList;
}
+
+ /**
+ * @param crossProfileDomainInfos list of cross profile domain info in descending priority order
+ * @return if the list contains a resolve info with non-negative priority
+ */
+ private boolean hasNonNegativePriority(List<CrossProfileDomainInfo> crossProfileDomainInfos) {
+ return crossProfileDomainInfos.size() > 0
+ && crossProfileDomainInfos.get(0).mResolveInfo != null
+ && crossProfileDomainInfos.get(0).mResolveInfo.priority >= 0;
+ }
}
diff --git a/services/core/java/com/android/server/pm/DefaultCrossProfileIntentFiltersUtils.java b/services/core/java/com/android/server/pm/DefaultCrossProfileIntentFiltersUtils.java
index cac9323..ceaaefd 100644
--- a/services/core/java/com/android/server/pm/DefaultCrossProfileIntentFiltersUtils.java
+++ b/services/core/java/com/android/server/pm/DefaultCrossProfileIntentFiltersUtils.java
@@ -319,4 +319,135 @@
HOME,
MOBILE_NETWORK_SETTINGS);
}
+
+ /**
+ * Clone profile's DefaultCrossProfileIntentFilter
+ */
+
+ /*
+ Allowing media capture from clone to parent profile as clone profile would not have camera
+ */
+ private static final DefaultCrossProfileIntentFilter CLONE_TO_PARENT_MEDIA_CAPTURE =
+ new DefaultCrossProfileIntentFilter.Builder(
+ DefaultCrossProfileIntentFilter.Direction.TO_PARENT,
+ /* flags= */ 0x00000018, // 0x00000018 means FLAG_IS_PACKAGE_FOR_FILTER
+ // and FLAG_ALLOW_CHAINED_RESOLUTION set
+ /* letsPersonalDataIntoProfile= */ false)
+ .addAction(MediaStore.ACTION_IMAGE_CAPTURE)
+ .addAction(MediaStore.ACTION_IMAGE_CAPTURE_SECURE)
+ .addAction(MediaStore.ACTION_VIDEO_CAPTURE)
+ .addAction(MediaStore.Audio.Media.RECORD_SOUND_ACTION)
+ .addAction(MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA)
+ .addAction(MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA_SECURE)
+ .addAction(MediaStore.INTENT_ACTION_VIDEO_CAMERA)
+ .addCategory(Intent.CATEGORY_DEFAULT)
+ .build();
+
+ /*
+ Allowing send action from clone to parent profile to share content from clone apps to parent
+ apps
+ */
+ private static final DefaultCrossProfileIntentFilter CLONE_TO_PARENT_SEND_ACTION =
+ new DefaultCrossProfileIntentFilter.Builder(
+ DefaultCrossProfileIntentFilter.Direction.TO_PARENT,
+ /* flags= */ 0x00000018, // 0x00000018 means FLAG_IS_PACKAGE_FOR_FILTER
+ // and FLAG_ALLOW_CHAINED_RESOLUTION set
+ /* letsPersonalDataIntoProfile= */ false)
+ .addAction(Intent.ACTION_SEND)
+ .addAction(Intent.ACTION_SEND_MULTIPLE)
+ .addAction(Intent.ACTION_SENDTO)
+ .addDataType("*/*")
+ .build();
+
+ /*
+ Allowing send action from parent to clone profile to share content from parent apps to clone
+ apps
+ */
+ private static final DefaultCrossProfileIntentFilter PARENT_TO_CLONE_SEND_ACTION =
+ new DefaultCrossProfileIntentFilter.Builder(
+ DefaultCrossProfileIntentFilter.Direction.TO_PROFILE,
+ /* flags= */ 0x00000018, // 0x00000018 means FLAG_IS_PACKAGE_FOR_FILTER
+ // and FLAG_ALLOW_CHAINED_RESOLUTION set
+ /* letsPersonalDataIntoProfile= */ false)
+ .addAction(Intent.ACTION_SEND)
+ .addAction(Intent.ACTION_SEND_MULTIPLE)
+ .addAction(Intent.ACTION_SENDTO)
+ .addDataType("*/*")
+ .build();
+
+ /*
+ Allowing view action from clone to parent profile to open any app-links or web links
+ */
+ private static final DefaultCrossProfileIntentFilter CLONE_TO_PARENT_VIEW_ACTION =
+ new DefaultCrossProfileIntentFilter.Builder(
+ DefaultCrossProfileIntentFilter.Direction.TO_PARENT,
+ /* flags= */ 0x00000018, // 0x00000018 means FLAG_IS_PACKAGE_FOR_FILTER
+ // and FLAG_ALLOW_CHAINED_RESOLUTION set
+ /* letsPersonalDataIntoProfile= */ false)
+ .addAction(Intent.ACTION_VIEW)
+ .addDataScheme("https")
+ .addDataScheme("http")
+ .build();
+
+ /*
+ Allowing view action from parent to clone profile to open any app-links or web links
+ */
+ private static final DefaultCrossProfileIntentFilter PARENT_TO_CLONE_VIEW_ACTION =
+ new DefaultCrossProfileIntentFilter.Builder(
+ DefaultCrossProfileIntentFilter.Direction.TO_PROFILE,
+ /* flags= */ 0x00000018, // 0x00000018 means FLAG_IS_PACKAGE_FOR_FILTER
+ // and FLAG_ALLOW_CHAINED_RESOLUTION set
+ /* letsPersonalDataIntoProfile= */ false)
+ .addAction(Intent.ACTION_VIEW)
+ .addDataScheme("https")
+ .addDataScheme("http")
+ .build();
+
+ /*
+ Allowing pick,insert and edit action from clone to parent profile to open picker or contacts
+ insert/edit.
+ */
+ private static final DefaultCrossProfileIntentFilter CLONE_TO_PARENT_PICK_INSERT_ACTION =
+ new DefaultCrossProfileIntentFilter.Builder(
+ DefaultCrossProfileIntentFilter.Direction.TO_PARENT,
+ /* flags= */ 0x00000018, // 0x00000018 means FLAG_IS_PACKAGE_FOR_FILTER
+ // and FLAG_ALLOW_CHAINED_RESOLUTION set
+ /* letsPersonalDataIntoProfile= */ false)
+ .addAction(Intent.ACTION_PICK)
+ .addAction(Intent.ACTION_GET_CONTENT)
+ .addAction(Intent.ACTION_EDIT)
+ .addAction(Intent.ACTION_INSERT)
+ .addAction(Intent.ACTION_INSERT_OR_EDIT)
+ .addDataType("*/*")
+ .build();
+
+ /*
+ Allowing pick,insert and edit action from parent to clone profile to open picker
+ */
+ private static final DefaultCrossProfileIntentFilter PARENT_TO_CLONE_PICK_INSERT_ACTION =
+ new DefaultCrossProfileIntentFilter.Builder(
+ DefaultCrossProfileIntentFilter.Direction.TO_PROFILE,
+ /* flags= */ 0x00000018, // 0x00000018 means FLAG_IS_PACKAGE_FOR_FILTER
+ // and FLAG_ALLOW_CHAINED_RESOLUTION set
+ /* letsPersonalDataIntoProfile= */ false)
+ .addAction(Intent.ACTION_PICK)
+ .addAction(Intent.ACTION_GET_CONTENT)
+ .addAction(Intent.ACTION_EDIT)
+ .addAction(Intent.ACTION_INSERT)
+ .addAction(Intent.ACTION_INSERT_OR_EDIT)
+ .addDataType("*/*")
+ .build();
+
+ public static List<DefaultCrossProfileIntentFilter> getDefaultCloneProfileFilters() {
+ return Arrays.asList(
+ PARENT_TO_CLONE_SEND_ACTION,
+ PARENT_TO_CLONE_VIEW_ACTION,
+ PARENT_TO_CLONE_PICK_INSERT_ACTION,
+ CLONE_TO_PARENT_MEDIA_CAPTURE,
+ CLONE_TO_PARENT_SEND_ACTION,
+ CLONE_TO_PARENT_VIEW_ACTION,
+ CLONE_TO_PARENT_PICK_INSERT_ACTION
+
+ );
+ }
}
diff --git a/services/core/java/com/android/server/pm/InstallPackageHelper.java b/services/core/java/com/android/server/pm/InstallPackageHelper.java
index 7dae4c6..2a0b44d 100644
--- a/services/core/java/com/android/server/pm/InstallPackageHelper.java
+++ b/services/core/java/com/android/server/pm/InstallPackageHelper.java
@@ -150,7 +150,6 @@
import com.android.internal.annotations.GuardedBy;
import com.android.internal.content.F2fsUtils;
-import com.android.internal.content.InstallLocationUtils;
import com.android.internal.security.VerityUtils;
import com.android.internal.util.ArrayUtils;
import com.android.internal.util.CollectionUtils;
@@ -875,7 +874,7 @@
}
}
- Map<String, ReconciledPackage> reconciledPackages;
+ List<ReconciledPackage> reconciledPackages;
synchronized (mPm.mLock) {
try {
Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "reconcilePackages");
@@ -1883,11 +1882,11 @@
}
@GuardedBy("mPm.mLock")
- private void commitPackagesLocked(Map<String, ReconciledPackage> reconciledPackages,
+ private void commitPackagesLocked(List<ReconciledPackage> reconciledPackages,
@NonNull int[] allUsers) {
// TODO: remove any expected failures from this method; this should only be able to fail due
// to unavoidable errors (I/O, etc.)
- for (ReconciledPackage reconciledPkg : reconciledPackages.values()) {
+ for (ReconciledPackage reconciledPkg : reconciledPackages) {
final InstallRequest installRequest = reconciledPkg.mInstallRequest;
final ParsedPackage parsedPackage = installRequest.getParsedPackage();
final String packageName = parsedPackage.getPackageName();
@@ -2205,9 +2204,9 @@
* locks on {@link com.android.server.pm.PackageManagerService.mLock}.
*/
@GuardedBy("mPm.mInstallLock")
- private void executePostCommitStepsLIF(Map<String, ReconciledPackage> reconciledPackages) {
+ private void executePostCommitStepsLIF(List<ReconciledPackage> reconciledPackages) {
final ArraySet<IncrementalStorage> incrementalStorages = new ArraySet<>();
- for (ReconciledPackage reconciledPkg : reconciledPackages.values()) {
+ for (ReconciledPackage reconciledPkg : reconciledPackages) {
final InstallRequest installRequest = reconciledPkg.mInstallRequest;
final boolean instantApp = ((installRequest.getScanFlags() & SCAN_AS_INSTANT_APP) != 0);
final boolean isApex = ((installRequest.getScanFlags() & SCAN_AS_APEX) != 0);
@@ -2338,45 +2337,6 @@
incrementalStorages);
}
- public int installLocationPolicy(PackageInfoLite pkgLite, int installFlags) {
- String packageName = pkgLite.packageName;
- int installLocation = pkgLite.installLocation;
- // reader
- synchronized (mPm.mLock) {
- // Currently installed package which the new package is attempting to replace or
- // null if no such package is installed.
- AndroidPackage installedPkg = mPm.mPackages.get(packageName);
-
- if (installedPkg != null) {
- if ((installFlags & PackageManager.INSTALL_REPLACE_EXISTING) != 0) {
- // Check for updated system application.
- if (installedPkg.isSystem()) {
- return InstallLocationUtils.RECOMMEND_INSTALL_INTERNAL;
- } else {
- // If current upgrade specifies particular preference
- if (installLocation == PackageInfo.INSTALL_LOCATION_INTERNAL_ONLY) {
- // Application explicitly specified internal.
- return InstallLocationUtils.RECOMMEND_INSTALL_INTERNAL;
- } else if (
- installLocation == PackageInfo.INSTALL_LOCATION_PREFER_EXTERNAL) {
- // App explicitly prefers external. Let policy decide
- } else {
- // Prefer previous location
- if (installedPkg.isExternalStorage()) {
- return InstallLocationUtils.RECOMMEND_INSTALL_EXTERNAL;
- }
- return InstallLocationUtils.RECOMMEND_INSTALL_INTERNAL;
- }
- }
- } else {
- // Invalid install. Return error code
- return InstallLocationUtils.RECOMMEND_FAILED_ALREADY_EXISTS;
- }
- }
- }
- return pkgLite.recommendedInstallLocation;
- }
-
Pair<Integer, String> verifyReplacingVersionCode(PackageInfoLite pkgLite,
long requiredInstalledVersionCode, int installFlags) {
if ((installFlags & PackageManager.INSTALL_APEX) != 0) {
@@ -3645,7 +3605,7 @@
boolean appIdCreated = false;
try {
final String pkgName = scanResult.mPkgSetting.getPackageName();
- final Map<String, ReconciledPackage> reconcileResult =
+ final List<ReconciledPackage> reconcileResult =
ReconcilePackageUtils.reconcilePackages(
Collections.singletonList(installRequest),
mPm.mPackages, Collections.singletonMap(pkgName,
@@ -3657,7 +3617,7 @@
} else {
installRequest.setScannedPackageSettingAppId(Process.INVALID_UID);
}
- commitReconciledScanResultLocked(reconcileResult.get(pkgName),
+ commitReconciledScanResultLocked(reconcileResult.get(0),
mPm.mUserManager.getUserIds());
} catch (PackageManagerException e) {
if (appIdCreated) {
diff --git a/services/core/java/com/android/server/pm/InstallRequest.java b/services/core/java/com/android/server/pm/InstallRequest.java
index 4443710..01a8bd0 100644
--- a/services/core/java/com/android/server/pm/InstallRequest.java
+++ b/services/core/java/com/android/server/pm/InstallRequest.java
@@ -59,8 +59,10 @@
@Nullable
private PackageRemovedInfo mRemovedInfo;
- private @PackageManagerService.ScanFlags int mScanFlags;
- private @ParsingPackageUtils.ParseFlags int mParseFlags;
+ @PackageManagerService.ScanFlags
+ private int mScanFlags;
+ @ParsingPackageUtils.ParseFlags
+ private int mParseFlags;
private boolean mReplace;
@Nullable /* The original Package if it is being replaced, otherwise {@code null} */
@@ -155,7 +157,7 @@
mParseFlags = parseFlags;
mScanFlags = scanFlags;
mScanResult = scanResult;
- mPackageMetrics = null; // No real logging from this code path
+ mPackageMetrics = null; // No logging from this code path
}
@Nullable
@@ -393,11 +395,13 @@
return mParsedPackage;
}
- public @ParsingPackageUtils.ParseFlags int getParseFlags() {
+ @ParsingPackageUtils.ParseFlags
+ public int getParseFlags() {
return mParseFlags;
}
- public @PackageManagerService.ScanFlags int getScanFlags() {
+ @PackageManagerService.ScanFlags
+ public int getScanFlags() {
return mScanFlags;
}
@@ -435,6 +439,11 @@
return mIsInstallForUsers;
}
+ public boolean isInstallFromAdb() {
+ return mInstallArgs != null
+ && (mInstallArgs.mInstallFlags & PackageManager.INSTALL_FROM_ADB) != 0;
+ }
+
@Nullable
public PackageSetting getOriginalPackageSetting() {
return mOriginalPs;
@@ -731,10 +740,10 @@
}
}
- public void onInstallCompleted() {
+ public void onInstallCompleted(Computer snapshot) {
if (getReturnCode() == INSTALL_SUCCEEDED) {
if (mPackageMetrics != null) {
- mPackageMetrics.onInstallSucceed();
+ mPackageMetrics.onInstallSucceed(snapshot);
}
}
}
diff --git a/services/core/java/com/android/server/pm/InstallingSession.java b/services/core/java/com/android/server/pm/InstallingSession.java
index d13822a..e4a0a3a 100644
--- a/services/core/java/com/android/server/pm/InstallingSession.java
+++ b/services/core/java/com/android/server/pm/InstallingSession.java
@@ -510,7 +510,7 @@
mInstallPackageHelper.installPackagesTraced(installRequests);
for (InstallRequest request : installRequests) {
- request.onInstallCompleted();
+ request.onInstallCompleted(mPm.snapshotComputer());
doPostInstall(request);
}
}
diff --git a/services/core/java/com/android/server/pm/PackageMetrics.java b/services/core/java/com/android/server/pm/PackageMetrics.java
index b725325..0391163 100644
--- a/services/core/java/com/android/server/pm/PackageMetrics.java
+++ b/services/core/java/com/android/server/pm/PackageMetrics.java
@@ -16,16 +16,26 @@
package com.android.server.pm;
+import static android.os.Process.INVALID_UID;
+
import android.annotation.IntDef;
+import android.content.pm.parsing.ApkLiteParseUtils;
import android.os.UserManager;
import android.util.Pair;
import android.util.SparseArray;
import com.android.internal.util.FrameworkStatsLog;
+import com.android.server.pm.pkg.PackageStateInternal;
+import java.io.File;
+import java.io.IOException;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
+import java.nio.file.Files;
+import java.nio.file.Path;
import java.util.ArrayList;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.stream.Stream;
/**
* Metrics class for reporting stats to logging infrastructures like Westworld
@@ -43,7 +53,8 @@
STEP_COMMIT,
})
@Retention(RetentionPolicy.SOURCE)
- public @interface StepInt {}
+ public @interface StepInt {
+ }
private final long mInstallStartTimestampMillis;
private final SparseArray<InstallStep> mInstallSteps = new SparseArray<>();
@@ -56,16 +67,30 @@
mInstallRequest = installRequest;
}
- public void onInstallSucceed() {
+ public void onInstallSucceed(Computer snapshot) {
+ // TODO(b/239722919): report to SecurityLog if on work profile or managed device
+ reportInstallationStats(snapshot, true /* success */);
+ }
+
+ private void reportInstallationStats(Computer snapshot, boolean success) {
+ // TODO(b/249294752): do not log if adb
final long installDurationMillis =
System.currentTimeMillis() - mInstallStartTimestampMillis;
// write to stats
final Pair<int[], long[]> stepDurations = getInstallStepDurations();
final int[] newUsers = mInstallRequest.getNewUsers();
final int[] originalUsers = mInstallRequest.getOriginUsers();
+ final String packageName = mInstallRequest.getName();
+ final String installerPackageName = mInstallRequest.getInstallerPackageName();
+ final int installerUid = installerPackageName == null ? INVALID_UID
+ : snapshot.getPackageUid(installerPackageName, 0, 0);
+ final PackageStateInternal ps = snapshot.getPackageStateInternal(packageName);
+ final long versionCode = success ? 0 : ps.getVersionCode();
+ final long apksSize = getApksSize(ps.getPath());
+
FrameworkStatsLog.write(FrameworkStatsLog.PACKAGE_INSTALLATION_SESSION_REPORTED,
0 /* session_id */,
- null /* package_name */,
+ success ? null : packageName /* not report package_name on success */,
mInstallRequest.getUid() /* uid */,
newUsers /* user_ids */,
getUserTypes(newUsers) /* user_types */,
@@ -73,13 +98,13 @@
getUserTypes(originalUsers) /* original_user_types */,
mInstallRequest.getReturnCode() /* public_return_code */,
0 /* internal_error_code */,
- 0 /* apks_size_bytes */,
- 0 /* version_code */,
+ apksSize /* apks_size_bytes */,
+ versionCode /* version_code */,
stepDurations.first /* install_steps */,
stepDurations.second /* step_duration_millis */,
installDurationMillis /* total_duration_millis */,
mInstallRequest.getInstallFlags() /* install_flags */,
- -1 /* installer_package_uid */,
+ installerUid /* installer_package_uid */,
-1 /* original_installer_package_uid */,
mInstallRequest.getDataLoaderType() /* data_loader_type */,
0 /* user_action_required_type */,
@@ -93,6 +118,19 @@
);
}
+ private long getApksSize(File apkDir) {
+ // TODO(b/249294752): also count apk sizes for failed installs
+ final AtomicLong apksSize = new AtomicLong();
+ try (Stream<Path> walkStream = Files.walk(apkDir.toPath())) {
+ walkStream.filter(p -> p.toFile().isFile()
+ && ApkLiteParseUtils.isApkFile(p.toFile())).forEach(
+ f -> apksSize.addAndGet(f.toFile().length()));
+ } catch (IOException e) {
+ // ignore
+ }
+ return apksSize.get();
+ }
+
public void onStepStarted(@StepInt int step) {
mInstallSteps.put(step, new InstallStep());
}
@@ -140,12 +178,15 @@
private static class InstallStep {
private final long mStartTimestampMillis;
private long mDurationMillis = -1;
+
InstallStep() {
mStartTimestampMillis = System.currentTimeMillis();
}
+
void finish() {
mDurationMillis = System.currentTimeMillis() - mStartTimestampMillis;
}
+
long getDurationMillis() {
return mDurationMillis;
}
diff --git a/services/core/java/com/android/server/pm/ReconcilePackageUtils.java b/services/core/java/com/android/server/pm/ReconcilePackageUtils.java
index 01d17f6..99bcbc9 100644
--- a/services/core/java/com/android/server/pm/ReconcilePackageUtils.java
+++ b/services/core/java/com/android/server/pm/ReconcilePackageUtils.java
@@ -35,6 +35,7 @@
import com.android.server.pm.pkg.parsing.ParsingPackageUtils;
import com.android.server.utils.WatchedLongSparseArray;
+import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@@ -48,14 +49,14 @@
* as install) led to the request.
*/
final class ReconcilePackageUtils {
- public static Map<String, ReconciledPackage> reconcilePackages(
+ public static List<ReconciledPackage> reconcilePackages(
List<InstallRequest> installRequests,
Map<String, AndroidPackage> allPackages,
Map<String, Settings.VersionInfo> versionInfos,
SharedLibrariesImpl sharedLibraries,
KeySetManagerService ksms, Settings settings)
throws ReconcileFailure {
- final Map<String, ReconciledPackage> result = new ArrayMap<>(installRequests.size());
+ final List<ReconciledPackage> result = new ArrayList<>(installRequests.size());
// make a copy of the existing set of packages so we can combine them with incoming packages
final ArrayMap<String, AndroidPackage> combinedPackages =
@@ -88,7 +89,6 @@
}
-
final DeletePackageAction deletePackageAction;
// we only want to try to delete for non system apps
if (installRequest.isInstallReplace() && !installRequest.isInstallSystem()) {
@@ -257,13 +257,11 @@
}
}
- result.put(installPackageName,
+ final ReconciledPackage reconciledPackage =
new ReconciledPackage(installRequests, allPackages, installRequest,
deletePackageAction, allowedSharedLibInfos, signingDetails,
- sharedUserSignaturesChanged, removeAppKeySetData));
- }
+ sharedUserSignaturesChanged, removeAppKeySetData);
- for (InstallRequest installRequest : installRequests) {
// Check all shared libraries and map to their actual file path.
// We only do this here for apps not on a system dir, because those
// are the only ones that can fail an install due to this. We
@@ -271,24 +269,21 @@
// library paths after the scan is done. Also during the initial
// scan don't update any libs as we do this wholesale after all
// apps are scanned to avoid dependency based scanning.
- if ((installRequest.getScanFlags() & SCAN_BOOTING) != 0
- || (installRequest.getParseFlags() & ParsingPackageUtils.PARSE_IS_SYSTEM_DIR)
- != 0) {
- continue;
+ if ((installRequest.getScanFlags() & SCAN_BOOTING) == 0
+ && (installRequest.getParseFlags() & ParsingPackageUtils.PARSE_IS_SYSTEM_DIR)
+ == 0) {
+ try {
+ reconciledPackage.mCollectedSharedLibraryInfos =
+ sharedLibraries.collectSharedLibraryInfos(
+ installRequest.getParsedPackage(), combinedPackages,
+ incomingSharedLibraries);
+ } catch (PackageManagerException e) {
+ throw new ReconcileFailure(e.error, e.getMessage());
+ }
}
- final String installPackageName = installRequest.getParsedPackage().getPackageName();
- try {
- result.get(installPackageName).mCollectedSharedLibraryInfos =
- sharedLibraries.collectSharedLibraryInfos(
- installRequest.getParsedPackage(), combinedPackages,
- incomingSharedLibraries);
- } catch (PackageManagerException e) {
- throw new ReconcileFailure(e.error, e.getMessage());
- }
- }
- for (InstallRequest installRequest : installRequests) {
installRequest.onReconcileFinished();
+ result.add(reconciledPackage);
}
return result;
diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java
index d255669..1c50f38 100644
--- a/services/core/java/com/android/server/pm/UserManagerService.java
+++ b/services/core/java/com/android/server/pm/UserManagerService.java
@@ -18,6 +18,7 @@
import static android.content.Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS;
import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
+import static android.os.UserManager.DEV_CREATE_OVERRIDE_PROPERTY;
import static android.os.UserManager.DISALLOW_USER_SWITCH;
import static android.os.UserManager.SYSTEM_USER_MODE_EMULATION_PROPERTY;
@@ -2814,7 +2815,8 @@
synchronized (mUsersLock) {
count = getAliveUsersExcludingGuestsCountLU();
}
- return count >= UserManager.getMaxSupportedUsers();
+ return count >= UserManager.getMaxSupportedUsers()
+ && !isCreationOverrideEnabled();
}
/**
@@ -2824,15 +2826,16 @@
* <p>For checking whether more profiles can be added to a particular parent use
* {@link #canAddMoreProfilesToUser}.
*/
- private boolean canAddMoreUsersOfType(UserTypeDetails userTypeDetails) {
- if (!userTypeDetails.isEnabled()) {
+ private boolean canAddMoreUsersOfType(@NonNull UserTypeDetails userTypeDetails) {
+ if (!isUserTypeEnabled(userTypeDetails)) {
return false;
}
final int max = userTypeDetails.getMaxAllowed();
if (max == UserTypeDetails.UNLIMITED_NUMBER_OF_USERS) {
return true; // Indicates that there is no max.
}
- return getNumberOfUsersOfType(userTypeDetails.getName()) < max;
+ return getNumberOfUsersOfType(userTypeDetails.getName()) < max
+ || isCreationOverrideEnabled();
}
/**
@@ -2843,7 +2846,7 @@
public int getRemainingCreatableUserCount(String userType) {
checkQueryOrCreateUsersPermission("get the remaining number of users that can be added.");
final UserTypeDetails type = mUserTypes.get(userType);
- if (type == null || !type.isEnabled()) {
+ if (type == null || !isUserTypeEnabled(type)) {
return 0;
}
synchronized (mUsersLock) {
@@ -2917,7 +2920,21 @@
public boolean isUserTypeEnabled(String userType) {
checkCreateUsersPermission("check if user type is enabled.");
final UserTypeDetails userTypeDetails = mUserTypes.get(userType);
- return userTypeDetails != null && userTypeDetails.isEnabled();
+ return userTypeDetails != null && isUserTypeEnabled(userTypeDetails);
+ }
+
+ /** Returns whether the creation of users of the given user type is enabled on this device. */
+ private boolean isUserTypeEnabled(@NonNull UserTypeDetails userTypeDetails) {
+ return userTypeDetails.isEnabled() || isCreationOverrideEnabled();
+ }
+
+ /**
+ * Returns whether to almost-always allow creating users even beyond their limit or if disabled.
+ * For Debug builds only.
+ */
+ private boolean isCreationOverrideEnabled() {
+ return Build.isDebuggable()
+ && SystemProperties.getBoolean(DEV_CREATE_OVERRIDE_PROPERTY, false);
}
@Override
@@ -2930,7 +2947,8 @@
@Override
public boolean canAddMoreProfilesToUser(String userType, @UserIdInt int userId,
boolean allowedToRemoveOne) {
- return 0 < getRemainingCreatableProfileCount(userType, userId, allowedToRemoveOne);
+ return 0 < getRemainingCreatableProfileCount(userType, userId, allowedToRemoveOne)
+ || isCreationOverrideEnabled();
}
@Override
@@ -2948,7 +2966,7 @@
checkQueryOrCreateUsersPermission(
"get the remaining number of profiles that can be added to the given user.");
final UserTypeDetails type = mUserTypes.get(userType);
- if (type == null || !type.isEnabled()) {
+ if (type == null || !isUserTypeEnabled(type)) {
return 0;
}
// Managed profiles have their own specific rules.
@@ -4407,7 +4425,7 @@
+ ") indicated SYSTEM user, which cannot be created.");
return null;
}
- if (!userTypeDetails.isEnabled()) {
+ if (!isUserTypeEnabled(userTypeDetails)) {
throwCheckedUserOperationException(
"Cannot add a user of disabled type " + userType + ".",
UserManager.USER_OPERATION_ERROR_MAX_USERS);
@@ -4479,27 +4497,12 @@
+ " for user " + parentId,
UserManager.USER_OPERATION_ERROR_MAX_USERS);
}
- // In legacy mode, restricted profile's parent can only be the owner user
- if (isRestricted && !UserManager.isSplitSystemUser()
- && (parentId != UserHandle.USER_SYSTEM)) {
+ if (isRestricted && (parentId != UserHandle.USER_SYSTEM)
+ && !isCreationOverrideEnabled()) {
throwCheckedUserOperationException(
- "Cannot add restricted profile - parent user must be owner",
+ "Cannot add restricted profile - parent user must be system",
UserManager.USER_OPERATION_ERROR_UNKNOWN);
}
- if (isRestricted && UserManager.isSplitSystemUser()) {
- if (parent == null) {
- throwCheckedUserOperationException(
- "Cannot add restricted profile - parent user must be specified",
- UserManager.USER_OPERATION_ERROR_UNKNOWN);
- }
- if (!parent.info.canHaveProfile()) {
- throwCheckedUserOperationException(
- "Cannot add restricted profile - profiles cannot be created for "
- + "the specified parent user id "
- + parentId,
- UserManager.USER_OPERATION_ERROR_UNKNOWN);
- }
- }
userId = getNextAvailableId();
Slog.i(LOG_TAG, "Creating user " + userId + " of type " + userType);
diff --git a/services/core/java/com/android/server/pm/UserTypeFactory.java b/services/core/java/com/android/server/pm/UserTypeFactory.java
index c35fe17..871420a9 100644
--- a/services/core/java/com/android/server/pm/UserTypeFactory.java
+++ b/services/core/java/com/android/server/pm/UserTypeFactory.java
@@ -126,11 +126,13 @@
.setCrossProfileIntentFilterAccessControl(
CrossProfileIntentFilter.ACCESS_LEVEL_SYSTEM)
.setIsCredentialSharableWithParent(true)
+ .setDefaultCrossProfileIntentFilters(getDefaultCloneCrossProfileIntentFilter())
.setDefaultUserProperties(new UserProperties.Builder()
.setStartWithParent(true)
.setShowInLauncher(UserProperties.SHOW_IN_LAUNCHER_WITH_PARENT)
.setShowInSettings(UserProperties.SHOW_IN_SETTINGS_WITH_PARENT)
- .setInheritDevicePolicy(UserProperties.INHERIT_DEVICE_POLICY_FROM_PARENT));
+ .setInheritDevicePolicy(UserProperties.INHERIT_DEVICE_POLICY_FROM_PARENT)
+ .setUseParentsContacts(true));
}
/**
@@ -310,6 +312,10 @@
return DefaultCrossProfileIntentFiltersUtils.getDefaultManagedProfileFilters();
}
+ private static List<DefaultCrossProfileIntentFilter> getDefaultCloneCrossProfileIntentFilter() {
+ return DefaultCrossProfileIntentFiltersUtils.getDefaultCloneProfileFilters();
+ }
+
/**
* Reads the given xml parser to obtain device user-type customization, and updates the given
* map of {@link UserTypeDetails.Builder}s accordingly.
@@ -391,6 +397,7 @@
}
setIntAttribute(parser, "enabled", builder::setEnabled);
+ setIntAttribute(parser, "max-allowed", builder::setMaxAllowed);
// Process child elements.
final int depth = parser.getDepth();
diff --git a/services/core/java/com/android/server/pm/permission/DefaultPermissionGrantPolicy.java b/services/core/java/com/android/server/pm/permission/DefaultPermissionGrantPolicy.java
index 8588267..28f86ed 100644
--- a/services/core/java/com/android/server/pm/permission/DefaultPermissionGrantPolicy.java
+++ b/services/core/java/com/android/server/pm/permission/DefaultPermissionGrantPolicy.java
@@ -616,6 +616,10 @@
grantPermissionsToSystemPackage(pm, getDefaultCaptivePortalLoginPackage(), userId,
NOTIFICATION_PERMISSIONS);
+ // Dock Manager
+ grantPermissionsToSystemPackage(pm, getDefaultDockManagerPackage(), userId,
+ NOTIFICATION_PERMISSIONS);
+
// Camera
grantPermissionsToSystemPackage(pm,
getDefaultSystemHandlerActivityPackage(pm, MediaStore.ACTION_IMAGE_CAPTURE, userId),
@@ -933,6 +937,10 @@
return mContext.getString(R.string.config_defaultCaptivePortalLoginPackageName);
}
+ private String getDefaultDockManagerPackage() {
+ return mContext.getString(R.string.config_defaultDockManagerPackageName);
+ }
+
@SafeVarargs
private final void grantPermissionToEachSystemPackage(PackageManagerWrapper pm,
ArrayList<String> packages, int userId, Set<String>... permissions) {
diff --git a/services/core/java/com/android/server/power/PowerManagerService.java b/services/core/java/com/android/server/power/PowerManagerService.java
index d8b1120..cc84c85 100644
--- a/services/core/java/com/android/server/power/PowerManagerService.java
+++ b/services/core/java/com/android/server/power/PowerManagerService.java
@@ -1053,7 +1053,7 @@
super(context);
mContext = context;
- mBinderService = new BinderService();
+ mBinderService = new BinderService(mContext);
mLocalService = new LocalService();
mNativeWrapper = injector.createNativeWrapper();
mSystemProperties = injector.createSystemPropertiesWrapper();
@@ -5485,12 +5485,17 @@
@VisibleForTesting
final class BinderService extends IPowerManager.Stub {
+ private final PowerManagerShellCommand mShellCommand;
+
+ BinderService(Context context) {
+ mShellCommand = new PowerManagerShellCommand(context, this);
+ }
+
@Override
public void onShellCommand(FileDescriptor in, FileDescriptor out,
FileDescriptor err, String[] args, ShellCallback callback,
ResultReceiver resultReceiver) {
- (new PowerManagerShellCommand(this)).exec(
- this, in, out, err, args, callback, resultReceiver);
+ mShellCommand.exec(this, in, out, err, args, callback, resultReceiver);
}
@Override // Binder call
diff --git a/services/core/java/com/android/server/power/PowerManagerShellCommand.java b/services/core/java/com/android/server/power/PowerManagerShellCommand.java
index a9b33ed..9439b76 100644
--- a/services/core/java/com/android/server/power/PowerManagerShellCommand.java
+++ b/services/core/java/com/android/server/power/PowerManagerShellCommand.java
@@ -16,10 +16,15 @@
package com.android.server.power;
+import android.content.Context;
import android.content.Intent;
+import android.os.PowerManager;
+import android.os.PowerManager.WakeLock;
import android.os.PowerManagerInternal;
import android.os.RemoteException;
import android.os.ShellCommand;
+import android.util.SparseArray;
+import android.view.Display;
import java.io.PrintWriter;
import java.util.List;
@@ -27,9 +32,13 @@
class PowerManagerShellCommand extends ShellCommand {
private static final int LOW_POWER_MODE_ON = 1;
- final PowerManagerService.BinderService mService;
+ private final Context mContext;
+ private final PowerManagerService.BinderService mService;
- PowerManagerShellCommand(PowerManagerService.BinderService service) {
+ private SparseArray<WakeLock> mProxWakelocks = new SparseArray<>();
+
+ PowerManagerShellCommand(Context context, PowerManagerService.BinderService service) {
+ mContext = context;
mService = service;
}
@@ -52,6 +61,8 @@
return runSuppressAmbientDisplay();
case "list-ambient-display-suppression-tokens":
return runListAmbientDisplaySuppressionTokens();
+ case "set-prox":
+ return runSetProx();
default:
return handleDefaultCommands(cmd);
}
@@ -117,6 +128,56 @@
return 0;
}
+
+ /** TODO: Consider updating this code to support all wakelock types. */
+ private int runSetProx() throws RemoteException {
+ PrintWriter pw = getOutPrintWriter();
+ final boolean acquire;
+ switch (getNextArgRequired().toLowerCase()) {
+ case "list":
+ pw.println("Wakelocks:");
+ pw.println(mProxWakelocks);
+ return 0;
+ case "acquire":
+ acquire = true;
+ break;
+ case "release":
+ acquire = false;
+ break;
+ default:
+ pw.println("Error: Allowed options are 'list' 'enable' and 'disable'.");
+ return -1;
+ }
+
+ int displayId = Display.INVALID_DISPLAY;
+ String displayOption = getNextArg();
+ if ("-d".equals(displayOption)) {
+ String idStr = getNextArg();
+ displayId = Integer.parseInt(idStr);
+ if (displayId < 0) {
+ pw.println("Error: Specified displayId (" + idStr + ") must a non-negative int.");
+ return -1;
+ }
+ }
+
+ int wakelockIndex = displayId + 1; // SparseArray doesn't support negative indexes
+ WakeLock wakelock = mProxWakelocks.get(wakelockIndex);
+ if (wakelock == null) {
+ PowerManager pm = mContext.getSystemService(PowerManager.class);
+ wakelock = pm.newWakeLock(PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK,
+ "PowerManagerShellCommand[" + displayId + "]", displayId);
+ mProxWakelocks.put(wakelockIndex, wakelock);
+ }
+
+ if (acquire) {
+ wakelock.acquire();
+ } else {
+ wakelock.release();
+ }
+ pw.println(wakelock);
+ return 0;
+ }
+
@Override
public void onHelp() {
final PrintWriter pw = getOutPrintWriter();
@@ -138,6 +199,11 @@
pw.println(" ambient display");
pw.println(" list-ambient-display-suppression-tokens");
pw.println(" prints the tokens used to suppress ambient display");
+ pw.println(" set-prox [list|acquire|release] (-d <display_id>)");
+ pw.println(" Acquires the proximity sensor wakelock. Wakelock is associated with");
+ pw.println(" a specific display if specified. 'list' lists wakelocks previously");
+ pw.println(" created by set-prox including their held status.");
+
pw.println();
Intent.printIntentArgsHelp(pw , "");
}
diff --git a/services/core/java/com/android/server/wm/ActivityClientController.java b/services/core/java/com/android/server/wm/ActivityClientController.java
index 7386a19..3b7bf48 100644
--- a/services/core/java/com/android/server/wm/ActivityClientController.java
+++ b/services/core/java/com/android/server/wm/ActivityClientController.java
@@ -1211,7 +1211,8 @@
synchronized (mGlobalLock) {
final ActivityRecord r = ActivityRecord.isInRootTaskLocked(token);
if (r != null) {
- r.reportFullyDrawnLocked(restoredFromBundle);
+ mTaskSupervisor.getActivityMetricsLogger().notifyFullyDrawn(r,
+ restoredFromBundle);
}
}
} finally {
diff --git a/services/core/java/com/android/server/wm/ActivityMetricsLogger.java b/services/core/java/com/android/server/wm/ActivityMetricsLogger.java
index f0de1d3..e1ab291 100644
--- a/services/core/java/com/android/server/wm/ActivityMetricsLogger.java
+++ b/services/core/java/com/android/server/wm/ActivityMetricsLogger.java
@@ -454,6 +454,7 @@
final int windowsFullyDrawnDelayMs;
final int activityRecordIdHashCode;
final boolean relaunched;
+ final long timestampNs;
private TransitionInfoSnapshot(TransitionInfo info) {
this(info, info.mLastLaunchedActivity, INVALID_DELAY);
@@ -483,6 +484,7 @@
activityRecordIdHashCode = System.identityHashCode(launchedActivity);
this.windowsFullyDrawnDelayMs = windowsFullyDrawnDelayMs;
relaunched = info.mRelaunched;
+ timestampNs = info.mLaunchingState.mStartRealtimeNs;
}
@WaitResult.LaunchState int getLaunchState() {
@@ -498,6 +500,10 @@
}
}
+ boolean isIntresetedToEventLog() {
+ return type == TYPE_TRANSITION_WARM_LAUNCH || type == TYPE_TRANSITION_COLD_LAUNCH;
+ }
+
PackageOptimizationInfo getPackageOptimizationInfo(ArtManagerInternal artManagerInternal) {
return artManagerInternal == null || launchedActivityAppRecordRequiredAbi == null
? PackageOptimizationInfo.createWithNoInfo()
@@ -1022,16 +1028,17 @@
// This will avoid any races with other operations that modify the ActivityRecord.
final TransitionInfoSnapshot infoSnapshot = new TransitionInfoSnapshot(info);
if (info.isInterestingToLoggerAndObserver()) {
- final long timestampNs = info.mLaunchingState.mStartRealtimeNs;
final long uptimeNs = info.mLaunchingState.mStartUptimeNs;
final int transitionDelay = info.mCurrentTransitionDelayMs;
final int processState = info.mProcessState;
final int processOomAdj = info.mProcessOomAdj;
mLoggerHandler.post(() -> logAppTransition(
- timestampNs, uptimeNs, transitionDelay, infoSnapshot, isHibernating,
+ uptimeNs, transitionDelay, infoSnapshot, isHibernating,
processState, processOomAdj));
}
- mLoggerHandler.post(() -> logAppDisplayed(infoSnapshot));
+ if (infoSnapshot.isIntresetedToEventLog()) {
+ mLoggerHandler.post(() -> logAppDisplayed(infoSnapshot));
+ }
if (info.mPendingFullyDrawn != null) {
info.mPendingFullyDrawn.run();
}
@@ -1040,7 +1047,7 @@
}
// This gets called on another thread without holding the activity manager lock.
- private void logAppTransition(long transitionStartTimeNs, long transitionDeviceUptimeNs,
+ private void logAppTransition(long transitionDeviceUptimeNs,
int currentTransitionDelayMs, TransitionInfoSnapshot info, boolean isHibernating,
int processState, int processOomAdj) {
final LogMaker builder = new LogMaker(APP_TRANSITION);
@@ -1108,7 +1115,7 @@
isIncremental,
isLoading,
info.launchedActivityName.hashCode(),
- TimeUnit.NANOSECONDS.toMillis(transitionStartTimeNs),
+ TimeUnit.NANOSECONDS.toMillis(info.timestampNs),
processState,
processOomAdj);
@@ -1132,10 +1139,6 @@
}
private void logAppDisplayed(TransitionInfoSnapshot info) {
- if (info.type != TYPE_TRANSITION_WARM_LAUNCH && info.type != TYPE_TRANSITION_COLD_LAUNCH) {
- return;
- }
-
EventLog.writeEvent(WM_ACTIVITY_LAUNCH_TIME,
info.userId, info.activityRecordIdHashCode, info.launchedActivityShortComponentName,
info.windowsDrawnDelayMs);
@@ -1181,8 +1184,7 @@
}
/** @see android.app.Activity#reportFullyDrawn */
- TransitionInfoSnapshot logAppTransitionReportedDrawn(ActivityRecord r,
- boolean restoredFromBundle) {
+ TransitionInfoSnapshot notifyFullyDrawn(ActivityRecord r, boolean restoredFromBundle) {
final TransitionInfo info = mLastTransitionInfo.get(r);
if (info == null) {
return null;
@@ -1191,7 +1193,7 @@
// There are still undrawn activities, postpone reporting fully drawn until all of its
// windows are drawn. So that is closer to an usable state.
info.mPendingFullyDrawn = () -> {
- logAppTransitionReportedDrawn(r, restoredFromBundle);
+ notifyFullyDrawn(r, restoredFromBundle);
info.mPendingFullyDrawn = null;
};
return null;
@@ -1204,7 +1206,9 @@
currentTimestampNs - info.mLaunchingState.mStartUptimeNs);
final TransitionInfoSnapshot infoSnapshot =
new TransitionInfoSnapshot(info, r, (int) startupTimeMs);
- mLoggerHandler.post(() -> logAppFullyDrawn(infoSnapshot));
+ if (infoSnapshot.isIntresetedToEventLog()) {
+ mLoggerHandler.post(() -> logAppFullyDrawn(infoSnapshot));
+ }
mLastTransitionInfo.remove(r);
if (!info.isInterestingToLoggerAndObserver()) {
@@ -1216,46 +1220,8 @@
// fullfils (handling reportFullyDrawn() callbacks).
Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER,
"ActivityManager:ReportingFullyDrawn " + info.mLastLaunchedActivity.packageName);
-
- final LogMaker builder = new LogMaker(APP_TRANSITION_REPORTED_DRAWN);
- builder.setPackageName(r.packageName);
- builder.addTaggedData(FIELD_CLASS_NAME, r.info.name);
- builder.addTaggedData(APP_TRANSITION_REPORTED_DRAWN_MS, startupTimeMs);
- builder.setType(restoredFromBundle
- ? TYPE_TRANSITION_REPORTED_DRAWN_WITH_BUNDLE
- : TYPE_TRANSITION_REPORTED_DRAWN_NO_BUNDLE);
- builder.addTaggedData(APP_TRANSITION_PROCESS_RUNNING,
- info.mProcessRunning ? 1 : 0);
- mMetricsLogger.write(builder);
- final PackageOptimizationInfo packageOptimizationInfo =
- infoSnapshot.getPackageOptimizationInfo(getArtManagerInternal());
- // Incremental info
- boolean isIncremental = false, isLoading = false;
- final String codePath = info.mLastLaunchedActivity.info.applicationInfo.getCodePath();
- if (codePath != null && IncrementalManager.isIncrementalPath(codePath)) {
- isIncremental = true;
- isLoading = isIncrementalLoading(info.mLastLaunchedActivity.packageName,
- info.mLastLaunchedActivity.mUserId);
- }
- FrameworkStatsLog.write(
- FrameworkStatsLog.APP_START_FULLY_DRAWN,
- info.mLastLaunchedActivity.info.applicationInfo.uid,
- info.mLastLaunchedActivity.packageName,
- restoredFromBundle
- ? FrameworkStatsLog.APP_START_FULLY_DRAWN__TYPE__WITH_BUNDLE
- : FrameworkStatsLog.APP_START_FULLY_DRAWN__TYPE__WITHOUT_BUNDLE,
- info.mLastLaunchedActivity.info.name,
- info.mProcessRunning,
- startupTimeMs,
- packageOptimizationInfo.getCompilationReason(),
- packageOptimizationInfo.getCompilationFilter(),
- info.mSourceType,
- info.mSourceEventDelayMs,
- isIncremental,
- isLoading,
- info.mLastLaunchedActivity.info.name.hashCode(),
- TimeUnit.NANOSECONDS.toMillis(info.mLaunchingState.mStartRealtimeNs));
-
+ mLoggerHandler.post(() -> logAppFullyDrawnMetrics(infoSnapshot, restoredFromBundle,
+ info.mProcessRunning));
// Ends the trace started at the beginning of this function. This is located here to allow
// the trace slice to have a noticable duration.
Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
@@ -1266,11 +1232,48 @@
return infoSnapshot;
}
- private void logAppFullyDrawn(TransitionInfoSnapshot info) {
- if (info.type != TYPE_TRANSITION_WARM_LAUNCH && info.type != TYPE_TRANSITION_COLD_LAUNCH) {
- return;
+ private void logAppFullyDrawnMetrics(TransitionInfoSnapshot info, boolean restoredFromBundle,
+ boolean processRunning) {
+ final LogMaker builder = new LogMaker(APP_TRANSITION_REPORTED_DRAWN);
+ builder.setPackageName(info.packageName);
+ builder.addTaggedData(FIELD_CLASS_NAME, info.launchedActivityName);
+ builder.addTaggedData(APP_TRANSITION_REPORTED_DRAWN_MS,
+ (long) info.windowsFullyDrawnDelayMs);
+ builder.setType(restoredFromBundle
+ ? TYPE_TRANSITION_REPORTED_DRAWN_WITH_BUNDLE
+ : TYPE_TRANSITION_REPORTED_DRAWN_NO_BUNDLE);
+ builder.addTaggedData(APP_TRANSITION_PROCESS_RUNNING, processRunning ? 1 : 0);
+ mMetricsLogger.write(builder);
+ final PackageOptimizationInfo packageOptimizationInfo =
+ info.getPackageOptimizationInfo(getArtManagerInternal());
+ // Incremental info
+ boolean isIncremental = false, isLoading = false;
+ final String codePath = info.applicationInfo.getCodePath();
+ if (codePath != null && IncrementalManager.isIncrementalPath(codePath)) {
+ isIncremental = true;
+ isLoading = isIncrementalLoading(info.packageName, info.userId);
}
+ FrameworkStatsLog.write(
+ FrameworkStatsLog.APP_START_FULLY_DRAWN,
+ info.applicationInfo.uid,
+ info.packageName,
+ restoredFromBundle
+ ? FrameworkStatsLog.APP_START_FULLY_DRAWN__TYPE__WITH_BUNDLE
+ : FrameworkStatsLog.APP_START_FULLY_DRAWN__TYPE__WITHOUT_BUNDLE,
+ info.launchedActivityName,
+ processRunning,
+ info.windowsFullyDrawnDelayMs,
+ packageOptimizationInfo.getCompilationReason(),
+ packageOptimizationInfo.getCompilationFilter(),
+ info.sourceType,
+ info.sourceEventDelayMs,
+ isIncremental,
+ isLoading,
+ info.launchedActivityName.hashCode(),
+ TimeUnit.NANOSECONDS.toMillis(info.timestampNs));
+ }
+ private void logAppFullyDrawn(TransitionInfoSnapshot info) {
StringBuilder sb = mStringBuilder;
sb.setLength(0);
sb.append("Fully drawn ");
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index e5401f5..8f8b57f 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -3131,8 +3131,8 @@
}
// Check to see if PiP is supported for the display this container is on.
- if (mDisplayContent != null && !mDisplayContent.mDwpcHelper.isWindowingModeSupported(
- WINDOWING_MODE_PINNED)) {
+ if (mDisplayContent != null && !mDisplayContent.mDwpcHelper.isEnteringPipAllowed(
+ getUid())) {
Slog.w(TAG, "Display " + mDisplayContent.getDisplayId()
+ " doesn't support enter picture-in-picture mode. caller = " + caller);
return false;
@@ -6497,15 +6497,6 @@
}
}
- void reportFullyDrawnLocked(boolean restoredFromBundle) {
- final TransitionInfoSnapshot info = mTaskSupervisor
- .getActivityMetricsLogger().logAppTransitionReportedDrawn(this, restoredFromBundle);
- if (info != null) {
- mTaskSupervisor.reportActivityLaunched(false /* timeout */, this,
- info.windowsFullyDrawnDelayMs, info.getLaunchState());
- }
- }
-
void onFirstWindowDrawn(WindowState win) {
firstWindowDrawn = true;
// stop tracking
@@ -6908,11 +6899,8 @@
* {@link android.view.Display#INVALID_DISPLAY} if not attached.
*/
int getDisplayId() {
- final Task rootTask = getRootTask();
- if (rootTask == null) {
- return INVALID_DISPLAY;
- }
- return rootTask.getDisplayId();
+ return task != null && task.mDisplayContent != null
+ ? task.mDisplayContent.mDisplayId : INVALID_DISPLAY;
}
final boolean isDestroyable() {
diff --git a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
index 3c457e1..2f70eda 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
@@ -27,6 +27,7 @@
import static android.app.ActivityManager.START_FLAG_NATIVE_DEBUGGING;
import static android.app.ActivityManager.START_FLAG_TRACK_ALLOCATION;
import static android.app.ActivityManager.START_TASK_TO_FRONT;
+import static android.app.ActivityOptions.ANIM_REMOTE_ANIMATION;
import static android.app.ITaskStackListener.FORCED_RESIZEABLE_REASON_SECONDARY_DISPLAY;
import static android.app.ITaskStackListener.FORCED_RESIZEABLE_REASON_SPLIT_SCREEN;
import static android.app.WaitResult.INVALID_DELAY;
@@ -2577,6 +2578,10 @@
// Apply options to prevent pendingOptions be taken when scheduling
// activity lifecycle transaction to make sure the override pending app
// transition will be applied immediately.
+ if (activityOptions.getAnimationType() == ANIM_REMOTE_ANIMATION) {
+ targetActivity.mPendingRemoteAnimation =
+ activityOptions.getRemoteAnimationAdapter();
+ }
targetActivity.applyOptionsAnimation();
if (activityOptions != null && activityOptions.getLaunchCookie() != null) {
targetActivity.mLaunchCookie = activityOptions.getLaunchCookie();
diff --git a/services/core/java/com/android/server/wm/DisplayArea.java b/services/core/java/com/android/server/wm/DisplayArea.java
index b84b2d8..bedeabe 100644
--- a/services/core/java/com/android/server/wm/DisplayArea.java
+++ b/services/core/java/com/android/server/wm/DisplayArea.java
@@ -369,6 +369,55 @@
}
@Override
+ ActivityRecord getActivity(Predicate<ActivityRecord> callback, boolean traverseTopToBottom,
+ ActivityRecord boundary) {
+ if (mType == Type.ABOVE_TASKS || mType == Type.BELOW_TASKS) {
+ return null;
+ }
+ return super.getActivity(callback, traverseTopToBottom, boundary);
+ }
+
+ @Override
+ Task getTask(Predicate<Task> callback, boolean traverseTopToBottom) {
+ if (mType == Type.ABOVE_TASKS || mType == Type.BELOW_TASKS) {
+ return null;
+ }
+ return super.getTask(callback, traverseTopToBottom);
+ }
+
+ @Override
+ boolean forAllActivities(Predicate<ActivityRecord> callback, boolean traverseTopToBottom) {
+ if (mType == Type.ABOVE_TASKS || mType == Type.BELOW_TASKS) {
+ return false;
+ }
+ return super.forAllActivities(callback, traverseTopToBottom);
+ }
+
+ @Override
+ boolean forAllRootTasks(Predicate<Task> callback, boolean traverseTopToBottom) {
+ if (mType == Type.ABOVE_TASKS || mType == Type.BELOW_TASKS) {
+ return false;
+ }
+ return super.forAllRootTasks(callback, traverseTopToBottom);
+ }
+
+ @Override
+ boolean forAllTasks(Predicate<Task> callback) {
+ if (mType == Type.ABOVE_TASKS || mType == Type.BELOW_TASKS) {
+ return false;
+ }
+ return super.forAllTasks(callback);
+ }
+
+ @Override
+ boolean forAllLeafTasks(Predicate<Task> callback) {
+ if (mType == Type.ABOVE_TASKS || mType == Type.BELOW_TASKS) {
+ return false;
+ }
+ return super.forAllLeafTasks(callback);
+ }
+
+ @Override
void forAllDisplayAreas(Consumer<DisplayArea> callback) {
super.forAllDisplayAreas(callback);
callback.accept(this);
diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java
index 8b34443..12efe0d 100644
--- a/services/core/java/com/android/server/wm/DisplayContent.java
+++ b/services/core/java/com/android/server/wm/DisplayContent.java
@@ -230,6 +230,7 @@
import android.view.WindowManager;
import android.view.WindowManager.DisplayImePolicy;
import android.view.WindowManagerPolicyConstants.PointerEventListener;
+import android.view.inputmethod.ImeTracker;
import android.window.DisplayWindowPolicyController;
import android.window.IDisplayAreaOrganizer;
import android.window.ScreenCapture;
@@ -454,7 +455,7 @@
/**
* Compat metrics computed based on {@link #mDisplayMetrics}.
- * @see #updateDisplayAndOrientation(int, Configuration)
+ * @see #updateDisplayAndOrientation(Configuration)
*/
private final DisplayMetrics mCompatDisplayMetrics = new DisplayMetrics();
@@ -5031,7 +5032,7 @@
* layer has been assigned since), to facilitate assigning the layer from the IME target, or
* fall back if there is no target.
* - the container doesn't always participate in window traversal, according to
- * {@link #skipImeWindowsDuringTraversal()}
+ * {@link #skipImeWindowsDuringTraversal(DisplayContent)}
*/
private static class ImeContainer extends DisplayArea.Tokens {
boolean mNeedsLayer = false;
@@ -6712,25 +6713,35 @@
mRemoteInsetsController.insetsControlChanged(stateController.getRawInsetsState(),
stateController.getControlsForDispatch(this));
} catch (RemoteException e) {
- Slog.w(TAG, "Failed to deliver inset state change", e);
+ Slog.w(TAG, "Failed to deliver inset control state change", e);
}
}
@Override
- public void showInsets(@WindowInsets.Type.InsetsType int types, boolean fromIme) {
+ public void showInsets(@WindowInsets.Type.InsetsType int types, boolean fromIme,
+ @Nullable ImeTracker.Token statsToken) {
try {
- mRemoteInsetsController.showInsets(types, fromIme);
+ ImeTracker.get().onProgress(statsToken,
+ ImeTracker.PHASE_WM_REMOTE_INSETS_CONTROL_TARGET_SHOW_INSETS);
+ mRemoteInsetsController.showInsets(types, fromIme, statsToken);
} catch (RemoteException e) {
Slog.w(TAG, "Failed to deliver showInsets", e);
+ ImeTracker.get().onFailed(statsToken,
+ ImeTracker.PHASE_WM_REMOTE_INSETS_CONTROL_TARGET_SHOW_INSETS);
}
}
@Override
- public void hideInsets(@InsetsType int types, boolean fromIme) {
+ public void hideInsets(@InsetsType int types, boolean fromIme,
+ @Nullable ImeTracker.Token statsToken) {
try {
- mRemoteInsetsController.hideInsets(types, fromIme);
+ ImeTracker.get().onProgress(statsToken,
+ ImeTracker.PHASE_WM_REMOTE_INSETS_CONTROL_TARGET_HIDE_INSETS);
+ mRemoteInsetsController.hideInsets(types, fromIme, statsToken);
} catch (RemoteException e) {
- Slog.w(TAG, "Failed to deliver showInsets", e);
+ Slog.w(TAG, "Failed to deliver hideInsets", e);
+ ImeTracker.get().onFailed(statsToken,
+ ImeTracker.PHASE_WM_REMOTE_INSETS_CONTROL_TARGET_HIDE_INSETS);
}
}
diff --git a/services/core/java/com/android/server/wm/DisplayPolicy.java b/services/core/java/com/android/server/wm/DisplayPolicy.java
index 2589148..1fef3c2 100644
--- a/services/core/java/com/android/server/wm/DisplayPolicy.java
+++ b/services/core/java/com/android/server/wm/DisplayPolicy.java
@@ -2071,7 +2071,8 @@
// Don't show status bar when swiping on already visible navigation bar.
// But restore the position of navigation bar if it has been moved by the control
// target.
- controlTarget.showInsets(Type.navigationBars(), false);
+ controlTarget.showInsets(Type.navigationBars(), false /* fromIme */,
+ null /* statsToken */);
return;
}
@@ -2079,10 +2080,12 @@
// Show transient bars if they are hidden; restore position if they are visible.
mDisplayContent.getInsetsPolicy().showTransient(SHOW_TYPES_FOR_SWIPE,
isGestureOnSystemBar);
- controlTarget.showInsets(restorePositionTypes, false);
+ controlTarget.showInsets(restorePositionTypes, false /* fromIme */,
+ null /* statsToken */);
} else {
// Restore visibilities and positions of system bars.
- controlTarget.showInsets(Type.statusBars() | Type.navigationBars(), false);
+ controlTarget.showInsets(Type.statusBars() | Type.navigationBars(),
+ false /* fromIme */, null /* statsToken */);
// To further allow the pull-down-from-the-top gesture to pull down the notification
// shade as a consistent motion, we reroute the touch events here from the currently
// touched window to the status bar after making it visible.
diff --git a/services/core/java/com/android/server/wm/DisplayWindowPolicyControllerHelper.java b/services/core/java/com/android/server/wm/DisplayWindowPolicyControllerHelper.java
index 5d49042..6f821b5 100644
--- a/services/core/java/com/android/server/wm/DisplayWindowPolicyControllerHelper.java
+++ b/services/core/java/com/android/server/wm/DisplayWindowPolicyControllerHelper.java
@@ -162,6 +162,17 @@
return mDisplayWindowPolicyController.canShowTasksInRecents();
}
+ /**
+ * @see DisplayWindowPolicyController#isEnteringPipAllowed(int)
+ */
+ public final boolean isEnteringPipAllowed(int uid) {
+ if (mDisplayWindowPolicyController == null) {
+ return true;
+ }
+ return mDisplayWindowPolicyController.isEnteringPipAllowed(uid);
+ }
+
+
void dump(String prefix, PrintWriter pw) {
if (mDisplayWindowPolicyController != null) {
pw.println();
diff --git a/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java b/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java
index 554791a..7fd093f 100644
--- a/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java
+++ b/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java
@@ -33,9 +33,11 @@
import android.os.Trace;
import android.util.proto.ProtoOutputStream;
import android.view.InsetsSource;
+import android.view.InsetsSourceConsumer;
import android.view.InsetsSourceControl;
import android.view.InsetsState;
import android.view.WindowInsets;
+import android.view.inputmethod.ImeTracker;
import android.window.TaskSnapshot;
import com.android.internal.annotations.VisibleForTesting;
@@ -49,6 +51,9 @@
*/
final class ImeInsetsSourceProvider extends WindowContainerInsetsSourceProvider {
+ /** The token tracking the current IME request or {@code null} otherwise. */
+ @Nullable
+ private ImeTracker.Token mImeRequesterStatsToken;
private InsetsControlTarget mImeRequester;
private Runnable mShowImeRunner;
private boolean mIsImeLayoutDrawn;
@@ -162,14 +167,20 @@
}
/**
- * Called from {@link WindowManagerInternal#showImePostLayout} when {@link InputMethodService}
- * requests to show IME on {@param imeTarget}.
+ * Called from {@link WindowManagerInternal#showImePostLayout}
+ * when {@link android.inputmethodservice.InputMethodService} requests to show IME
+ * on {@param imeTarget}.
*
- * @param imeTarget imeTarget on which IME request is coming from.
+ * @param imeTarget imeTarget on which IME show request is coming from.
+ * @param statsToken the token tracking the current IME show request or {@code null} otherwise.
*/
- void scheduleShowImePostLayout(InsetsControlTarget imeTarget) {
+ void scheduleShowImePostLayout(InsetsControlTarget imeTarget,
+ @Nullable ImeTracker.Token statsToken) {
boolean targetChanged = isTargetChangedWithinActivity(imeTarget);
mImeRequester = imeTarget;
+ // There was still a stats token, so that request presumably failed.
+ ImeTracker.get().onFailed(mImeRequesterStatsToken, ImeTracker.PHASE_WM_SHOW_IME_RUNNER);
+ mImeRequesterStatsToken = statsToken;
if (targetChanged) {
// target changed, check if new target can show IME.
ProtoLog.d(WM_DEBUG_IME, "IME target changed within ActivityRecord");
@@ -183,15 +194,20 @@
ProtoLog.d(WM_DEBUG_IME, "Schedule IME show for %s", mImeRequester.getWindow() == null
? mImeRequester : mImeRequester.getWindow().getName());
mShowImeRunner = () -> {
+ ImeTracker.get().onProgress(mImeRequesterStatsToken,
+ ImeTracker.PHASE_WM_SHOW_IME_RUNNER);
ProtoLog.d(WM_DEBUG_IME, "Run showImeRunner");
// Target should still be the same.
if (isReadyToShowIme()) {
+ ImeTracker.get().onProgress(mImeRequesterStatsToken,
+ ImeTracker.PHASE_WM_SHOW_IME_READY);
final InsetsControlTarget target = mDisplayContent.getImeTarget(IME_TARGET_CONTROL);
ProtoLog.i(WM_DEBUG_IME, "call showInsets(ime) on %s",
target.getWindow() != null ? target.getWindow().getName() : "");
setImeShowing(true);
- target.showInsets(WindowInsets.Type.ime(), true /* fromIme */);
+ target.showInsets(WindowInsets.Type.ime(), true /* fromIme */,
+ mImeRequesterStatsToken);
Trace.asyncTraceEnd(TRACE_TAG_WINDOW_MANAGER, "WMS.showImePostLayout", 0);
if (target != mImeRequester && mImeRequester != null) {
ProtoLog.w(WM_DEBUG_IME,
@@ -199,7 +215,12 @@
(mImeRequester.getWindow() != null
? mImeRequester.getWindow().getName() : ""));
}
+ } else {
+ ImeTracker.get().onFailed(mImeRequesterStatsToken,
+ ImeTracker.PHASE_WM_SHOW_IME_READY);
}
+ // Clear token here so we don't report an error in abortShowImePostLayout().
+ mImeRequesterStatsToken = null;
abortShowImePostLayout();
};
mDisplayContent.mWmService.requestTraversal();
@@ -234,6 +255,8 @@
mImeRequester = null;
mIsImeLayoutDrawn = false;
mShowImeRunner = null;
+ ImeTracker.get().onCancelled(mImeRequesterStatsToken, ImeTracker.PHASE_WM_SHOW_IME_RUNNER);
+ mImeRequesterStatsToken = null;
}
@VisibleForTesting
diff --git a/services/core/java/com/android/server/wm/InsetsControlTarget.java b/services/core/java/com/android/server/wm/InsetsControlTarget.java
index d35b7c3..8ecbc17 100644
--- a/services/core/java/com/android/server/wm/InsetsControlTarget.java
+++ b/services/core/java/com/android/server/wm/InsetsControlTarget.java
@@ -16,9 +16,11 @@
package com.android.server.wm;
+import android.annotation.Nullable;
import android.inputmethodservice.InputMethodService;
import android.view.WindowInsets;
import android.view.WindowInsets.Type.InsetsType;
+import android.view.inputmethod.ImeTracker;
/**
* Generalization of an object that can control insets state.
@@ -57,8 +59,10 @@
*
* @param types to specify which types of insets source window should be shown.
* @param fromIme {@code true} if IME show request originated from {@link InputMethodService}.
+ * @param statsToken the token tracking the current IME show request or {@code null} otherwise.
*/
- default void showInsets(@InsetsType int types, boolean fromIme) {
+ default void showInsets(@InsetsType int types, boolean fromIme,
+ @Nullable ImeTracker.Token statsToken) {
}
/**
@@ -66,8 +70,10 @@
*
* @param types to specify which types of insets source window should be hidden.
* @param fromIme {@code true} if IME hide request originated from {@link InputMethodService}.
+ * @param statsToken the token tracking the current IME hide request or {@code null} otherwise.
*/
- default void hideInsets(@InsetsType int types, boolean fromIme) {
+ default void hideInsets(@InsetsType int types, boolean fromIme,
+ @Nullable ImeTracker.Token statsToken) {
}
/**
diff --git a/services/core/java/com/android/server/wm/InsetsPolicy.java b/services/core/java/com/android/server/wm/InsetsPolicy.java
index b9fa80c..f66fa0f 100644
--- a/services/core/java/com/android/server/wm/InsetsPolicy.java
+++ b/services/core/java/com/android/server/wm/InsetsPolicy.java
@@ -799,7 +799,7 @@
show ? ANIMATION_TYPE_SHOW : ANIMATION_TYPE_HIDE, show
? LAYOUT_INSETS_DURING_ANIMATION_SHOWN
: LAYOUT_INSETS_DURING_ANIMATION_HIDDEN,
- null /* translator */);
+ null /* translator */, null /* statsToken */);
SurfaceAnimationThread.getHandler().post(
() -> mListener.onReady(mAnimationControl, typesReady));
}
diff --git a/services/core/java/com/android/server/wm/WindowManagerInternal.java b/services/core/java/com/android/server/wm/WindowManagerInternal.java
index c206a15..bab3a05 100644
--- a/services/core/java/com/android/server/wm/WindowManagerInternal.java
+++ b/services/core/java/com/android/server/wm/WindowManagerInternal.java
@@ -43,6 +43,7 @@
import android.view.SurfaceControlViewHost;
import android.view.WindowInfo;
import android.view.WindowManager.DisplayImePolicy;
+import android.view.inputmethod.ImeTracker;
import com.android.internal.policy.KeyInterceptionInfo;
import com.android.server.input.InputManagerService;
@@ -729,16 +730,20 @@
* Show IME on imeTargetWindow once IME has finished layout.
*
* @param imeTargetWindowToken token of the (IME target) window on which IME should be shown.
+ * @param statsToken the token tracking the current IME show request or {@code null} otherwise.
*/
- public abstract void showImePostLayout(IBinder imeTargetWindowToken);
+ public abstract void showImePostLayout(IBinder imeTargetWindowToken,
+ @Nullable ImeTracker.Token statsToken);
/**
* Hide IME using imeTargetWindow when requested.
*
* @param imeTargetWindowToken token of the (IME target) window on which IME should be hidden.
* @param displayId the id of the display the IME is on.
+ * @param statsToken the token tracking the current IME hide request or {@code null} otherwise.
*/
- public abstract void hideIme(IBinder imeTargetWindowToken, int displayId);
+ public abstract void hideIme(IBinder imeTargetWindowToken, int displayId,
+ @Nullable ImeTracker.Token statsToken);
/**
* Tell window manager about a package that should be running with a restricted range of
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index 4080223..3419207 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -292,6 +292,7 @@
import android.view.WindowManagerPolicyConstants.PointerEventListener;
import android.view.displayhash.DisplayHash;
import android.view.displayhash.VerifiedDisplayHash;
+import android.view.inputmethod.ImeTracker;
import android.window.ClientWindowFrames;
import android.window.ITaskFpsCallback;
import android.window.ScreenCapture;
@@ -8014,7 +8015,8 @@
}
@Override
- public void showImePostLayout(IBinder imeTargetWindowToken) {
+ public void showImePostLayout(IBinder imeTargetWindowToken,
+ @Nullable ImeTracker.Token statsToken) {
synchronized (mGlobalLock) {
InputTarget imeTarget = getInputTargetFromWindowTokenLocked(imeTargetWindowToken);
if (imeTarget == null) {
@@ -8023,17 +8025,18 @@
Trace.asyncTraceBegin(TRACE_TAG_WINDOW_MANAGER, "WMS.showImePostLayout", 0);
final InsetsControlTarget controlTarget = imeTarget.getImeControlTarget();
imeTarget = controlTarget.getWindow();
- // If InsetsControlTarget doesn't have a window, its using remoteControlTarget which
- // is controlled by default display
+ // If InsetsControlTarget doesn't have a window, it's using remoteControlTarget
+ // which is controlled by default display
final DisplayContent dc = imeTarget != null
? imeTarget.getDisplayContent() : getDefaultDisplayContentLocked();
dc.getInsetsStateController().getImeSourceProvider()
- .scheduleShowImePostLayout(controlTarget);
+ .scheduleShowImePostLayout(controlTarget, statsToken);
}
}
@Override
- public void hideIme(IBinder imeTargetWindowToken, int displayId) {
+ public void hideIme(IBinder imeTargetWindowToken, int displayId,
+ @Nullable ImeTracker.Token statsToken) {
Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "WMS.hideIme");
synchronized (mGlobalLock) {
WindowState imeTarget = mWindowMap.get(imeTargetWindowToken);
@@ -8049,10 +8052,15 @@
dc.getInsetsStateController().getImeSourceProvider().abortShowImePostLayout();
}
if (dc != null && dc.getImeTarget(IME_TARGET_CONTROL) != null) {
+ ImeTracker.get().onProgress(statsToken,
+ ImeTracker.PHASE_WM_HAS_IME_INSETS_CONTROL_TARGET);
ProtoLog.d(WM_DEBUG_IME, "hideIme Control target: %s ",
dc.getImeTarget(IME_TARGET_CONTROL));
- dc.getImeTarget(IME_TARGET_CONTROL).hideInsets(
- WindowInsets.Type.ime(), true /* fromIme */);
+ dc.getImeTarget(IME_TARGET_CONTROL).hideInsets(WindowInsets.Type.ime(),
+ true /* fromIme */, statsToken);
+ } else {
+ ImeTracker.get().onFailed(statsToken,
+ ImeTracker.PHASE_WM_HAS_IME_INSETS_CONTROL_TARGET);
}
if (dc != null) {
dc.getInsetsStateController().getImeSourceProvider().setImeShowing(false);
diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java
index 2712cb7..5c5c703 100644
--- a/services/core/java/com/android/server/wm/WindowState.java
+++ b/services/core/java/com/android/server/wm/WindowState.java
@@ -249,6 +249,7 @@
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.view.animation.Interpolator;
+import android.view.inputmethod.ImeTracker;
import android.window.ClientWindowFrames;
import android.window.OnBackInvokedCallbackInfo;
@@ -4018,7 +4019,7 @@
mClient.insetsControlChanged(getCompatInsetsState(),
stateController.getControlsForDispatch(this));
} catch (RemoteException e) {
- Slog.w(TAG, "Failed to deliver inset state change to w=" + this, e);
+ Slog.w(TAG, "Failed to deliver inset control state change to w=" + this, e);
}
}
@@ -4028,20 +4029,30 @@
}
@Override
- public void showInsets(@InsetsType int types, boolean fromIme) {
+ public void showInsets(@InsetsType int types, boolean fromIme,
+ @Nullable ImeTracker.Token statsToken) {
try {
- mClient.showInsets(types, fromIme);
+ ImeTracker.get().onProgress(statsToken,
+ ImeTracker.PHASE_WM_WINDOW_INSETS_CONTROL_TARGET_SHOW_INSETS);
+ mClient.showInsets(types, fromIme, statsToken);
} catch (RemoteException e) {
Slog.w(TAG, "Failed to deliver showInsets", e);
+ ImeTracker.get().onFailed(statsToken,
+ ImeTracker.PHASE_WM_WINDOW_INSETS_CONTROL_TARGET_SHOW_INSETS);
}
}
@Override
- public void hideInsets(@InsetsType int types, boolean fromIme) {
+ public void hideInsets(@InsetsType int types, boolean fromIme,
+ @Nullable ImeTracker.Token statsToken) {
try {
- mClient.hideInsets(types, fromIme);
+ ImeTracker.get().onProgress(statsToken,
+ ImeTracker.PHASE_WM_WINDOW_INSETS_CONTROL_TARGET_HIDE_INSETS);
+ mClient.hideInsets(types, fromIme, statsToken);
} catch (RemoteException e) {
- Slog.w(TAG, "Failed to deliver showInsets", e);
+ Slog.w(TAG, "Failed to deliver hideInsets", e);
+ ImeTracker.get().onFailed(statsToken,
+ ImeTracker.PHASE_WM_WINDOW_INSETS_CONTROL_TARGET_HIDE_INSETS);
}
}
diff --git a/services/core/jni/com_android_server_input_InputManagerService.cpp b/services/core/jni/com_android_server_input_InputManagerService.cpp
index 3197124..2030347 100644
--- a/services/core/jni/com_android_server_input_InputManagerService.cpp
+++ b/services/core/jni/com_android_server_input_InputManagerService.cpp
@@ -260,10 +260,9 @@
// --- NativeInputManager ---
-class NativeInputManager : public virtual RefBase,
- public virtual InputReaderPolicyInterface,
- public virtual InputDispatcherPolicyInterface,
- public virtual PointerControllerPolicyInterface {
+class NativeInputManager : public virtual InputReaderPolicyInterface,
+ public virtual InputDispatcherPolicyInterface,
+ public virtual PointerControllerPolicyInterface {
protected:
virtual ~NativeInputManager();
@@ -1520,8 +1519,14 @@
return 0;
}
- NativeInputManager* im = new NativeInputManager(serviceObj, messageQueue->getLooper());
- im->incStrong(0);
+ static std::once_flag nativeInitialize;
+ NativeInputManager* im = nullptr;
+ std::call_once(nativeInitialize, [&]() {
+ // Create the NativeInputManager, which should not be destroyed or deallocated for the
+ // lifetime of the process.
+ im = new NativeInputManager(serviceObj, messageQueue->getLooper());
+ });
+ LOG_ALWAYS_FATAL_IF(im == nullptr, "NativeInputManager was already initialized.");
return reinterpret_cast<jlong>(im);
}
diff --git a/services/core/jni/com_android_server_location_GnssLocationProvider.cpp b/services/core/jni/com_android_server_location_GnssLocationProvider.cpp
index 9fa23c2..e1de05c 100644
--- a/services/core/jni/com_android_server_location_GnssLocationProvider.cpp
+++ b/services/core/jni/com_android_server_location_GnssLocationProvider.cpp
@@ -482,6 +482,17 @@
agnssRilIface->setSetId(type, setid_string);
}
+static void android_location_gnss_hal_GnssNative_inject_ni_supl_message_data(JNIEnv* env, jclass,
+ jbyteArray data,
+ jint length,
+ jint slotIndex) {
+ if (agnssRilIface == nullptr) {
+ ALOGE("%s: IAGnssRil interface not available.", __func__);
+ return;
+ }
+ agnssRilIface->injectNiSuplMessageData(data, length, slotIndex);
+}
+
static jint android_location_gnss_hal_GnssNative_read_nmea(JNIEnv* env, jclass,
jbyteArray nmeaArray, jint buffer_size) {
return gnssHal->readNmea(nmeaArray, buffer_size);
@@ -974,6 +985,8 @@
android_location_gnss_hal_GnssNative_agps_set_reference_location_cellid)},
{"native_set_agps_server", "(ILjava/lang/String;I)V",
reinterpret_cast<void*>(android_location_gnss_hal_GnssNative_set_agps_server)},
+ {"native_inject_ni_supl_message_data", "([BII)V",
+ reinterpret_cast<void*>(android_location_gnss_hal_GnssNative_inject_ni_supl_message_data)},
{"native_send_ni_response", "(II)V",
reinterpret_cast<void*>(android_location_gnss_hal_GnssNative_send_ni_response)},
{"native_get_internal_state", "()Ljava/lang/String;",
diff --git a/services/core/jni/gnss/AGnssRil.cpp b/services/core/jni/gnss/AGnssRil.cpp
index 34e4976..c7a1af7 100644
--- a/services/core/jni/gnss/AGnssRil.cpp
+++ b/services/core/jni/gnss/AGnssRil.cpp
@@ -84,8 +84,19 @@
networkAttributes.capabilities = static_cast<int32_t>(capabilities),
networkAttributes.apn = jniApn.c_str();
- auto result = mIAGnssRil->updateNetworkState(networkAttributes);
- return checkAidlStatus(result, "IAGnssRilAidl updateNetworkState() failed.");
+ auto status = mIAGnssRil->updateNetworkState(networkAttributes);
+ return checkAidlStatus(status, "IAGnssRilAidl updateNetworkState() failed.");
+}
+
+jboolean AGnssRil::injectNiSuplMessageData(const jbyteArray& msgData, jint length, jint slotIndex) {
+ JNIEnv* env = getJniEnv();
+ jbyte* bytes = reinterpret_cast<jbyte*>(env->GetPrimitiveArrayCritical(msgData, 0));
+ auto status = mIAGnssRil->injectNiSuplMessageData(std::vector<uint8_t>((const uint8_t*)bytes,
+ (const uint8_t*)bytes +
+ length),
+ static_cast<int>(slotIndex));
+ env->ReleasePrimitiveArrayCritical(msgData, bytes, JNI_ABORT);
+ return checkAidlStatus(status, "IAGnssRil injectNiSuplMessageData() failed.");
}
// Implementation of AGnssRil_V1_0
@@ -151,6 +162,11 @@
return checkHidlReturn(result, "IAGnssRil_V1_0 updateNetworkState() failed.");
}
+jboolean AGnssRil_V1_0::injectNiSuplMessageData(const jbyteArray&, jint, jint) {
+ ALOGI("IAGnssRil_V1_0 interface does not support injectNiSuplMessageData.");
+ return JNI_FALSE;
+}
+
// Implementation of AGnssRil_V2_0
AGnssRil_V2_0::AGnssRil_V2_0(const sp<IAGnssRil_V2_0>& iAGnssRil)
diff --git a/services/core/jni/gnss/AGnssRil.h b/services/core/jni/gnss/AGnssRil.h
index ce14a77d..b7e0282 100644
--- a/services/core/jni/gnss/AGnssRil.h
+++ b/services/core/jni/gnss/AGnssRil.h
@@ -43,6 +43,8 @@
virtual jboolean updateNetworkState(jboolean connected, jint type, jboolean roaming,
jboolean available, const jstring& apn, jlong networkHandle,
jshort capabilities) = 0;
+ virtual jboolean injectNiSuplMessageData(const jbyteArray& msgData, jint length,
+ jint slotIndex) = 0;
};
class AGnssRil : public AGnssRilInterface {
@@ -55,6 +57,8 @@
jboolean updateNetworkState(jboolean connected, jint type, jboolean roaming, jboolean available,
const jstring& apn, jlong networkHandle,
jshort capabilities) override;
+ jboolean injectNiSuplMessageData(const jbyteArray& msgData, jint length,
+ jint slotIndex) override;
private:
const sp<android::hardware::gnss::IAGnssRil> mIAGnssRil;
@@ -70,6 +74,7 @@
jboolean updateNetworkState(jboolean connected, jint type, jboolean roaming, jboolean available,
const jstring& apn, jlong networkHandle,
jshort capabilities) override;
+ jboolean injectNiSuplMessageData(const jbyteArray&, jint, jint) override;
private:
const sp<android::hardware::gnss::V1_0::IAGnssRil> mAGnssRil_V1_0;
diff --git a/services/credentials/java/com/android/server/credentials/CreateRequestSession.java b/services/credentials/java/com/android/server/credentials/CreateRequestSession.java
new file mode 100644
index 0000000..e07bc77
--- /dev/null
+++ b/services/credentials/java/com/android/server/credentials/CreateRequestSession.java
@@ -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.server.credentials;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.credentials.CreateCredentialRequest;
+import android.credentials.CredentialManager;
+import android.credentials.ICreateCredentialCallback;
+import android.credentials.ui.ProviderData;
+import android.credentials.ui.RequestInfo;
+import android.service.credentials.CredentialProviderInfo;
+import android.util.Log;
+
+import java.util.ArrayList;
+
+/**
+ * Central session for a single {@link CredentialManager#executeCreateCredential} request.
+ * This class listens to the responses from providers, and the UX app, and updates the
+ * provider(s) state maintained in {@link ProviderCreateSession}.
+ */
+public final class CreateRequestSession extends RequestSession<CreateCredentialRequest,
+ ICreateCredentialCallback> {
+ private static final String TAG = "CreateRequestSession";
+
+ CreateRequestSession(@NonNull Context context, int userId,
+ CreateCredentialRequest request,
+ ICreateCredentialCallback callback,
+ String callingPackage) {
+ super(context, userId, request, callback, RequestInfo.TYPE_CREATE, callingPackage);
+ }
+
+ /**
+ * Creates a new provider session, and adds it to list of providers that are contributing to
+ * this request session.
+ *
+ * @return the provider session that was started
+ */
+ @Override
+ @Nullable
+ public ProviderSession initiateProviderSession(CredentialProviderInfo providerInfo,
+ RemoteCredentialService remoteCredentialService) {
+ ProviderCreateSession providerCreateSession = ProviderCreateSession
+ .createNewSession(mContext, mUserId, providerInfo,
+ this, remoteCredentialService);
+ if (providerCreateSession != null) {
+ Log.i(TAG, "In startProviderSession - provider session created and being added");
+ mProviders.put(providerCreateSession.getComponentName().flattenToString(),
+ providerCreateSession);
+ }
+ return providerCreateSession;
+ }
+
+ @Override
+ protected void launchUiWithProviderData(ArrayList<ProviderData> providerDataList) {
+ mHandler.post(() -> mCredentialManagerUi.show(RequestInfo.newCreateRequestInfo(
+ mRequestId, mClientRequest, mIsFirstUiTurn, mClientCallingPackage),
+ providerDataList));
+ }
+}
diff --git a/services/credentials/java/com/android/server/credentials/CredentialManagerService.java b/services/credentials/java/com/android/server/credentials/CredentialManagerService.java
index 321f022..374da1c 100644
--- a/services/credentials/java/com/android/server/credentials/CredentialManagerService.java
+++ b/services/credentials/java/com/android/server/credentials/CredentialManagerService.java
@@ -23,6 +23,7 @@
import android.content.Context;
import android.content.pm.PackageManager;
import android.credentials.CreateCredentialRequest;
+import android.credentials.GetCredentialOption;
import android.credentials.GetCredentialRequest;
import android.credentials.IClearCredentialSessionCallback;
import android.credentials.ICreateCredentialCallback;
@@ -33,6 +34,7 @@
import android.os.ICancellationSignal;
import android.os.UserHandle;
import android.provider.Settings;
+import android.service.credentials.GetCredentialsRequest;
import android.text.TextUtils;
import android.util.Log;
import android.util.Slog;
@@ -43,6 +45,7 @@
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
+import java.util.stream.Collectors;
/**
* Entry point service for credential management.
@@ -91,17 +94,15 @@
return new ArrayList<>();
}
List<CredentialManagerServiceImpl> serviceList = new ArrayList<>(serviceNames.length);
- for (int i = 0; i < serviceNames.length; i++) {
- Log.i(TAG, "in newServiceListLocked, service: " + serviceNames[i]);
- if (TextUtils.isEmpty(serviceNames[i])) {
+ for (String serviceName : serviceNames) {
+ Log.i(TAG, "in newServiceListLocked, service: " + serviceName);
+ if (TextUtils.isEmpty(serviceName)) {
continue;
}
try {
serviceList.add(new CredentialManagerServiceImpl(this, mLock, resolvedUserId,
- serviceNames[i]));
- } catch (PackageManager.NameNotFoundException e) {
- Log.i(TAG, "Unable to add serviceInfo : " + e.getMessage());
- } catch (SecurityException e) {
+ serviceName));
+ } catch (PackageManager.NameNotFoundException | SecurityException e) {
Log.i(TAG, "Unable to add serviceInfo : " + e.getMessage());
}
}
@@ -115,15 +116,31 @@
synchronized (mLock) {
final List<CredentialManagerServiceImpl> services =
getServiceListForUserLocked(userId);
- services.forEach(s -> {
+ for (CredentialManagerServiceImpl s : services) {
c.accept(s);
- });
+ }
}
} finally {
Binder.restoreCallingIdentity(origId);
}
}
+ private List<ProviderSession> initiateProviderSessions(RequestSession session,
+ List<String> requestOptions) {
+ List<ProviderSession> providerSessions = new ArrayList<>();
+ // Invoke all services of a user to initiate a provider session
+ runForUser((service) -> {
+ if (service.isServiceCapable(requestOptions)) {
+ ProviderSession providerSession = service
+ .initiateProviderSessionForRequest(session);
+ if (providerSession != null) {
+ providerSessions.add(providerSession);
+ }
+ }
+ });
+ return providerSessions;
+ }
+
final class CredentialManagerServiceStub extends ICredentialManager.Stub {
@Override
public ICancellationSignal executeGetCredential(
@@ -137,11 +154,22 @@
// New request session, scoped for this request only.
final GetRequestSession session = new GetRequestSession(getContext(),
UserHandle.getCallingUserId(),
- callback);
+ callback,
+ request,
+ callingPackage);
- // Invoke all services of a user
- runForUser((service) -> {
- service.getCredential(request, session, callingPackage);
+ // Initiate all provider sessions
+ List<ProviderSession> providerSessions =
+ initiateProviderSessions(session, request.getGetCredentialOptions()
+ .stream().map(GetCredentialOption::getType)
+ .collect(Collectors.toList()));
+ // TODO : Return error when no providers available
+
+ // Iterate over all provider sessions and invoke the request
+ providerSessions.forEach(providerGetSession -> {
+ providerGetSession.getRemoteCredentialService().onGetCredentials(
+ (GetCredentialsRequest) providerGetSession.getProviderRequest(),
+ /*callback=*/providerGetSession);
});
return cancelTransport;
}
@@ -151,9 +179,29 @@
CreateCredentialRequest request,
ICreateCredentialCallback callback,
String callingPackage) {
- // TODO: implement.
- Log.i(TAG, "executeCreateCredential");
+ Log.i(TAG, "starting executeCreateCredential with callingPackage: " + callingPackage);
+ // TODO : Implement cancellation
ICancellationSignal cancelTransport = CancellationSignal.createTransport();
+
+ // New request session, scoped for this request only.
+ final CreateRequestSession session = new CreateRequestSession(getContext(),
+ UserHandle.getCallingUserId(),
+ request,
+ callback,
+ callingPackage);
+
+ // Initiate all provider sessions
+ List<ProviderSession> providerSessions =
+ initiateProviderSessions(session, List.of(request.getType()));
+ // TODO : Return error when no providers available
+
+ // Iterate over all provider sessions and invoke the request
+ providerSessions.forEach(providerCreateSession -> {
+ providerCreateSession.getRemoteCredentialService().onCreateCredential(
+ (android.service.credentials.CreateCredentialRequest)
+ providerCreateSession.getProviderRequest(),
+ /*callback=*/providerCreateSession);
+ });
return cancelTransport;
}
diff --git a/services/credentials/java/com/android/server/credentials/CredentialManagerServiceImpl.java b/services/credentials/java/com/android/server/credentials/CredentialManagerServiceImpl.java
index cc03f9b..0c32304 100644
--- a/services/credentials/java/com/android/server/credentials/CredentialManagerServiceImpl.java
+++ b/services/credentials/java/com/android/server/credentials/CredentialManagerServiceImpl.java
@@ -21,13 +21,13 @@
import android.content.ComponentName;
import android.content.pm.PackageManager;
import android.content.pm.ServiceInfo;
-import android.credentials.GetCredentialRequest;
import android.service.credentials.CredentialProviderInfo;
-import android.service.credentials.GetCredentialsRequest;
import android.util.Slog;
import com.android.server.infra.AbstractPerUserSystemService;
+import java.util.List;
+
/**
* Per-user, per remote service implementation of {@link CredentialManagerService}
@@ -61,50 +61,38 @@
return mInfo.getServiceInfo();
}
- public void getCredential(GetCredentialRequest request, GetRequestSession requestSession,
- String callingPackage) {
- Slog.i(TAG, "in getCredential in CredManServiceImpl");
+ /**
+ * Starts a provider session and associates it with the given request session. */
+ @Nullable
+ public ProviderSession initiateProviderSessionForRequest(
+ RequestSession requestSession) {
+ Slog.i(TAG, "in initiateProviderSessionForRequest in CredManServiceImpl");
if (mInfo == null) {
- Slog.i(TAG, "in getCredential in CredManServiceImpl, but mInfo is null");
- return;
+ Slog.i(TAG, "in initiateProviderSessionForRequest in CredManServiceImpl, "
+ + "but mInfo is null. This shouldn't happen");
+ return null;
}
-
- // TODO : Determine if remoteService instance can be reused across requests
final RemoteCredentialService remoteService = new RemoteCredentialService(
getContext(), mInfo.getServiceInfo().getComponentName(), mUserId);
- ProviderGetSession providerSession = new ProviderGetSession(mInfo,
- requestSession, mUserId, remoteService);
- // Set the provider info to the session when the request is initiated. This happens here
- // because there is one serviceImpl per remote provider, and so we can only retrieve
- // the provider information in the scope of this instance, whereas the session is for the
- // entire request.
- requestSession.addProviderSession(providerSession);
- GetCredentialsRequest filteredRequest = getRequestWithValidType(request, callingPackage);
- if (filteredRequest != null) {
- remoteService.onGetCredentials(getRequestWithValidType(request, callingPackage),
- providerSession);
- }
+ ProviderSession providerSession =
+ requestSession.initiateProviderSession(mInfo, remoteService);
+ return providerSession;
}
- @Nullable
- private GetCredentialsRequest getRequestWithValidType(GetCredentialRequest request,
- String callingPackage) {
- GetCredentialsRequest.Builder builder =
- new GetCredentialsRequest.Builder(callingPackage);
- request.getGetCredentialOptions().forEach( option -> {
- if (mInfo.hasCapability(option.getType())) {
- Slog.i(TAG, "Provider can handle: " + option.getType());
- builder.addGetCredentialOption(option);
- } else {
- Slog.i(TAG, "Skipping request as provider cannot handle it");
- }
- });
-
- try {
- return builder.build();
- } catch (IllegalArgumentException | NullPointerException e) {
- Slog.i(TAG, "issue with request build: " + e.getMessage());
+ /** Return true if at least one capability found. */
+ boolean isServiceCapable(List<String> requestedOptions) {
+ if (mInfo == null) {
+ Slog.i(TAG, "in isServiceCapable, mInfo is null");
+ return false;
}
- return null;
+ for (String capability : requestedOptions) {
+ if (mInfo.hasCapability(capability)) {
+ Slog.i(TAG, "Provider can handle: " + capability);
+ return true;
+ } else {
+ Slog.i(TAG, "Provider cannot handle: " + capability);
+ }
+ }
+ return false;
}
}
diff --git a/services/credentials/java/com/android/server/credentials/CredentialManagerUi.java b/services/credentials/java/com/android/server/credentials/CredentialManagerUi.java
index dcf094f..e889594 100644
--- a/services/credentials/java/com/android/server/credentials/CredentialManagerUi.java
+++ b/services/credentials/java/com/android/server/credentials/CredentialManagerUi.java
@@ -37,6 +37,7 @@
@NonNull
private final CredentialManagerUiCallback mCallbacks;
@NonNull private final Context mContext;
+ // TODO : Use for starting the activity for this user
private final int mUserId;
@NonNull private final ResultReceiver mResultReceiver = new ResultReceiver(
new Handler(Looper.getMainLooper())) {
@@ -56,7 +57,7 @@
Slog.i(TAG, "No selection found in UI result");
}
} else if (resultCode == UserSelectionDialogResult.RESULT_CODE_DIALOG_CANCELED) {
- mCallbacks.onUiCancelation();
+ mCallbacks.onUiCancellation();
}
}
@@ -67,7 +68,7 @@
/** Called when the user makes a selection. */
void onUiSelection(UserSelectionDialogResult selection);
/** Called when the user cancels the UI. */
- void onUiCancelation();
+ void onUiCancellation();
}
public CredentialManagerUi(Context context, int userId,
CredentialManagerUiCallback callbacks) {
@@ -83,9 +84,8 @@
*/
public void show(RequestInfo requestInfo, ArrayList<ProviderData> providerDataList) {
Log.i(TAG, "In show");
- Intent intent = IntentFactory.newIntent(
- requestInfo, providerDataList,
- new ArrayList<>(), mResultReceiver);
+ Intent intent = IntentFactory.newIntent(requestInfo, providerDataList, new ArrayList<>(),
+ mResultReceiver);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
mContext.startActivity(intent);
}
diff --git a/services/credentials/java/com/android/server/credentials/GetRequestSession.java b/services/credentials/java/com/android/server/credentials/GetRequestSession.java
index 80f0fec..8238632 100644
--- a/services/credentials/java/com/android/server/credentials/GetRequestSession.java
+++ b/services/credentials/java/com/android/server/credentials/GetRequestSession.java
@@ -16,9 +16,10 @@
package com.android.server.credentials;
-import android.content.ComponentName;
+import android.annotation.Nullable;
import android.content.Context;
import android.credentials.Credential;
+import android.credentials.GetCredentialRequest;
import android.credentials.GetCredentialResponse;
import android.credentials.IGetCredentialCallback;
import android.credentials.ui.ProviderData;
@@ -26,62 +27,52 @@
import android.credentials.ui.UserSelectionDialogResult;
import android.os.RemoteException;
import android.service.credentials.CredentialEntry;
+import android.service.credentials.CredentialProviderInfo;
import android.util.Log;
-import android.util.Slog;
import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.concurrent.atomic.AtomicBoolean;
/**
* Central session for a single getCredentials request. This class listens to the
* responses from providers, and the UX app, and updates the provider(S) state.
*/
-public final class GetRequestSession extends RequestSession {
+public final class GetRequestSession extends RequestSession<GetCredentialRequest,
+ IGetCredentialCallback> {
private static final String TAG = "GetRequestSession";
- private final IGetCredentialCallback mClientCallback;
- private final Map<String, ProviderGetSession> mProviders;
-
public GetRequestSession(Context context, int userId,
- IGetCredentialCallback callback) {
- super(context, userId, RequestInfo.TYPE_GET);
- mClientCallback = callback;
- mProviders = new HashMap<>();
+ IGetCredentialCallback callback, GetCredentialRequest request,
+ String callingPackage) {
+ super(context, userId, request, callback, RequestInfo.TYPE_GET, callingPackage);
}
/**
- * Adds a new provider to the list of providers that are contributing to this session.
+ * Creates a new provider session, and adds it list of providers that are contributing to
+ * this session.
+ * @return the provider session created within this request session, for the given provider
+ * info.
*/
- public void addProviderSession(ProviderGetSession providerSession) {
- mProviders.put(providerSession.getComponentName().flattenToString(),
- providerSession);
- }
-
@Override
- public void onProviderStatusChanged(ProviderSession.Status status,
- ComponentName componentName) {
- Log.i(TAG, "in onStatusChanged");
- if (ProviderSession.isTerminatingStatus(status)) {
- Log.i(TAG, "in onStatusChanged terminating status");
-
- ProviderGetSession session = mProviders.remove(componentName.flattenToString());
- if (session != null) {
- Slog.i(TAG, "Provider session removed.");
- } else {
- Slog.i(TAG, "Provider session null, did not exist.");
- }
- } else if (ProviderSession.isCompletionStatus(status)) {
- Log.i(TAG, "in onStatusChanged isCompletionStatus status");
- onProviderResponseComplete();
+ @Nullable
+ public ProviderSession initiateProviderSession(CredentialProviderInfo providerInfo,
+ RemoteCredentialService remoteCredentialService) {
+ ProviderGetSession providerGetSession = ProviderGetSession
+ .createNewSession(mContext, mUserId, providerInfo,
+ this, remoteCredentialService);
+ if (providerGetSession != null) {
+ Log.i(TAG, "In startProviderSession - provider session created and being added");
+ mProviders.put(providerGetSession.getComponentName().flattenToString(),
+ providerGetSession);
}
+ return providerGetSession;
}
+ // TODO: Override for this method not needed once get selection logic is
+ // moved to ProviderGetSession
@Override
public void onUiSelection(UserSelectionDialogResult selection) {
String providerId = selection.getProviderId();
- ProviderGetSession providerSession = mProviders.get(providerId);
+ ProviderGetSession providerSession = (ProviderGetSession) mProviders.get(providerId);
if (providerSession != null) {
CredentialEntry credentialEntry = providerSession.getCredentialEntry(
selection.getEntrySubkey());
@@ -89,57 +80,17 @@
respondToClientAndFinish(credentialEntry.getCredential());
}
// TODO : Handle action chips and authentication selection
- return;
}
// TODO : finish session and respond to client if provider not found
}
@Override
- public void onUiCancelation() {
- // User canceled the activity
- // TODO : Send error code to client
- finishSession();
- }
-
- private void onProviderResponseComplete() {
- Log.i(TAG, "in onProviderResponseComplete");
- if (isResponseCompleteAcrossProviders()) {
- Log.i(TAG, "in onProviderResponseComplete - isResponseCompleteAcrossProviders");
- getProviderDataAndInitiateUi();
- }
- }
-
- private void getProviderDataAndInitiateUi() {
- ArrayList<ProviderData> providerDataList = new ArrayList<>();
- for (ProviderGetSession session : mProviders.values()) {
- Log.i(TAG, "preparing data for : " + session.getComponentName());
- providerDataList.add(session.prepareUiData());
- }
- if (!providerDataList.isEmpty()) {
- Log.i(TAG, "provider list not empty about to initiate ui");
- initiateUi(providerDataList);
- }
- }
-
- private void initiateUi(ArrayList<ProviderData> providerDataList) {
+ protected void launchUiWithProviderData(ArrayList<ProviderData> providerDataList) {
mHandler.post(() -> mCredentialManagerUi.show(RequestInfo.newGetRequestInfo(
mRequestId, null, mIsFirstUiTurn, ""),
providerDataList));
}
- /**
- * Iterates over all provider sessions and returns true if all have responded.
- */
- private boolean isResponseCompleteAcrossProviders() {
- AtomicBoolean isRequestComplete = new AtomicBoolean(true);
- mProviders.forEach( (packageName, session) -> {
- if (session.getStatus() != ProviderSession.Status.COMPLETE) {
- isRequestComplete.set(false);
- }
- });
- return isRequestComplete.get();
- }
-
private void respondToClientAndFinish(Credential credential) {
try {
mClientCallback.onResponse(new GetCredentialResponse(credential));
@@ -148,17 +99,4 @@
}
finishSession();
}
-
- private void finishSession() {
- clearProviderSessions();
- }
-
- private void clearProviderSessions() {
- for (ProviderGetSession session : mProviders.values()) {
- // TODO : Evaluate if we should unbind remote services here or wait for them
- // to automatically unbind when idle. Re-binding frequently also has a cost.
- //session.destroy();
- }
- mProviders.clear();
- }
}
diff --git a/services/credentials/java/com/android/server/credentials/ProviderCreateSession.java b/services/credentials/java/com/android/server/credentials/ProviderCreateSession.java
new file mode 100644
index 0000000..49c416f
--- /dev/null
+++ b/services/credentials/java/com/android/server/credentials/ProviderCreateSession.java
@@ -0,0 +1,231 @@
+/*
+ * 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.credentials;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.UserIdInt;
+import android.content.Context;
+import android.credentials.Credential;
+import android.credentials.ui.CreateCredentialProviderData;
+import android.credentials.ui.Entry;
+import android.os.Bundle;
+import android.service.credentials.CreateCredentialRequest;
+import android.service.credentials.CreateCredentialResponse;
+import android.service.credentials.CredentialProviderInfo;
+import android.service.credentials.CredentialProviderService;
+import android.service.credentials.SaveEntry;
+import android.util.Log;
+import android.util.Slog;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Central provider session that listens for provider callbacks, and maintains provider state.
+ * Will likely split this into remote response state and UI state.
+ */
+public final class ProviderCreateSession extends ProviderSession<
+ CreateCredentialRequest, CreateCredentialResponse> {
+ private static final String TAG = "ProviderCreateSession";
+
+ // Key to be used as an entry key for a save entry
+ private static final String SAVE_ENTRY_KEY = "save_entry_key";
+
+ @NonNull
+ private final Map<String, SaveEntry> mUiSaveEntries = new HashMap<>();
+ /** The complete request to be used in the second round. */
+ private final CreateCredentialRequest mCompleteRequest;
+
+ /** Creates a new provider session to be used by the request session. */
+ @Nullable public static ProviderCreateSession createNewSession(
+ Context context,
+ @UserIdInt int userId,
+ CredentialProviderInfo providerInfo,
+ CreateRequestSession createRequestSession,
+ RemoteCredentialService remoteCredentialService) {
+ CreateCredentialRequest providerRequest =
+ createProviderRequest(providerInfo.getCapabilities(),
+ createRequestSession.mClientRequest,
+ createRequestSession.mClientCallingPackage);
+ if (providerRequest != null) {
+ return new ProviderCreateSession(context, providerInfo, createRequestSession, userId,
+ remoteCredentialService, providerRequest);
+ }
+ Log.i(TAG, "Unable to create provider session");
+ return null;
+ }
+
+ @Nullable
+ private static CreateCredentialRequest createProviderRequest(List<String> providerCapabilities,
+ android.credentials.CreateCredentialRequest clientRequest,
+ String clientCallingPackage) {
+ String capability = clientRequest.getType();
+ if (providerCapabilities.contains(capability)) {
+ return new CreateCredentialRequest(clientCallingPackage, capability,
+ clientRequest.getData());
+ }
+ Log.i(TAG, "Unable to create provider request - capabilities do not match");
+ return null;
+ }
+
+ private static CreateCredentialRequest getFirstRoundRequest(CreateCredentialRequest request) {
+ // TODO: Replace with first round bundle from request when ready
+ return new CreateCredentialRequest(
+ request.getCallingPackage(),
+ request.getType(),
+ new Bundle());
+ }
+
+ private ProviderCreateSession(
+ @NonNull Context context,
+ @NonNull CredentialProviderInfo info,
+ @NonNull ProviderInternalCallback callbacks,
+ @UserIdInt int userId,
+ @NonNull RemoteCredentialService remoteCredentialService,
+ @NonNull CreateCredentialRequest request) {
+ super(context, info, getFirstRoundRequest(request), callbacks, userId,
+ remoteCredentialService);
+ // TODO : Replace with proper splitting of request
+ mCompleteRequest = request;
+ setStatus(Status.PENDING);
+ }
+
+ /** Returns the save entry maintained in state by this provider session. */
+ public SaveEntry getUiSaveEntry(String entryId) {
+ return mUiSaveEntries.get(entryId);
+ }
+
+ @Override
+ public void onProviderResponseSuccess(
+ @Nullable CreateCredentialResponse response) {
+ Log.i(TAG, "in onProviderResponseSuccess");
+ onUpdateResponse(response);
+ }
+
+ /** Called when the provider response resulted in a failure. */
+ @Override
+ public void onProviderResponseFailure(int errorCode, @Nullable CharSequence message) {
+ updateStatusAndInvokeCallback(toStatus(errorCode));
+ }
+
+ /** Called when provider service dies. */
+ @Override
+ public void onProviderServiceDied(RemoteCredentialService service) {
+ if (service.getComponentName().equals(mProviderInfo.getServiceInfo().getComponentName())) {
+ updateStatusAndInvokeCallback(Status.SERVICE_DEAD);
+ } else {
+ Slog.i(TAG, "Component names different in onProviderServiceDied - "
+ + "this should not happen");
+ }
+ }
+
+ private void onUpdateResponse(CreateCredentialResponse response) {
+ Log.i(TAG, "updateResponse with save entries");
+ mProviderResponse = response;
+ updateStatusAndInvokeCallback(Status.SAVE_ENTRIES_RECEIVED);
+ }
+
+ @Override
+ @Nullable protected CreateCredentialProviderData prepareUiData()
+ throws IllegalArgumentException {
+ Log.i(TAG, "In prepareUiData");
+ if (!ProviderSession.isUiInvokingStatus(getStatus())) {
+ Log.i(TAG, "In prepareUiData not in uiInvokingStatus");
+ return null;
+ }
+ final CreateCredentialResponse response = getProviderResponse();
+ if (response == null) {
+ Log.i(TAG, "In prepareUiData response null");
+ throw new IllegalStateException("Response must be in completion mode");
+ }
+ if (response.getSaveEntries() != null) {
+ Log.i(TAG, "In prepareUiData save entries not null");
+ return prepareUiProviderData(
+ prepareUiSaveEntries(response.getSaveEntries()),
+ null,
+ /*isDefaultProvider=*/false);
+ }
+ return null;
+ }
+
+ @Override
+ public void onProviderIntentResult(Bundle resultData) {
+ Credential credential = resultData.getParcelable(
+ CredentialProviderService.EXTRA_SAVE_CREDENTIAL,
+ Credential.class);
+ if (credential == null) {
+ Log.i(TAG, "Credential returned from intent is null");
+ return;
+ }
+ updateFinalCredentialResponse(credential);
+ }
+
+ @Override
+ public void onUiEntrySelected(String entryType, String entryKey) {
+ if (entryType.equals(SAVE_ENTRY_KEY)) {
+ SaveEntry saveEntry = mUiSaveEntries.get(entryKey);
+ if (saveEntry == null) {
+ Log.i(TAG, "Save entry not found");
+ return;
+ }
+ // TODO: Uncomment when pending intent works
+ // onSaveEntrySelected(saveEntry);
+ }
+ }
+
+ @Override
+ public void onProviderIntentCancelled() {
+ //TODO (Implement)
+ }
+
+ private List<Entry> prepareUiSaveEntries(@NonNull List<SaveEntry> saveEntries) {
+ Log.i(TAG, "in populateUiSaveEntries");
+ List<Entry> uiSaveEntries = new ArrayList<>();
+
+ // Populate the save entries
+ for (SaveEntry saveEntry : saveEntries) {
+ String entryId = generateEntryId();
+ mUiSaveEntries.put(entryId, saveEntry);
+ Log.i(TAG, "in prepareUiProviderData creating ui entry with id " + entryId);
+ uiSaveEntries.add(new Entry(SAVE_ENTRY_KEY, entryId, saveEntry.getSlice()));
+ }
+ return uiSaveEntries;
+ }
+
+ private void updateFinalCredentialResponse(@NonNull Credential credential) {
+ mFinalCredentialResponse = credential;
+ updateStatusAndInvokeCallback(Status.CREDENTIAL_RECEIVED_FROM_INTENT);
+ }
+
+ private CreateCredentialProviderData prepareUiProviderData(List<Entry> saveEntries,
+ Entry remoteEntry, boolean isDefaultProvider) {
+ return new CreateCredentialProviderData.Builder(
+ mComponentName.flattenToString())
+ .setSaveEntries(saveEntries)
+ .setIsDefaultProvider(isDefaultProvider)
+ .build();
+ }
+
+ private void onSaveEntrySelected(SaveEntry saveEntry) {
+ mProviderIntentController.setupAndInvokePendingIntent(saveEntry.getPendingIntent(),
+ mProviderRequest);
+ setStatus(Status.PENDING_INTENT_INVOKED);
+ }
+}
diff --git a/services/credentials/java/com/android/server/credentials/ProviderGetSession.java b/services/credentials/java/com/android/server/credentials/ProviderGetSession.java
index ff2107a..362d981 100644
--- a/services/credentials/java/com/android/server/credentials/ProviderGetSession.java
+++ b/services/credentials/java/com/android/server/credentials/ProviderGetSession.java
@@ -18,13 +18,16 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
-import android.app.slice.Slice;
+import android.annotation.UserIdInt;
+import android.content.Context;
+import android.credentials.GetCredentialOption;
import android.credentials.ui.Entry;
import android.credentials.ui.GetCredentialProviderData;
+import android.os.Bundle;
import android.service.credentials.Action;
import android.service.credentials.CredentialEntry;
import android.service.credentials.CredentialProviderInfo;
-import android.service.credentials.CredentialsDisplayContent;
+import android.service.credentials.GetCredentialsRequest;
import android.service.credentials.GetCredentialsResponse;
import android.util.Log;
import android.util.Slog;
@@ -38,75 +41,95 @@
/**
* Central provider session that listens for provider callbacks, and maintains provider state.
* Will likely split this into remote response state and UI state.
+ *
+ * @hide
*/
-public final class ProviderGetSession extends ProviderSession<GetCredentialsResponse>
- implements RemoteCredentialService.ProviderCallbacks<GetCredentialsResponse> {
+public final class ProviderGetSession extends ProviderSession<GetCredentialsRequest,
+ GetCredentialsResponse>
+ implements
+ RemoteCredentialService.ProviderCallbacks<GetCredentialsResponse> {
private static final String TAG = "ProviderGetSession";
// Key to be used as an entry key for a credential entry
private static final String CREDENTIAL_ENTRY_KEY = "credential_key";
- private GetCredentialsResponse mResponse;
-
@NonNull
- private final Map<String, CredentialEntry> mUiCredentials = new HashMap<>();
-
+ private final Map<String, CredentialEntry> mUiCredentialEntries = new HashMap<>();
@NonNull
- private final Map<String, Action> mUiActions = new HashMap<>();
+ private final Map<String, Action> mUiActionsEntries = new HashMap<>();
+ private Action mAuthenticationAction = null;
- public ProviderGetSession(CredentialProviderInfo info,
- ProviderInternalCallback callbacks,
- int userId, RemoteCredentialService remoteCredentialService) {
- super(info, callbacks, userId, remoteCredentialService);
- setStatus(Status.PENDING);
- }
-
- /** Updates the response being maintained in state by this provider session. */
- @Override
- public void updateResponse(GetCredentialsResponse response) {
- if (response.getAuthenticationAction() != null) {
- // TODO : Implement authentication logic
- } else if (response.getCredentialsDisplayContent() != null) {
- Log.i(TAG , "updateResponse with credentialEntries");
- mResponse = response;
- updateStatusAndInvokeCallback(Status.COMPLETE);
+ /** Creates a new provider session to be used by the request session. */
+ @Nullable public static ProviderGetSession createNewSession(
+ Context context,
+ @UserIdInt int userId,
+ CredentialProviderInfo providerInfo,
+ GetRequestSession getRequestSession,
+ RemoteCredentialService remoteCredentialService) {
+ GetCredentialsRequest providerRequest =
+ createProviderRequest(providerInfo.getCapabilities(),
+ getRequestSession.mClientRequest,
+ getRequestSession.mClientCallingPackage);
+ if (providerRequest != null) {
+ return new ProviderGetSession(context, providerInfo, getRequestSession, userId,
+ remoteCredentialService, providerRequest);
}
+ Log.i(TAG, "Unable to create provider session");
+ return null;
}
- /** Returns the response being maintained in this provider session. */
- @Override
@Nullable
- public GetCredentialsResponse getResponse() {
- return mResponse;
+ private static GetCredentialsRequest createProviderRequest(List<String> providerCapabilities,
+ android.credentials.GetCredentialRequest clientRequest,
+ String clientCallingPackage) {
+ List<GetCredentialOption> filteredOptions = new ArrayList<>();
+ for (GetCredentialOption option : clientRequest.getGetCredentialOptions()) {
+ if (providerCapabilities.contains(option.getType())) {
+ Log.i(TAG, "In createProviderRequest - capability found : " + option.getType());
+ filteredOptions.add(option);
+ } else {
+ Log.i(TAG, "In createProviderRequest - capability not "
+ + "found : " + option.getType());
+ }
+ }
+ if (!filteredOptions.isEmpty()) {
+ return new GetCredentialsRequest.Builder(clientCallingPackage).setGetCredentialOptions(
+ filteredOptions).build();
+ }
+ Log.i(TAG, "In createProviderRequest - returning null");
+ return null;
+ }
+
+ public ProviderGetSession(Context context,
+ CredentialProviderInfo info,
+ ProviderInternalCallback callbacks,
+ int userId, RemoteCredentialService remoteCredentialService,
+ GetCredentialsRequest request) {
+ super(context, info, request, callbacks, userId, remoteCredentialService);
+ setStatus(Status.PENDING);
}
/** Returns the credential entry maintained in state by this provider session. */
@Nullable
public CredentialEntry getCredentialEntry(@NonNull String entryId) {
- return mUiCredentials.get(entryId);
- }
-
- /** Returns the action entry maintained in state by this provider session. */
- @Nullable
- public Action getAction(@NonNull String entryId) {
- return mUiActions.get(entryId);
+ return mUiCredentialEntries.get(entryId);
}
/** Called when the provider response has been updated by an external source. */
- @Override
+ @Override // Callback from the remote provider
public void onProviderResponseSuccess(@Nullable GetCredentialsResponse response) {
Log.i(TAG, "in onProviderResponseSuccess");
- updateResponse(response);
+ onUpdateResponse(response);
}
/** Called when the provider response resulted in a failure. */
- @Override
+ @Override // Callback from the remote provider
public void onProviderResponseFailure(int errorCode, @Nullable CharSequence message) {
updateStatusAndInvokeCallback(toStatus(errorCode));
}
/** Called when provider service dies. */
- @Override
+ @Override // Callback from the remote provider
public void onProviderServiceDied(RemoteCredentialService service) {
if (service.getComponentName().equals(mProviderInfo.getServiceInfo().getComponentName())) {
updateStatusAndInvokeCallback(Status.SERVICE_DEAD);
@@ -116,77 +139,106 @@
}
}
- @Override
- protected GetCredentialProviderData prepareUiData() throws IllegalArgumentException {
- Log.i(TAG, "In prepareUiData");
- if (!ProviderSession.isCompletionStatus(getStatus())) {
- Log.i(TAG, "In prepareUiData not complete");
+ @Override // Callback from the provider intent controller class
+ public void onProviderIntentResult(Bundle resultData) {
+ // TODO : Implement
+ }
- throw new IllegalStateException("Status must be in completion mode");
+ @Override
+ public void onProviderIntentCancelled() {
+ // TODO : Implement
+ }
+
+ @Override // Selection call from the request provider
+ protected void onUiEntrySelected(String entryType, String entryId) {
+ // TODO: Implement
+ }
+
+ @Override // Call from request session to data to be shown on the UI
+ @Nullable protected GetCredentialProviderData prepareUiData() throws IllegalArgumentException {
+ Log.i(TAG, "In prepareUiData");
+ if (!ProviderSession.isUiInvokingStatus(getStatus())) {
+ Log.i(TAG, "In prepareUiData - provider does not want to show UI: "
+ + mComponentName.flattenToString());
+ return null;
}
- GetCredentialsResponse response = getResponse();
+ GetCredentialsResponse response = getProviderResponse();
if (response == null) {
Log.i(TAG, "In prepareUiData response null");
-
throw new IllegalStateException("Response must be in completion mode");
}
if (response.getAuthenticationAction() != null) {
- Log.i(TAG, "In prepareUiData auth not null");
-
- return prepareUiProviderDataWithAuthentication(response.getAuthenticationAction());
+ Log.i(TAG, "In prepareUiData - top level authentication mode");
+ return prepareUiProviderData(null, null,
+ prepareUiAuthenticationActionEntry(response.getAuthenticationAction()),
+ /*remoteEntry=*/null);
}
if (response.getCredentialsDisplayContent() != null){
- Log.i(TAG, "In prepareUiData credentials not null");
-
- return prepareUiProviderDataWithCredentials(response.getCredentialsDisplayContent());
+ Log.i(TAG, "In prepareUiData displayContent not null");
+ return prepareUiProviderData(populateUiActionEntries(
+ response.getCredentialsDisplayContent().getActions()),
+ prepareUiCredentialEntries(response.getCredentialsDisplayContent()
+ .getCredentialEntries()),
+ /*authenticationActionEntry=*/null, /*remoteEntry=*/null);
}
return null;
}
- /**
- * To be called by {@link ProviderGetSession} when the UI is to be invoked.
- */
- @Nullable
- private GetCredentialProviderData prepareUiProviderDataWithCredentials(@NonNull
- CredentialsDisplayContent content) {
- Log.i(TAG, "in prepareUiProviderData");
- List<Entry> credentialEntries = new ArrayList<>();
- List<Entry> actionChips = new ArrayList<>();
- Entry authenticationEntry = null;
+ private Entry prepareUiAuthenticationActionEntry(@NonNull Action authenticationAction) {
+ String entryId = generateEntryId();
+ mUiActionsEntries.put(entryId, authenticationAction);
+ return new Entry(ACTION_ENTRY_KEY, entryId, authenticationAction.getSlice());
+ }
+
+ private List<Entry> prepareUiCredentialEntries(@NonNull
+ List<CredentialEntry> credentialEntries) {
+ Log.i(TAG, "in prepareUiProviderDataWithCredentials");
+ List<Entry> credentialUiEntries = new ArrayList<>();
// Populate the credential entries
- for (CredentialEntry credentialEntry : content.getCredentialEntries()) {
- String entryId = UUID.randomUUID().toString();
- mUiCredentials.put(entryId, credentialEntry);
+ for (CredentialEntry credentialEntry : credentialEntries) {
+ String entryId = generateEntryId();
+ mUiCredentialEntries.put(entryId, credentialEntry);
Log.i(TAG, "in prepareUiProviderData creating ui entry with id " + entryId);
- Slice slice = credentialEntry.getSlice();
- // TODO : Remove conversion of string to int after change in Entry class
- credentialEntries.add(new Entry(CREDENTIAL_ENTRY_KEY, entryId,
+ credentialUiEntries.add(new Entry(CREDENTIAL_ENTRY_KEY, entryId,
credentialEntry.getSlice()));
}
- // populate the action chip
- for (Action action : content.getActions()) {
- String entryId = UUID.randomUUID().toString();
- mUiActions.put(entryId, action);
- // TODO : Remove conversion of string to int after change in Entry class
- actionChips.add(new Entry(ACTION_ENTRY_KEY, entryId,
- action.getSlice()));
- }
+ return credentialUiEntries;
+ }
- return new GetCredentialProviderData.Builder(mComponentName.flattenToString())
+ private List<Entry> populateUiActionEntries(@Nullable List<Action> actions) {
+ List<Entry> actionEntries = new ArrayList<>();
+ for (Action action : actions) {
+ String entryId = UUID.randomUUID().toString();
+ mUiActionsEntries.put(entryId, action);
+ // TODO : Remove conversion of string to int after change in Entry class
+ actionEntries.add(new Entry(ACTION_ENTRY_KEY, entryId, action.getSlice()));
+ }
+ return actionEntries;
+ }
+
+ private GetCredentialProviderData prepareUiProviderData(List<Entry> actionEntries,
+ List<Entry> credentialEntries, Entry authenticationActionEntry,
+ Entry remoteEntry) {
+ return new GetCredentialProviderData.Builder(
+ mComponentName.flattenToString()).setActionChips(actionEntries)
.setCredentialEntries(credentialEntries)
- .setActionChips(actionChips)
- .setAuthenticationEntry(authenticationEntry)
+ .setAuthenticationEntry(authenticationActionEntry)
.build();
}
- /**
- * To be called by {@link ProviderGetSession} when the UI is to be invoked.
- */
- @Nullable
- private GetCredentialProviderData prepareUiProviderDataWithAuthentication(@NonNull
- Action authenticationEntry) {
- // TODO : Implement authentication flow
- return null;
+ /** Updates the response being maintained in state by this provider session. */
+ private void onUpdateResponse(GetCredentialsResponse response) {
+ mProviderResponse = response;
+ if (response.getAuthenticationAction() != null) {
+ Log.i(TAG , "updateResponse with authentication entry");
+ // TODO validate authentication action
+ mAuthenticationAction = response.getAuthenticationAction();
+ updateStatusAndInvokeCallback(Status.REQUIRES_AUTHENTICATION);
+ } else if (response.getCredentialsDisplayContent() != null) {
+ Log.i(TAG , "updateResponse with credentialEntries");
+ // TODO validate response
+ updateStatusAndInvokeCallback(Status.CREDENTIALS_RECEIVED);
+ }
}
}
diff --git a/services/credentials/java/com/android/server/credentials/ProviderIntentController.java b/services/credentials/java/com/android/server/credentials/ProviderIntentController.java
new file mode 100644
index 0000000..0f2e8ec
--- /dev/null
+++ b/services/credentials/java/com/android/server/credentials/ProviderIntentController.java
@@ -0,0 +1,117 @@
+/*
+ * 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.credentials;
+
+import android.annotation.NonNull;
+import android.annotation.SuppressLint;
+import android.annotation.UserIdInt;
+import android.app.Activity;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentSender;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Parcel;
+import android.os.ResultReceiver;
+import android.service.credentials.CreateCredentialRequest;
+import android.service.credentials.CredentialProviderService;
+import android.util.Log;
+
+/**
+ * Class that invokes providers' pending intents and listens to the responses.
+ */
+@SuppressLint("LongLogTag")
+public class ProviderIntentController {
+ private static final String TAG = "ProviderIntentController";
+ /**
+ * Interface to be implemented by any class that wishes to get callbacks from the UI.
+ */
+ public interface ProviderIntentControllerCallback {
+ /** Called when the user makes a selection. */
+ void onProviderIntentResult(Bundle resultData);
+ /** Called when the user cancels the UI. */
+ void onProviderIntentCancelled();
+ }
+
+ private final int mUserId;
+ private final Context mContext;
+ private final ProviderIntentControllerCallback mCallback;
+ private final ResultReceiver mResultReceiver = new ResultReceiver(
+ new Handler(Looper.getMainLooper())) {
+ @Override
+ protected void onReceiveResult(int resultCode, Bundle resultData) {
+ Log.i(TAG, "onReceiveResult in providerIntentController");
+
+ if (resultCode == Activity.RESULT_OK) {
+ Log.i(TAG, "onReceiveResult - ACTIVITYOK");
+ mCallback.onProviderIntentResult(resultData);
+ } else if (resultCode == Activity.RESULT_CANCELED) {
+ Log.i(TAG, "onReceiveResult - RESULTCANCELED");
+ mCallback.onProviderIntentCancelled();
+ }
+ // Drop unknown result
+ }
+ };
+
+ public ProviderIntentController(@UserIdInt int userId,
+ Context context,
+ ProviderIntentControllerCallback callback) {
+ mUserId = userId;
+ mContext = context;
+ mCallback = callback;
+ }
+
+ /** Sets up the request data and invokes the given pending intent. */
+ public void setupAndInvokePendingIntent(@NonNull PendingIntent pendingIntent,
+ CreateCredentialRequest request) {
+ Log.i(TAG, "in invokePendingIntent");
+ setupIntent(pendingIntent, request);
+ Log.i(TAG, "in invokePendingIntent receiver set up");
+ Log.i(TAG, "creator package: " + pendingIntent.getIntentSender()
+ .getCreatorPackage());
+
+ try {
+ mContext.startIntentSender(pendingIntent.getIntentSender(),
+ null, 0, 0, 0);
+ } catch (IntentSender.SendIntentException e) {
+ Log.i(TAG, "Error while invoking pending intent");
+ }
+
+ }
+
+ private void setupIntent(PendingIntent pendingIntent, CreateCredentialRequest request) {
+ pendingIntent.getIntent().putExtra(Intent.EXTRA_RESULT_RECEIVER,
+ toIpcFriendlyResultReceiver(mResultReceiver));
+ pendingIntent.getIntent().putExtra(
+ CredentialProviderService.EXTRA_CREATE_CREDENTIAL_REQUEST_PARAMS,
+ request.getData());
+ }
+
+ private <T extends ResultReceiver> ResultReceiver toIpcFriendlyResultReceiver(
+ T resultReceiver) {
+ final Parcel parcel = Parcel.obtain();
+ resultReceiver.writeToParcel(parcel, 0);
+ parcel.setDataPosition(0);
+
+ final ResultReceiver ipcFriendly = ResultReceiver.CREATOR.createFromParcel(parcel);
+ parcel.recycle();
+
+ return ipcFriendly;
+ }
+}
diff --git a/services/credentials/java/com/android/server/credentials/ProviderSession.java b/services/credentials/java/com/android/server/credentials/ProviderSession.java
index 3a9f964..14a9157 100644
--- a/services/credentials/java/com/android/server/credentials/ProviderSession.java
+++ b/services/credentials/java/com/android/server/credentials/ProviderSession.java
@@ -17,25 +17,70 @@
package com.android.server.credentials;
import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.content.ComponentName;
+import android.content.Context;
+import android.credentials.Credential;
import android.credentials.ui.ProviderData;
+import android.os.Bundle;
import android.service.credentials.CredentialProviderException;
import android.service.credentials.CredentialProviderInfo;
+import java.util.UUID;
+
/**
* Provider session storing the state of provider response and ui entries.
- * @param <T> The request type expected from the remote provider, for a given request session.
+ * @param <T> The request to be sent to the provider
+ * @param <R> The response to be expected from the provider
*/
-public abstract class ProviderSession<T> implements RemoteCredentialService.ProviderCallbacks<T> {
+public abstract class ProviderSession<T, R> implements RemoteCredentialService.ProviderCallbacks<R>,
+ ProviderIntentController.ProviderIntentControllerCallback {
// Key to be used as the entry key for an action entry
protected static final String ACTION_ENTRY_KEY = "action_key";
+ @NonNull protected final Context mContext;
@NonNull protected final ComponentName mComponentName;
@NonNull protected final CredentialProviderInfo mProviderInfo;
@NonNull protected final RemoteCredentialService mRemoteCredentialService;
@NonNull protected final int mUserId;
@NonNull protected Status mStatus = Status.NOT_STARTED;
@NonNull protected final ProviderInternalCallback mCallbacks;
+ @NonNull protected final ProviderIntentController mProviderIntentController;
+ @Nullable protected Credential mFinalCredentialResponse;
+ @NonNull protected final T mProviderRequest;
+ @Nullable protected R mProviderResponse;
+
+ /**
+ * Returns true if the given status reflects that the provider state is ready to be shown
+ * on the credMan UI.
+ */
+ public static boolean isUiInvokingStatus(Status status) {
+ return status == Status.CREDENTIALS_RECEIVED || status == Status.SAVE_ENTRIES_RECEIVED;
+ }
+
+ /**
+ * Returns true if the given status reflects that the provider is waiting for a remote
+ * response.
+ */
+ public static boolean isStatusWaitingForRemoteResponse(Status status) {
+ return status == Status.PENDING;
+ }
+
+ /**
+ * Returns true if the given status means that the provider session must be terminated.
+ */
+ public static boolean isTerminatingStatus(Status status) {
+ return status == Status.CANCELED || status == Status.SERVICE_DEAD;
+ }
+
+ /**
+ * Returns true if the given status reflects that the provider is done getting the response,
+ * and is ready to return the final credential back to the user.
+ */
+ public static boolean isCompletionStatus(Status status) {
+ return status == Status.CREDENTIAL_RECEIVED_FROM_INTENT
+ || status == Status.CREDENTIAL_RECEIVED_FROM_SELECTION;
+ }
/**
* Interface to be implemented by any class that wishes to get a callback when a particular
@@ -49,35 +94,49 @@
void onProviderStatusChanged(Status status, ComponentName componentName);
}
- protected ProviderSession(@NonNull CredentialProviderInfo info,
+ protected ProviderSession(@NonNull Context context, @NonNull CredentialProviderInfo info,
+ @NonNull T providerRequest,
@NonNull ProviderInternalCallback callbacks,
@NonNull int userId,
@NonNull RemoteCredentialService remoteCredentialService) {
+ mContext = context;
mProviderInfo = info;
+ mProviderRequest = providerRequest;
mCallbacks = callbacks;
mUserId = userId;
mComponentName = info.getServiceInfo().getComponentName();
mRemoteCredentialService = remoteCredentialService;
+ mProviderIntentController = new ProviderIntentController(userId, context, this);
}
- /** Update the response state stored with the provider session. */
- protected abstract void updateResponse (T response);
-
- /** Update the response state stored with the provider session. */
- protected abstract T getResponse ();
-
- /** Should be overridden to prepare, and stores state for {@link ProviderData} to be
- * shown on the UI. */
- protected abstract ProviderData prepareUiData();
-
/** Provider status at various states of the request session. */
+ // TODO: Review status values, and adjust where needed
enum Status {
NOT_STARTED,
PENDING,
REQUIRES_AUTHENTICATION,
- COMPLETE,
+ CREDENTIALS_RECEIVED,
SERVICE_DEAD,
- CANCELED
+ CREDENTIAL_RECEIVED_FROM_INTENT,
+ PENDING_INTENT_INVOKED,
+ CREDENTIAL_RECEIVED_FROM_SELECTION,
+ SAVE_ENTRIES_RECEIVED, CANCELED
+ }
+
+ /** Converts exception to a provider session status. */
+ @NonNull
+ public static Status toStatus(
+ @CredentialProviderException.CredentialProviderError int errorCode) {
+ // TODO : Add more mappings as more flows are supported
+ return Status.CANCELED;
+ }
+
+ protected String generateEntryId() {
+ return UUID.randomUUID().toString();
+ }
+
+ public Credential getFinalCredentialResponse() {
+ return mFinalCredentialResponse;
}
protected void setStatus(@NonNull Status status) {
@@ -94,31 +153,38 @@
return mComponentName;
}
+ @NonNull
+ protected RemoteCredentialService getRemoteCredentialService() {
+ return mRemoteCredentialService;
+ }
+
/** Updates the status .*/
protected void updateStatusAndInvokeCallback(@NonNull Status status) {
setStatus(status);
mCallbacks.onProviderStatusChanged(status, mComponentName);
}
- @NonNull
- public static Status toStatus(
- @CredentialProviderException.CredentialProviderError int errorCode) {
- // TODO : Add more mappings as more flows are supported
- return Status.CANCELED;
+ /** Get the request to be sent to the provider. */
+ protected T getProviderRequest() {
+ return mProviderRequest;
}
- /**
- * Returns true if the given status means that the provider session must be terminated.
- */
- public static boolean isTerminatingStatus(Status status) {
- return status == Status.CANCELED || status == Status.SERVICE_DEAD;
+ /** Update the response state stored with the provider session. */
+ @Nullable protected R getProviderResponse() {
+ return mProviderResponse;
}
- /**
- * Returns true if the given status means that the provider is done getting the response,
- * and is ready for user interaction.
- */
- public static boolean isCompletionStatus(Status status) {
- return status == Status.COMPLETE || status == Status.REQUIRES_AUTHENTICATION;
- }
+ /** Should be overridden to prepare, and stores state for {@link ProviderData} to be
+ * shown on the UI. */
+ @Nullable protected abstract ProviderData prepareUiData();
+
+ /** Should be overridden to handle the selected entry from the UI. */
+ protected abstract void onUiEntrySelected(String entryType, String entryId);
+
+ @Override
+ public abstract void onProviderIntentResult(Bundle resultData);
+
+ @Override
+ public abstract void onProviderIntentCancelled();
+
}
diff --git a/services/credentials/java/com/android/server/credentials/RemoteCredentialService.java b/services/credentials/java/com/android/server/credentials/RemoteCredentialService.java
index d0b6e7d..c2464b5 100644
--- a/services/credentials/java/com/android/server/credentials/RemoteCredentialService.java
+++ b/services/credentials/java/com/android/server/credentials/RemoteCredentialService.java
@@ -24,11 +24,14 @@
import android.os.Handler;
import android.os.ICancellationSignal;
import android.os.RemoteException;
+import android.service.credentials.CreateCredentialRequest;
+import android.service.credentials.CreateCredentialResponse;
import android.service.credentials.CredentialProviderException;
import android.service.credentials.CredentialProviderException.CredentialProviderError;
import android.service.credentials.CredentialProviderService;
import android.service.credentials.GetCredentialsRequest;
import android.service.credentials.GetCredentialsResponse;
+import android.service.credentials.ICreateCredentialCallback;
import android.service.credentials.ICredentialProviderService;
import android.service.credentials.IGetCredentialsCallback;
import android.text.format.DateUtils;
@@ -76,7 +79,7 @@
public RemoteCredentialService(@NonNull Context context,
@NonNull ComponentName componentName, int userId) {
super(context, new Intent(CredentialProviderService.SERVICE_INTERFACE)
- .setComponent(componentName), Context.BIND_ALLOW_BACKGROUND_ACTIVITY_STARTS,
+ .setComponent(componentName), /*bindingFlags=*/0,
userId, ICredentialProviderService.Stub::asInterface);
mComponentName = componentName;
}
@@ -101,7 +104,7 @@
* provider service.
* @param request the request to be sent to the provider
* @param callback the callback to be used to send back the provider response to the
- * {@link ProviderSession} class that maintains provider state
+ * {@link ProviderGetSession} class that maintains provider state
*/
public void onGetCredentials(@NonNull GetCredentialsRequest request,
ProviderCallbacks<GetCredentialsResponse> callback) {
@@ -114,21 +117,21 @@
CompletableFuture<GetCredentialsResponse> getCredentials = new CompletableFuture<>();
ICancellationSignal cancellationSignal =
service.onGetCredentials(request, new IGetCredentialsCallback.Stub() {
- @Override
- public void onSuccess(GetCredentialsResponse response) {
- Log.i(TAG, "In onSuccess in RemoteCredentialService");
- getCredentials.complete(response);
- }
+ @Override
+ public void onSuccess(GetCredentialsResponse response) {
+ Log.i(TAG, "In onSuccess in RemoteCredentialService");
+ getCredentials.complete(response);
+ }
- @Override
- public void onFailure(@CredentialProviderError int errorCode,
- CharSequence message) {
- Log.i(TAG, "In onFailure in RemoteCredentialService");
- String errorMsg = message == null ? "" : String.valueOf(message);
- getCredentials.completeExceptionally(new CredentialProviderException(
- errorCode, errorMsg));
- }
- });
+ @Override
+ public void onFailure(@CredentialProviderError int errorCode,
+ CharSequence message) {
+ Log.i(TAG, "In onFailure in RemoteCredentialService");
+ String errorMsg = message == null ? "" : String.valueOf(message);
+ getCredentials.completeExceptionally(new CredentialProviderException(
+ errorCode, errorMsg));
+ }
+ });
CompletableFuture<GetCredentialsResponse> future = futureRef.get();
if (future != null && future.isCancelled()) {
dispatchCancellationSignal(cancellationSignal);
@@ -137,38 +140,91 @@
}
return getCredentials;
}).orTimeout(TIMEOUT_REQUEST_MILLIS, TimeUnit.MILLISECONDS);
- futureRef.set(connectThenExecute);
- connectThenExecute.whenComplete((result, error) -> Handler.getMain().post(() -> {
- if (error == null) {
- Log.i(TAG, "In RemoteCredentialService execute error is null");
- callback.onProviderResponseSuccess(result);
+ futureRef.set(connectThenExecute);
+ connectThenExecute.whenComplete((result, error) -> Handler.getMain().post(() ->
+ handleExecutionResponse(result, error, cancellationSink, callback)));
+ }
+
+ /** Main entry point to be called for executing a createCredential call on the remote
+ * provider service.
+ * @param request the request to be sent to the provider
+ * @param callback the callback to be used to send back the provider response to the
+ * {@link ProviderCreateSession} class that maintains provider state
+ */
+ public void onCreateCredential(@NonNull CreateCredentialRequest request,
+ ProviderCallbacks<CreateCredentialResponse> callback) {
+ Log.i(TAG, "In onCreateCredential in RemoteCredentialService");
+ AtomicReference<ICancellationSignal> cancellationSink = new AtomicReference<>();
+ AtomicReference<CompletableFuture<CreateCredentialResponse>> futureRef =
+ new AtomicReference<>();
+
+ CompletableFuture<CreateCredentialResponse> connectThenExecute = postAsync(service -> {
+ CompletableFuture<CreateCredentialResponse> createCredentialFuture =
+ new CompletableFuture<>();
+ ICancellationSignal cancellationSignal = service.onCreateCredential(
+ request, new ICreateCredentialCallback.Stub() {
+ @Override
+ public void onSuccess(CreateCredentialResponse response) {
+ Log.i(TAG, "In onSuccess onCreateCredential "
+ + "in RemoteCredentialService");
+ createCredentialFuture.complete(response);
+ }
+
+ @Override
+ public void onFailure(@CredentialProviderError int errorCode,
+ CharSequence message) {
+ Log.i(TAG, "In onFailure in RemoteCredentialService");
+ String errorMsg = message == null ? "" : String.valueOf(message);
+ createCredentialFuture.completeExceptionally(
+ new CredentialProviderException(errorCode, errorMsg));
+ }});
+ CompletableFuture<CreateCredentialResponse> future = futureRef.get();
+ if (future != null && future.isCancelled()) {
+ dispatchCancellationSignal(cancellationSignal);
} else {
- if (error instanceof TimeoutException) {
- Log.i(TAG, "In RemoteCredentialService execute error is timeout");
- dispatchCancellationSignal(cancellationSink.get());
- callback.onProviderResponseFailure(
- CredentialProviderException.ERROR_TIMEOUT,
- error.getMessage());
- } else if (error instanceof CancellationException) {
- Log.i(TAG, "In RemoteCredentialService execute error is cancellation");
- dispatchCancellationSignal(cancellationSink.get());
- callback.onProviderResponseFailure(
- CredentialProviderException.ERROR_TASK_CANCELED,
- error.getMessage());
- } else if (error instanceof CredentialProviderException) {
- Log.i(TAG, "In RemoteCredentialService execute error is provider error");
- callback.onProviderResponseFailure(((CredentialProviderException) error)
- .getErrorCode(),
- error.getMessage());
- } else {
- Log.i(TAG, "In RemoteCredentialService execute error is unknown");
- callback.onProviderResponseFailure(
- CredentialProviderException.ERROR_UNKNOWN,
- error.getMessage());
- }
+ cancellationSink.set(cancellationSignal);
}
- }));
+ return createCredentialFuture;
+ }).orTimeout(TIMEOUT_REQUEST_MILLIS, TimeUnit.MILLISECONDS);
+
+ futureRef.set(connectThenExecute);
+ connectThenExecute.whenComplete((result, error) -> Handler.getMain().post(() ->
+ handleExecutionResponse(result, error, cancellationSink, callback)));
+ }
+
+ private <T> void handleExecutionResponse(T result,
+ Throwable error,
+ AtomicReference<ICancellationSignal> cancellationSink,
+ ProviderCallbacks<T> callback) {
+ if (error == null) {
+ Log.i(TAG, "In RemoteCredentialService execute error is null");
+ callback.onProviderResponseSuccess(result);
+ } else {
+ if (error instanceof TimeoutException) {
+ Log.i(TAG, "In RemoteCredentialService execute error is timeout");
+ dispatchCancellationSignal(cancellationSink.get());
+ callback.onProviderResponseFailure(
+ CredentialProviderException.ERROR_TIMEOUT,
+ error.getMessage());
+ } else if (error instanceof CancellationException) {
+ Log.i(TAG, "In RemoteCredentialService execute error is cancellation");
+ dispatchCancellationSignal(cancellationSink.get());
+ callback.onProviderResponseFailure(
+ CredentialProviderException.ERROR_TASK_CANCELED,
+ error.getMessage());
+ } else if (error instanceof CredentialProviderException) {
+ Log.i(TAG, "In RemoteCredentialService execute error is provider error");
+ callback.onProviderResponseFailure(((CredentialProviderException) error)
+ .getErrorCode(),
+ error.getMessage());
+ } else {
+ Log.i(TAG, "In RemoteCredentialService execute error is unknown");
+ callback.onProviderResponseFailure(
+ CredentialProviderException.ERROR_UNKNOWN,
+ error.getMessage());
+ }
+ }
}
private void dispatchCancellationSignal(@Nullable ICancellationSignal signal) {
diff --git a/services/credentials/java/com/android/server/credentials/RequestSession.java b/services/credentials/java/com/android/server/credentials/RequestSession.java
index 1bacbb3..056d0e8 100644
--- a/services/credentials/java/com/android/server/credentials/RequestSession.java
+++ b/services/credentials/java/com/android/server/credentials/RequestSession.java
@@ -20,18 +20,30 @@
import android.annotation.UserIdInt;
import android.content.ComponentName;
import android.content.Context;
+import android.credentials.ui.ProviderData;
import android.credentials.ui.UserSelectionDialogResult;
import android.os.Binder;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
+import android.service.credentials.CredentialProviderInfo;
+import android.util.Log;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Map;
/**
* Base class of a request session, that listens to UI events. This class must be extended
* every time a new response type is expected from the providers.
*/
-abstract class RequestSession implements CredentialManagerUi.CredentialManagerUiCallback,
+abstract class RequestSession<T, U> implements CredentialManagerUi.CredentialManagerUiCallback,
ProviderSession.ProviderInternalCallback {
+ private static final String TAG = "RequestSession";
+
+ // TODO: Revise access levels of attributes
+ @NonNull protected final T mClientRequest;
+ @NonNull protected final U mClientCallback;
@NonNull protected final IBinder mRequestId;
@NonNull protected final Context mContext;
@NonNull protected final CredentialManagerUi mCredentialManagerUi;
@@ -39,29 +51,120 @@
@NonNull protected final Handler mHandler;
@NonNull protected boolean mIsFirstUiTurn = true;
@UserIdInt protected final int mUserId;
+ @NonNull protected final String mClientCallingPackage;
+
+ protected final Map<String, ProviderSession> mProviders = new HashMap<>();
protected RequestSession(@NonNull Context context,
- @UserIdInt int userId, @NonNull String requestType) {
+ @UserIdInt int userId, @NonNull T clientRequest, U clientCallback,
+ @NonNull String requestType,
+ String clientCallingPackage) {
mContext = context;
mUserId = userId;
+ mClientRequest = clientRequest;
+ mClientCallback = clientCallback;
mRequestType = requestType;
+ mClientCallingPackage = clientCallingPackage;
mHandler = new Handler(Looper.getMainLooper(), null, true);
mRequestId = new Binder();
mCredentialManagerUi = new CredentialManagerUi(mContext,
mUserId, this);
}
- /** Returns the unique identifier of this request session. */
- public IBinder getRequestId() {
- return mRequestId;
+ public abstract ProviderSession initiateProviderSession(CredentialProviderInfo providerInfo,
+ RemoteCredentialService remoteCredentialService);
+
+ protected abstract void launchUiWithProviderData(ArrayList<ProviderData> providerDataList);
+
+ // UI callbacks
+
+ @Override // from CredentialManagerUiCallbacks
+ public void onUiSelection(UserSelectionDialogResult selection) {
+ String providerId = selection.getProviderId();
+ Log.i(TAG, "onUiSelection, providerId: " + providerId);
+ ProviderSession providerSession = mProviders.get(providerId);
+ if (providerSession == null) {
+ Log.i(TAG, "providerSession not found in onUiSelection");
+ return;
+ }
+ Log.i(TAG, "Provider session found");
+ providerSession.onUiEntrySelected(selection.getEntryKey(),
+ selection.getEntrySubkey());
}
- @Override // from CredentialManagerUiCallback
- public abstract void onUiSelection(UserSelectionDialogResult selection);
+ @Override // from CredentialManagerUiCallbacks
+ public void onUiCancellation() {
+ // User canceled the activity
+ finishSession();
+ }
- @Override // from CredentialManagerUiCallback
- public abstract void onUiCancelation();
+ @Override // from provider session
+ public void onProviderStatusChanged(ProviderSession.Status status,
+ ComponentName componentName) {
+ Log.i(TAG, "in onStatusChanged with status: " + status);
+ if (ProviderSession.isTerminatingStatus(status)) {
+ Log.i(TAG, "in onStatusChanged terminating status");
+ onProviderTerminated(componentName);
+ //TODO: Check if this was the provider we were waiting for and can invoke the UI now
+ } else if (ProviderSession.isCompletionStatus(status)) {
+ Log.i(TAG, "in onStatusChanged isCompletionStatus status");
+ onProviderResponseComplete(componentName);
+ } else if (ProviderSession.isUiInvokingStatus(status)) {
+ Log.i(TAG, "in onStatusChanged isUiInvokingStatus status");
+ onProviderResponseRequiresUi();
+ }
+ }
- @Override // from ProviderInternalCallback
- public abstract void onProviderStatusChanged(ProviderSession.Status status, ComponentName componentName);
+ protected void onProviderTerminated(ComponentName componentName) {
+ //TODO: Implement
+ }
+
+ protected void onProviderResponseComplete(ComponentName componentName) {
+ //TODO: Implement
+ }
+
+ protected void onProviderResponseRequiresUi() {
+ Log.i(TAG, "in onProviderResponseComplete");
+ // TODO: Determine whether UI has already been invoked, and deal accordingly
+ if (!isAnyProviderPending()) {
+ Log.i(TAG, "in onProviderResponseComplete - isResponseCompleteAcrossProviders");
+ getProviderDataAndInitiateUi();
+ } else {
+ Log.i(TAG, "Can't invoke UI - waiting on some providers");
+ }
+ }
+
+ protected void finishSession() {
+ clearProviderSessions();
+ }
+
+ protected void clearProviderSessions() {
+ //TODO: Implement
+ mProviders.clear();
+ }
+
+ private boolean isAnyProviderPending() {
+ for (ProviderSession session : mProviders.values()) {
+ if (ProviderSession.isStatusWaitingForRemoteResponse(session.getStatus())) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private void getProviderDataAndInitiateUi() {
+ ArrayList<ProviderData> providerDataList = new ArrayList<>();
+ for (ProviderSession session : mProviders.values()) {
+ Log.i(TAG, "preparing data for : " + session.getComponentName());
+ ProviderData providerData = session.prepareUiData();
+ if (providerData != null) {
+ Log.i(TAG, "Provider data is not null");
+ providerDataList.add(providerData);
+ }
+ }
+ if (!providerDataList.isEmpty()) {
+ Log.i(TAG, "provider list not empty about to initiate ui");
+ launchUiWithProviderData(providerDataList);
+ }
+ }
}
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
index 316c736..89cbf53 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
@@ -646,6 +646,15 @@
@EnabledAfter(targetSdkVersion = Build.VERSION_CODES.Q)
private static final long USE_SET_LOCATION_ENABLED = 117835097L;
+ /**
+ * Forces wipeDataNoLock to attempt removing the user or throw an error as
+ * opposed to trying to factory reset the device first and only then falling back to user
+ * removal.
+ */
+ @ChangeId
+ @EnabledSince(targetSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ public static final long EXPLICIT_WIPE_BEHAVIOUR = 242193913L;
+
// Only add to the end of the list. Do not change or rearrange these values, that will break
// historical data. Do not use negative numbers or zero, logger only handles positive
// integers.
@@ -6699,8 +6708,8 @@
}
@Override
- public void wipeDataWithReason(int flags, String wipeReasonForUser,
- boolean calledOnParentInstance) {
+ public void wipeDataWithReason(int flags, @NonNull String wipeReasonForUser,
+ boolean calledOnParentInstance, boolean factoryReset) {
if (!mHasFeature && !hasCallingOrSelfPermission(permission.MASTER_CLEAR)) {
return;
}
@@ -6782,7 +6791,8 @@
"DevicePolicyManager.wipeDataWithReason() from %s, organization-owned? %s",
adminName, calledByProfileOwnerOnOrgOwnedDevice);
- wipeDataNoLock(adminComp, flags, internalReason, wipeReasonForUser, userId);
+ wipeDataNoLock(adminComp, flags, internalReason, wipeReasonForUser, userId,
+ calledOnParentInstance, factoryReset);
}
private String getGenericWipeReason(
@@ -6844,8 +6854,13 @@
Slogf.i(LOG_TAG, "Cleaning up device-wide policies done.");
}
+ /**
+ * @param factoryReset null: legacy behaviour, false: attempt to remove user, true: attempt to
+ * factory reset
+ */
private void wipeDataNoLock(ComponentName admin, int flags, String internalReason,
- String wipeReasonForUser, int userId) {
+ @NonNull String wipeReasonForUser, int userId, boolean calledOnParentInstance,
+ @Nullable Boolean factoryReset) {
wtfIfInLock();
mInjector.binderWithCleanCallingIdentity(() -> {
@@ -6863,7 +6878,37 @@
+ " restriction is set for user " + userId);
}
- if (userId == UserHandle.USER_SYSTEM) {
+ boolean isSystemUser = userId == UserHandle.USER_SYSTEM;
+ boolean wipeDevice;
+ if (factoryReset == null || !CompatChanges.isChangeEnabled(EXPLICIT_WIPE_BEHAVIOUR)) {
+ // Legacy mode
+ wipeDevice = isSystemUser;
+ } else {
+ // Explicit behaviour
+ if (factoryReset) {
+ // TODO(b/254031494) Replace with new factory reset permission checks
+ boolean hasPermission = isDeviceOwnerUserId(userId)
+ || (isOrganizationOwnedDeviceWithManagedProfile()
+ && calledOnParentInstance);
+ Preconditions.checkState(hasPermission,
+ "Admin %s does not have permission to factory reset the device.",
+ userId);
+ wipeDevice = true;
+ } else {
+ Preconditions.checkCallAuthorization(!isSystemUser,
+ "User %s is a system user and cannot be removed", userId);
+ boolean isLastNonHeadlessUser = getUserInfo(userId).isFull()
+ && mUserManager.getAliveUsers().stream()
+ .filter((it) -> it.getUserHandle().getIdentifier() != userId)
+ .noneMatch(UserInfo::isFull);
+ Preconditions.checkState(!isLastNonHeadlessUser,
+ "Removing user %s would leave the device without any active users. "
+ + "Consider factory resetting the device instead.",
+ userId);
+ wipeDevice = false;
+ }
+ }
+ if (wipeDevice) {
forceWipeDeviceNoLock(
(flags & WIPE_EXTERNAL_STORAGE) != 0,
internalReason,
@@ -7131,7 +7176,7 @@
}
@Override
- public void reportFailedPasswordAttempt(int userHandle) {
+ public void reportFailedPasswordAttempt(int userHandle, boolean parent) {
Preconditions.checkArgumentNonnegative(userHandle, "Invalid userId");
final CallerIdentity caller = getCallerIdentity();
@@ -7153,7 +7198,7 @@
saveSettingsLocked(userHandle);
if (mHasFeature) {
strictestAdmin = getAdminWithMinimumFailedPasswordsForWipeLocked(
- userHandle, /* parent */ false);
+ userHandle, /* parent= */ false);
int max = strictestAdmin != null
? strictestAdmin.maximumFailedPasswordsForWipe : 0;
if (max > 0 && policy.mFailedPasswordAttempts >= max) {
@@ -7183,10 +7228,13 @@
// IMPORTANT: Call without holding the lock to prevent deadlock.
try {
wipeDataNoLock(strictestAdmin.info.getComponent(),
- /*flags=*/ 0,
- /*reason=*/ "reportFailedPasswordAttempt()",
+ /* flags= */ 0,
+ /* reason= */ "reportFailedPasswordAttempt()",
getFailedPasswordAttemptWipeMessage(),
- userId);
+ userId,
+ /* calledOnParentInstance= */ parent,
+ // factoryReset=null to enable U- behaviour
+ /* factoryReset= */ null);
} catch (SecurityException e) {
Slogf.w(LOG_TAG, "Failed to wipe user " + userId
+ " after max failed password attempts reached.", e);
@@ -7195,7 +7243,7 @@
if (mInjector.securityLogIsLoggingEnabled()) {
SecurityLog.writeEvent(SecurityLog.TAG_KEYGUARD_DISMISS_AUTH_ATTEMPT,
- /*result*/ 0, /*method strength*/ 1);
+ /* result= */ 0, /* method strength= */ 1);
}
}
diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceTestBase.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceTestBase.java
index 3fbc400..640bde3 100644
--- a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceTestBase.java
+++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceTestBase.java
@@ -244,7 +244,7 @@
.setCurrentMethodVisible();
}
verify(mMockInputMethod, times(showSoftInput ? 1 : 0))
- .showSoftInput(any(), anyInt(), any());
+ .showSoftInput(any(), any(), anyInt(), any());
}
protected void verifyHideSoftInput(boolean setNotVisible, boolean hideSoftInput)
@@ -254,6 +254,6 @@
.setCurrentMethodNotVisible();
}
verify(mMockInputMethod, times(hideSoftInput ? 1 : 0))
- .hideSoftInput(any(), anyInt(), any());
+ .hideSoftInput(any(), any(), anyInt(), any());
}
}
diff --git a/services/tests/mockingservicestests/src/com/android/server/appop/AppOpsUidStateTrackerTest.java b/services/tests/mockingservicestests/src/com/android/server/appop/AppOpsUidStateTrackerTest.java
index e1713b0..98e895a 100644
--- a/services/tests/mockingservicestests/src/com/android/server/appop/AppOpsUidStateTrackerTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/appop/AppOpsUidStateTrackerTest.java
@@ -22,6 +22,7 @@
import static android.app.AppOpsManager.OP_CAMERA;
import static android.app.AppOpsManager.OP_COARSE_LOCATION;
import static android.app.AppOpsManager.OP_FINE_LOCATION;
+import static android.app.AppOpsManager.OP_RECEIVE_EXPLICIT_USER_INTERACTION_AUDIO;
import static android.app.AppOpsManager.OP_RECORD_AUDIO;
import static android.app.AppOpsManager.OP_WIFI_SCAN;
import static android.app.AppOpsManager.UID_STATE_BACKGROUND;
@@ -127,6 +128,8 @@
assertEquals(MODE_IGNORED, mIntf.evalMode(UID, OP_CAMERA, MODE_FOREGROUND));
assertEquals(MODE_IGNORED, mIntf.evalMode(UID, OP_COARSE_LOCATION, MODE_FOREGROUND));
assertEquals(MODE_IGNORED, mIntf.evalMode(UID, OP_FINE_LOCATION, MODE_FOREGROUND));
+ assertEquals(MODE_IGNORED,
+ mIntf.evalMode(UID, OP_RECEIVE_EXPLICIT_USER_INTERACTION_AUDIO, MODE_FOREGROUND));
}
@Test
@@ -137,6 +140,8 @@
.update();
assertEquals(MODE_ALLOWED, mIntf.evalMode(UID, OP_RECORD_AUDIO, MODE_FOREGROUND));
+ assertEquals(MODE_ALLOWED,
+ mIntf.evalMode(UID, OP_RECEIVE_EXPLICIT_USER_INTERACTION_AUDIO, MODE_FOREGROUND));
assertEquals(MODE_IGNORED, mIntf.evalMode(UID, OP_CAMERA, MODE_FOREGROUND));
assertEquals(MODE_IGNORED, mIntf.evalMode(UID, OP_COARSE_LOCATION, MODE_FOREGROUND));
@@ -151,6 +156,8 @@
.update();
assertEquals(MODE_ALLOWED, mIntf.evalMode(UID, OP_RECORD_AUDIO, MODE_FOREGROUND));
+ assertEquals(MODE_ALLOWED,
+ mIntf.evalMode(UID, OP_RECEIVE_EXPLICIT_USER_INTERACTION_AUDIO, MODE_FOREGROUND));
assertEquals(MODE_IGNORED, mIntf.evalMode(UID, OP_CAMERA, MODE_FOREGROUND));
assertEquals(MODE_IGNORED, mIntf.evalMode(UID, OP_COARSE_LOCATION, MODE_FOREGROUND));
@@ -169,6 +176,8 @@
assertEquals(MODE_IGNORED, mIntf.evalMode(UID, OP_RECORD_AUDIO, MODE_FOREGROUND));
assertEquals(MODE_IGNORED, mIntf.evalMode(UID, OP_COARSE_LOCATION, MODE_FOREGROUND));
assertEquals(MODE_IGNORED, mIntf.evalMode(UID, OP_FINE_LOCATION, MODE_FOREGROUND));
+ assertEquals(MODE_IGNORED,
+ mIntf.evalMode(UID, OP_RECEIVE_EXPLICIT_USER_INTERACTION_AUDIO, MODE_FOREGROUND));
}
@Test
@@ -183,6 +192,8 @@
assertEquals(MODE_IGNORED, mIntf.evalMode(UID, OP_RECORD_AUDIO, MODE_FOREGROUND));
assertEquals(MODE_IGNORED, mIntf.evalMode(UID, OP_COARSE_LOCATION, MODE_FOREGROUND));
assertEquals(MODE_IGNORED, mIntf.evalMode(UID, OP_FINE_LOCATION, MODE_FOREGROUND));
+ assertEquals(MODE_IGNORED,
+ mIntf.evalMode(UID, OP_RECEIVE_EXPLICIT_USER_INTERACTION_AUDIO, MODE_FOREGROUND));
}
@Test
@@ -197,6 +208,8 @@
assertEquals(MODE_IGNORED, mIntf.evalMode(UID, OP_RECORD_AUDIO, MODE_FOREGROUND));
assertEquals(MODE_IGNORED, mIntf.evalMode(UID, OP_CAMERA, MODE_FOREGROUND));
+ assertEquals(MODE_IGNORED,
+ mIntf.evalMode(UID, OP_RECEIVE_EXPLICIT_USER_INTERACTION_AUDIO, MODE_FOREGROUND));
}
@Test
@@ -211,6 +224,8 @@
assertEquals(MODE_IGNORED, mIntf.evalMode(UID, OP_RECORD_AUDIO, MODE_FOREGROUND));
assertEquals(MODE_IGNORED, mIntf.evalMode(UID, OP_CAMERA, MODE_FOREGROUND));
+ assertEquals(MODE_IGNORED,
+ mIntf.evalMode(UID, OP_RECEIVE_EXPLICIT_USER_INTERACTION_AUDIO, MODE_FOREGROUND));
}
@Test
@@ -314,6 +329,8 @@
.update();
assertEquals(MODE_ALLOWED, mIntf.evalMode(UID, OP_RECORD_AUDIO, MODE_FOREGROUND));
+ assertEquals(MODE_ALLOWED,
+ mIntf.evalMode(UID, OP_RECEIVE_EXPLICIT_USER_INTERACTION_AUDIO, MODE_FOREGROUND));
}
@Test
@@ -328,6 +345,8 @@
.update();
assertEquals(MODE_IGNORED, mIntf.evalMode(UID, OP_RECORD_AUDIO, MODE_FOREGROUND));
+ assertEquals(MODE_IGNORED,
+ mIntf.evalMode(UID, OP_RECEIVE_EXPLICIT_USER_INTERACTION_AUDIO, MODE_FOREGROUND));
}
@Test
@@ -403,6 +422,8 @@
assertEquals(MODE_ALLOWED, mIntf.evalMode(UID, OP_CAMERA, MODE_FOREGROUND));
assertEquals(MODE_ALLOWED, mIntf.evalMode(UID, OP_COARSE_LOCATION, MODE_FOREGROUND));
assertEquals(MODE_ALLOWED, mIntf.evalMode(UID, OP_FINE_LOCATION, MODE_FOREGROUND));
+ assertEquals(MODE_ALLOWED,
+ mIntf.evalMode(UID, OP_RECEIVE_EXPLICIT_USER_INTERACTION_AUDIO, MODE_FOREGROUND));
}
@Test
@@ -418,6 +439,8 @@
assertEquals(MODE_ALLOWED, mIntf.evalMode(UID, OP_CAMERA, MODE_FOREGROUND));
assertEquals(MODE_ALLOWED, mIntf.evalMode(UID, OP_COARSE_LOCATION, MODE_FOREGROUND));
assertEquals(MODE_ALLOWED, mIntf.evalMode(UID, OP_FINE_LOCATION, MODE_FOREGROUND));
+ assertEquals(MODE_ALLOWED,
+ mIntf.evalMode(UID, OP_RECEIVE_EXPLICIT_USER_INTERACTION_AUDIO, MODE_FOREGROUND));
}
@Test
@@ -433,6 +456,8 @@
assertEquals(MODE_ALLOWED, mIntf.evalMode(UID, OP_CAMERA, MODE_FOREGROUND));
assertEquals(MODE_ALLOWED, mIntf.evalMode(UID, OP_COARSE_LOCATION, MODE_FOREGROUND));
assertEquals(MODE_ALLOWED, mIntf.evalMode(UID, OP_FINE_LOCATION, MODE_FOREGROUND));
+ assertEquals(MODE_ALLOWED,
+ mIntf.evalMode(UID, OP_RECEIVE_EXPLICIT_USER_INTERACTION_AUDIO, MODE_FOREGROUND));
}
@Test
diff --git a/services/tests/servicestests/res/xml/usertypes_test_full.xml b/services/tests/servicestests/res/xml/usertypes_test_full.xml
index 099ccbe..9568143 100644
--- a/services/tests/servicestests/res/xml/usertypes_test_full.xml
+++ b/services/tests/servicestests/res/xml/usertypes_test_full.xml
@@ -16,7 +16,8 @@
<user-types>
<full-type
name='android.test.1'
- max-allowed-per-parent='12' >
+ max-allowed-per-parent='12'
+ max-allowed='17' >
<default-restrictions no_remove_user='true' no_bluetooth='true' />
<badge-colors>
<item res='@*android:color/profile_badge_1' />
diff --git a/services/tests/servicestests/res/xml/usertypes_test_profile.xml b/services/tests/servicestests/res/xml/usertypes_test_profile.xml
index b27f49d..1a6dae37 100644
--- a/services/tests/servicestests/res/xml/usertypes_test_profile.xml
+++ b/services/tests/servicestests/res/xml/usertypes_test_profile.xml
@@ -33,6 +33,7 @@
<user-properties
showInLauncher='2020'
startWithParent='false'
+ useParentsContacts='false'
/>
</profile-type>
<profile-type name='custom.test.1' max-allowed-per-parent='14' />
diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/audio/VirtualAudioControllerTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/audio/VirtualAudioControllerTest.java
index 4c939f0..0262f56 100644
--- a/services/tests/servicestests/src/com/android/server/companion/virtual/audio/VirtualAudioControllerTest.java
+++ b/services/tests/servicestests/src/com/android/server/companion/virtual/audio/VirtualAudioControllerTest.java
@@ -82,6 +82,7 @@
/* blockedActivities= */ new ArraySet<>(),
VirtualDeviceParams.ACTIVITY_POLICY_DEFAULT_ALLOWED,
/* activityListener= */ null,
+ /* pipBlockedCallback= */ null,
/* activityBlockedCallback= */ null,
/* secureWindowCallback= */ null,
/* deviceProfile= */ DEVICE_PROFILE_APP_STREAMING);
diff --git a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java
index ddb3049..8e669f0 100644
--- a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java
+++ b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java
@@ -140,7 +140,7 @@
import android.security.keystore.AttestationUtils;
import android.telephony.TelephonyManager;
import android.telephony.data.ApnSetting;
-import android.test.MoreAsserts; // TODO(b/171932723): replace by Truth
+import android.test.MoreAsserts;
import android.util.ArraySet;
import android.util.Log;
import android.util.Pair;
@@ -5087,7 +5087,7 @@
}
@Test
- public void testWipeDataDeviceOwner() throws Exception {
+ public void testWipeDevice_DeviceOwner() throws Exception {
setDeviceOwner();
when(getServices().userManager.getUserRestrictionSource(
UserManager.DISALLOW_FACTORY_RESET,
@@ -5096,7 +5096,7 @@
when(mContext.getResources().getString(R.string.work_profile_deleted_description_dpm_wipe)).
thenReturn("Just a test string.");
- dpm.wipeData(0);
+ dpm.wipeDevice(0);
verifyRebootWipeUserData(/* wipeEuicc= */ false);
}
@@ -5111,13 +5111,13 @@
when(mContext.getResources().getString(R.string.work_profile_deleted_description_dpm_wipe)).
thenReturn("Just a test string.");
- dpm.wipeData(WIPE_EUICC);
+ dpm.wipeDevice(WIPE_EUICC);
verifyRebootWipeUserData(/* wipeEuicc= */ true);
}
@Test
- public void testWipeDataDeviceOwnerDisallowed() throws Exception {
+ public void testWipeDevice_DeviceOwnerDisallowed() throws Exception {
setDeviceOwner();
when(getServices().userManager.getUserRestrictionSource(
UserManager.DISALLOW_FACTORY_RESET,
@@ -5128,7 +5128,7 @@
// The DO is not allowed to wipe the device if the user restriction was set
// by the system
assertExpectException(SecurityException.class, /* messageRegex= */ null,
- () -> dpm.wipeData(0));
+ () -> dpm.wipeDevice(0));
}
@Test
@@ -7986,7 +7986,7 @@
}
@Test
- public void testWipeData_financeDo_success() throws Exception {
+ public void testWipeDevice_financeDo_success() throws Exception {
setDeviceOwner();
dpm.setDeviceOwnerType(admin1, DEVICE_OWNER_TYPE_FINANCED);
when(getServices().userManager.getUserRestrictionSource(
@@ -7997,7 +7997,7 @@
.getString(R.string.work_profile_deleted_description_dpm_wipe))
.thenReturn("Test string");
- dpm.wipeData(0);
+ dpm.wipeDevice(0);
verifyRebootWipeUserData(/* wipeEuicc= */ false);
}
diff --git a/services/tests/servicestests/src/com/android/server/job/BackgroundRestrictionsTest.java b/services/tests/servicestests/src/com/android/server/job/BackgroundRestrictionsTest.java
index 9c8e72c..f5029ec 100644
--- a/services/tests/servicestests/src/com/android/server/job/BackgroundRestrictionsTest.java
+++ b/services/tests/servicestests/src/com/android/server/job/BackgroundRestrictionsTest.java
@@ -71,7 +71,7 @@
private static final String TEST_APP_PACKAGE = "com.android.servicestests.apps.jobtestapp";
private static final String TEST_APP_ACTIVITY = TEST_APP_PACKAGE + ".TestJobActivity";
private static final long POLL_INTERVAL = 500;
- private static final long DEFAULT_WAIT_TIMEOUT = 5000;
+ private static final long DEFAULT_WAIT_TIMEOUT = 10_000;
private Context mContext;
private AppOpsManager mAppOpsManager;
diff --git a/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceUserPropertiesTest.java b/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceUserPropertiesTest.java
index 5ece871..1f952c4 100644
--- a/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceUserPropertiesTest.java
+++ b/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceUserPropertiesTest.java
@@ -61,11 +61,13 @@
.setStartWithParent(false)
.setShowInSettings(45)
.setInheritDevicePolicy(67)
+ .setUseParentsContacts(false)
.build();
final UserProperties actualProps = new UserProperties(defaultProps);
actualProps.setShowInLauncher(14);
actualProps.setShowInSettings(32);
actualProps.setInheritDevicePolicy(51);
+ actualProps.setUseParentsContacts(true);
// Write the properties to xml.
final ByteArrayOutputStream baos = new ByteArrayOutputStream();
@@ -152,11 +154,14 @@
// Items requiring hasManagePermission - put them here using hasManagePermission.
assertEqualGetterOrThrows(orig::getShowInSettings, copy::getShowInSettings,
hasManagePermission);
+ assertEqualGetterOrThrows(orig::getUseParentsContacts,
+ copy::getUseParentsContacts, hasManagePermission);
// Items requiring hasQueryPermission - put them here using hasQueryPermission.
// Items with no permission requirements.
assertEqualGetterOrThrows(orig::getShowInLauncher, copy::getShowInLauncher, true);
+
}
/**
@@ -196,7 +201,7 @@
assertThat(expected.getShowInLauncher()).isEqualTo(actual.getShowInLauncher());
assertThat(expected.getStartWithParent()).isEqualTo(actual.getStartWithParent());
assertThat(expected.getShowInSettings()).isEqualTo(actual.getShowInSettings());
- assertThat(expected.getInheritDevicePolicy()).isEqualTo(
- actual.getInheritDevicePolicy());
+ assertThat(expected.getInheritDevicePolicy()).isEqualTo(actual.getInheritDevicePolicy());
+ assertThat(expected.getUseParentsContacts()).isEqualTo(actual.getUseParentsContacts());
}
}
diff --git a/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceUserTypeTest.java b/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceUserTypeTest.java
index 5f48004..d7c1e37 100644
--- a/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceUserTypeTest.java
+++ b/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceUserTypeTest.java
@@ -83,7 +83,8 @@
/* flags= */0,
/* letsPersonalDataIntoProfile= */false).build());
final UserProperties.Builder userProps = new UserProperties.Builder()
- .setShowInLauncher(17);
+ .setShowInLauncher(17)
+ .setUseParentsContacts(true);
final UserTypeDetails type = new UserTypeDetails.Builder()
.setName("a.name")
.setEnabled(1)
@@ -140,6 +141,7 @@
}
assertEquals(17, type.getDefaultUserPropertiesReference().getShowInLauncher());
+ assertTrue(type.getDefaultUserPropertiesReference().getUseParentsContacts());
assertEquals(23, type.getBadgeLabel(0));
assertEquals(24, type.getBadgeLabel(1));
@@ -182,6 +184,7 @@
final UserProperties props = type.getDefaultUserPropertiesReference();
assertNotNull(props);
assertFalse(props.getStartWithParent());
+ assertFalse(props.getUseParentsContacts());
assertEquals(UserProperties.SHOW_IN_LAUNCHER_WITH_PARENT, props.getShowInLauncher());
assertFalse(type.hasBadge());
@@ -263,7 +266,8 @@
final Bundle restrictions = makeRestrictionsBundle("no_config_vpn", "no_config_tethering");
final UserProperties.Builder props = new UserProperties.Builder()
.setShowInLauncher(19)
- .setStartWithParent(true);
+ .setStartWithParent(true)
+ .setUseParentsContacts(true);
final ArrayMap<String, UserTypeDetails.Builder> builders = new ArrayMap<>();
builders.put(userTypeAosp1, new UserTypeDetails.Builder()
.setName(userTypeAosp1)
@@ -289,7 +293,9 @@
assertEquals(Resources.ID_NULL, aospType.getIconBadge());
assertTrue(UserRestrictionsUtils.areEqual(restrictions, aospType.getDefaultRestrictions()));
assertEquals(19, aospType.getDefaultUserPropertiesReference().getShowInLauncher());
- assertEquals(true, aospType.getDefaultUserPropertiesReference().getStartWithParent());
+ assertTrue(aospType.getDefaultUserPropertiesReference().getStartWithParent());
+ assertTrue(aospType.getDefaultUserPropertiesReference()
+ .getUseParentsContacts());
// userTypeAosp2 should be modified.
aospType = builders.get(userTypeAosp2).createUserTypeDetails();
@@ -319,7 +325,9 @@
makeRestrictionsBundle("no_remove_user", "no_bluetooth"),
aospType.getDefaultRestrictions()));
assertEquals(2020, aospType.getDefaultUserPropertiesReference().getShowInLauncher());
- assertEquals(false, aospType.getDefaultUserPropertiesReference().getStartWithParent());
+ assertFalse(aospType.getDefaultUserPropertiesReference().getStartWithParent());
+ assertFalse(aospType.getDefaultUserPropertiesReference()
+ .getUseParentsContacts());
// userTypeOem1 should be created.
UserTypeDetails.Builder customType = builders.get(userTypeOem1);
@@ -347,6 +355,7 @@
UserTypeDetails details = builders.get(userTypeFull).createUserTypeDetails();
assertEquals(UNLIMITED_NUMBER_OF_USERS, details.getMaxAllowedPerParent());
assertFalse(details.isEnabled());
+ assertEquals(17, details.getMaxAllowed());
assertTrue(UserRestrictionsUtils.areEqual(
makeRestrictionsBundle("no_remove_user", "no_bluetooth"),
details.getDefaultRestrictions()));
diff --git a/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java b/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java
index a3c45b7..2e7e583 100644
--- a/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java
+++ b/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java
@@ -19,6 +19,7 @@
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
+import static org.junit.Assert.assertFalse;
import static org.junit.Assert.fail;
import static org.junit.Assume.assumeTrue;
import static org.testng.Assert.assertThrows;
@@ -164,6 +165,14 @@
@Test
public void testCloneUser() throws Exception {
+
+ // Get the default properties for clone user type.
+ final UserTypeDetails userTypeDetails =
+ UserTypeFactory.getUserTypes().get(UserManager.USER_TYPE_PROFILE_CLONE);
+ assertWithMessage("No %s type on device", UserManager.USER_TYPE_PROFILE_CLONE)
+ .that(userTypeDetails).isNotNull();
+ final UserProperties typeProps = userTypeDetails.getDefaultUserPropertiesReference();
+
// Test that only one clone user can be created
final int primaryUserId = mUserManager.getPrimaryUser().id;
UserInfo userInfo = createProfileForUser("Clone user1",
@@ -187,6 +196,16 @@
.collect(Collectors.toList());
assertThat(cloneUsers.size()).isEqualTo(1);
+ // Check that the new clone user has the expected properties (relative to the defaults)
+ // provided that the test caller has the necessary permissions.
+ UserProperties cloneUserProperties =
+ mUserManager.getUserProperties(UserHandle.of(userInfo.id));
+ assertThat(typeProps.getUseParentsContacts())
+ .isEqualTo(cloneUserProperties.getUseParentsContacts());
+ assertThat(typeProps.getShowInLauncher())
+ .isEqualTo(cloneUserProperties.getShowInLauncher());
+ assertThrows(SecurityException.class, cloneUserProperties::getStartWithParent);
+
// Verify clone user parent
assertThat(mUserManager.getProfileParent(primaryUserId)).isNull();
UserInfo parentProfileInfo = mUserManager.getProfileParent(userInfo.id);
@@ -600,6 +619,7 @@
// provided that the test caller has the necessary permissions.
assertThat(userProps.getShowInLauncher()).isEqualTo(typeProps.getShowInLauncher());
assertThat(userProps.getShowInSettings()).isEqualTo(typeProps.getShowInSettings());
+ assertFalse(userProps.getUseParentsContacts());
assertThrows(SecurityException.class, userProps::getStartWithParent);
assertThrows(SecurityException.class, userProps::getInheritDevicePolicy);
}
diff --git a/services/tests/servicestests/test-apps/JobTestApp/src/com/android/servicestests/apps/jobtestapp/TestJobService.java b/services/tests/servicestests/test-apps/JobTestApp/src/com/android/servicestests/apps/jobtestapp/TestJobService.java
index 3e79407..b8585f2 100644
--- a/services/tests/servicestests/test-apps/JobTestApp/src/com/android/servicestests/apps/jobtestapp/TestJobService.java
+++ b/services/tests/servicestests/test-apps/JobTestApp/src/com/android/servicestests/apps/jobtestapp/TestJobService.java
@@ -34,7 +34,8 @@
public boolean onStartJob(JobParameters params) {
Log.i(TAG, "Test job executing: " + params.getJobId());
Intent reportJobStartIntent = new Intent(ACTION_JOB_STARTED);
- reportJobStartIntent.putExtra(JOB_PARAMS_EXTRA_KEY, params);
+ reportJobStartIntent.putExtra(JOB_PARAMS_EXTRA_KEY, params)
+ .addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
sendBroadcast(reportJobStartIntent);
return true;
}
@@ -43,7 +44,8 @@
public boolean onStopJob(JobParameters params) {
Log.i(TAG, "Test job stopped executing: " + params.getJobId());
Intent reportJobStopIntent = new Intent(ACTION_JOB_STOPPED);
- reportJobStopIntent.putExtra(JOB_PARAMS_EXTRA_KEY, params);
+ reportJobStopIntent.putExtra(JOB_PARAMS_EXTRA_KEY, params)
+ .addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
sendBroadcast(reportJobStopIntent);
// Deadline constraint is dropped on reschedule, so it's more reliable to use a new job.
return false;
diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityMetricsLaunchObserverTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityMetricsLaunchObserverTests.java
index 376399a..85c4975 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityMetricsLaunchObserverTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityMetricsLaunchObserverTests.java
@@ -324,7 +324,7 @@
// The activity reports fully drawn before windows drawn, then the fully drawn event will
// be pending (see {@link WindowingModeTransitionInfo#pendingFullyDrawn}).
- mActivityMetricsLogger.logAppTransitionReportedDrawn(mTopActivity, false);
+ mActivityMetricsLogger.notifyFullyDrawn(mTopActivity, false /* restoredFromBundle */);
notifyTransitionStarting(mTopActivity);
// The pending fully drawn event should send when the actual windows drawn event occurs.
final ActivityMetricsLogger.TransitionInfoSnapshot info = notifyWindowsDrawn(mTopActivity);
@@ -337,7 +337,7 @@
verifyNoMoreInteractions(mLaunchObserver);
final ActivityMetricsLogger.TransitionInfoSnapshot fullyDrawnInfo = mActivityMetricsLogger
- .logAppTransitionReportedDrawn(mTopActivity, false /* restoredFromBundle */);
+ .notifyFullyDrawn(mTopActivity, false /* restoredFromBundle */);
assertWithMessage("Invisible event must be dropped").that(fullyDrawnInfo).isNull();
}
diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
index 3a8e1cc..bd8da4e 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
@@ -2284,8 +2284,7 @@
doReturn(false).when(mAtm).shouldDisableNonVrUiLocked();
spyOn(mDisplayContent.mDwpcHelper);
- doReturn(false).when(mDisplayContent.mDwpcHelper).isWindowingModeSupported(
- WINDOWING_MODE_PINNED);
+ doReturn(false).when(mDisplayContent.mDwpcHelper).isEnteringPipAllowed(anyInt());
assertFalse(activity.checkEnterPictureInPictureState("TEST", false /* beforeStopping */));
}
diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTests.java
index 52af8ad..d99946f 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTests.java
@@ -45,6 +45,7 @@
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
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.Mockito.mock;
@@ -375,10 +376,10 @@
displayPolicy.setCanSystemBarsBeShownByUser(false);
displayPolicy.requestTransientBars(windowState, true);
- verify(controlTarget, never()).showInsets(anyInt(), anyBoolean());
+ verify(controlTarget, never()).showInsets(anyInt(), anyBoolean(), any() /* statsToken */);
displayPolicy.setCanSystemBarsBeShownByUser(true);
displayPolicy.requestTransientBars(windowState, true);
- verify(controlTarget).showInsets(anyInt(), anyBoolean());
+ verify(controlTarget).showInsets(anyInt(), anyBoolean(), any() /* statsToken */);
}
}
diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayWindowPolicyControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayWindowPolicyControllerTests.java
index 21197ba..db1d15a 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayWindowPolicyControllerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayWindowPolicyControllerTests.java
@@ -246,5 +246,10 @@
public boolean canShowTasksInRecents() {
return true;
}
+
+ @Override
+ public boolean isEnteringPipAllowed(int uid) {
+ return true;
+ }
}
}
diff --git a/services/tests/wmtests/src/com/android/server/wm/ImeInsetsSourceProviderTest.java b/services/tests/wmtests/src/com/android/server/wm/ImeInsetsSourceProviderTest.java
index eb8b89d..a26cad9 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ImeInsetsSourceProviderTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ImeInsetsSourceProviderTest.java
@@ -55,7 +55,7 @@
mDisplayContent.setImeControlTarget(popup);
mDisplayContent.setImeLayeringTarget(appWin);
popup.mAttrs.format = PixelFormat.TRANSPARENT;
- mImeProvider.scheduleShowImePostLayout(appWin);
+ mImeProvider.scheduleShowImePostLayout(appWin, null /* statsToken */);
assertTrue(mImeProvider.isReadyToShowIme());
}
@@ -64,7 +64,7 @@
WindowState target = createWindow(null, TYPE_APPLICATION, "app");
mDisplayContent.setImeLayeringTarget(target);
mDisplayContent.updateImeInputAndControlTarget(target);
- mImeProvider.scheduleShowImePostLayout(target);
+ mImeProvider.scheduleShowImePostLayout(target, null /* statsToken */);
assertTrue(mImeProvider.isReadyToShowIme());
}
@@ -78,7 +78,7 @@
mDisplayContent.setImeLayeringTarget(target);
mDisplayContent.setImeControlTarget(target);
- mImeProvider.scheduleShowImePostLayout(target);
+ mImeProvider.scheduleShowImePostLayout(target, null /* statsToken */);
assertFalse(mImeProvider.isImeShowing());
mImeProvider.checkShowImePostLayout();
assertTrue(mImeProvider.isImeShowing());
diff --git a/services/tests/wmtests/src/com/android/server/wm/TestIWindow.java b/services/tests/wmtests/src/com/android/server/wm/TestIWindow.java
index bb5aceb..6e72bf3 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TestIWindow.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TestIWindow.java
@@ -16,6 +16,7 @@
package com.android.server.wm;
+import android.annotation.Nullable;
import android.os.Bundle;
import android.os.ParcelFileDescriptor;
import android.os.RemoteException;
@@ -26,6 +27,7 @@
import android.view.InsetsSourceControl;
import android.view.InsetsState;
import android.view.ScrollCaptureResponse;
+import android.view.inputmethod.ImeTracker;
import android.window.ClientWindowFrames;
import com.android.internal.os.IResultReceiver;
@@ -117,10 +119,12 @@
}
@Override
- public void showInsets(int types, boolean fromIme) throws RemoteException {
+ public void showInsets(int types, boolean fromIme, @Nullable ImeTracker.Token statsToken)
+ throws RemoteException {
}
@Override
- public void hideInsets(int types, boolean fromIme) throws RemoteException {
+ public void hideInsets(int types, boolean fromIme, @Nullable ImeTracker.Token statsToken)
+ throws RemoteException {
}
}
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 0139f6a..6bd3412 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java
@@ -1000,7 +1000,7 @@
mDisplayContent.setImeLayeringTarget(app);
mDisplayContent.setImeInputTarget(app);
assertTrue(mDisplayContent.shouldImeAttachedToApp());
- controller.getImeSourceProvider().scheduleShowImePostLayout(app);
+ controller.getImeSourceProvider().scheduleShowImePostLayout(app, null /* statsToken */);
controller.getImeSourceProvider().getSource().setVisible(true);
controller.updateAboveInsetsState(false);
@@ -1037,7 +1037,7 @@
mDisplayContent.setImeLayeringTarget(app);
mDisplayContent.setImeInputTarget(app);
assertTrue(mDisplayContent.shouldImeAttachedToApp());
- controller.getImeSourceProvider().scheduleShowImePostLayout(app);
+ controller.getImeSourceProvider().scheduleShowImePostLayout(app, null /* statsToken */);
controller.getImeSourceProvider().getSource().setVisible(true);
controller.updateAboveInsetsState(false);
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 eca7cbb..ab042d1 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
@@ -100,6 +100,7 @@
import android.view.View;
import android.view.WindowManager;
import android.view.WindowManager.DisplayImePolicy;
+import android.view.inputmethod.ImeTracker;
import android.window.ITransitionPlayer;
import android.window.ScreenCapture;
import android.window.StartingWindowInfo;
@@ -848,11 +849,13 @@
}
@Override
- public void showInsets(int i, boolean b) throws RemoteException {
+ public void showInsets(int i, boolean b, @Nullable ImeTracker.Token t)
+ throws RemoteException {
}
@Override
- public void hideInsets(int i, boolean b) throws RemoteException {
+ public void hideInsets(int i, boolean b, @Nullable ImeTracker.Token t)
+ throws RemoteException {
}
@Override
diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java
index 1fc54d6..0721c28 100644
--- a/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java
+++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java
@@ -19,7 +19,6 @@
import static android.Manifest.permission.CAPTURE_AUDIO_HOTWORD;
import static android.Manifest.permission.RECORD_AUDIO;
import static android.service.attention.AttentionService.PROXIMITY_UNKNOWN;
-import static android.service.voice.HotwordDetectedResult.EXTRA_PROXIMITY_METERS;
import static android.service.voice.HotwordDetectionService.AUDIO_SOURCE_EXTERNAL;
import static android.service.voice.HotwordDetectionService.AUDIO_SOURCE_MICROPHONE;
import static android.service.voice.HotwordDetectionService.ENABLE_PROXIMITY_RESULT;
@@ -206,7 +205,7 @@
@Nullable AttentionManagerInternal mAttentionManagerInternal = null;
final AttentionManagerInternal.ProximityUpdateCallbackInternal mProximityCallbackInternal =
- this::setProximityMeters;
+ this::setProximityValue;
volatile HotwordDetectionServiceIdentity mIdentity;
@@ -504,7 +503,7 @@
mSoftwareCallback.onError();
return;
}
- saveProximityMetersToBundle(result);
+ saveProximityValueToBundle(result);
HotwordDetectedResult newResult;
try {
newResult = mHotwordAudioStreamManager.startCopyingAudioStreams(result);
@@ -639,7 +638,7 @@
externalCallback.onError(CALLBACK_ONDETECTED_GOT_SECURITY_EXCEPTION);
return;
}
- saveProximityMetersToBundle(result);
+ saveProximityValueToBundle(result);
HotwordDetectedResult newResult;
try {
newResult = mHotwordAudioStreamManager.startCopyingAudioStreams(result);
@@ -1242,15 +1241,15 @@
});
}
- private void saveProximityMetersToBundle(HotwordDetectedResult result) {
+ private void saveProximityValueToBundle(HotwordDetectedResult result) {
synchronized (mLock) {
if (result != null && mProximityMeters != PROXIMITY_UNKNOWN) {
- result.getExtras().putDouble(EXTRA_PROXIMITY_METERS, mProximityMeters);
+ result.setProximity(mProximityMeters);
}
}
}
- private void setProximityMeters(double proximityMeters) {
+ private void setProximityValue(double proximityMeters) {
synchronized (mLock) {
mProximityMeters = proximityMeters;
}
diff --git a/telecomm/java/android/telecom/Call.java b/telecomm/java/android/telecom/Call.java
index 432af3a..5cef2cb 100644
--- a/telecomm/java/android/telecom/Call.java
+++ b/telecomm/java/android/telecom/Call.java
@@ -168,6 +168,18 @@
public static final String AVAILABLE_PHONE_ACCOUNTS = "selectPhoneAccountAccounts";
/**
+ * Extra key intended for {@link InCallService}s that notify the user of an incoming call. When
+ * EXTRA_IS_SUPPRESSED_BY_DO_NOT_DISTURB returns true, the {@link InCallService} should not
+ * interrupt the user of the incoming call because the call is being suppressed by Do Not
+ * Disturb settings.
+ *
+ * This extra will be removed from the {@link Call} object for {@link InCallService}s that do
+ * not hold the {@link android.Manifest.permission#READ_CONTACTS} permission.
+ */
+ public static final String EXTRA_IS_SUPPRESSED_BY_DO_NOT_DISTURB =
+ "android.telecom.extra.IS_SUPPRESSED_BY_DO_NOT_DISTURB";
+
+ /**
* Key for extra used to pass along a list of {@link PhoneAccountSuggestion}s to the in-call
* UI when a call enters the {@link #STATE_SELECT_PHONE_ACCOUNT} state. The list included here
* will have the same length and be in the same order as the list passed with
diff --git a/telecomm/java/android/telecom/ParcelableCallAnalytics.java b/telecomm/java/android/telecom/ParcelableCallAnalytics.java
index ff87ab0..a69dfb0b 100644
--- a/telecomm/java/android/telecom/ParcelableCallAnalytics.java
+++ b/telecomm/java/android/telecom/ParcelableCallAnalytics.java
@@ -111,6 +111,8 @@
public static final int FILTERING_INITIATED = 106;
public static final int FILTERING_COMPLETED = 107;
public static final int FILTERING_TIMED_OUT = 108;
+ public static final int DND_CHECK_INITIATED = 109;
+ public static final int DND_CHECK_COMPLETED = 110;
public static final int SKIP_RINGING = 200;
public static final int SILENCE = 201;
@@ -195,6 +197,7 @@
public static final int BLOCK_CHECK_FINISHED_TIMING = 9;
public static final int FILTERING_COMPLETED_TIMING = 10;
public static final int FILTERING_TIMED_OUT_TIMING = 11;
+ public static final int DND_PRE_CALL_PRE_CHECK_TIMING = 12;
/** {@hide} */
public static final int START_CONNECTION_TO_REQUEST_DISCONNECT_TIMING = 12;
diff --git a/telephony/java/android/telephony/TelephonyManager.java b/telephony/java/android/telephony/TelephonyManager.java
index e099e69..d473c6a 100644
--- a/telephony/java/android/telephony/TelephonyManager.java
+++ b/telephony/java/android/telephony/TelephonyManager.java
@@ -17339,7 +17339,7 @@
* Subsequent attempts will return the same error until the request is made on the default
* data subscription.
*/
- public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_NOT_DEFAULT_DATA_SUB = 14;
+ public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_NOT_DEFAULT_DATA_SUBSCRIPTION = 14;
/**
* Purchase premium capability was successful and is waiting for the network to setup the
@@ -17369,7 +17369,7 @@
PURCHASE_PREMIUM_CAPABILITY_RESULT_FEATURE_NOT_SUPPORTED,
PURCHASE_PREMIUM_CAPABILITY_RESULT_NETWORK_NOT_AVAILABLE,
PURCHASE_PREMIUM_CAPABILITY_RESULT_ENTITLEMENT_CHECK_FAILED,
- PURCHASE_PREMIUM_CAPABILITY_RESULT_NOT_DEFAULT_DATA_SUB,
+ PURCHASE_PREMIUM_CAPABILITY_RESULT_NOT_DEFAULT_DATA_SUBSCRIPTION,
PURCHASE_PREMIUM_CAPABILITY_RESULT_PENDING_NETWORK_SETUP})
public @interface PurchasePremiumCapabilityResult {}
@@ -17409,8 +17409,8 @@
return "NETWORK_NOT_AVAILABLE";
case PURCHASE_PREMIUM_CAPABILITY_RESULT_ENTITLEMENT_CHECK_FAILED:
return "ENTITLEMENT_CHECK_FAILED";
- case PURCHASE_PREMIUM_CAPABILITY_RESULT_NOT_DEFAULT_DATA_SUB:
- return "NOT_DEFAULT_DATA_SUB";
+ case PURCHASE_PREMIUM_CAPABILITY_RESULT_NOT_DEFAULT_DATA_SUBSCRIPTION:
+ return "NOT_DEFAULT_DATA_SUBSCRIPTION";
case PURCHASE_PREMIUM_CAPABILITY_RESULT_PENDING_NETWORK_SETUP:
return "PENDING_NETWORK_SETUP";
default:
diff --git a/tests/FlickerTests/AndroidTest.xml b/tests/FlickerTests/AndroidTest.xml
index d91aa1e..a7d6a01 100644
--- a/tests/FlickerTests/AndroidTest.xml
+++ b/tests/FlickerTests/AndroidTest.xml
@@ -15,6 +15,8 @@
<option name="run-command" value="cmd window tracing frame" />
<!-- ensure lock screen mode is swipe -->
<option name="run-command" value="locksettings set-disabled false" />
+ <!-- disable betterbug as it's log collection dialogues cause flakes in e2e tests -->
+ <option name="run-command" value="pm disable com.google.android.internal.betterbug" />
<!-- restart launcher to activate TAPL -->
<option name="run-command" value="setprop ro.test_harness 1 ; am force-stop com.google.android.apps.nexuslauncher" />
</target_preparer>