diff options
88 files changed, 2589 insertions, 852 deletions
diff --git a/core/api/current.txt b/core/api/current.txt index bc9f10fc7a1a..f550952fc4cf 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -6969,6 +6969,7 @@ package android.app { method @Deprecated public void onStart(android.content.Intent, int); method public int onStartCommand(android.content.Intent, int, int); method public void onTaskRemoved(android.content.Intent); + method public void onTimeout(int); method public void onTrimMemory(int); method public boolean onUnbind(android.content.Intent); method public final void startForeground(int, android.app.Notification); @@ -12475,6 +12476,7 @@ package android.content.pm { field @Deprecated public static final int FOREGROUND_SERVICE_TYPE_NONE = 0; // 0x0 field @RequiresPermission(allOf={android.Manifest.permission.FOREGROUND_SERVICE_PHONE_CALL}, anyOf={android.Manifest.permission.MANAGE_OWN_CALLS}, conditional=true) public static final int FOREGROUND_SERVICE_TYPE_PHONE_CALL = 4; // 0x4 field @RequiresPermission(value=android.Manifest.permission.FOREGROUND_SERVICE_REMOTE_MESSAGING, conditional=true) public static final int FOREGROUND_SERVICE_TYPE_REMOTE_MESSAGING = 512; // 0x200 + field public static final int FOREGROUND_SERVICE_TYPE_SHORT_SERVICE = 2048; // 0x800 field @RequiresPermission(value=android.Manifest.permission.FOREGROUND_SERVICE_SPECIAL_USE, conditional=true) public static final int FOREGROUND_SERVICE_TYPE_SPECIAL_USE = 1073741824; // 0x40000000 field @RequiresPermission(value=android.Manifest.permission.FOREGROUND_SERVICE_SYSTEM_EXEMPTED, conditional=true) public static final int FOREGROUND_SERVICE_TYPE_SYSTEM_EXEMPTED = 1024; // 0x400 field public int flags; @@ -13025,10 +13027,12 @@ package android.credentials { } public final class CreateCredentialRequest implements android.os.Parcelable { - ctor public CreateCredentialRequest(@NonNull String, @NonNull android.os.Bundle); + ctor public CreateCredentialRequest(@NonNull String, @NonNull android.os.Bundle, @NonNull android.os.Bundle, boolean); method public int describeContents(); - method @NonNull public android.os.Bundle getData(); + method @NonNull public android.os.Bundle getCandidateQueryData(); + method @NonNull public android.os.Bundle getCredentialData(); method @NonNull public String getType(); + method public boolean requireSystemProvider(); method public void writeToParcel(@NonNull android.os.Parcel, int); field @NonNull public static final android.os.Parcelable.Creator<android.credentials.CreateCredentialRequest> CREATOR; } @@ -13066,10 +13070,11 @@ package android.credentials { } public final class GetCredentialOption implements android.os.Parcelable { - ctor public GetCredentialOption(@NonNull String, @NonNull android.os.Bundle); + ctor public GetCredentialOption(@NonNull String, @NonNull android.os.Bundle, boolean); method public int describeContents(); method @NonNull public android.os.Bundle getData(); method @NonNull public String getType(); + method public boolean requireSystemProvider(); method public void writeToParcel(@NonNull android.os.Parcel, int); field @NonNull public static final android.os.Parcelable.Creator<android.credentials.GetCredentialOption> CREATOR; } @@ -32697,7 +32702,6 @@ package android.os { method @NonNull @RequiresPermission(anyOf={"android.permission.MANAGE_USERS", "android.permission.QUERY_USERS", "android.permission.INTERACT_ACROSS_USERS"}, conditional=true) public android.content.pm.UserProperties getUserProperties(@NonNull android.os.UserHandle); method public android.os.Bundle getUserRestrictions(); method @RequiresPermission(anyOf={"android.permission.MANAGE_USERS", "android.permission.INTERACT_ACROSS_USERS"}, conditional=true) public android.os.Bundle getUserRestrictions(android.os.UserHandle); - method @NonNull @RequiresPermission(anyOf={"android.permission.MANAGE_USERS", "android.permission.INTERACT_ACROSS_USERS"}) public java.util.Set<android.os.UserHandle> getVisibleUsers(); method public boolean hasUserRestriction(String); method public boolean isDemoUser(); method public static boolean isHeadlessSystemUserMode(); @@ -32711,7 +32715,6 @@ package android.os { method @RequiresPermission(anyOf={"android.permission.MANAGE_USERS", "android.permission.INTERACT_ACROSS_USERS"}, conditional=true) public boolean isUserRunningOrStopping(android.os.UserHandle); method public boolean isUserUnlocked(); method @RequiresPermission(anyOf={"android.permission.MANAGE_USERS", "android.permission.INTERACT_ACROSS_USERS"}, conditional=true) public boolean isUserUnlocked(android.os.UserHandle); - method public boolean isUserVisible(); method @RequiresPermission(anyOf={"android.permission.MANAGE_USERS", "android.permission.MODIFY_QUIET_MODE"}, conditional=true) public boolean requestQuietModeEnabled(boolean, @NonNull android.os.UserHandle); method public boolean requestQuietModeEnabled(boolean, @NonNull android.os.UserHandle, int); method @Deprecated public boolean setRestrictionsChallenge(String); diff --git a/core/api/system-current.txt b/core/api/system-current.txt index c47e3b2f0e91..13914c54a813 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -2998,13 +2998,13 @@ package android.companion.virtual { public static final class VirtualDeviceParams.Builder { ctor public VirtualDeviceParams.Builder(); - method @NonNull public android.companion.virtual.VirtualDeviceParams.Builder addDevicePolicy(int, int); method @NonNull public android.companion.virtual.VirtualDeviceParams.Builder addVirtualSensorConfig(@NonNull android.companion.virtual.sensor.VirtualSensorConfig); method @NonNull public android.companion.virtual.VirtualDeviceParams build(); method @NonNull public android.companion.virtual.VirtualDeviceParams.Builder setAllowedActivities(@NonNull java.util.Set<android.content.ComponentName>); method @NonNull public android.companion.virtual.VirtualDeviceParams.Builder setAllowedCrossTaskNavigations(@NonNull java.util.Set<android.content.ComponentName>); method @NonNull public android.companion.virtual.VirtualDeviceParams.Builder setBlockedActivities(@NonNull java.util.Set<android.content.ComponentName>); method @NonNull public android.companion.virtual.VirtualDeviceParams.Builder setBlockedCrossTaskNavigations(@NonNull java.util.Set<android.content.ComponentName>); + method @NonNull public android.companion.virtual.VirtualDeviceParams.Builder setDevicePolicy(int, int); method @NonNull @RequiresPermission(value=android.Manifest.permission.ADD_ALWAYS_UNLOCKED_DISPLAY, conditional=true) public android.companion.virtual.VirtualDeviceParams.Builder setLockState(int); method @NonNull public android.companion.virtual.VirtualDeviceParams.Builder setName(@NonNull String); method @NonNull public android.companion.virtual.VirtualDeviceParams.Builder setUsersWithMatchingAccounts(@NonNull java.util.Set<android.os.UserHandle>); @@ -10097,6 +10097,7 @@ package android.os { method @NonNull public java.util.List<android.os.UserHandle> getAllProfiles(); method @NonNull public java.util.List<android.os.UserHandle> getEnabledProfiles(); method @Nullable @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.CREATE_USERS, android.Manifest.permission.QUERY_USERS}) public android.os.UserHandle getMainUser(); + method @Nullable @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.CREATE_USERS, android.Manifest.permission.QUERY_USERS}) public android.os.UserHandle getPreviousForegroundUser(); method @Nullable @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.INTERACT_ACROSS_USERS}) public android.os.UserHandle getProfileParent(@NonNull android.os.UserHandle); method @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.CREATE_USERS, android.Manifest.permission.QUERY_USERS}) public int getRemainingCreatableProfileCount(@NonNull String); method @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.CREATE_USERS, android.Manifest.permission.QUERY_USERS}) public int getRemainingCreatableUserCount(@NonNull String); @@ -10110,6 +10111,7 @@ package android.os { method @Deprecated @android.os.UserManager.UserRestrictionSource @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.QUERY_USERS}) public int getUserRestrictionSource(String, android.os.UserHandle); method @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.QUERY_USERS}) public java.util.List<android.os.UserManager.EnforcingUser> getUserRestrictionSources(String, android.os.UserHandle); method @RequiresPermission(allOf={android.Manifest.permission.READ_PHONE_STATE, android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.INTERACT_ACROSS_USERS}, conditional=true) public int getUserSwitchability(); + method @NonNull @RequiresPermission(anyOf={"android.permission.INTERACT_ACROSS_USERS", "android.permission.MANAGE_USERS"}) public java.util.Set<android.os.UserHandle> getVisibleUsers(); method @RequiresPermission(android.Manifest.permission.MANAGE_USERS) public boolean hasRestrictedProfiles(); method @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.INTERACT_ACROSS_USERS}, conditional=true) public boolean hasUserRestrictionForUser(@NonNull String, @NonNull android.os.UserHandle); method @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.CREATE_USERS, android.Manifest.permission.QUERY_USERS}) public boolean isAdminUser(); @@ -10127,6 +10129,7 @@ package android.os { method @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.CREATE_USERS, android.Manifest.permission.QUERY_USERS, android.Manifest.permission.GET_ACCOUNTS_PRIVILEGED}) public boolean isUserNameSet(); method @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.CREATE_USERS, android.Manifest.permission.QUERY_USERS}) public boolean isUserOfType(@NonNull String); method @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.INTERACT_ACROSS_USERS}, conditional=true) public boolean isUserUnlockingOrUnlocked(@NonNull android.os.UserHandle); + method @RequiresPermission(anyOf={"android.permission.INTERACT_ACROSS_USERS", "android.permission.MANAGE_USERS"}) public boolean isUserVisible(); method @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.CREATE_USERS}) public boolean removeUser(@NonNull android.os.UserHandle); method @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.CREATE_USERS}) public int removeUserWhenPossible(@NonNull android.os.UserHandle, boolean); method @RequiresPermission(android.Manifest.permission.MANAGE_USERS) public void setUserIcon(@NonNull android.graphics.Bitmap) throws android.os.UserManager.UserOperationException; diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java index 884870bff3af..96ced41f36ca 100644 --- a/core/java/android/app/ActivityThread.java +++ b/core/java/android/app/ActivityThread.java @@ -502,6 +502,7 @@ public final class ActivityThread extends ClientTransactionHandler @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023) static volatile Handler sMainThreadHandler; // set once in main() + private long mStartSeq; // Only accesssed from the main thread Bundle mCoreSettings = null; @@ -6809,6 +6810,14 @@ public final class ActivityThread extends ClientTransactionHandler Application app; final StrictMode.ThreadPolicy savedPolicy = StrictMode.allowThreadDiskWrites(); final StrictMode.ThreadPolicy writesAllowedPolicy = StrictMode.getThreadPolicy(); + + final IActivityManager mgr = ActivityManager.getService(); + try { + mgr.finishAttachApplication(mStartSeq); + } catch (RemoteException ex) { + throw ex.rethrowFromSystemServer(); + } + try { // If the app is being launched for full backup or restore, bring it up in // a restricted environment with the base application class. @@ -7649,6 +7658,8 @@ public final class ActivityThread extends ClientTransactionHandler sCurrentActivityThread = this; mConfigurationController = new ConfigurationController(this); mSystemThread = system; + mStartSeq = startSeq; + if (!system) { android.ddm.DdmHandleAppName.setAppName("<pre-initialized>", UserHandle.myUserId()); diff --git a/core/java/android/app/IActivityManager.aidl b/core/java/android/app/IActivityManager.aidl index 902f172b6ad7..3edaabde5f2a 100644 --- a/core/java/android/app/IActivityManager.aidl +++ b/core/java/android/app/IActivityManager.aidl @@ -147,6 +147,7 @@ interface IActivityManager { oneway void finishReceiver(in IBinder who, int resultCode, in String resultData, in Bundle map, boolean abortBroadcast, int flags); void attachApplication(in IApplicationThread app, long startSeq); + void finishAttachApplication(long startSeq); List<ActivityManager.RunningTaskInfo> getTasks(int maxNum); @UnsupportedAppUsage void moveTaskToFront(in IApplicationThread caller, in String callingPackage, int task, @@ -718,8 +719,8 @@ interface IActivityManager { /** * Control the app freezer state. Returns true in case of success, false if the operation - * didn't succeed (for example, when the app freezer isn't supported). - * Handling the freezer state via this method is reentrant, that is it can be + * didn't succeed (for example, when the app freezer isn't supported). + * Handling the freezer state via this method is reentrant, that is it can be * disabled and re-enabled multiple times in parallel. As long as there's a 1:1 disable to * enable match, the freezer is re-enabled at last enable only. * @param enable set it to true to enable the app freezer, false to disable it. diff --git a/core/java/android/app/Service.java b/core/java/android/app/Service.java index 6d7a161d1687..3a7d483c69c3 100644 --- a/core/java/android/app/Service.java +++ b/core/java/android/app/Service.java @@ -1128,12 +1128,10 @@ public abstract class Service extends ContextWrapper implements ComponentCallbac /** * Callback called on timeout for {@link ServiceInfo#FOREGROUND_SERVICE_TYPE_SHORT_SERVICE}. + * See {@link ServiceInfo#FOREGROUND_SERVICE_TYPE_SHORT_SERVICE} for more details. * - * TODO Implement it - * TODO Javadoc - * - * @param startId - * @hide + * @param startId the startId passed to {@link #onStartCommand(Intent, int, int)} when + * the service started. */ public void onTimeout(int startId) { } diff --git a/core/java/android/companion/virtual/VirtualDeviceParams.java b/core/java/android/companion/virtual/VirtualDeviceParams.java index bad26c6ed10d..e2b5c5d74efe 100644 --- a/core/java/android/companion/virtual/VirtualDeviceParams.java +++ b/core/java/android/companion/virtual/VirtualDeviceParams.java @@ -313,7 +313,7 @@ public final class VirtualDeviceParams implements Parcelable { * Returns the policy specified for this policy type, or {@link #DEVICE_POLICY_DEFAULT} if no * policy for this type has been explicitly specified. * - * @see Builder#addDevicePolicy + * @see Builder#setDevicePolicy */ public @DevicePolicy int getDevicePolicy(@PolicyType int policyType) { return mDevicePolicies.get(policyType, DEVICE_POLICY_DEFAULT); @@ -615,7 +615,7 @@ public final class VirtualDeviceParams implements Parcelable { } /** - * Add a policy for this virtual device. + * Specifies a policy for this virtual device. * * Policies define the system behavior that may be specific for this virtual device. A * policy can be defined for each {@code PolicyType}, but they are all optional. @@ -624,7 +624,7 @@ public final class VirtualDeviceParams implements Parcelable { * @param devicePolicy the value of the policy, i.e. how to interpret the device behavior. */ @NonNull - public Builder addDevicePolicy(@PolicyType int policyType, @DevicePolicy int devicePolicy) { + public Builder setDevicePolicy(@PolicyType int policyType, @DevicePolicy int devicePolicy) { mDevicePolicies.put(policyType, devicePolicy); return this; } @@ -632,13 +632,13 @@ public final class VirtualDeviceParams implements Parcelable { /** * Adds a configuration for a sensor that should be created for this virtual device. * - * Device sensors must remain valid for the entire lifetime of the device, hence they are + * <p>Device sensors must remain valid for the entire lifetime of the device, hence they are * created together with the device itself, and removed when the device is removed. * - * Requires {@link #DEVICE_POLICY_CUSTOM} to be set for {@link #POLICY_TYPE_SENSORS}. + * <p>Requires {@link #DEVICE_POLICY_CUSTOM} to be set for {@link #POLICY_TYPE_SENSORS}. * * @see android.companion.virtual.sensor.VirtualSensor - * @see #addDevicePolicy + * @see #setDevicePolicy */ @NonNull public Builder addVirtualSensorConfig(@NonNull VirtualSensorConfig virtualSensorConfig) { diff --git a/core/java/android/content/pm/ServiceInfo.java b/core/java/android/content/pm/ServiceInfo.java index 9e6cf62f2397..7ea6733fa2ff 100644 --- a/core/java/android/content/pm/ServiceInfo.java +++ b/core/java/android/content/pm/ServiceInfo.java @@ -366,24 +366,48 @@ public class ServiceInfo extends ComponentInfo public static final int FOREGROUND_SERVICE_TYPE_SYSTEM_EXEMPTED = 1 << 10; /** - * Foreground service type corresponding to {@code shortService} in - * the {@link android.R.attr#foregroundServiceType} attribute. + * A foreground service type for "short-lived" services, which corresponds to + * {@code shortService} in the {@link android.R.attr#foregroundServiceType} attribute in the + * manifest. * - * TODO Implement it + * <p>Unlike other foreground service types, this type is not associated with a specific use + * case, and it will not require any special permissions + * (besides {@link Manifest.permission#FOREGROUND_SERVICE}). * - * TODO Expand the javadoc + * However, this type has the following restrictions. * - * This type is not associated with specific use cases unlike other types, but this has - * unique restrictions. * <ul> - * <li>Has a timeout - * <li>Cannot start other foreground services from this * <li> - * </ul> + * The type has a 1 minute timeout. + * A foreground service of this type must be stopped within the timeout by + * {@link android.app.Service#stopSelf), + * or {@link android.content.Context#stopService). + * {@link android.app.Service#stopForeground) will also work, which will demote the + * service to a "background" service, which will soon be stopped by the system. * - * @see Service#onTimeout + * <p>The system will <em>not</em> automatically stop it. * - * @hide + * <p>If the service isn't stopped within the timeout, + * {@link android.app.Service#onTimeout(int)} will be called. + * If the service is still not stopped after the callback, + * the app will be declared an ANR. + * + * <li> + * A foreground service of this type cannot be made "sticky" + * (see {@link android.app.Service#START_STICKY}). That is, if an app is killed + * due to a crash or out-of memory while it's running a short foregorund-service, + * the system will not restart the service. + * <li> + * Other foreground services cannot be started from short foreground services. + * Unlike other foreground service types, when an app is running in the background + * while only having a "short" foreground service, it's not allowed to start + * other foreground services, due to the restriction describe here: + * <a href="/guide/components/foreground-services#background-start-restrictions> + * Restrictions on background starts + * </a> + * </ul> + * + * @see android.app.Service#onTimeout(int) */ public static final int FOREGROUND_SERVICE_TYPE_SHORT_SERVICE = 1 << 11; diff --git a/core/java/android/content/res/loader/ResourcesProvider.java b/core/java/android/content/res/loader/ResourcesProvider.java index 463dcaced723..a5a1fa689f9b 100644 --- a/core/java/android/content/res/loader/ResourcesProvider.java +++ b/core/java/android/content/res/loader/ResourcesProvider.java @@ -18,7 +18,10 @@ package android.content.res.loader; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.SuppressLint; import android.content.Context; +import android.content.om.OverlayInfo; +import android.content.om.OverlayManager; import android.content.pm.ApplicationInfo; import android.content.res.ApkAssets; import android.content.res.AssetFileDescriptor; @@ -27,11 +30,17 @@ import android.util.Log; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.content.om.OverlayManagerImpl; import com.android.internal.util.ArrayUtils; +import com.android.internal.util.Preconditions; import java.io.Closeable; import java.io.File; +import java.io.FileNotFoundException; import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Objects; /** * Provides methods to load resources data from APKs ({@code .apk}) and resources tables @@ -63,6 +72,48 @@ public class ResourcesProvider implements AutoCloseable, Closeable { } /** + * Creates a ResourcesProvider instance from the specified overlay information. + * + * <p>In order to enable the registered overlays, an application can create a {@link + * ResourcesProvider} instance according to the specified {@link OverlayInfo} instance and put + * them into a {@link ResourcesLoader} instance. The application calls {@link + * android.content.res.Resources#addLoaders(ResourcesLoader...)} to load the overlays. + * + * @param overlayInfo is the information about the specified overlay + * @return the resources provider instance for the {@code overlayInfo} + * @throws IOException when the files can't be loaded. + * @see OverlayManager#getOverlayInfosForTarget(String) to get the list of overlay info. + * @hide + */ + @SuppressLint("WrongConstant") // TODO(b/238713267): ApkAssets blocks PROPERTY_LOADER + @NonNull + public static ResourcesProvider loadOverlay(@NonNull OverlayInfo overlayInfo) + throws IOException { + Objects.requireNonNull(overlayInfo); + Preconditions.checkArgument(overlayInfo.isFabricated(), "Not accepted overlay"); + Preconditions.checkStringNotEmpty( + overlayInfo.getTargetOverlayableName(), "Without overlayable name"); + final String overlayName = + OverlayManagerImpl.checkOverlayNameValid(overlayInfo.getOverlayName()); + final String path = + Preconditions.checkStringNotEmpty( + overlayInfo.getBaseCodePath(), "Invalid base path"); + + final Path frroPath = Path.of(path); + if (!Files.isRegularFile(frroPath)) { + throw new FileNotFoundException("The frro file not found"); + } + final Path idmapPath = frroPath.getParent().resolve(overlayName + ".idmap"); + if (!Files.isRegularFile(idmapPath)) { + throw new FileNotFoundException("The idmap file not found"); + } + + return new ResourcesProvider( + ApkAssets.loadOverlayFromPath( + idmapPath.toString(), 0 /* flags: self targeting overlay */)); + } + + /** * Creates a ResourcesProvider from an APK ({@code .apk}) file descriptor. * * <p>The file descriptor is duplicated and the original may be closed by the application at any diff --git a/core/java/android/credentials/CreateCredentialRequest.java b/core/java/android/credentials/CreateCredentialRequest.java index 22ef23019dcd..45890392bed7 100644 --- a/core/java/android/credentials/CreateCredentialRequest.java +++ b/core/java/android/credentials/CreateCredentialRequest.java @@ -39,10 +39,17 @@ public final class CreateCredentialRequest implements Parcelable { private final String mType; /** - * The request data. + * The full credential creation request data. */ @NonNull - private final Bundle mData; + private final Bundle mCredentialData; + + /** + * The partial request data that will be sent to the provider during the initial creation + * candidate query stage. + */ + @NonNull + private final Bundle mCandidateQueryData; /** * Determines whether or not the request must only be fulfilled by a system provider. @@ -58,18 +65,39 @@ public final class CreateCredentialRequest implements Parcelable { } /** - * Returns the request data. + * Returns the full credential creation request data. + * + * For security reason, a provider will receive the request data in two stages. First it gets + * a partial request, {@link #getCandidateQueryData()} that do not contain sensitive user + * information; it uses this information to provide credential creation candidates that the + * [@code CredentialManager] will show to the user. Next, this full request data will be sent to + * a provider only if the user further grants the consent by choosing a candidate from the + * provider. + */ + @NonNull + public Bundle getCredentialData() { + return mCredentialData; + } + + /** + * Returns the partial request data that will be sent to the provider during the initial + * creation candidate query stage. + * + * For security reason, a provider will receive the request data in two stages. First it gets + * this partial request that do not contain sensitive user information; it uses this information + * to provide credential creation candidates that the [@code CredentialManager] will show to + * the user. Next, the full request data, {@link #getCredentialData()}, will be sent to a + * provider only if the user further grants the consent by choosing a candidate from the + * provider. */ @NonNull - public Bundle getData() { - return mData; + public Bundle getCandidateQueryData() { + return mCandidateQueryData; } /** * Returns true if the request must only be fulfilled by a system provider, and false * otherwise. - * - * @hide */ public boolean requireSystemProvider() { return mRequireSystemProvider; @@ -78,7 +106,8 @@ public final class CreateCredentialRequest implements Parcelable { @Override public void writeToParcel(@NonNull Parcel dest, int flags) { dest.writeString8(mType); - dest.writeBundle(mData); + dest.writeBundle(mCredentialData); + dest.writeBundle(mCandidateQueryData); dest.writeBoolean(mRequireSystemProvider); } @@ -91,7 +120,8 @@ public final class CreateCredentialRequest implements Parcelable { public String toString() { return "CreateCredentialRequest {" + "type=" + mType - + ", data=" + mData + + ", credentialData=" + mCredentialData + + ", candidateQueryData=" + mCandidateQueryData + ", requireSystemProvider=" + mRequireSystemProvider + "}"; } @@ -100,44 +130,37 @@ public final class CreateCredentialRequest implements Parcelable { * Constructs a {@link CreateCredentialRequest}. * * @param type the requested credential type - * @param data the request data - * - * @throws IllegalArgumentException If type is empty - */ - public CreateCredentialRequest(@NonNull String type, @NonNull Bundle data) { - this(type, data, /*requireSystemProvider=*/ false); - } - - /** - * Constructs a {@link CreateCredentialRequest}. - * - * @param type the requested credential type - * @param data the request data - * @param requireSystemProvider whether or not the request must only be fulfilled by a system - * provider + * @param credentialData the full credential creation request data + * @param candidateQueryData the partial request data that will be sent to the provider + * during the initial creation candidate query stage + * @param requireSystemProvider whether the request must only be fulfilled by a system provider * * @throws IllegalArgumentException If type is empty. - * - * @hide */ public CreateCredentialRequest( @NonNull String type, - @NonNull Bundle data, + @NonNull Bundle credentialData, + @NonNull Bundle candidateQueryData, boolean requireSystemProvider) { mType = Preconditions.checkStringNotEmpty(type, "type must not be empty"); - mData = requireNonNull(data, "data must not be null"); + mCredentialData = requireNonNull(credentialData, "credentialData must not be null"); + mCandidateQueryData = requireNonNull(candidateQueryData, + "candidateQueryData must not be null"); mRequireSystemProvider = requireSystemProvider; } private CreateCredentialRequest(@NonNull Parcel in) { String type = in.readString8(); - Bundle data = in.readBundle(); + Bundle credentialData = in.readBundle(); + Bundle candidateQueryData = in.readBundle(); boolean requireSystemProvider = in.readBoolean(); mType = type; AnnotationValidations.validate(NonNull.class, null, mType); - mData = data; - AnnotationValidations.validate(NonNull.class, null, mData); + mCredentialData = credentialData; + AnnotationValidations.validate(NonNull.class, null, mCredentialData); + mCandidateQueryData = candidateQueryData; + AnnotationValidations.validate(NonNull.class, null, mCandidateQueryData); mRequireSystemProvider = requireSystemProvider; } diff --git a/core/java/android/credentials/GetCredentialOption.java b/core/java/android/credentials/GetCredentialOption.java index a0d3c0b2e6ed..ed93daef20d3 100644 --- a/core/java/android/credentials/GetCredentialOption.java +++ b/core/java/android/credentials/GetCredentialOption.java @@ -67,8 +67,6 @@ public final class GetCredentialOption implements Parcelable { /** * Returns true if the request must only be fulfilled by a system provider, and false * otherwise. - * - * @hide */ public boolean requireSystemProvider() { return mRequireSystemProvider; @@ -100,24 +98,10 @@ public final class GetCredentialOption implements Parcelable { * * @param type the requested credential type * @param data the request data - * - * @throws IllegalArgumentException If type is empty - */ - public GetCredentialOption(@NonNull String type, @NonNull Bundle data) { - this(type, data, /*requireSystemProvider=*/ false); - } - - /** - * Constructs a {@link GetCredentialOption}. - * - * @param type the requested credential type - * @param data the request data * @param requireSystemProvider whether or not the request must only be fulfilled by a system * provider * * @throws IllegalArgumentException If type is empty. - * - * @hide */ public GetCredentialOption( @NonNull String type, diff --git a/core/java/android/os/IUserManager.aidl b/core/java/android/os/IUserManager.aidl index eae7ce06008c..d31540a65f2f 100644 --- a/core/java/android/os/IUserManager.aidl +++ b/core/java/android/os/IUserManager.aidl @@ -59,6 +59,7 @@ interface IUserManager { ParcelFileDescriptor getUserIcon(int userId); UserInfo getPrimaryUser(); int getMainUserId(); + int getPreviousFullUserToEnterForeground(); List<UserInfo> getUsers(boolean excludePartial, boolean excludeDying, boolean excludePreCreated); List<UserInfo> getProfiles(int userId, boolean enabledOnly); int[] getProfileIds(int userId, boolean enabledOnly); diff --git a/core/java/android/os/UserManager.java b/core/java/android/os/UserManager.java index 954d1fca4ce0..dd02e022c639 100644 --- a/core/java/android/os/UserManager.java +++ b/core/java/android/os/UserManager.java @@ -2976,8 +2976,15 @@ public class UserManager { * </ol> * * @return whether the user is visible at the moment, as defined above. + * + * @hide */ + @SystemApi @UserHandleAware + @RequiresPermission(anyOf = { + "android.permission.INTERACT_ACROSS_USERS", + "android.permission.MANAGE_USERS" + }) public boolean isUserVisible() { try { return mService.isUserVisible(mUserId); @@ -2990,9 +2997,14 @@ public class UserManager { * Gets the visible users (as defined by {@link #isUserVisible()}. * * @return visible users at the moment. + * + * @hide */ - @RequiresPermission(anyOf = {Manifest.permission.MANAGE_USERS, - Manifest.permission.INTERACT_ACROSS_USERS}) + @SystemApi + @RequiresPermission(anyOf = { + "android.permission.INTERACT_ACROSS_USERS", + "android.permission.MANAGE_USERS" + }) public @NonNull Set<UserHandle> getVisibleUsers() { ArraySet<UserHandle> result = new ArraySet<>(); try { @@ -4300,6 +4312,43 @@ public class UserManager { } /** + * Returns the user who was last in the foreground, not including the current user and not + * including profiles. + * + * <p>Returns {@code null} if there is no previous user, for example if there + * is only one full user (i.e. only one user which is not a profile) on the device. + * + * <p>This method may be used for example to find the user to switch back to if the + * current user is removed, or if creating a new user is aborted. + * + * <p>Note that reboots do not interrupt this calculation; the previous user need not have + * used the device since it rebooted. + * + * <p>Note also that on devices that support multiple users on multiple displays, it is possible + * that the returned user will be visible on a secondary display, as the foreground user is the + * one associated with the main display. + * + * @hide + */ + @SystemApi + @RequiresPermission(anyOf = { + android.Manifest.permission.MANAGE_USERS, + android.Manifest.permission.CREATE_USERS, + android.Manifest.permission.QUERY_USERS + }) + public @Nullable UserHandle getPreviousForegroundUser() { + try { + final int previousUser = mService.getPreviousFullUserToEnterForeground(); + if (previousUser == UserHandle.USER_NULL) { + return null; + } + return UserHandle.of(previousUser); + } catch (RemoteException re) { + throw re.rethrowFromSystemServer(); + } + } + + /** * Checks whether it's possible to add more users. * * @return true if more users can be added, false if limit has been reached. diff --git a/core/java/android/util/FeatureFlagUtils.java b/core/java/android/util/FeatureFlagUtils.java index 608cbda28370..4277d01c091a 100644 --- a/core/java/android/util/FeatureFlagUtils.java +++ b/core/java/android/util/FeatureFlagUtils.java @@ -140,6 +140,13 @@ public class FeatureFlagUtils { public static final String SETTINGS_ADB_METRICS_WRITER = "settings_adb_metrics_writer"; /** + * Flag to show stylus-specific preferences in Connected Devices + * @hide + */ + public static final String SETTINGS_SHOW_STYLUS_PREFERENCES = + "settings_show_stylus_preferences"; + + /** * Flag to enable/disable biometrics enrollment v2 * @hide */ @@ -181,10 +188,12 @@ public class FeatureFlagUtils { DEFAULT_FLAGS.put(SETTINGS_NEW_KEYBOARD_TRACKPAD_GESTURE, "false"); DEFAULT_FLAGS.put(SETTINGS_ENABLE_SPA, "false"); DEFAULT_FLAGS.put(SETTINGS_ADB_METRICS_WRITER, "false"); + DEFAULT_FLAGS.put(SETTINGS_SHOW_STYLUS_PREFERENCES, "false"); DEFAULT_FLAGS.put(SETTINGS_BIOMETRICS2_ENROLLMENT, "false"); } private static final Set<String> PERSISTENT_FLAGS; + static { PERSISTENT_FLAGS = new HashSet<>(); PERSISTENT_FLAGS.add(SETTINGS_ALLOW_INTENT_REDIRECTION_FOR_CLONE_PROFILE); diff --git a/core/proto/android/server/activitymanagerservice.proto b/core/proto/android/server/activitymanagerservice.proto index 465000069aab..7393c6f9324a 100644 --- a/core/proto/android/server/activitymanagerservice.proto +++ b/core/proto/android/server/activitymanagerservice.proto @@ -977,12 +977,11 @@ message UserControllerProto { optional int32 profile = 2; } repeated UserProfile user_profile_group_ids = 4; - repeated int32 visible_users_array = 5; // current_user contains the id of the current user, while current_profiles contains the ids of // both the current user and its profiles (if any) - optional int32 current_user = 6; - repeated int32 current_profiles = 7; + optional int32 current_user = 5; + repeated int32 current_profiles = 6; } // sync with com.android.server.am.AppTimeTracker.java diff --git a/core/res/res/values/attrs_manifest.xml b/core/res/res/values/attrs_manifest.xml index 6460007b52de..eb7034437423 100644 --- a/core/res/res/values/attrs_manifest.xml +++ b/core/res/res/values/attrs_manifest.xml @@ -1705,8 +1705,7 @@ --> <flag name="systemExempted" value="0x400" /> <!-- "Short service" foreground service type. See - TODO: Change it to a real link - {@code android.content.pm.ServiceInfo#FOREGROUND_SERVICE_TYPE_SHORT_SERVICE}. + {@link android.content.pm.ServiceInfo#FOREGROUND_SERVICE_TYPE_SHORT_SERVICE}. for more details. --> <flag name="shortService" value="0x800" /> diff --git a/libs/hwui/Android.bp b/libs/hwui/Android.bp index c0a4fdf5eb74..88cfed9357d8 100644 --- a/libs/hwui/Android.bp +++ b/libs/hwui/Android.bp @@ -589,6 +589,7 @@ cc_defaults { "ProfileData.cpp", "ProfileDataContainer.cpp", "Readback.cpp", + "Tonemapper.cpp", "TreeInfo.cpp", "WebViewFunctorManager.cpp", "protos/graphicsstats.proto", diff --git a/libs/hwui/Readback.cpp b/libs/hwui/Readback.cpp index 02c2e67a319b..8dcd6dbe6421 100644 --- a/libs/hwui/Readback.cpp +++ b/libs/hwui/Readback.cpp @@ -16,16 +16,6 @@ #include "Readback.h" -#include <sync/sync.h> -#include <system/window.h> - -#include <gui/TraceUtils.h> -#include "DeferredLayerUpdater.h" -#include "Properties.h" -#include "hwui/Bitmap.h" -#include "pipeline/skia/LayerDrawable.h" -#include "renderthread/EglManager.h" -#include "renderthread/VulkanManager.h" #include <SkBitmap.h> #include <SkBlendMode.h> #include <SkCanvas.h> @@ -38,6 +28,19 @@ #include <SkRefCnt.h> #include <SkSamplingOptions.h> #include <SkSurface.h> +#include <gui/TraceUtils.h> +#include <private/android/AHardwareBufferHelpers.h> +#include <shaders/shaders.h> +#include <sync/sync.h> +#include <system/window.h> + +#include "DeferredLayerUpdater.h" +#include "Properties.h" +#include "Tonemapper.h" +#include "hwui/Bitmap.h" +#include "pipeline/skia/LayerDrawable.h" +#include "renderthread/EglManager.h" +#include "renderthread/VulkanManager.h" #include "utils/Color.h" #include "utils/MathUtils.h" #include "utils/NdkUtils.h" @@ -91,8 +94,18 @@ void Readback::copySurfaceInto(ANativeWindow* window, const std::shared_ptr<Copy } } - sk_sp<SkColorSpace> colorSpace = DataSpaceToColorSpace( - static_cast<android_dataspace>(ANativeWindow_getBuffersDataSpace(window))); + int32_t dataspace = ANativeWindow_getBuffersDataSpace(window); + + // If the application is not updating the Surface themselves, e.g., another + // process is producing buffers for the application to display, then + // ANativeWindow_getBuffersDataSpace will return an unknown answer, so grab + // the dataspace from buffer metadata instead, if it exists. + if (dataspace == 0) { + dataspace = AHardwareBuffer_getDataSpace(sourceBuffer.get()); + } + + sk_sp<SkColorSpace> colorSpace = + DataSpaceToColorSpace(static_cast<android_dataspace>(dataspace)); sk_sp<SkImage> image = SkImage::MakeFromAHardwareBuffer(sourceBuffer.get(), kPremul_SkAlphaType, colorSpace); @@ -227,6 +240,10 @@ void Readback::copySurfaceInto(ANativeWindow* window, const std::shared_ptr<Copy const bool hasBufferCrop = cropRect.left < cropRect.right && cropRect.top < cropRect.bottom; auto constraint = hasBufferCrop ? SkCanvas::kStrict_SrcRectConstraint : SkCanvas::kFast_SrcRectConstraint; + + static constexpr float kMaxLuminanceNits = 4000.f; + tonemapPaint(image->imageInfo(), canvas->imageInfo(), kMaxLuminanceNits, paint); + canvas->drawImageRect(image, imageSrcRect, imageDstRect, sampling, &paint, constraint); canvas->restore(); diff --git a/libs/hwui/SkiaInterpolator.cpp b/libs/hwui/SkiaInterpolator.cpp index 0695dd1ab218..153c3b6f7e04 100644 --- a/libs/hwui/SkiaInterpolator.cpp +++ b/libs/hwui/SkiaInterpolator.cpp @@ -17,6 +17,8 @@ #include "SkiaInterpolator.h" #include "include/core/SkMath.h" +#include "include/core/SkScalar.h" +#include "include/core/SkTypes.h" #include "include/private/SkFixed.h" #include "include/private/SkMalloc.h" #include "include/private/SkTo.h" diff --git a/libs/hwui/Tonemapper.cpp b/libs/hwui/Tonemapper.cpp new file mode 100644 index 000000000000..a7e76b631140 --- /dev/null +++ b/libs/hwui/Tonemapper.cpp @@ -0,0 +1,107 @@ +/* + * 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. + */ + +#include "Tonemapper.h" + +#include <SkRuntimeEffect.h> +#include <log/log.h> +#include <shaders/shaders.h> + +#include "utils/Color.h" + +namespace android::uirenderer { + +namespace { + +class ColorFilterRuntimeEffectBuilder : public SkRuntimeEffectBuilder { +public: + explicit ColorFilterRuntimeEffectBuilder(sk_sp<SkRuntimeEffect> effect) + : SkRuntimeEffectBuilder(std::move(effect)) {} + + sk_sp<SkColorFilter> makeColorFilter() { + return this->effect()->makeColorFilter(this->uniforms()); + } +}; + +static sk_sp<SkColorFilter> createLinearEffectColorFilter(const shaders::LinearEffect& linearEffect, + float maxDisplayLuminance, + float currentDisplayLuminanceNits, + float maxLuminance) { + auto shaderString = SkString(shaders::buildLinearEffectSkSL(linearEffect)); + auto [runtimeEffect, error] = SkRuntimeEffect::MakeForColorFilter(std::move(shaderString)); + if (!runtimeEffect) { + LOG_ALWAYS_FATAL("LinearColorFilter construction error: %s", error.c_str()); + } + + ColorFilterRuntimeEffectBuilder effectBuilder(std::move(runtimeEffect)); + + const auto uniforms = + shaders::buildLinearEffectUniforms(linearEffect, android::mat4(), maxDisplayLuminance, + currentDisplayLuminanceNits, maxLuminance); + + for (const auto& uniform : uniforms) { + effectBuilder.uniform(uniform.name.c_str()).set(uniform.value.data(), uniform.value.size()); + } + + return effectBuilder.makeColorFilter(); +} + +static bool extractTransfer(ui::Dataspace dataspace) { + return dataspace & HAL_DATASPACE_TRANSFER_MASK; +} + +static bool isHdrDataspace(ui::Dataspace dataspace) { + const auto transfer = extractTransfer(dataspace); + + return transfer == HAL_DATASPACE_TRANSFER_ST2084 || transfer == HAL_DATASPACE_TRANSFER_HLG; +} + +static ui::Dataspace getDataspace(const SkImageInfo& image) { + return static_cast<ui::Dataspace>( + ColorSpaceToADataSpace(image.colorSpace(), image.colorType())); +} + +} // namespace + +// Given a source and destination image info, and the max content luminance, generate a tonemaping +// shader and tag it on the supplied paint. +void tonemapPaint(const SkImageInfo& source, const SkImageInfo& destination, float maxLuminanceNits, + SkPaint& paint) { + const auto sourceDataspace = getDataspace(source); + const auto destinationDataspace = getDataspace(destination); + + if (extractTransfer(sourceDataspace) != extractTransfer(destinationDataspace) && + (isHdrDataspace(sourceDataspace) || isHdrDataspace(destinationDataspace))) { + const auto effect = shaders::LinearEffect{ + .inputDataspace = sourceDataspace, + .outputDataspace = destinationDataspace, + .undoPremultipliedAlpha = source.alphaType() == kPremul_SkAlphaType, + .fakeInputDataspace = destinationDataspace, + .type = shaders::LinearEffect::SkSLType::ColorFilter}; + constexpr float kMaxDisplayBrightnessNits = 1000.f; + constexpr float kCurrentDisplayBrightnessNits = 500.f; + sk_sp<SkColorFilter> colorFilter = createLinearEffectColorFilter( + effect, kMaxDisplayBrightnessNits, kCurrentDisplayBrightnessNits, maxLuminanceNits); + + if (paint.getColorFilter()) { + paint.setColorFilter(SkColorFilters::Compose(paint.refColorFilter(), colorFilter)); + } else { + paint.setColorFilter(colorFilter); + } + } +} + +} // namespace android::uirenderer diff --git a/libs/hwui/Tonemapper.h b/libs/hwui/Tonemapper.h new file mode 100644 index 000000000000..c0d5325fa9f8 --- /dev/null +++ b/libs/hwui/Tonemapper.h @@ -0,0 +1,28 @@ +/* + * 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. + */ + +#pragma once + +#include <SkCanvas.h> + +namespace android::uirenderer { + +// Given a source and destination image info, and the max content luminance, generate a tonemaping +// shader and tag it on the supplied paint. +void tonemapPaint(const SkImageInfo& source, const SkImageInfo& destination, float maxLuminanceNits, + SkPaint& paint); + +} // namespace android::uirenderer diff --git a/libs/hwui/pipeline/skia/LayerDrawable.cpp b/libs/hwui/pipeline/skia/LayerDrawable.cpp index 3ba540921f64..99f54c19d2e5 100644 --- a/libs/hwui/pipeline/skia/LayerDrawable.cpp +++ b/libs/hwui/pipeline/skia/LayerDrawable.cpp @@ -25,6 +25,7 @@ #include "SkColorFilter.h" #include "SkRuntimeEffect.h" #include "SkSurface.h" +#include "Tonemapper.h" #include "gl/GrGLTypes.h" #include "math/mat4.h" #include "system/graphics-base-v1.0.h" @@ -76,37 +77,6 @@ static bool shouldFilterRect(const SkMatrix& matrix, const SkRect& srcRect, cons isIntegerAligned(dstDevRect.y())); } -static sk_sp<SkShader> createLinearEffectShader(sk_sp<SkShader> shader, - const shaders::LinearEffect& linearEffect, - float maxDisplayLuminance, - float currentDisplayLuminanceNits, - float maxLuminance) { - auto shaderString = SkString(shaders::buildLinearEffectSkSL(linearEffect)); - auto [runtimeEffect, error] = SkRuntimeEffect::MakeForShader(std::move(shaderString)); - if (!runtimeEffect) { - LOG_ALWAYS_FATAL("LinearColorFilter construction error: %s", error.c_str()); - } - - SkRuntimeShaderBuilder effectBuilder(std::move(runtimeEffect)); - - effectBuilder.child("child") = std::move(shader); - - const auto uniforms = shaders::buildLinearEffectUniforms( - linearEffect, mat4(), maxDisplayLuminance, currentDisplayLuminanceNits, maxLuminance); - - for (const auto& uniform : uniforms) { - effectBuilder.uniform(uniform.name.c_str()).set(uniform.value.data(), uniform.value.size()); - } - - return effectBuilder.makeShader(); -} - -static bool isHdrDataspace(ui::Dataspace dataspace) { - const auto transfer = dataspace & HAL_DATASPACE_TRANSFER_MASK; - - return transfer == HAL_DATASPACE_TRANSFER_ST2084 || transfer == HAL_DATASPACE_TRANSFER_HLG; -} - static void adjustCropForYUV(uint32_t format, int bufferWidth, int bufferHeight, SkRect* cropRect) { // Chroma channels of YUV420 images are subsampled we may need to shrink the crop region by // a whole texel on each side. Since skia still adds its own 0.5 inset, we apply an @@ -215,31 +185,10 @@ bool LayerDrawable::DrawLayer(GrRecordingContext* context, sampling = SkSamplingOptions(SkFilterMode::kLinear); } - const auto sourceDataspace = static_cast<ui::Dataspace>( - ColorSpaceToADataSpace(layerImage->colorSpace(), layerImage->colorType())); - const SkImageInfo& imageInfo = canvas->imageInfo(); - const auto destinationDataspace = static_cast<ui::Dataspace>( - ColorSpaceToADataSpace(imageInfo.colorSpace(), imageInfo.colorType())); - - if (isHdrDataspace(sourceDataspace) || isHdrDataspace(destinationDataspace)) { - const auto effect = shaders::LinearEffect{ - .inputDataspace = sourceDataspace, - .outputDataspace = destinationDataspace, - .undoPremultipliedAlpha = layerImage->alphaType() == kPremul_SkAlphaType, - .fakeInputDataspace = destinationDataspace}; - auto shader = layerImage->makeShader(sampling, - SkMatrix::RectToRect(skiaSrcRect, skiaDestRect)); - constexpr float kMaxDisplayBrightess = 1000.f; - constexpr float kCurrentDisplayBrightness = 500.f; - shader = createLinearEffectShader(std::move(shader), effect, kMaxDisplayBrightess, - kCurrentDisplayBrightness, - layer->getMaxLuminanceNits()); - paint.setShader(shader); - canvas->drawRect(skiaDestRect, paint); - } else { - canvas->drawImageRect(layerImage.get(), skiaSrcRect, skiaDestRect, sampling, &paint, - constraint); - } + tonemapPaint(layerImage->imageInfo(), canvas->imageInfo(), layer->getMaxLuminanceNits(), + paint); + canvas->drawImageRect(layerImage.get(), skiaSrcRect, skiaDestRect, sampling, &paint, + constraint); canvas->restore(); // restore the original matrix diff --git a/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt b/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt index 0cc11946ca85..e3ed3d925566 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt @@ -59,7 +59,7 @@ class CredentialManagerRepo( ) { val requestInfo: RequestInfo private val providerEnabledList: List<ProviderData> - private val providerDisabledList: List<DisabledProviderData> + private val providerDisabledList: List<DisabledProviderData>? // TODO: require non-null. val resultReceiver: ResultReceiver? @@ -143,7 +143,7 @@ class CredentialManagerRepo( providerEnabledList as List<CreateCredentialProviderData>, requestDisplayInfo, context) val providerDisabledList = CreateFlowUtils.toDisabledProviderList( // Handle runtime cast error - providerDisabledList as List<DisabledProviderData>, context) + providerDisabledList, context) var defaultProvider: EnabledProviderInfo? = null var remoteEntry: RemoteInfo? = null providerEnabledList.forEach{providerInfo -> providerInfo.createOptions = @@ -209,7 +209,7 @@ class CredentialManagerRepo( ) } - private fun testDisabledProviderList(): List<DisabledProviderData> { + private fun testDisabledProviderList(): List<DisabledProviderData>? { return listOf( DisabledProviderData("com.lastpass.lpandroid"), DisabledProviderData("com.google.android.youtube") @@ -458,12 +458,15 @@ class CredentialManagerRepo( " \"residentKey\": \"required\",\n" + " \"requireResidentKey\": true\n" + " }}") - val data = request.data + val credentialData = request.data return RequestInfo.newCreateRequestInfo( Binder(), CreateCredentialRequest( TYPE_PUBLIC_KEY_CREDENTIAL, - data + credentialData, + // TODO: populate with actual data + /*candidateQueryData=*/ Bundle(), + /*requireSystemProvider=*/ false ), /*isFirstUsage=*/false, "tribank" @@ -476,7 +479,10 @@ class CredentialManagerRepo( Binder(), CreateCredentialRequest( TYPE_PASSWORD_CREDENTIAL, - data + data, + // TODO: populate with actual data + /*candidateQueryData=*/ Bundle(), + /*requireSystemProvider=*/ false ), /*isFirstUsage=*/false, "tribank" @@ -489,7 +495,9 @@ class CredentialManagerRepo( Binder(), CreateCredentialRequest( "other-sign-ins", - data + data, + /*candidateQueryData=*/ Bundle(), + /*requireSystemProvider=*/ false ), /*isFirstUsage=*/false, "tribank" @@ -501,7 +509,8 @@ class CredentialManagerRepo( Binder(), GetCredentialRequest.Builder() .addGetCredentialOption( - GetCredentialOption(TYPE_PUBLIC_KEY_CREDENTIAL, Bundle()) + GetCredentialOption( + TYPE_PUBLIC_KEY_CREDENTIAL, Bundle(), /*requireSystemProvider=*/ false) ) .build(), /*isFirstUsage=*/false, diff --git a/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt b/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt index b96f686c02fb..357c55dc2770 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt @@ -209,12 +209,12 @@ class CreateFlowUtils { } fun toDisabledProviderList( - providerDataList: List<DisabledProviderData>, + providerDataList: List<DisabledProviderData>?, context: Context, - ): List<com.android.credentialmanager.createflow.DisabledProviderInfo> { + ): List<com.android.credentialmanager.createflow.DisabledProviderInfo>? { // TODO: get from the actual service info val packageManager = context.packageManager - return providerDataList.map { + return providerDataList?.map { val pkgInfo = packageManager .getPackageInfo(it.providerFlattenedComponentName, PackageManager.PackageInfoFlags.of(0)) diff --git a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/CreateCredentialRequest.kt b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/CreateCredentialRequest.kt index 7e7dbde8655a..008e1b6317de 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/CreateCredentialRequest.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/CreateCredentialRequest.kt @@ -38,14 +38,18 @@ open class CreateCredentialRequest( return try { when (from.type) { Credential.TYPE_PASSWORD_CREDENTIAL -> - CreatePasswordRequest.createFrom(from.data) + CreatePasswordRequest.createFrom(from.credentialData) PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL -> - CreatePublicKeyCredentialBaseRequest.createFrom(from.data) + CreatePublicKeyCredentialBaseRequest.createFrom(from.credentialData) else -> - CreateCredentialRequest(from.type, from.data, from.requireSystemProvider()) + CreateCredentialRequest( + from.type, from.credentialData, from.requireSystemProvider() + ) } } catch (e: FrameworkClassParsingException) { - CreateCredentialRequest(from.type, from.data, from.requireSystemProvider()) + CreateCredentialRequest( + from.type, from.credentialData, from.requireSystemProvider() + ) } } } diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SpaLogger.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SpaLogger.kt index 00a0362abd91..6ecb7fa94c3c 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SpaLogger.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SpaLogger.kt @@ -16,6 +16,7 @@ package com.android.settingslib.spa.framework.common +import android.os.Bundle import android.util.Log // Defines the category of the log, for quick filter @@ -38,10 +39,13 @@ enum class LogEvent { // Entry related events. ENTRY_CLICK, - ENTRY_SWITCH_ON, - ENTRY_SWITCH_OFF, + ENTRY_SWITCH, } +internal const val LOG_DATA_DISPLAY_NAME = "name" +internal const val LOG_DATA_SESSION_NAME = "session" +internal const val LOG_DATA_SWITCH_STATUS = "switch" + /** * The interface of logger in Spa */ @@ -54,7 +58,7 @@ interface SpaLogger { id: String, event: LogEvent, category: LogCategory = LogCategory.DEFAULT, - details: String? = null + extraData: Bundle = Bundle.EMPTY ) { } } @@ -64,8 +68,8 @@ class LocalLogger : SpaLogger { Log.d("SpaMsg-$category", "[$tag] $msg") } - override fun event(id: String, event: LogEvent, category: LogCategory, details: String?) { - val extraMsg = if (details == null) "" else " ($details)" - Log.d("SpaEvent-$category", "[$id] $event$extraMsg") + override fun event(id: String, event: LogEvent, category: LogCategory, extraData: Bundle) { + val extraMsg = extraData.toString().removeRange(0, 6) + Log.d("SpaEvent-$category", "[$id] $event $extraMsg") } }
\ No newline at end of file diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/util/EntryLogger.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/util/EntryLogger.kt index 8d0a35c371e3..1c881878f751 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/util/EntryLogger.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/util/EntryLogger.kt @@ -16,17 +16,22 @@ package com.android.settingslib.spa.framework.util +import android.os.Bundle import androidx.compose.runtime.Composable +import androidx.core.os.bundleOf +import com.android.settingslib.spa.framework.common.LOG_DATA_SWITCH_STATUS import com.android.settingslib.spa.framework.common.LocalEntryDataProvider import com.android.settingslib.spa.framework.common.LogCategory import com.android.settingslib.spa.framework.common.LogEvent import com.android.settingslib.spa.framework.common.SpaEnvironmentFactory @Composable -fun logEntryEvent(): (event: LogEvent) -> Unit { - val entryId = LocalEntryDataProvider.current.entryId ?: return {} - return { - SpaEnvironmentFactory.instance.logger.event(entryId, it, category = LogCategory.VIEW) +fun logEntryEvent(): (event: LogEvent, extraData: Bundle) -> Unit { + val entryId = LocalEntryDataProvider.current.entryId ?: return { _, _ -> } + return { event, extraData -> + SpaEnvironmentFactory.instance.logger.event( + entryId, event, category = LogCategory.VIEW, extraData = extraData + ) } } @@ -35,7 +40,7 @@ fun wrapOnClickWithLog(onClick: (() -> Unit)?): (() -> Unit)? { if (onClick == null) return null val logEvent = logEntryEvent() return { - logEvent(LogEvent.ENTRY_CLICK) + logEvent(LogEvent.ENTRY_CLICK, Bundle.EMPTY) onClick() } } @@ -45,8 +50,7 @@ fun wrapOnSwitchWithLog(onSwitch: ((checked: Boolean) -> Unit)?): ((checked: Boo if (onSwitch == null) return null val logEvent = logEntryEvent() return { - val event = if (it) LogEvent.ENTRY_SWITCH_ON else LogEvent.ENTRY_SWITCH_OFF - logEvent(event) + logEvent(LogEvent.ENTRY_SWITCH, bundleOf(LOG_DATA_SWITCH_STATUS to it)) onSwitch(it) } } diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/util/Flows.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/util/Flows.kt index d801840565ed..97e3ac2147ca 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/util/Flows.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/util/Flows.kt @@ -27,6 +27,13 @@ import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.map /** + * Returns a [Flow] whose values are a list which containing the results of applying the given + * [transform] function to each element in the original flow's list. + */ +inline fun <T, R> Flow<List<T>>.mapItem(crossinline transform: (T) -> R): Flow<List<R>> = + map { list -> list.map(transform) } + +/** * Returns a [Flow] whose values are a list which containing the results of asynchronously applying * the given [transform] function to each element in the original flow's list. */ diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/util/PageLogger.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/util/PageLogger.kt index b9e4b782455d..a88125472b52 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/util/PageLogger.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/util/PageLogger.kt @@ -21,8 +21,11 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.remember import androidx.compose.ui.platform.LocalLifecycleOwner +import androidx.core.os.bundleOf import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver +import com.android.settingslib.spa.framework.common.LOG_DATA_DISPLAY_NAME +import com.android.settingslib.spa.framework.common.LOG_DATA_SESSION_NAME import com.android.settingslib.spa.framework.common.LogCategory import com.android.settingslib.spa.framework.common.LogEvent import com.android.settingslib.spa.framework.common.SettingsPageProvider @@ -37,21 +40,21 @@ internal fun SettingsPageProvider.PageEvent(arguments: Bundle? = null) { val navController = LocalNavController.current DisposableEffect(lifecycleOwner) { val observer = LifecycleEventObserver { _, event -> - val spaLogger = SpaEnvironmentFactory.instance.logger - if (event == Lifecycle.Event.ON_START) { - spaLogger.event( - page.id, - LogEvent.PAGE_ENTER, + val logPageEvent: (event: LogEvent) -> Unit = { + SpaEnvironmentFactory.instance.logger.event( + id = page.id, + event = it, category = LogCategory.FRAMEWORK, - details = navController.sessionSourceName ?: page.displayName, + extraData = bundleOf( + LOG_DATA_DISPLAY_NAME to page.displayName, + LOG_DATA_SESSION_NAME to navController.sessionSourceName, + ) ) + } + if (event == Lifecycle.Event.ON_START) { + logPageEvent(LogEvent.PAGE_ENTER) } else if (event == Lifecycle.Event.ON_STOP) { - spaLogger.event( - page.id, - LogEvent.PAGE_LEAVE, - category = LogCategory.FRAMEWORK, - details = navController.sessionSourceName ?: page.displayName, - ) + logPageEvent(LogEvent.PAGE_LEAVE) } } diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/slice/SettingsSliceDataRepositoryTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/slice/SettingsSliceDataRepositoryTest.kt index 90e25f9a3b70..1bdba299dc98 100644 --- a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/slice/SettingsSliceDataRepositoryTest.kt +++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/slice/SettingsSliceDataRepositoryTest.kt @@ -18,12 +18,12 @@ package com.android.settingslib.spa.slice import android.content.Context import android.net.Uri +import androidx.arch.core.executor.testing.InstantTaskExecutorRule import androidx.lifecycle.Observer import androidx.slice.Slice import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import com.android.settingslib.spa.framework.common.createSettingsPage -import com.android.settingslib.spa.testutils.InstantTaskExecutorRule import com.android.settingslib.spa.tests.testutils.SpaEnvironmentForTest import com.android.settingslib.spa.tests.testutils.SppHome import com.android.settingslib.spa.tests.testutils.SppLayer2 diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/tests/testutils/SpaEnvironmentForTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/tests/testutils/SpaEnvironmentForTest.kt index 63859546d5f6..f38bd088060a 100644 --- a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/tests/testutils/SpaEnvironmentForTest.kt +++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/tests/testutils/SpaEnvironmentForTest.kt @@ -51,7 +51,7 @@ class SpaLoggerForTest : SpaLogger { messageCount[key] = (messageCount[key] ?: 0) + 1 } - override fun event(id: String, event: LogEvent, category: LogCategory, details: String?) { + override fun event(id: String, event: LogEvent, category: LogCategory, extraData: Bundle) { val key = EventCountKey(id, event, category) eventCount[key] = (eventCount[key] ?: 0) + 1 } diff --git a/packages/SettingsLib/Spa/testutils/Android.bp b/packages/SettingsLib/Spa/testutils/Android.bp index 48df569f1c54..de87ddedc122 100644 --- a/packages/SettingsLib/Spa/testutils/Android.bp +++ b/packages/SettingsLib/Spa/testutils/Android.bp @@ -24,7 +24,7 @@ android_library { srcs: ["src/**/*.kt"], static_libs: [ - "androidx.arch.core_core-runtime", + "androidx.arch.core_core-testing", "androidx.compose.ui_ui-test-junit4", "androidx.compose.ui_ui-test-manifest", "mockito", diff --git a/packages/SettingsLib/Spa/testutils/build.gradle b/packages/SettingsLib/Spa/testutils/build.gradle index be8df436712d..81e54c13a625 100644 --- a/packages/SettingsLib/Spa/testutils/build.gradle +++ b/packages/SettingsLib/Spa/testutils/build.gradle @@ -47,7 +47,7 @@ android { } dependencies { - api "androidx.arch.core:core-runtime:2.1.0" + api "androidx.arch.core:core-testing:2.1.0" api "androidx.compose.ui:ui-test-junit4:$jetpack_compose_version" api "com.google.truth:truth:1.1.3" api "org.mockito:mockito-core:2.21.0" diff --git a/packages/SettingsLib/Spa/testutils/src/com/android/settingslib/spa/testutils/InstantTaskExecutorRule.kt b/packages/SettingsLib/Spa/testutils/src/com/android/settingslib/spa/testutils/InstantTaskExecutorRule.kt deleted file mode 100644 index 43c18d4a3e93..000000000000 --- a/packages/SettingsLib/Spa/testutils/src/com/android/settingslib/spa/testutils/InstantTaskExecutorRule.kt +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright (C) 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.settingslib.spa.testutils - -import androidx.arch.core.executor.ArchTaskExecutor -import androidx.arch.core.executor.TaskExecutor -import org.junit.rules.TestWatcher -import org.junit.runner.Description - -/** - * Test rule that makes ArchTaskExecutor main thread assertions pass. There is one such assert - * in LifecycleRegistry. - - * This is a copy of androidx/arch/core/executor/testing/InstantTaskExecutorRule which should be - * replaced once the dependency issue is solved. - */ -class InstantTaskExecutorRule : TestWatcher() { - override fun starting(description: Description) { - super.starting(description) - ArchTaskExecutor.getInstance().setDelegate( - object : TaskExecutor() { - override fun executeOnDiskIO(runnable: Runnable) { - runnable.run() - } - - override fun postToMainThread(runnable: Runnable) { - runnable.run() - } - - override fun isMainThread(): Boolean { - return true - } - } - ) - } - - override fun finished(description: Description) { - super.finished(description) - ArchTaskExecutor.getInstance().setDelegate(null) - } -} diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppListModel.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppListModel.kt index a7122d0eb03a..69999089b280 100644 --- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppListModel.kt +++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppListModel.kt @@ -33,7 +33,8 @@ interface AppListModel<T : AppRecord> { * * @return the [AppRecord] list which will be displayed. */ - fun filter(userIdFlow: Flow<Int>, option: Int, recordListFlow: Flow<List<T>>): Flow<List<T>> + fun filter(userIdFlow: Flow<Int>, option: Int, recordListFlow: Flow<List<T>>): Flow<List<T>> = + recordListFlow /** * This function is called when the App List's loading is finished and displayed to the user. @@ -67,5 +68,5 @@ interface AppListModel<T : AppRecord> { * @return null if no summary should be displayed. */ @Composable - fun getSummary(option: Int, record: T): State<String>? + fun getSummary(option: Int, record: T): State<String>? = null } diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppListViewModelTest.kt b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppListViewModelTest.kt index 65c547a97fd3..b9c875ba803a 100644 --- a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppListViewModelTest.kt +++ b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppListViewModelTest.kt @@ -21,7 +21,7 @@ import android.content.pm.ApplicationInfo import androidx.compose.runtime.Composable import androidx.test.ext.junit.runners.AndroidJUnit4 import com.android.settingslib.spa.framework.compose.stateOf -import com.android.settingslib.spa.framework.util.asyncMapItem +import com.android.settingslib.spa.framework.util.mapItem import com.android.settingslib.spa.testutils.waitUntil import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -116,16 +116,7 @@ private class TestAppListModel : AppListModel<TestAppRecord> { var onFirstLoadedCalled = false override fun transform(userIdFlow: Flow<Int>, appListFlow: Flow<List<ApplicationInfo>>) = - appListFlow.asyncMapItem { TestAppRecord(it) } - - @Composable - override fun getSummary(option: Int, record: TestAppRecord) = null - - override fun filter( - userIdFlow: Flow<Int>, - option: Int, - recordListFlow: Flow<List<TestAppRecord>>, - ) = recordListFlow + appListFlow.mapItem(::TestAppRecord) override suspend fun onFirstLoaded(recordList: List<TestAppRecord>) { onFirstLoadedCalled = true diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/tests/testutils/TestAppListModel.kt b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/tests/testutils/TestAppListModel.kt index d5564877f681..ada4016bea13 100644 --- a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/tests/testutils/TestAppListModel.kt +++ b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/tests/testutils/TestAppListModel.kt @@ -17,8 +17,7 @@ package com.android.settingslib.spaprivileged.tests.testutils import android.content.pm.ApplicationInfo -import androidx.compose.runtime.Composable -import com.android.settingslib.spa.framework.util.asyncMapItem +import com.android.settingslib.spa.framework.util.mapItem import com.android.settingslib.spaprivileged.model.app.AppListModel import com.android.settingslib.spaprivileged.model.app.AppRecord import kotlinx.coroutines.flow.Flow @@ -35,16 +34,7 @@ class TestAppListModel( override fun getSpinnerOptions() = options override fun transform(userIdFlow: Flow<Int>, appListFlow: Flow<List<ApplicationInfo>>) = - appListFlow.asyncMapItem { TestAppRecord(it) } - - @Composable - override fun getSummary(option: Int, record: TestAppRecord) = null - - override fun filter( - userIdFlow: Flow<Int>, - option: Int, - recordListFlow: Flow<List<TestAppRecord>>, - ) = recordListFlow + appListFlow.mapItem(::TestAppRecord) override fun getGroupTitle(option: Int, record: TestAppRecord) = if (enableGrouping) record.group else null diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/quickaffordance/data/content/FakeKeyguardQuickAffordanceProviderClient.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/quickaffordance/data/content/FakeKeyguardQuickAffordanceProviderClient.kt new file mode 100644 index 000000000000..f490c5459d46 --- /dev/null +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/quickaffordance/data/content/FakeKeyguardQuickAffordanceProviderClient.kt @@ -0,0 +1,190 @@ +/* + * 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.shared.quickaffordance.data.content + +import android.graphics.drawable.BitmapDrawable +import android.graphics.drawable.Drawable +import com.android.systemui.shared.keyguard.shared.model.KeyguardQuickAffordanceSlots +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.combine + +class FakeKeyguardQuickAffordanceProviderClient( + slots: List<KeyguardQuickAffordanceProviderClient.Slot> = + listOf( + KeyguardQuickAffordanceProviderClient.Slot( + id = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START, + capacity = 1, + ), + KeyguardQuickAffordanceProviderClient.Slot( + id = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END, + capacity = 1, + ), + ), + affordances: List<KeyguardQuickAffordanceProviderClient.Affordance> = + listOf( + KeyguardQuickAffordanceProviderClient.Affordance( + id = AFFORDANCE_1, + name = AFFORDANCE_1, + iconResourceId = 0, + ), + KeyguardQuickAffordanceProviderClient.Affordance( + id = AFFORDANCE_2, + name = AFFORDANCE_2, + iconResourceId = 0, + ), + KeyguardQuickAffordanceProviderClient.Affordance( + id = AFFORDANCE_3, + name = AFFORDANCE_3, + iconResourceId = 0, + ), + ), + flags: List<KeyguardQuickAffordanceProviderClient.Flag> = + listOf( + KeyguardQuickAffordanceProviderClient.Flag( + name = KeyguardQuickAffordanceProviderContract.FlagsTable.FLAG_NAME_FEATURE_ENABLED, + value = true, + ) + ), +) : KeyguardQuickAffordanceProviderClient { + + private val slots = MutableStateFlow(slots) + private val affordances = MutableStateFlow(affordances) + private val flags = MutableStateFlow(flags) + + private val selections = MutableStateFlow<Map<String, List<String>>>(emptyMap()) + + override suspend fun insertSelection(slotId: String, affordanceId: String) { + val slotCapacity = + querySlots().find { it.id == slotId }?.capacity + ?: error("Slot with ID \"$slotId\" not found!") + val affordances = selections.value.getOrDefault(slotId, mutableListOf()).toMutableList() + while (affordances.size + 1 > slotCapacity) { + affordances.removeAt(0) + } + affordances.remove(affordanceId) + affordances.add(affordanceId) + selections.value = selections.value.toMutableMap().apply { this[slotId] = affordances } + } + + override suspend fun querySlots(): List<KeyguardQuickAffordanceProviderClient.Slot> { + return slots.value + } + + override suspend fun queryFlags(): List<KeyguardQuickAffordanceProviderClient.Flag> { + return flags.value + } + + override fun observeSlots(): Flow<List<KeyguardQuickAffordanceProviderClient.Slot>> { + return slots.asStateFlow() + } + + override fun observeFlags(): Flow<List<KeyguardQuickAffordanceProviderClient.Flag>> { + return flags.asStateFlow() + } + + override suspend fun queryAffordances(): + List<KeyguardQuickAffordanceProviderClient.Affordance> { + return affordances.value + } + + override fun observeAffordances(): + Flow<List<KeyguardQuickAffordanceProviderClient.Affordance>> { + return affordances.asStateFlow() + } + + override suspend fun querySelections(): List<KeyguardQuickAffordanceProviderClient.Selection> { + return toSelectionList(selections.value, affordances.value) + } + + override fun observeSelections(): Flow<List<KeyguardQuickAffordanceProviderClient.Selection>> { + return combine(selections, affordances) { selections, affordances -> + toSelectionList(selections, affordances) + } + } + + override suspend fun deleteSelection(slotId: String, affordanceId: String) { + val affordances = selections.value.getOrDefault(slotId, mutableListOf()).toMutableList() + affordances.remove(affordanceId) + + selections.value = selections.value.toMutableMap().apply { this[slotId] = affordances } + } + + override suspend fun deleteAllSelections(slotId: String) { + selections.value = selections.value.toMutableMap().apply { this[slotId] = emptyList() } + } + + override suspend fun getAffordanceIcon(iconResourceId: Int, tintColor: Int): Drawable { + return BitmapDrawable() + } + + fun setFlag( + name: String, + value: Boolean, + ) { + flags.value = + flags.value.toMutableList().apply { + removeIf { it.name == name } + add(KeyguardQuickAffordanceProviderClient.Flag(name = name, value = value)) + } + } + + fun setSlotCapacity(slotId: String, capacity: Int) { + slots.value = + slots.value.toMutableList().apply { + val index = indexOfFirst { it.id == slotId } + check(index != -1) { "Slot with ID \"$slotId\" doesn't exist!" } + set( + index, + KeyguardQuickAffordanceProviderClient.Slot(id = slotId, capacity = capacity) + ) + } + } + + fun addAffordance(affordance: KeyguardQuickAffordanceProviderClient.Affordance): Int { + affordances.value = affordances.value + listOf(affordance) + return affordances.value.size - 1 + } + + private fun toSelectionList( + selections: Map<String, List<String>>, + affordances: List<KeyguardQuickAffordanceProviderClient.Affordance>, + ): List<KeyguardQuickAffordanceProviderClient.Selection> { + return selections + .map { (slotId, affordanceIds) -> + affordanceIds.map { affordanceId -> + val affordanceName = + affordances.find { it.id == affordanceId }?.name + ?: error("No affordance with ID of \"$affordanceId\"!") + KeyguardQuickAffordanceProviderClient.Selection( + slotId = slotId, + affordanceId = affordanceId, + affordanceName = affordanceName, + ) + } + } + .flatten() + } + + companion object { + const val AFFORDANCE_1 = "affordance_1" + const val AFFORDANCE_2 = "affordance_2" + const val AFFORDANCE_3 = "affordance_3" + } +} diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/quickaffordance/data/content/KeyguardQuickAffordanceProviderClient.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/quickaffordance/data/content/KeyguardQuickAffordanceProviderClient.kt new file mode 100644 index 000000000000..3213b2e97ac9 --- /dev/null +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/quickaffordance/data/content/KeyguardQuickAffordanceProviderClient.kt @@ -0,0 +1,479 @@ +/* + * 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.shared.quickaffordance.data.content + +import android.annotation.SuppressLint +import android.content.ContentValues +import android.content.Context +import android.database.ContentObserver +import android.graphics.Color +import android.graphics.drawable.Drawable +import android.net.Uri +import androidx.annotation.DrawableRes +import com.android.systemui.shared.quickaffordance.data.content.KeyguardQuickAffordanceProviderContract as Contract +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.withContext + +/** Client for using a content provider implementing the [Contract]. */ +interface KeyguardQuickAffordanceProviderClient { + + /** + * Selects an affordance with the given ID for a slot on the lock screen with the given ID. + * + * Note that the maximum number of selected affordances on this slot is automatically enforced. + * Selecting a slot that is already full (e.g. already has a number of selected affordances at + * its maximum capacity) will automatically remove the oldest selected affordance before adding + * the one passed in this call. Additionally, selecting an affordance that's already one of the + * selected affordances on the slot will move the selected affordance to the newest location in + * the slot. + */ + suspend fun insertSelection( + slotId: String, + affordanceId: String, + ) + + /** Returns all available slots supported by the device. */ + suspend fun querySlots(): List<Slot> + + /** Returns the list of flags. */ + suspend fun queryFlags(): List<Flag> + + /** + * Returns [Flow] for observing the collection of slots. + * + * @see [querySlots] + */ + fun observeSlots(): Flow<List<Slot>> + + /** + * Returns [Flow] for observing the collection of flags. + * + * @see [queryFlags] + */ + fun observeFlags(): Flow<List<Flag>> + + /** + * Returns all available affordances supported by the device, regardless of current slot + * placement. + */ + suspend fun queryAffordances(): List<Affordance> + + /** + * Returns [Flow] for observing the collection of affordances. + * + * @see [queryAffordances] + */ + fun observeAffordances(): Flow<List<Affordance>> + + /** Returns the current slot-affordance selections. */ + suspend fun querySelections(): List<Selection> + + /** + * Returns [Flow] for observing the collection of selections. + * + * @see [querySelections] + */ + fun observeSelections(): Flow<List<Selection>> + + /** Unselects an affordance with the given ID from the slot with the given ID. */ + suspend fun deleteSelection( + slotId: String, + affordanceId: String, + ) + + /** Unselects all affordances from the slot with the given ID. */ + suspend fun deleteAllSelections( + slotId: String, + ) + + /** Returns a [Drawable] with the given ID, loaded from the system UI package. */ + suspend fun getAffordanceIcon( + @DrawableRes iconResourceId: Int, + tintColor: Int = Color.WHITE, + ): Drawable + + /** Models a slot. A position that quick affordances can be positioned in. */ + data class Slot( + /** Unique ID of the slot. */ + val id: String, + /** + * The maximum number of quick affordances that are allowed to be positioned in this slot. + */ + val capacity: Int, + ) + + /** + * Models a quick affordance. An action that can be selected by the user to appear in one or + * more slots on the lock screen. + */ + data class Affordance( + /** Unique ID of the quick affordance. */ + val id: String, + /** User-facing label for this affordance. */ + val name: String, + /** + * Resource ID for the user-facing icon for this affordance. This resource is hosted by the + * System UI process so it must be used with + * `PackageManager.getResourcesForApplication(String)`. + */ + val iconResourceId: Int, + /** + * Whether the affordance is enabled. Disabled affordances should be shown on the picker but + * should be rendered as "disabled". When tapped, the enablement properties should be used + * to populate UI that would explain to the user what to do in order to re-enable this + * affordance. + */ + val isEnabled: Boolean = true, + /** + * If the affordance is disabled, this is a set of instruction messages to be shown to the + * user when the disabled affordance is selected. The instructions should help the user + * figure out what to do in order to re-neable this affordance. + */ + val enablementInstructions: List<String>? = null, + /** + * If the affordance is disabled, this is a label for a button shown together with the set + * of instruction messages when the disabled affordance is selected. The button should help + * send the user to a flow that would help them achieve the instructions and re-enable this + * affordance. + * + * If `null`, the button should not be shown. + */ + val enablementActionText: String? = null, + /** + * If the affordance is disabled, this is a "component name" of the format + * `packageName/action` to be used as an `Intent` for `startActivity` when the action button + * (shown together with the set of instruction messages when the disabled affordance is + * selected) is clicked by the user. The button should help send the user to a flow that + * would help them achieve the instructions and re-enable this affordance. + * + * If `null`, the button should not be shown. + */ + val enablementActionComponentName: String? = null, + ) + + /** Models a selection of a quick affordance on a slot. */ + data class Selection( + /** The unique ID of the slot. */ + val slotId: String, + /** The unique ID of the quick affordance. */ + val affordanceId: String, + /** The user-visible label for the quick affordance. */ + val affordanceName: String, + ) + + /** Models a System UI flag. */ + data class Flag( + /** The name of the flag. */ + val name: String, + /** The value of the flag. */ + val value: Boolean, + ) +} + +class KeyguardQuickAffordanceProviderClientImpl( + private val context: Context, + private val backgroundDispatcher: CoroutineDispatcher, +) : KeyguardQuickAffordanceProviderClient { + + override suspend fun insertSelection( + slotId: String, + affordanceId: String, + ) { + withContext(backgroundDispatcher) { + context.contentResolver.insert( + Contract.SelectionTable.URI, + ContentValues().apply { + put(Contract.SelectionTable.Columns.SLOT_ID, slotId) + put(Contract.SelectionTable.Columns.AFFORDANCE_ID, affordanceId) + } + ) + } + } + + override suspend fun querySlots(): List<KeyguardQuickAffordanceProviderClient.Slot> { + return withContext(backgroundDispatcher) { + context.contentResolver + .query( + Contract.SlotTable.URI, + null, + null, + null, + null, + ) + ?.use { cursor -> + buildList { + val idColumnIndex = cursor.getColumnIndex(Contract.SlotTable.Columns.ID) + val capacityColumnIndex = + cursor.getColumnIndex(Contract.SlotTable.Columns.CAPACITY) + if (idColumnIndex == -1 || capacityColumnIndex == -1) { + return@buildList + } + + while (cursor.moveToNext()) { + add( + KeyguardQuickAffordanceProviderClient.Slot( + id = cursor.getString(idColumnIndex), + capacity = cursor.getInt(capacityColumnIndex), + ) + ) + } + } + } + } + ?: emptyList() + } + + override suspend fun queryFlags(): List<KeyguardQuickAffordanceProviderClient.Flag> { + return withContext(backgroundDispatcher) { + context.contentResolver + .query( + Contract.FlagsTable.URI, + null, + null, + null, + null, + ) + ?.use { cursor -> + buildList { + val nameColumnIndex = + cursor.getColumnIndex(Contract.FlagsTable.Columns.NAME) + val valueColumnIndex = + cursor.getColumnIndex(Contract.FlagsTable.Columns.VALUE) + if (nameColumnIndex == -1 || valueColumnIndex == -1) { + return@buildList + } + + while (cursor.moveToNext()) { + add( + KeyguardQuickAffordanceProviderClient.Flag( + name = cursor.getString(nameColumnIndex), + value = cursor.getInt(valueColumnIndex) == 1, + ) + ) + } + } + } + } + ?: emptyList() + } + + override fun observeSlots(): Flow<List<KeyguardQuickAffordanceProviderClient.Slot>> { + return observeUri(Contract.SlotTable.URI).map { querySlots() } + } + + override fun observeFlags(): Flow<List<KeyguardQuickAffordanceProviderClient.Flag>> { + return observeUri(Contract.FlagsTable.URI).map { queryFlags() } + } + + override suspend fun queryAffordances(): + List<KeyguardQuickAffordanceProviderClient.Affordance> { + return withContext(backgroundDispatcher) { + context.contentResolver + .query( + Contract.AffordanceTable.URI, + null, + null, + null, + null, + ) + ?.use { cursor -> + buildList { + val idColumnIndex = + cursor.getColumnIndex(Contract.AffordanceTable.Columns.ID) + val nameColumnIndex = + cursor.getColumnIndex(Contract.AffordanceTable.Columns.NAME) + val iconColumnIndex = + cursor.getColumnIndex(Contract.AffordanceTable.Columns.ICON) + val isEnabledColumnIndex = + cursor.getColumnIndex(Contract.AffordanceTable.Columns.IS_ENABLED) + val enablementInstructionsColumnIndex = + cursor.getColumnIndex( + Contract.AffordanceTable.Columns.ENABLEMENT_INSTRUCTIONS + ) + val enablementActionTextColumnIndex = + cursor.getColumnIndex( + Contract.AffordanceTable.Columns.ENABLEMENT_ACTION_TEXT + ) + val enablementComponentNameColumnIndex = + cursor.getColumnIndex( + Contract.AffordanceTable.Columns.ENABLEMENT_COMPONENT_NAME + ) + if ( + idColumnIndex == -1 || + nameColumnIndex == -1 || + iconColumnIndex == -1 || + isEnabledColumnIndex == -1 || + enablementInstructionsColumnIndex == -1 || + enablementActionTextColumnIndex == -1 || + enablementComponentNameColumnIndex == -1 + ) { + return@buildList + } + + while (cursor.moveToNext()) { + add( + KeyguardQuickAffordanceProviderClient.Affordance( + id = cursor.getString(idColumnIndex), + name = cursor.getString(nameColumnIndex), + iconResourceId = cursor.getInt(iconColumnIndex), + isEnabled = cursor.getInt(isEnabledColumnIndex) == 1, + enablementInstructions = + cursor + .getString(enablementInstructionsColumnIndex) + ?.split( + Contract.AffordanceTable + .ENABLEMENT_INSTRUCTIONS_DELIMITER + ), + enablementActionText = + cursor.getString(enablementActionTextColumnIndex), + enablementActionComponentName = + cursor.getString(enablementComponentNameColumnIndex), + ) + ) + } + } + } + } + ?: emptyList() + } + + override fun observeAffordances(): + Flow<List<KeyguardQuickAffordanceProviderClient.Affordance>> { + return observeUri(Contract.AffordanceTable.URI).map { queryAffordances() } + } + + override suspend fun querySelections(): List<KeyguardQuickAffordanceProviderClient.Selection> { + return withContext(backgroundDispatcher) { + context.contentResolver + .query( + Contract.SelectionTable.URI, + null, + null, + null, + null, + ) + ?.use { cursor -> + buildList { + val slotIdColumnIndex = + cursor.getColumnIndex(Contract.SelectionTable.Columns.SLOT_ID) + val affordanceIdColumnIndex = + cursor.getColumnIndex(Contract.SelectionTable.Columns.AFFORDANCE_ID) + val affordanceNameColumnIndex = + cursor.getColumnIndex(Contract.SelectionTable.Columns.AFFORDANCE_NAME) + if ( + slotIdColumnIndex == -1 || + affordanceIdColumnIndex == -1 || + affordanceNameColumnIndex == -1 + ) { + return@buildList + } + + while (cursor.moveToNext()) { + add( + KeyguardQuickAffordanceProviderClient.Selection( + slotId = cursor.getString(slotIdColumnIndex), + affordanceId = cursor.getString(affordanceIdColumnIndex), + affordanceName = cursor.getString(affordanceNameColumnIndex), + ) + ) + } + } + } + } + ?: emptyList() + } + + override fun observeSelections(): Flow<List<KeyguardQuickAffordanceProviderClient.Selection>> { + return observeUri(Contract.SelectionTable.URI).map { querySelections() } + } + + override suspend fun deleteSelection( + slotId: String, + affordanceId: String, + ) { + withContext(backgroundDispatcher) { + context.contentResolver.delete( + Contract.SelectionTable.URI, + "${Contract.SelectionTable.Columns.SLOT_ID} = ? AND" + + " ${Contract.SelectionTable.Columns.AFFORDANCE_ID} = ?", + arrayOf( + slotId, + affordanceId, + ), + ) + } + } + + override suspend fun deleteAllSelections( + slotId: String, + ) { + withContext(backgroundDispatcher) { + context.contentResolver.delete( + Contract.SelectionTable.URI, + Contract.SelectionTable.Columns.SLOT_ID, + arrayOf( + slotId, + ), + ) + } + } + + @SuppressLint("UseCompatLoadingForDrawables") + override suspend fun getAffordanceIcon( + @DrawableRes iconResourceId: Int, + tintColor: Int, + ): Drawable { + return withContext(backgroundDispatcher) { + context.packageManager + .getResourcesForApplication(SYSTEM_UI_PACKAGE_NAME) + .getDrawable(iconResourceId, context.theme) + .apply { setTint(tintColor) } + } + } + + private fun observeUri( + uri: Uri, + ): Flow<Unit> { + return callbackFlow { + val observer = + object : ContentObserver(null) { + override fun onChange(selfChange: Boolean) { + trySend(Unit) + } + } + + context.contentResolver.registerContentObserver( + uri, + /* notifyForDescendants= */ true, + observer, + ) + + awaitClose { context.contentResolver.unregisterContentObserver(observer) } + } + .onStart { emit(Unit) } + } + + companion object { + private const val SYSTEM_UI_PACKAGE_NAME = "com.android.systemui" + } +} diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/keyguard/data/content/KeyguardQuickAffordanceProviderContract.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/quickaffordance/data/content/KeyguardQuickAffordanceProviderContract.kt index 98d8d3eb9a4a..17be74b08690 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/keyguard/data/content/KeyguardQuickAffordanceProviderContract.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/quickaffordance/data/content/KeyguardQuickAffordanceProviderContract.kt @@ -15,7 +15,7 @@ * */ -package com.android.systemui.shared.keyguard.data.content +package com.android.systemui.shared.quickaffordance.data.content import android.content.ContentResolver import android.net.Uri diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/keyguard/shared/model/KeyguardQuickAffordanceSlots.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/quickaffordance/shared/model/KeyguardQuickAffordanceSlots.kt index 2dc7a280e423..2dc7a280e423 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/keyguard/shared/model/KeyguardQuickAffordanceSlots.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/quickaffordance/shared/model/KeyguardQuickAffordanceSlots.kt diff --git a/packages/SystemUI/src/com/android/systemui/backup/BackupHelper.kt b/packages/SystemUI/src/com/android/systemui/backup/BackupHelper.kt index 5616a00592f2..621b99d6804a 100644 --- a/packages/SystemUI/src/com/android/systemui/backup/BackupHelper.kt +++ b/packages/SystemUI/src/com/android/systemui/backup/BackupHelper.kt @@ -29,13 +29,15 @@ import android.os.UserHandle import android.util.Log import com.android.systemui.controls.controller.AuxiliaryPersistenceWrapper import com.android.systemui.controls.controller.ControlsFavoritePersistenceWrapper +import com.android.systemui.keyguard.domain.backup.KeyguardQuickAffordanceBackupHelper import com.android.systemui.people.widget.PeopleBackupHelper /** * Helper for backing up elements in SystemUI * - * This helper is invoked by BackupManager whenever a backup or restore is required in SystemUI. - * The helper can be used to back up any element that is stored in [Context.getFilesDir]. + * This helper is invoked by BackupManager whenever a backup or restore is required in SystemUI. The + * helper can be used to back up any element that is stored in [Context.getFilesDir] or + * [Context.getSharedPreferences]. * * After restoring is done, a [ACTION_RESTORE_FINISHED] intent will be send to SystemUI user 0, * indicating that restoring is finished for a given user. @@ -47,9 +49,11 @@ open class BackupHelper : BackupAgentHelper() { internal const val CONTROLS = ControlsFavoritePersistenceWrapper.FILE_NAME private const val NO_OVERWRITE_FILES_BACKUP_KEY = "systemui.files_no_overwrite" private const val PEOPLE_TILES_BACKUP_KEY = "systemui.people.shared_preferences" + private const val KEYGUARD_QUICK_AFFORDANCES_BACKUP_KEY = + "systemui.keyguard.quickaffordance.shared_preferences" val controlsDataLock = Any() const val ACTION_RESTORE_FINISHED = "com.android.systemui.backup.RESTORE_FINISHED" - private const val PERMISSION_SELF = "com.android.systemui.permission.SELF" + const val PERMISSION_SELF = "com.android.systemui.permission.SELF" } override fun onCreate(userHandle: UserHandle, operationType: Int) { @@ -67,17 +71,27 @@ open class BackupHelper : BackupAgentHelper() { } val keys = PeopleBackupHelper.getFilesToBackup() - addHelper(PEOPLE_TILES_BACKUP_KEY, PeopleBackupHelper( - this, userHandle, keys.toTypedArray())) + addHelper( + PEOPLE_TILES_BACKUP_KEY, + PeopleBackupHelper(this, userHandle, keys.toTypedArray()) + ) + addHelper( + KEYGUARD_QUICK_AFFORDANCES_BACKUP_KEY, + KeyguardQuickAffordanceBackupHelper( + context = this, + userId = userHandle.identifier, + ), + ) } override fun onRestoreFinished() { super.onRestoreFinished() - val intent = Intent(ACTION_RESTORE_FINISHED).apply { - `package` = packageName - putExtra(Intent.EXTRA_USER_ID, userId) - flags = Intent.FLAG_RECEIVER_REGISTERED_ONLY - } + val intent = + Intent(ACTION_RESTORE_FINISHED).apply { + `package` = packageName + putExtra(Intent.EXTRA_USER_ID, userId) + flags = Intent.FLAG_RECEIVER_REGISTERED_ONLY + } sendBroadcastAsUser(intent, UserHandle.SYSTEM, PERMISSION_SELF) } @@ -90,7 +104,9 @@ open class BackupHelper : BackupAgentHelper() { * @property lock a lock to hold while backing up and restoring the files. * @property context the context of the [BackupAgent] * @property fileNamesAndPostProcess a map from the filenames to back up and the post processing + * ``` * actions to take + * ``` */ private class NoOverwriteFileBackupHelper( val lock: Any, @@ -115,23 +131,23 @@ open class BackupHelper : BackupAgentHelper() { data: BackupDataOutput?, newState: ParcelFileDescriptor? ) { - synchronized(lock) { - super.performBackup(oldState, data, newState) - } + synchronized(lock) { super.performBackup(oldState, data, newState) } } } } + private fun getPPControlsFile(context: Context): () -> Unit { return { val filesDir = context.filesDir val file = Environment.buildPath(filesDir, BackupHelper.CONTROLS) if (file.exists()) { - val dest = Environment.buildPath(filesDir, - AuxiliaryPersistenceWrapper.AUXILIARY_FILE_NAME) + val dest = + Environment.buildPath(filesDir, AuxiliaryPersistenceWrapper.AUXILIARY_FILE_NAME) file.copyTo(dest) val jobScheduler = context.getSystemService(JobScheduler::class.java) jobScheduler?.schedule( - AuxiliaryPersistenceWrapper.DeletionJobService.getJobForContext(context)) + AuxiliaryPersistenceWrapper.DeletionJobService.getJobForContext(context) + ) } } -}
\ No newline at end of file +} diff --git a/packages/SystemUI/src/com/android/systemui/broadcast/BroadcastDispatcher.kt b/packages/SystemUI/src/com/android/systemui/broadcast/BroadcastDispatcher.kt index 537cbc5a267d..a0a892de0085 100644 --- a/packages/SystemUI/src/com/android/systemui/broadcast/BroadcastDispatcher.kt +++ b/packages/SystemUI/src/com/android/systemui/broadcast/BroadcastDispatcher.kt @@ -64,8 +64,9 @@ private const val TAG = "BroadcastDispatcher" * from SystemUI. That way the number of calls to [BroadcastReceiver.onReceive] can be reduced for * a given broadcast. * - * Use only for IntentFilters with actions and optionally categories. It does not support, - * permissions, schemes, data types, data authorities or priority different than 0. + * Use only for IntentFilters with actions and optionally categories. It does not support schemes, + * data types, data authorities or priority different than 0. + * * Cannot be used for getting sticky broadcasts (either as return of registering or as re-delivery). * Broadcast handling may be asynchronous *without* calling goAsync(), as it's running within sysui * and doesn't need to worry about being killed. diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt index aa6c619d9969..2d558ad49e2d 100644 --- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt +++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt @@ -346,6 +346,12 @@ object Flags { // TODO(b/256873975): Tracking Bug @JvmField @Keep val WM_BUBBLE_BAR = unreleasedFlag(1111, "wm_bubble_bar") + // TODO(b/260271148): Tracking bug + @Keep + @JvmField + val WM_DESKTOP_WINDOWING_2 = + sysPropBooleanFlag(1112, "persist.wm.debug.desktop_mode_2", default = false) + // 1200 - predictive back @Keep @JvmField diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardQuickAffordanceProvider.kt b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardQuickAffordanceProvider.kt index 29febb6dd0d9..4ae37c51f278 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardQuickAffordanceProvider.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardQuickAffordanceProvider.kt @@ -29,7 +29,7 @@ import android.util.Log import com.android.systemui.SystemUIAppComponentFactoryBase import com.android.systemui.SystemUIAppComponentFactoryBase.ContextAvailableCallback import com.android.systemui.keyguard.domain.interactor.KeyguardQuickAffordanceInteractor -import com.android.systemui.shared.keyguard.data.content.KeyguardQuickAffordanceProviderContract as Contract +import com.android.systemui.shared.quickaffordance.data.content.KeyguardQuickAffordanceProviderContract as Contract import javax.inject.Inject import kotlinx.coroutines.runBlocking diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/CameraQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/CameraQuickAffordanceConfig.kt index 3c09aab60443..dbc376e62950 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/CameraQuickAffordanceConfig.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/CameraQuickAffordanceConfig.kt @@ -26,14 +26,17 @@ import com.android.systemui.common.shared.model.ContentDescription import com.android.systemui.common.shared.model.Icon import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application +import dagger.Lazy +import javax.inject.Inject import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flowOf -import javax.inject.Inject @SysUISingleton -class CameraQuickAffordanceConfig @Inject constructor( - @Application private val context: Context, - private val cameraGestureHelper: CameraGestureHelper, +class CameraQuickAffordanceConfig +@Inject +constructor( + @Application private val context: Context, + private val cameraGestureHelper: Lazy<CameraGestureHelper>, ) : KeyguardQuickAffordanceConfig { override val key: String @@ -46,17 +49,23 @@ class CameraQuickAffordanceConfig @Inject constructor( get() = com.android.internal.R.drawable.perm_group_camera override val lockScreenState: Flow<KeyguardQuickAffordanceConfig.LockScreenState> - get() = flowOf( - KeyguardQuickAffordanceConfig.LockScreenState.Visible( - icon = Icon.Resource( + get() = + flowOf( + KeyguardQuickAffordanceConfig.LockScreenState.Visible( + icon = + Icon.Resource( com.android.internal.R.drawable.perm_group_camera, ContentDescription.Resource(R.string.accessibility_camera_button) - ) + ) + ) ) - ) - override fun onTriggered(expandable: Expandable?): KeyguardQuickAffordanceConfig.OnTriggeredResult { - cameraGestureHelper.launchCamera(StatusBarManager.CAMERA_LAUNCH_SOURCE_QUICK_AFFORDANCE) + override fun onTriggered( + expandable: Expandable? + ): KeyguardQuickAffordanceConfig.OnTriggeredResult { + cameraGestureHelper + .get() + .launchCamera(StatusBarManager.CAMERA_LAUNCH_SOURCE_QUICK_AFFORDANCE) return KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled } -}
\ No newline at end of file +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceConfig.kt index 4477310dca41..98b1a731b82c 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceConfig.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceConfig.kt @@ -21,7 +21,7 @@ import android.content.Intent import com.android.systemui.animation.Expandable import com.android.systemui.common.shared.model.Icon import com.android.systemui.keyguard.shared.quickaffordance.ActivationState -import com.android.systemui.shared.keyguard.data.content.KeyguardQuickAffordanceProviderContract as Contract +import com.android.systemui.shared.quickaffordance.data.content.KeyguardQuickAffordanceProviderContract as Contract import kotlinx.coroutines.flow.Flow /** Defines interface that can act as data source for a single quick affordance model. */ diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceSelectionManager.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceSelectionManager.kt index b29cf45cc709..4f37e5f389ee 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceSelectionManager.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceSelectionManager.kt @@ -18,9 +18,11 @@ package com.android.systemui.keyguard.data.quickaffordance import android.content.Context +import android.content.IntentFilter import android.content.SharedPreferences -import androidx.annotation.VisibleForTesting import com.android.systemui.R +import com.android.systemui.backup.BackupHelper +import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow import com.android.systemui.dagger.SysUISingleton @@ -28,14 +30,18 @@ import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.settings.UserFileManager import com.android.systemui.settings.UserTracker import javax.inject.Inject +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.onStart /** * Manages and provides access to the current "selections" of keyguard quick affordances, answering * the question "which affordances should the keyguard show?". */ +@OptIn(ExperimentalCoroutinesApi::class) @SysUISingleton class KeyguardQuickAffordanceSelectionManager @Inject @@ -43,15 +49,10 @@ constructor( @Application context: Context, private val userFileManager: UserFileManager, private val userTracker: UserTracker, + broadcastDispatcher: BroadcastDispatcher, ) { - private val sharedPrefs: SharedPreferences - get() = - userFileManager.getSharedPreferences( - FILE_NAME, - Context.MODE_PRIVATE, - userTracker.userId, - ) + private var sharedPrefs: SharedPreferences = instantiateSharedPrefs() private val userId: Flow<Int> = conflatedCallbackFlow { val callback = @@ -78,21 +79,54 @@ constructor( } } + /** + * Emits an event each time a Backup & Restore restoration job is completed. Does not emit an + * initial value. + */ + private val backupRestorationEvents: Flow<Unit> = + broadcastDispatcher.broadcastFlow( + filter = IntentFilter(BackupHelper.ACTION_RESTORE_FINISHED), + flags = Context.RECEIVER_NOT_EXPORTED, + permission = BackupHelper.PERMISSION_SELF, + ) + /** IDs of affordances to show, indexed by slot ID, and sorted in descending priority order. */ val selections: Flow<Map<String, List<String>>> = - userId.flatMapLatest { - conflatedCallbackFlow { - val listener = - SharedPreferences.OnSharedPreferenceChangeListener { _, _ -> - trySend(getSelections()) - } + combine( + userId, + backupRestorationEvents.onStart { + // We emit an initial event to make sure that the combine emits at least once, + // even + // if we never get a Backup & Restore restoration event (which is the most + // common + // case anyway as restoration really only happens on initial device setup). + emit(Unit) + } + ) { _, _ -> + } + .flatMapLatest { + conflatedCallbackFlow { + // We want to instantiate a new SharedPreferences instance each time either the + // user + // ID changes or we have a backup & restore restoration event. The reason is + // that + // our sharedPrefs instance needs to be replaced with a new one as it depends on + // the + // user ID and when the B&R job completes, the backing file is replaced but the + // existing instance still has a stale in-memory cache. + sharedPrefs = instantiateSharedPrefs() - sharedPrefs.registerOnSharedPreferenceChangeListener(listener) - send(getSelections()) + val listener = + SharedPreferences.OnSharedPreferenceChangeListener { _, _ -> + trySend(getSelections()) + } - awaitClose { sharedPrefs.unregisterOnSharedPreferenceChangeListener(listener) } + sharedPrefs.registerOnSharedPreferenceChangeListener(listener) + send(getSelections()) + + awaitClose { sharedPrefs.unregisterOnSharedPreferenceChangeListener(listener) } + } } - } /** * Returns a snapshot of the IDs of affordances to show, indexed by slot ID, and sorted in @@ -144,9 +178,17 @@ constructor( sharedPrefs.edit().putString(key, value).apply() } + private fun instantiateSharedPrefs(): SharedPreferences { + return userFileManager.getSharedPreferences( + FILE_NAME, + Context.MODE_PRIVATE, + userTracker.userId, + ) + } + companion object { private const val TAG = "KeyguardQuickAffordanceSelectionManager" - @VisibleForTesting const val FILE_NAME = "quick_affordance_selections" + const val FILE_NAME = "quick_affordance_selections" private const val KEY_PREFIX_SLOT = "slot_" private const val SLOT_AFFORDANCES_DELIMITER = ":" private const val AFFORDANCE_DELIMITER = "," diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/backup/KeyguardQuickAffordanceBackupHelper.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/backup/KeyguardQuickAffordanceBackupHelper.kt new file mode 100644 index 000000000000..0e865cee0b76 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/backup/KeyguardQuickAffordanceBackupHelper.kt @@ -0,0 +1,44 @@ +/* + * 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.keyguard.domain.backup + +import android.app.backup.SharedPreferencesBackupHelper +import android.content.Context +import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceSelectionManager +import com.android.systemui.settings.UserFileManagerImpl + +/** Handles backup & restore for keyguard quick affordances. */ +class KeyguardQuickAffordanceBackupHelper( + context: Context, + userId: Int, +) : + SharedPreferencesBackupHelper( + context, + if (UserFileManagerImpl.isPrimaryUser(userId)) { + KeyguardQuickAffordanceSelectionManager.FILE_NAME + } else { + UserFileManagerImpl.secondaryUserFile( + context = context, + fileName = KeyguardQuickAffordanceSelectionManager.FILE_NAME, + directoryName = UserFileManagerImpl.SHARED_PREFS, + userId = userId, + ) + .also { UserFileManagerImpl.ensureParentDirExists(it) } + .toString() + } + ) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt index 2d94d760cb54..ee7154ff7219 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt @@ -34,8 +34,8 @@ import com.android.systemui.keyguard.shared.model.KeyguardSlotPickerRepresentati import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordancePosition import com.android.systemui.plugins.ActivityStarter import com.android.systemui.settings.UserTracker -import com.android.systemui.shared.keyguard.data.content.KeyguardQuickAffordanceProviderContract import com.android.systemui.shared.keyguard.shared.model.KeyguardQuickAffordanceSlots +import com.android.systemui.shared.quickaffordance.data.content.KeyguardQuickAffordanceProviderContract import com.android.systemui.statusbar.policy.KeyguardStateController import dagger.Lazy import javax.inject.Inject diff --git a/packages/SystemUI/src/com/android/systemui/settings/UserFileManagerImpl.kt b/packages/SystemUI/src/com/android/systemui/settings/UserFileManagerImpl.kt index d450afa59c7d..bfba6dfddfac 100644 --- a/packages/SystemUI/src/com/android/systemui/settings/UserFileManagerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/settings/UserFileManagerImpl.kt @@ -35,12 +35,14 @@ import java.io.File import javax.inject.Inject /** - * Implementation for retrieving file paths for file storage of system and secondary users. - * Files lie in {File Directory}/UserFileManager/{User Id} for secondary user. - * For system user, we use the conventional {File Directory} + * Implementation for retrieving file paths for file storage of system and secondary users. Files + * lie in {File Directory}/UserFileManager/{User Id} for secondary user. For system user, we use the + * conventional {File Directory} */ @SysUISingleton -class UserFileManagerImpl @Inject constructor( +class UserFileManagerImpl +@Inject +constructor( // Context of system process and system user. private val context: Context, val userManager: UserManager, @@ -49,80 +51,114 @@ class UserFileManagerImpl @Inject constructor( ) : UserFileManager, CoreStartable { companion object { private const val FILES = "files" - @VisibleForTesting internal const val SHARED_PREFS = "shared_prefs" + const val SHARED_PREFS = "shared_prefs" @VisibleForTesting internal const val ID = "UserFileManager" - } - private val broadcastReceiver = object : BroadcastReceiver() { + /** Returns `true` if the given user ID is that for the primary/system user. */ + fun isPrimaryUser(userId: Int): Boolean { + return UserHandle(userId).isSystem + } + /** - * Listen to Intent.ACTION_USER_REMOVED to clear user data. + * Returns a [File] pointing to the correct path for a secondary user ID. + * + * Note that there is no check for the type of user. This should only be called for + * secondary users, never for the system user. For that, make sure to call [isPrimaryUser]. + * + * Note also that there is no guarantee that the parent directory structure for the file + * exists on disk. For that, call [ensureParentDirExists]. + * + * @param context The context + * @param fileName The name of the file + * @param directoryName The name of the directory that would contain the file + * @param userId The ID of the user to build a file path for */ - override fun onReceive(context: Context, intent: Intent) { - if (intent.action == Intent.ACTION_USER_REMOVED) { - clearDeletedUserData() + fun secondaryUserFile( + context: Context, + fileName: String, + directoryName: String, + userId: Int, + ): File { + return Environment.buildPath( + context.filesDir, + ID, + userId.toString(), + directoryName, + fileName, + ) + } + + /** + * Checks to see if parent dir of the file exists. If it does not, we create the parent dirs + * recursively. + */ + fun ensureParentDirExists(file: File) { + val parent = file.parentFile + if (!parent.exists()) { + if (!parent.mkdirs()) { + Log.e(ID, "Could not create parent directory for file: ${file.absolutePath}") + } } } } - /** - * Poll for user-specific directories to delete upon start up. - */ + private val broadcastReceiver = + object : BroadcastReceiver() { + /** Listen to Intent.ACTION_USER_REMOVED to clear user data. */ + override fun onReceive(context: Context, intent: Intent) { + if (intent.action == Intent.ACTION_USER_REMOVED) { + clearDeletedUserData() + } + } + } + + /** Poll for user-specific directories to delete upon start up. */ override fun start() { clearDeletedUserData() - val filter = IntentFilter().apply { - addAction(Intent.ACTION_USER_REMOVED) - } + val filter = IntentFilter().apply { addAction(Intent.ACTION_USER_REMOVED) } broadcastDispatcher.registerReceiver(broadcastReceiver, filter, backgroundExecutor) } - /** - * Return the file based on current user. - */ + /** Return the file based on current user. */ override fun getFile(fileName: String, userId: Int): File { - return if (UserHandle(userId).isSystem) { - Environment.buildPath( - context.filesDir, - fileName - ) + return if (isPrimaryUser(userId)) { + Environment.buildPath(context.filesDir, fileName) } else { - val secondaryFile = Environment.buildPath( - context.filesDir, - ID, - userId.toString(), - FILES, - fileName - ) + val secondaryFile = + secondaryUserFile( + context = context, + userId = userId, + directoryName = FILES, + fileName = fileName, + ) ensureParentDirExists(secondaryFile) secondaryFile } } - /** - * Get shared preferences from user. - */ + /** Get shared preferences from user. */ override fun getSharedPreferences( fileName: String, @Context.PreferencesMode mode: Int, userId: Int ): SharedPreferences { - if (UserHandle(userId).isSystem) { + if (isPrimaryUser(userId)) { return context.getSharedPreferences(fileName, mode) } - val secondaryUserDir = Environment.buildPath( - context.filesDir, - ID, - userId.toString(), - SHARED_PREFS, - fileName - ) + + val secondaryUserDir = + secondaryUserFile( + context = context, + fileName = fileName, + directoryName = SHARED_PREFS, + userId = userId, + ) ensureParentDirExists(secondaryUserDir) return context.getSharedPreferences(secondaryUserDir, mode) } - /** - * Remove dirs for deleted users. - */ + /** Remove dirs for deleted users. */ @VisibleForTesting internal fun clearDeletedUserData() { backgroundExecutor.execute { @@ -133,10 +169,11 @@ class UserFileManagerImpl @Inject constructor( dirsToDelete.forEach { dir -> try { - val dirToDelete = Environment.buildPath( - file, - dir, - ) + val dirToDelete = + Environment.buildPath( + file, + dir, + ) dirToDelete.deleteRecursively() } catch (e: Exception) { Log.e(ID, "Deletion failed.", e) @@ -144,18 +181,4 @@ class UserFileManagerImpl @Inject constructor( } } } - - /** - * Checks to see if parent dir of the file exists. If it does not, we create the parent dirs - * recursively. - */ - @VisibleForTesting - internal fun ensureParentDirExists(file: File) { - val parent = file.parentFile - if (!parent.exists()) { - if (!parent.mkdirs()) { - Log.e(ID, "Could not create parent directory for file: ${file.absolutePath}") - } - } - } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardQuickAffordanceProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardQuickAffordanceProviderTest.kt index cedde58746d2..32c5b3f99d41 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardQuickAffordanceProviderTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardQuickAffordanceProviderTest.kt @@ -36,8 +36,8 @@ import com.android.systemui.keyguard.domain.interactor.KeyguardQuickAffordanceIn import com.android.systemui.plugins.ActivityStarter import com.android.systemui.settings.UserFileManager import com.android.systemui.settings.UserTracker -import com.android.systemui.shared.keyguard.data.content.KeyguardQuickAffordanceProviderContract as Contract import com.android.systemui.shared.keyguard.shared.model.KeyguardQuickAffordanceSlots +import com.android.systemui.shared.quickaffordance.data.content.KeyguardQuickAffordanceProviderContract as Contract import com.android.systemui.statusbar.policy.KeyguardStateController import com.android.systemui.util.FakeSharedPreferences import com.android.systemui.util.mockito.mock @@ -89,6 +89,7 @@ class KeyguardQuickAffordanceProviderTest : SysuiTestCase() { .thenReturn(FakeSharedPreferences()) }, userTracker = userTracker, + broadcastDispatcher = fakeBroadcastDispatcher, ) val quickAffordanceRepository = KeyguardQuickAffordanceRepository( diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/CameraQuickAffordanceConfigTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/CameraQuickAffordanceConfigTest.kt index 623becf166d3..7205f3068abb 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/CameraQuickAffordanceConfigTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/CameraQuickAffordanceConfigTest.kt @@ -37,25 +37,29 @@ class CameraQuickAffordanceConfigTest : SysuiTestCase() { @Mock private lateinit var cameraGestureHelper: CameraGestureHelper @Mock private lateinit var context: Context + private lateinit var underTest: CameraQuickAffordanceConfig @Before fun setUp() { MockitoAnnotations.initMocks(this) - underTest = CameraQuickAffordanceConfig( + + underTest = + CameraQuickAffordanceConfig( context, - cameraGestureHelper, - ) + ) { + cameraGestureHelper + } } @Test fun `affordance triggered -- camera launch called`() { - //when + // When val result = underTest.onTriggered(null) - //then + // Then verify(cameraGestureHelper) - .launchCamera(StatusBarManager.CAMERA_LAUNCH_SOURCE_QUICK_AFFORDANCE) + .launchCamera(StatusBarManager.CAMERA_LAUNCH_SOURCE_QUICK_AFFORDANCE) assertEquals(KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled, result) } -}
\ No newline at end of file +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceLegacySettingSyncerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceLegacySettingSyncerTest.kt index 8ef921eaa50a..552b8cb96525 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceLegacySettingSyncerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceLegacySettingSyncerTest.kt @@ -89,6 +89,7 @@ class KeyguardQuickAffordanceLegacySettingSyncerTest : SysuiTestCase() { .thenReturn(FakeSharedPreferences()) }, userTracker = FakeUserTracker(), + broadcastDispatcher = fakeBroadcastDispatcher, ) settings = FakeSettings() settings.putInt(Settings.Secure.LOCKSCREEN_SHOW_CONTROLS, 0) diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceSelectionManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceSelectionManagerTest.kt index d8ee9f113d33..6a2376b5bc4e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceSelectionManagerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceSelectionManagerTest.kt @@ -17,6 +17,7 @@ package com.android.systemui.keyguard.data.quickaffordance +import android.content.Intent import android.content.SharedPreferences import android.content.pm.UserInfo import androidx.test.filters.SmallTest @@ -27,10 +28,15 @@ import com.android.systemui.settings.UserFileManager import com.android.systemui.util.FakeSharedPreferences import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.toList import kotlinx.coroutines.launch import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.resetMain import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.setMain +import org.junit.After import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -38,8 +44,12 @@ import org.junit.runners.JUnit4 import org.mockito.ArgumentMatchers.anyInt import org.mockito.ArgumentMatchers.anyString import org.mockito.Mock +import org.mockito.Mockito.atLeastOnce +import org.mockito.Mockito.clearInvocations +import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations +@OptIn(ExperimentalCoroutinesApi::class) @SmallTest @RunWith(JUnit4::class) class KeyguardQuickAffordanceSelectionManagerTest : SysuiTestCase() { @@ -60,15 +70,23 @@ class KeyguardQuickAffordanceSelectionManagerTest : SysuiTestCase() { sharedPrefs.getOrPut(userId) { FakeSharedPreferences() } } userTracker = FakeUserTracker() + val dispatcher = UnconfinedTestDispatcher() + Dispatchers.setMain(dispatcher) underTest = KeyguardQuickAffordanceSelectionManager( context = context, userFileManager = userFileManager, userTracker = userTracker, + broadcastDispatcher = fakeBroadcastDispatcher, ) } + @After + fun tearDown() { + Dispatchers.resetMain() + } + @Test fun setSelections() = runTest { overrideResource(R.array.config_keyguardQuickAffordanceDefaults, arrayOf<String>()) @@ -318,6 +336,22 @@ class KeyguardQuickAffordanceSelectionManagerTest : SysuiTestCase() { job.cancel() } + @Test + fun `responds to backup and restore by reloading the selections from disk`() = runTest { + overrideResource(R.array.config_keyguardQuickAffordanceDefaults, arrayOf<String>()) + val affordanceIdsBySlotId = mutableListOf<Map<String, List<String>>>() + val job = + launch(UnconfinedTestDispatcher()) { + underTest.selections.toList(affordanceIdsBySlotId) + } + clearInvocations(userFileManager) + + fakeBroadcastDispatcher.registeredReceivers.firstOrNull()?.onReceive(context, Intent()) + + verify(userFileManager, atLeastOnce()).getSharedPreferences(anyString(), anyInt(), anyInt()) + job.cancel() + } + private fun assertSelections( observed: Map<String, List<String>>?, expected: Map<String, List<String>>, diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepositoryTest.kt index 5c75417c3473..652fae968744 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepositoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepositoryTest.kt @@ -76,6 +76,7 @@ class KeyguardQuickAffordanceRepositoryTest : SysuiTestCase() { .thenReturn(FakeSharedPreferences()) }, userTracker = FakeUserTracker(), + broadcastDispatcher = fakeBroadcastDispatcher, ) underTest = diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt index c2650ec455d8..ba7c40b6b381 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt @@ -252,6 +252,7 @@ class KeyguardQuickAffordanceInteractorParameterizedTest : SysuiTestCase() { .thenReturn(FakeSharedPreferences()) }, userTracker = userTracker, + broadcastDispatcher = fakeBroadcastDispatcher, ) val quickAffordanceRepository = KeyguardQuickAffordanceRepository( diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt index b79030602368..8d0c4ef4b3da 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt @@ -113,6 +113,7 @@ class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() { .thenReturn(FakeSharedPreferences()) }, userTracker = userTracker, + broadcastDispatcher = fakeBroadcastDispatcher, ) val quickAffordanceRepository = KeyguardQuickAffordanceRepository( diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt index 8b166bd89426..32849cdce02e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt @@ -136,6 +136,7 @@ class KeyguardBottomAreaViewModelTest : SysuiTestCase() { .thenReturn(FakeSharedPreferences()) }, userTracker = userTracker, + broadcastDispatcher = fakeBroadcastDispatcher, ) val quickAffordanceRepository = KeyguardQuickAffordanceRepository( diff --git a/packages/SystemUI/tests/src/com/android/systemui/settings/UserFileManagerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/settings/UserFileManagerImplTest.kt index 6d9b01e28aa4..020a86611552 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/settings/UserFileManagerImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/settings/UserFileManagerImplTest.kt @@ -50,24 +50,20 @@ class UserFileManagerImplTest : SysuiTestCase() { lateinit var userFileManager: UserFileManagerImpl lateinit var backgroundExecutor: FakeExecutor - @Mock - lateinit var userManager: UserManager - @Mock - lateinit var broadcastDispatcher: BroadcastDispatcher + @Mock lateinit var userManager: UserManager + @Mock lateinit var broadcastDispatcher: BroadcastDispatcher @Before fun setUp() { MockitoAnnotations.initMocks(this) backgroundExecutor = FakeExecutor(FakeSystemClock()) - userFileManager = UserFileManagerImpl(context, userManager, - broadcastDispatcher, backgroundExecutor) + userFileManager = + UserFileManagerImpl(context, userManager, broadcastDispatcher, backgroundExecutor) } @After fun end() { - val dir = Environment.buildPath( - context.filesDir, - UserFileManagerImpl.ID) + val dir = Environment.buildPath(context.filesDir, UserFileManagerImpl.ID) dir.deleteRecursively() } @@ -82,13 +78,14 @@ class UserFileManagerImplTest : SysuiTestCase() { @Test fun testGetSharedPreferences() { val secondarySharedPref = userFileManager.getSharedPreferences(TEST_FILE_NAME, 0, 11) - val secondaryUserDir = Environment.buildPath( - context.filesDir, - UserFileManagerImpl.ID, - "11", - UserFileManagerImpl.SHARED_PREFS, - TEST_FILE_NAME - ) + val secondaryUserDir = + Environment.buildPath( + context.filesDir, + UserFileManagerImpl.ID, + "11", + UserFileManagerImpl.SHARED_PREFS, + TEST_FILE_NAME + ) assertThat(secondarySharedPref).isNotNull() assertThat(secondaryUserDir.exists()) @@ -101,32 +98,35 @@ class UserFileManagerImplTest : SysuiTestCase() { val userFileManager = spy(userFileManager) userFileManager.start() verify(userFileManager).clearDeletedUserData() - verify(broadcastDispatcher).registerReceiver(any(BroadcastReceiver::class.java), - any(IntentFilter::class.java), - any(Executor::class.java), isNull(), eq(Context.RECEIVER_EXPORTED), isNull()) + verify(broadcastDispatcher) + .registerReceiver( + any(BroadcastReceiver::class.java), + any(IntentFilter::class.java), + any(Executor::class.java), + isNull(), + eq(Context.RECEIVER_EXPORTED), + isNull() + ) } @Test fun testClearDeletedUserData() { - val dir = Environment.buildPath( - context.filesDir, - UserFileManagerImpl.ID, - "11", - "files" - ) + val dir = Environment.buildPath(context.filesDir, UserFileManagerImpl.ID, "11", "files") dir.mkdirs() - val file = Environment.buildPath( - context.filesDir, - UserFileManagerImpl.ID, - "11", - "files", - TEST_FILE_NAME - ) - val secondaryUserDir = Environment.buildPath( - context.filesDir, - UserFileManagerImpl.ID, - "11", - ) + val file = + Environment.buildPath( + context.filesDir, + UserFileManagerImpl.ID, + "11", + "files", + TEST_FILE_NAME + ) + val secondaryUserDir = + Environment.buildPath( + context.filesDir, + UserFileManagerImpl.ID, + "11", + ) file.createNewFile() assertThat(secondaryUserDir.exists()).isTrue() assertThat(file.exists()).isTrue() @@ -139,15 +139,16 @@ class UserFileManagerImplTest : SysuiTestCase() { @Test fun testEnsureParentDirExists() { - val file = Environment.buildPath( - context.filesDir, - UserFileManagerImpl.ID, - "11", - "files", - TEST_FILE_NAME - ) + val file = + Environment.buildPath( + context.filesDir, + UserFileManagerImpl.ID, + "11", + "files", + TEST_FILE_NAME + ) assertThat(file.parentFile.exists()).isFalse() - userFileManager.ensureParentDirExists(file) + UserFileManagerImpl.ensureParentDirExists(file) assertThat(file.parentFile.exists()).isTrue() } } diff --git a/services/core/java/com/android/server/BinaryTransparencyService.java b/services/core/java/com/android/server/BinaryTransparencyService.java index 59024e797d7a..6cd7ce8a1c43 100644 --- a/services/core/java/com/android/server/BinaryTransparencyService.java +++ b/services/core/java/com/android/server/BinaryTransparencyService.java @@ -310,27 +310,27 @@ public class BinaryTransparencyService extends SystemService { Bundle packageMeasurement = measurePackage(packageInfo); results.add(packageMeasurement); - if (record) { + if (record && (mba_status == MBA_STATUS_UPDATED_PRELOAD)) { // compute digests of signing info String[] signerDigestHexStrings = computePackageSignerSha256Digests( packageInfo.signingInfo); // now we should have all the bits for the atom - /* TODO: Uncomment and test after merging new atom definition. + byte[] cDigest = packageMeasurement.getByteArray(BUNDLE_CONTENT_DIGEST); FrameworkStatsLog.write(FrameworkStatsLog.MOBILE_BUNDLED_APP_INFO_GATHERED, packageInfo.packageName, packageInfo.getLongVersionCode(), - HexEncoding.encodeToString(packageMeasurement.getByteArray( - BUNDLE_CONTENT_DIGEST), false), + (cDigest != null) ? HexEncoding.encodeToString( + packageMeasurement.getByteArray(BUNDLE_CONTENT_DIGEST), + false) : null, packageMeasurement.getInt(BUNDLE_CONTENT_DIGEST_ALGORITHM), signerDigestHexStrings, // signer_cert_digest - mba_status, // mba_status + mba_status, // mba_status null, // initiator null, // initiator_signer_digest null, // installer null // originator ); - */ } } if (DEBUG) { @@ -377,12 +377,13 @@ public class BinaryTransparencyService extends SystemService { } // we should now have all the info needed for the atom - /* TODO: Uncomment and test after merging new atom definition. + byte[] cDigest = packageMeasurement.getByteArray(BUNDLE_CONTENT_DIGEST); FrameworkStatsLog.write(FrameworkStatsLog.MOBILE_BUNDLED_APP_INFO_GATHERED, packageInfo.packageName, packageInfo.getLongVersionCode(), - HexEncoding.encodeToString(packageMeasurement.getByteArray( - BUNDLE_CONTENT_DIGEST), false), + (cDigest != null) ? HexEncoding.encodeToString( + packageMeasurement.getByteArray(BUNDLE_CONTENT_DIGEST), + false) : null, packageMeasurement.getInt(BUNDLE_CONTENT_DIGEST_ALGORITHM), signerDigestHexStrings, MBA_STATUS_NEW_INSTALL, // mba_status @@ -391,7 +392,6 @@ public class BinaryTransparencyService extends SystemService { installer, originator ); - */ } } if (DEBUG) { diff --git a/services/core/java/com/android/server/DockObserver.java b/services/core/java/com/android/server/DockObserver.java index 540ed4cdb330..3487613d313c 100644 --- a/services/core/java/com/android/server/DockObserver.java +++ b/services/core/java/com/android/server/DockObserver.java @@ -19,6 +19,7 @@ package com.android.server; import android.content.ContentResolver; import android.content.Context; import android.content.Intent; +import android.database.ContentObserver; import android.media.AudioManager; import android.media.Ringtone; import android.media.RingtoneManager; @@ -73,6 +74,7 @@ final class DockObserver extends SystemService { private final boolean mAllowTheaterModeWakeFromDock; private final List<ExtconStateConfig> mExtconStateConfigs; + private DeviceProvisionedObserver mDeviceProvisionedObserver; static final class ExtconStateProvider { private final Map<String, String> mState; @@ -110,7 +112,7 @@ final class DockObserver extends SystemService { Slog.w(TAG, "No state file found at: " + stateFilePath); return new ExtconStateProvider(new HashMap<>()); } catch (Exception e) { - Slog.e(TAG, "" , e); + Slog.e(TAG, "", e); return new ExtconStateProvider(new HashMap<>()); } } @@ -136,7 +138,7 @@ final class DockObserver extends SystemService { private static List<ExtconStateConfig> loadExtconStateConfigs(Context context) { String[] rows = context.getResources().getStringArray( - com.android.internal.R.array.config_dockExtconStateMapping); + com.android.internal.R.array.config_dockExtconStateMapping); try { ArrayList<ExtconStateConfig> configs = new ArrayList<>(); for (String row : rows) { @@ -167,6 +169,7 @@ final class DockObserver extends SystemService { com.android.internal.R.bool.config_allowTheaterModeWakeFromDock); mKeepDreamingWhenUndocking = context.getResources().getBoolean( com.android.internal.R.bool.config_keepDreamingWhenUndocking); + mDeviceProvisionedObserver = new DeviceProvisionedObserver(mHandler); mExtconStateConfigs = loadExtconStateConfigs(context); @@ -199,15 +202,19 @@ final class DockObserver extends SystemService { if (phase == PHASE_ACTIVITY_MANAGER_READY) { synchronized (mLock) { mSystemReady = true; - - // don't bother broadcasting undocked here - if (mReportedDockState != Intent.EXTRA_DOCK_STATE_UNDOCKED) { - updateLocked(); - } + mDeviceProvisionedObserver.onSystemReady(); + updateIfDockedLocked(); } } } + private void updateIfDockedLocked() { + // don't bother broadcasting undocked here + if (mReportedDockState != Intent.EXTRA_DOCK_STATE_UNDOCKED) { + updateLocked(); + } + } + private void setActualDockStateLocked(int newState) { mActualDockState = newState; if (!mUpdatesStopped) { @@ -252,8 +259,7 @@ final class DockObserver extends SystemService { // Skip the dock intent if not yet provisioned. final ContentResolver cr = getContext().getContentResolver(); - if (Settings.Global.getInt(cr, - Settings.Global.DEVICE_PROVISIONED, 0) == 0) { + if (!mDeviceProvisionedObserver.isDeviceProvisioned()) { Slog.i(TAG, "Device not provisioned, skipping dock broadcast"); return; } @@ -419,4 +425,48 @@ final class DockObserver extends SystemService { } } } + + private final class DeviceProvisionedObserver extends ContentObserver { + private boolean mRegistered; + + public DeviceProvisionedObserver(Handler handler) { + super(handler); + } + + @Override + public void onChange(boolean selfChange, Uri uri) { + synchronized (mLock) { + updateRegistration(); + if (isDeviceProvisioned()) { + // Send the dock broadcast if device is docked after provisioning. + updateIfDockedLocked(); + } + } + } + + void onSystemReady() { + updateRegistration(); + } + + private void updateRegistration() { + boolean register = !isDeviceProvisioned(); + if (register == mRegistered) { + return; + } + final ContentResolver resolver = getContext().getContentResolver(); + if (register) { + resolver.registerContentObserver( + Settings.Global.getUriFor(Settings.Global.DEVICE_PROVISIONED), + false, this); + } else { + resolver.unregisterContentObserver(this); + } + mRegistered = register; + } + + boolean isDeviceProvisioned() { + return Settings.Global.getInt(getContext().getContentResolver(), + Settings.Global.DEVICE_PROVISIONED, 0) != 0; + } + } } diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java index 35b46c1104f7..50be45804e4f 100644 --- a/services/core/java/com/android/server/am/ActivityManagerService.java +++ b/services/core/java/com/android/server/am/ActivityManagerService.java @@ -87,6 +87,7 @@ import static android.os.Process.getTotalMemory; import static android.os.Process.isSdkSandboxUid; import static android.os.Process.isThreadInProcess; import static android.os.Process.killProcess; +import static android.os.Process.killProcessGroup; import static android.os.Process.killProcessQuiet; import static android.os.Process.myPid; import static android.os.Process.myUid; @@ -952,13 +953,6 @@ public class ActivityManagerService extends IActivityManager.Stub } return false; } - - boolean doRemoveIfNoThreadInternal(int pid, ProcessRecord app) { - if (app == null || app.getThread() != null) { - return false; - } - return doRemoveInternal(pid, app); - } } private final PendingStartActivityUids mPendingStartActivityUids; @@ -990,7 +984,7 @@ public class ActivityManagerService extends IActivityManager.Stub * method. */ @GuardedBy("this") - void removePidLocked(int pid, ProcessRecord app) { + boolean removePidLocked(int pid, ProcessRecord app) { final boolean removed; synchronized (mPidsSelfLocked) { removed = mPidsSelfLocked.doRemoveInternal(pid, app); @@ -1001,26 +995,6 @@ public class ActivityManagerService extends IActivityManager.Stub } mAtmInternal.onProcessUnMapped(pid); } - } - - /** - * Removes the process record from the map if it doesn't have a thread. - * <p>NOTE: Callers should avoid acquiring the mPidsSelfLocked lock before calling this - * method. - */ - @GuardedBy("this") - private boolean removePidIfNoThreadLocked(ProcessRecord app) { - final boolean removed; - final int pid = app.getPid(); - synchronized (mPidsSelfLocked) { - removed = mPidsSelfLocked.doRemoveIfNoThreadInternal(pid, app); - } - if (removed) { - synchronized (sActiveProcessInfoSelfLocked) { - sActiveProcessInfoSelfLocked.remove(pid); - } - mAtmInternal.onProcessUnMapped(pid); - } return removed; } @@ -2364,7 +2338,7 @@ public class ActivityManagerService extends IActivityManager.Stub mAppErrors = null; mPackageWatchdog = null; mAppOpsService = mInjector.getAppOpsService(null /* file */, null /* handler */); - mBatteryStatsService = null; + mBatteryStatsService = mInjector.getBatteryStatsService(); mHandler = new MainHandler(handlerThread.getLooper()); mHandlerThread = handlerThread; mConstants = new ActivityManagerConstants(mContext, this, mHandler); @@ -2379,7 +2353,7 @@ public class ActivityManagerService extends IActivityManager.Stub mIntentFirewall = null; mProcessStats = new ProcessStatsService(this, mContext.getCacheDir()); mCpHelper = new ContentProviderHelper(this, false); - mServices = null; + mServices = mInjector.getActiveServices(this); mSystemThread = null; mUiHandler = injector.getUiHandler(null /* service */); mUidObserverController = new UidObserverController(mUiHandler); @@ -4771,7 +4745,7 @@ public class ActivityManagerService extends IActivityManager.Stub @GuardedBy("this") void handleProcessStartOrKillTimeoutLocked(ProcessRecord app, boolean isKillTimeout) { final int pid = app.getPid(); - boolean gone = isKillTimeout || removePidIfNoThreadLocked(app); + boolean gone = isKillTimeout || removePidLocked(pid, app); if (gone) { if (isKillTimeout) { @@ -4852,7 +4826,7 @@ public class ActivityManagerService extends IActivityManager.Stub } @GuardedBy("this") - private boolean attachApplicationLocked(@NonNull IApplicationThread thread, + private void attachApplicationLocked(@NonNull IApplicationThread thread, int pid, int callingUid, long startSeq) { // Find the application record that is being attached... either via @@ -4917,7 +4891,7 @@ public class ActivityManagerService extends IActivityManager.Stub // Ignore exceptions. } } - return false; + return; } // If this application record is still attached to a previous @@ -4942,7 +4916,7 @@ public class ActivityManagerService extends IActivityManager.Stub mProcessList.startProcessLocked(app, new HostingRecord(HostingRecord.HOSTING_TYPE_LINK_FAIL, processName), ZYGOTE_POLICY_FLAG_EMPTY); - return false; + return; } EventLogTags.writeAmProcBound(app.userId, pid, app.processName); @@ -4965,8 +4939,6 @@ public class ActivityManagerService extends IActivityManager.Stub app.setUnlocked(StorageManager.isUserKeyUnlocked(app.userId)); } - mHandler.removeMessages(PROC_START_TIMEOUT_MSG, app); - boolean normalMode = mProcessesReady || isAllowedWhileBooting(app.info); List<ProviderInfo> providers = normalMode ? mCpHelper.generateApplicationProvidersLocked(app) @@ -5132,7 +5104,7 @@ public class ActivityManagerService extends IActivityManager.Stub app.killLocked("error during bind", ApplicationExitInfo.REASON_INITIALIZATION_FAILURE, true); handleAppDiedLocked(app, pid, false, true, false /* fromBinderDied */); - return false; + return; } // Remove this record from the list of starting applications. @@ -5140,103 +5112,155 @@ public class ActivityManagerService extends IActivityManager.Stub if (DEBUG_PROCESSES && mProcessesOnHold.contains(app)) Slog.v(TAG_PROCESSES, "Attach application locked removing on hold: " + app); mProcessesOnHold.remove(app); + } - boolean badApp = false; - boolean didSomething = false; + @Override + public final void attachApplication(IApplicationThread thread, long startSeq) { + if (thread == null) { + throw new SecurityException("Invalid application interface"); + } + synchronized (this) { + int callingPid = Binder.getCallingPid(); + final int callingUid = Binder.getCallingUid(); + final long origId = Binder.clearCallingIdentity(); + attachApplicationLocked(thread, callingPid, callingUid, startSeq); + Binder.restoreCallingIdentity(origId); + } + } - // See if the top visible activity is waiting to run in this process... - if (normalMode) { - try { - didSomething = mAtmInternal.attachApplication(app.getWindowProcessController()); - } catch (Exception e) { - Slog.wtf(TAG, "Exception thrown launching activities in " + app, e); - badApp = true; - } + private void finishAttachApplicationInner(long startSeq, int uid, int pid) { + final long startTime = SystemClock.uptimeMillis(); + // Find the application record that is being attached... either via + // the pid if we are running in multiple processes, or just pull the + // next app record if we are emulating process with anonymous threads. + final ProcessRecord app; + synchronized (mPidsSelfLocked) { + app = mPidsSelfLocked.get(pid); } - // Find any services that should be running in this process... - if (!badApp) { - try { - didSomething |= mServices.attachApplicationLocked(app, processName); - checkTime(startTime, "attachApplicationLocked: after mServices.attachApplicationLocked"); - } catch (Exception e) { - Slog.wtf(TAG, "Exception thrown starting services in " + app, e); - badApp = true; - } + if (app != null && app.getStartUid() == uid && app.getStartSeq() == startSeq) { + mHandler.removeMessages(PROC_START_TIMEOUT_MSG, app); + } else { + Slog.wtf(TAG, "Mismatched or missing ProcessRecord: " + app + ". Pid: " + pid + + ". Uid: " + uid); + killProcess(pid); + killProcessGroup(uid, pid); + mProcessList.noteAppKill(pid, uid, + ApplicationExitInfo.REASON_INITIALIZATION_FAILURE, + ApplicationExitInfo.SUBREASON_UNKNOWN, + "wrong startSeq"); + app.killLocked("unexpected process record", + ApplicationExitInfo.REASON_OTHER, true); + return; } - // Check if a next-broadcast receiver is in this process... - if (!badApp) { - try { - for (BroadcastQueue queue : mBroadcastQueues) { - didSomething |= queue.onApplicationAttachedLocked(app); + synchronized (this) { + final boolean normalMode = mProcessesReady || isAllowedWhileBooting(app.info); + final String processName = app.processName; + boolean badApp = false; + boolean didSomething = false; + + // See if the top visible activity is waiting to run in this process... + if (normalMode) { + try { + didSomething = mAtmInternal.attachApplication(app.getWindowProcessController()); + } catch (Exception e) { + Slog.wtf(TAG, "Exception thrown launching activities in " + app, e); + badApp = true; } - checkTime(startTime, "attachApplicationLocked: after dispatching broadcasts"); - } catch (Exception e) { - // If the app died trying to launch the receiver we declare it 'bad' - Slog.wtf(TAG, "Exception thrown dispatching broadcasts in " + app, e); - badApp = true; } - } - // Check whether the next backup agent is in this process... - if (!badApp && backupTarget != null && backupTarget.app == app) { - if (DEBUG_BACKUP) Slog.v(TAG_BACKUP, - "New app is backup target, launching agent for " + app); - notifyPackageUse(backupTarget.appInfo.packageName, - PackageManager.NOTIFY_PACKAGE_USE_BACKUP); - try { - thread.scheduleCreateBackupAgent(backupTarget.appInfo, - backupTarget.backupMode, backupTarget.userId, - backupTarget.backupDestination); - } catch (Exception e) { - Slog.wtf(TAG, "Exception thrown creating backup agent in " + app, e); - badApp = true; + // Find any services that should be running in this process... + if (!badApp) { + try { + didSomething |= mServices.attachApplicationLocked(app, processName); + checkTime(startTime, "finishAttachApplicationInner: " + + "after mServices.attachApplicationLocked"); + } catch (Exception e) { + Slog.wtf(TAG, "Exception thrown starting services in " + app, e); + badApp = true; + } } - } - if (badApp) { - app.killLocked("error during init", ApplicationExitInfo.REASON_INITIALIZATION_FAILURE, - true); - handleAppDiedLocked(app, pid, false, true, false /* fromBinderDied */); - return false; - } + // Check if a next-broadcast receiver is in this process... + if (!badApp) { + try { + for (BroadcastQueue queue : mBroadcastQueues) { + didSomething |= queue.onApplicationAttachedLocked(app); + } + checkTime(startTime, "finishAttachApplicationInner: " + + "after dispatching broadcasts"); + } catch (Exception e) { + // If the app died trying to launch the receiver we declare it 'bad' + Slog.wtf(TAG, "Exception thrown dispatching broadcasts in " + app, e); + badApp = true; + } + } - if (!didSomething) { - updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_PROCESS_BEGIN); - checkTime(startTime, "attachApplicationLocked: after updateOomAdjLocked"); - } + // Check whether the next backup agent is in this process... + final BackupRecord backupTarget = mBackupTargets.get(app.userId); + if (!badApp && backupTarget != null && backupTarget.app == app) { + if (DEBUG_BACKUP) { + Slog.v(TAG_BACKUP, + "New app is backup target, launching agent for " + app); + } + notifyPackageUse(backupTarget.appInfo.packageName, + PackageManager.NOTIFY_PACKAGE_USE_BACKUP); + try { + app.getThread().scheduleCreateBackupAgent(backupTarget.appInfo, + backupTarget.backupMode, backupTarget.userId, + backupTarget.backupDestination); + } catch (Exception e) { + Slog.wtf(TAG, "Exception thrown creating backup agent in " + app, e); + badApp = true; + } + } - final HostingRecord hostingRecord = app.getHostingRecord(); - String shortAction = getShortAction(hostingRecord.getAction()); - FrameworkStatsLog.write( - FrameworkStatsLog.PROCESS_START_TIME, - app.info.uid, - pid, - app.info.packageName, - FrameworkStatsLog.PROCESS_START_TIME__TYPE__COLD, - app.getStartElapsedTime(), - (int) (bindApplicationTimeMillis - app.getStartUptime()), - (int) (SystemClock.uptimeMillis() - app.getStartUptime()), - hostingRecord.getType(), - hostingRecord.getName(), - shortAction, - HostingRecord.getHostingTypeIdStatsd(hostingRecord.getType()), - HostingRecord.getTriggerTypeForStatsd(hostingRecord.getTriggerType())); - return true; + if (badApp) { + app.killLocked("error during init", + ApplicationExitInfo.REASON_INITIALIZATION_FAILURE, true); + handleAppDiedLocked(app, pid, false, true, false /* fromBinderDied */); + return; + } + + if (!didSomething) { + updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_PROCESS_BEGIN); + checkTime(startTime, "finishAttachApplicationInner: after updateOomAdjLocked"); + } + + final HostingRecord hostingRecord = app.getHostingRecord(); + final String shortAction = getShortAction(hostingRecord.getAction()); + FrameworkStatsLog.write( + FrameworkStatsLog.PROCESS_START_TIME, + app.info.uid, + pid, + app.info.packageName, + FrameworkStatsLog.PROCESS_START_TIME__TYPE__COLD, + app.getStartElapsedTime(), + (int) (app.getBindApplicationTime() - app.getStartUptime()), + (int) (SystemClock.uptimeMillis() - app.getStartUptime()), + hostingRecord.getType(), + hostingRecord.getName(), + shortAction, + HostingRecord.getHostingTypeIdStatsd(hostingRecord.getType()), + HostingRecord.getTriggerTypeForStatsd(hostingRecord.getTriggerType())); + } } @Override - public final void attachApplication(IApplicationThread thread, long startSeq) { - if (thread == null) { - throw new SecurityException("Invalid application interface"); + public final void finishAttachApplication(long startSeq) { + final int pid = Binder.getCallingPid(); + final int uid = Binder.getCallingUid(); + + if (pid == MY_PID && uid == SYSTEM_UID) { + return; } - synchronized (this) { - int callingPid = Binder.getCallingPid(); - final int callingUid = Binder.getCallingUid(); - final long origId = Binder.clearCallingIdentity(); - attachApplicationLocked(thread, callingPid, callingUid, startSeq); + + final long origId = Binder.clearCallingIdentity(); + try { + finishAttachApplicationInner(startSeq, uid, pid); + } finally { Binder.restoreCallingIdentity(origId); } } @@ -18805,6 +18829,21 @@ public class ActivityManagerService extends IActivityManager.Stub return new ProcessList(); } + /** + * Returns the {@link BatteryStatsService} instance + */ + public BatteryStatsService getBatteryStatsService() { + return new BatteryStatsService(mContext, SystemServiceManager.ensureSystemDir(), + BackgroundThread.get().getHandler()); + } + + /** + * Returns the {@link ActiveServices} instance + */ + public ActiveServices getActiveServices(ActivityManagerService service) { + return new ActiveServices(service); + } + private boolean ensureHasNetworkManagementInternal() { if (mNmi == null) { mNmi = LocalServices.getService(NetworkManagementInternal.class); diff --git a/services/core/java/com/android/server/am/ProcessList.java b/services/core/java/com/android/server/am/ProcessList.java index ecea96e927e3..937bbc9cced6 100644 --- a/services/core/java/com/android/server/am/ProcessList.java +++ b/services/core/java/com/android/server/am/ProcessList.java @@ -2508,7 +2508,7 @@ public final class ProcessList { } @GuardedBy("mService") - private String isProcStartValidLocked(ProcessRecord app, long expectedStartSeq) { + String isProcStartValidLocked(ProcessRecord app, long expectedStartSeq) { StringBuilder sb = null; if (app.isKilledByAm()) { if (sb == null) sb = new StringBuilder(); diff --git a/services/core/java/com/android/server/am/ProcessRecord.java b/services/core/java/com/android/server/am/ProcessRecord.java index 0a8c6400a6fd..4706c26889de 100644 --- a/services/core/java/com/android/server/am/ProcessRecord.java +++ b/services/core/java/com/android/server/am/ProcessRecord.java @@ -200,6 +200,11 @@ class ProcessRecord implements WindowProcessListener { private volatile long mStartElapsedTime; /** + * When the process was sent the bindApplication request + */ + private volatile long mBindApplicationTime; + + /** * This will be same as {@link #uid} usually except for some apps used during factory testing. */ private volatile int mStartUid; @@ -739,6 +744,10 @@ class ProcessRecord implements WindowProcessListener { return mStartElapsedTime; } + long getBindApplicationTime() { + return mBindApplicationTime; + } + int getStartUid() { return mStartUid; } diff --git a/services/core/java/com/android/server/am/UserController.java b/services/core/java/com/android/server/am/UserController.java index 4d86140816ea..2ea49b338b38 100644 --- a/services/core/java/com/android/server/am/UserController.java +++ b/services/core/java/com/android/server/am/UserController.java @@ -100,7 +100,6 @@ import android.util.EventLog; import android.util.IntArray; import android.util.Pair; import android.util.SparseArray; -import android.util.SparseBooleanArray; import android.util.SparseIntArray; import android.util.proto.ProtoOutputStream; import android.view.Display; @@ -177,7 +176,8 @@ class UserController implements Handler.Callback { static final int START_USER_SWITCH_FG_MSG = 120; static final int COMPLETE_USER_SWITCH_MSG = 130; static final int USER_COMPLETED_EVENT_MSG = 140; - static final int USER_VISIBILITY_CHANGED_MSG = 150; + + private static final int NO_ARG2 = 0; // Message constant to clear {@link UserJourneySession} from {@link mUserIdToUserJourneyMap} if // the user journey, defined in the UserLifecycleJourneyReported atom for statsd, is not @@ -437,20 +437,6 @@ class UserController implements Handler.Callback { /** @see #getLastUserUnlockingUptime */ private volatile long mLastUserUnlockingUptime = 0; - // TODO(b/244333150) remove this array and let UserVisibilityMediator call the listeners - // directly, as that class should be responsible for all user visibility logic (for example, - // when the foreground user is switched out, its profiles also become invisible) - /** - * List of visible users (as defined by {@link UserManager#isUserVisible()}). - * - * <p>It's only used to call {@link UserManagerInternal} when the visibility is changed upon - * the user starting or stopping. - * - * <p>Note: only the key is used, not the value. - */ - @GuardedBy("mLock") - private final SparseBooleanArray mVisibleUsers = new SparseBooleanArray(); - private final UserLifecycleListener mUserLifecycleListener = new UserLifecycleListener() { @Override public void onUserCreated(UserInfo user, Object token) { @@ -1092,24 +1078,11 @@ class UserController implements Handler.Callback { // instead. userManagerInternal.unassignUserFromDisplayOnStop(userId); - final boolean visibilityChanged; - boolean visibleBefore; - synchronized (mLock) { - visibleBefore = mVisibleUsers.get(userId); - if (visibleBefore) { - deleteVisibleUserLocked(userId); - visibilityChanged = true; - } else { - visibilityChanged = false; - } - } - updateStartedUserArrayLU(); final boolean allowDelayedLockingCopied = allowDelayedLocking; Runnable finishUserStoppingAsync = () -> - mHandler.post(() -> finishUserStopping(userId, uss, allowDelayedLockingCopied, - visibilityChanged)); + mHandler.post(() -> finishUserStopping(userId, uss, allowDelayedLockingCopied)); if (mInjector.getUserManager().isPreCreated(userId)) { finishUserStoppingAsync.run(); @@ -1146,22 +1119,8 @@ class UserController implements Handler.Callback { } } - private void addVisibleUserLocked(@UserIdInt int userId) { - if (DEBUG_MU) { - Slogf.d(TAG, "adding %d to mVisibleUsers", userId); - } - mVisibleUsers.put(userId, true); - } - - private void deleteVisibleUserLocked(@UserIdInt int userId) { - if (DEBUG_MU) { - Slogf.d(TAG, "deleting %d from mVisibleUsers", userId); - } - mVisibleUsers.delete(userId); - } - private void finishUserStopping(final int userId, final UserState uss, - final boolean allowDelayedLocking, final boolean visibilityChanged) { + final boolean allowDelayedLocking) { EventLog.writeEvent(EventLogTags.UC_FINISH_USER_STOPPING, userId); synchronized (mLock) { if (uss.state != UserState.STATE_STOPPING) { @@ -1179,9 +1138,6 @@ class UserController implements Handler.Callback { BatteryStats.HistoryItem.EVENT_USER_RUNNING_FINISH, Integer.toString(userId), userId); mInjector.getSystemServiceManager().onUserStopping(userId); - if (visibilityChanged) { - mInjector.onUserVisibilityChanged(userId, /* visible= */ false); - } Runnable finishUserStoppedAsync = () -> mHandler.post(() -> finishUserStopped(uss, allowDelayedLocking)); @@ -1655,25 +1611,13 @@ class UserController implements Handler.Callback { userInfo.profileGroupId, foreground, displayId); t.traceEnd(); - boolean visible; - switch (result) { - case UserManagerInternal.USER_ASSIGNMENT_RESULT_SUCCESS_VISIBLE: - visible = true; - break; - case UserManagerInternal.USER_ASSIGNMENT_RESULT_SUCCESS_INVISIBLE: - visible = false; - break; - default: - Slogf.wtf(TAG, "Wrong result from assignUserToDisplayOnStart(): %d", result); - // Fall through - case UserManagerInternal.USER_ASSIGNMENT_RESULT_FAILURE: - Slogf.e(TAG, "%s user(%d) / display (%d) assignment failed: %s", - (foreground ? "fg" : "bg"), userId, displayId, - UserManagerInternal.userAssignmentResultToString(result)); - return false; + if (result == UserManagerInternal.USER_ASSIGNMENT_RESULT_FAILURE) { + Slogf.e(TAG, "%s user(%d) / display (%d) assignment failed: %s", + (foreground ? "fg" : "bg"), userId, displayId, + UserManagerInternal.userAssignmentResultToString(result)); + return false; } - // TODO(b/239982558): might need something similar for bg users on secondary display if (foreground && isUserSwitchUiEnabled()) { t.traceBegin("startFreezingScreen"); @@ -1724,15 +1668,7 @@ class UserController implements Handler.Callback { // Make sure the old user is no longer considering the display to be on. mInjector.reportGlobalUsageEvent(UsageEvents.Event.SCREEN_NON_INTERACTIVE); boolean userSwitchUiEnabled; - // TODO(b/244333150): temporary state until the callback logic is moved to - // UserVisibilityManager - int previousCurrentUserId; boolean notifyPreviousCurrentUserId; synchronized (mLock) { - previousCurrentUserId = mCurrentUserId; - notifyPreviousCurrentUserId = mVisibleUsers.get(previousCurrentUserId); - if (notifyPreviousCurrentUserId) { - deleteVisibleUserLocked(previousCurrentUserId); - } mCurrentUserId = userId; mTargetUserId = UserHandle.USER_NULL; // reset, mCurrentUserId has caught up userSwitchUiEnabled = mUserSwitchUiEnabled; @@ -1753,10 +1689,6 @@ class UserController implements Handler.Callback { mInjector.getWindowManager().lockNow(null); } } - if (notifyPreviousCurrentUserId) { - mHandler.sendMessage(mHandler.obtainMessage(USER_VISIBILITY_CHANGED_MSG, - previousCurrentUserId, 0)); - } } else { final Integer currentUserIdInt = mCurrentUserId; @@ -1768,12 +1700,6 @@ class UserController implements Handler.Callback { } t.traceEnd(); - if (visible) { - synchronized (mLock) { - addVisibleUserLocked(userId); - } - } - // Make sure user is in the started state. If it is currently // stopping, we need to knock that off. if (uss.state == UserState.STATE_STOPPING) { @@ -1810,20 +1736,10 @@ class UserController implements Handler.Callback { // Booting up a new user, need to tell system services about it. // Note that this is on the same handler as scheduling of broadcasts, // which is important because it needs to go first. - mHandler.sendMessage(mHandler.obtainMessage(USER_START_MSG, userId, - visible ? 1 : 0)); + mHandler.sendMessage(mHandler.obtainMessage(USER_START_MSG, userId, NO_ARG2)); t.traceEnd(); } - if (visible) { - // User was already running and became visible (for example, when switching to a - // user that was started in the background before), so it's necessary to explicitly - // notify the services (while when the user starts from BOOTING, USER_START_MSG - // takes care of that. - mHandler.sendMessage( - mHandler.obtainMessage(USER_VISIBILITY_CHANGED_MSG, userId, 1)); - } - t.traceBegin("sendMessages"); if (foreground) { mHandler.sendMessage(mHandler.obtainMessage(USER_CURRENT_MSG, userId, oldUserId)); @@ -2248,11 +2164,6 @@ class UserController implements Handler.Callback { uss.switching = false; stopGuestOrEphemeralUserIfBackground(oldUserId); stopUserOnSwitchIfEnforced(oldUserId); - if (oldUserId == UserHandle.USER_SYSTEM) { - // System user is never stopped, but its visibility is changed (as it is brought to the - // background) - updateSystemUserVisibility(t, /* visible= */ false); - } t.traceEnd(); // end continueUserSwitch } @@ -2614,27 +2525,10 @@ class UserController implements Handler.Callback { // Don't need to call on HSUM because it will be called when the system user is // restarted on background mInjector.onUserStarting(UserHandle.USER_SYSTEM); - mInjector.onUserVisibilityChanged(UserHandle.USER_SYSTEM, /* visible= */ true); + mInjector.onSystemUserVisibilityChanged(/* visible= */ true); } } - private void updateSystemUserVisibility(TimingsTraceAndSlog t, boolean visible) { - t.traceBegin("update-system-userVisibility-" + visible); - if (DEBUG_MU) { - Slogf.d(TAG, "updateSystemUserVisibility(): visible=%b", visible); - } - int userId = UserHandle.USER_SYSTEM; - synchronized (mLock) { - if (visible) { - addVisibleUserLocked(userId); - } else { - deleteVisibleUserLocked(userId); - } - } - mInjector.onUserVisibilityChanged(userId, visible); - t.traceEnd(); - } - /** * Refreshes the internal caches related to user profiles. * @@ -3032,9 +2926,6 @@ class UserController implements Handler.Callback { proto.end(uToken); } } - for (int i = 0; i < mVisibleUsers.size(); i++) { - proto.write(UserControllerProto.VISIBLE_USERS_ARRAY, mVisibleUsers.keyAt(i)); - } proto.write(UserControllerProto.CURRENT_USER, mCurrentUserId); for (int i = 0; i < mCurrentProfileIds.length; i++) { proto.write(UserControllerProto.CURRENT_PROFILES, mCurrentProfileIds[i]); @@ -3094,7 +2985,6 @@ class UserController implements Handler.Callback { pw.println(" mSwitchingToSystemUserMessage: " + mSwitchingToSystemUserMessage); } pw.println(" mLastUserUnlockingUptime: " + mLastUserUnlockingUptime); - pw.println(" mVisibleUsers: " + mVisibleUsers); } } @@ -3212,10 +3102,6 @@ class UserController implements Handler.Callback { case COMPLETE_USER_SWITCH_MSG: completeUserSwitch(msg.arg1); break; - case USER_VISIBILITY_CHANGED_MSG: - mInjector.onUserVisibilityChanged(/* userId= */ msg.arg1, - /* visible= */ msg.arg2 == 1); - break; } return false; } @@ -3750,8 +3636,8 @@ class UserController implements Handler.Callback { getSystemServiceManager().onUserStarting(TimingsTraceAndSlog.newAsyncLog(), userId); } - void onUserVisibilityChanged(@UserIdInt int userId, boolean visible) { - getUserManagerInternal().onUserVisibilityChanged(userId, visible); + void onSystemUserVisibilityChanged(boolean visible) { + getUserManagerInternal().onSystemUserVisibilityChanged(visible); } } } diff --git a/services/core/java/com/android/server/pm/UserManagerInternal.java b/services/core/java/com/android/server/pm/UserManagerInternal.java index df132a97418a..0f920c6a4e90 100644 --- a/services/core/java/com/android/server/pm/UserManagerInternal.java +++ b/services/core/java/com/android/server/pm/UserManagerInternal.java @@ -435,8 +435,10 @@ public abstract class UserManagerInternal { /** Removes a {@link UserVisibilityListener}. */ public abstract void removeUserVisibilityListener(UserVisibilityListener listener); - /** TODO(b/244333150): temporary method until UserVisibilityMediator handles that logic */ - public abstract void onUserVisibilityChanged(@UserIdInt int userId, boolean visible); + // TODO(b/242195409): remove this method if not needed anymore + /** Notify {@link UserVisibilityListener listeners} that the visibility of the + * {@link android.os.UserHandle#USER_SYSTEM} changed. */ + public abstract void onSystemUserVisibilityChanged(boolean visible); /** Return the integer types of the given user IDs. Only used for reporting metrics to statsd. */ diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java index b5c1206bdfa4..3234e87a7125 100644 --- a/services/core/java/com/android/server/pm/UserManagerService.java +++ b/services/core/java/com/android/server/pm/UserManagerService.java @@ -97,7 +97,6 @@ import android.text.TextUtils; import android.util.ArrayMap; import android.util.ArraySet; import android.util.AtomicFile; -import android.util.EventLog; import android.util.IndentingPrintWriter; import android.util.IntArray; import android.util.Slog; @@ -126,7 +125,6 @@ import com.android.server.BundleUtils; import com.android.server.LocalServices; import com.android.server.LockGuard; import com.android.server.SystemService; -import com.android.server.am.EventLogTags; import com.android.server.am.UserState; import com.android.server.pm.UserManagerInternal.UserLifecycleListener; import com.android.server.pm.UserManagerInternal.UserRestrictionsListener; @@ -191,6 +189,7 @@ public class UserManagerService extends IUserManager.Stub { private static final String ATTR_CREATION_TIME = "created"; private static final String ATTR_LAST_LOGGED_IN_TIME = "lastLoggedIn"; private static final String ATTR_LAST_LOGGED_IN_FINGERPRINT = "lastLoggedInFingerprint"; + private static final String ATTR_LAST_ENTERED_FOREGROUND_TIME = "lastEnteredForeground"; private static final String ATTR_SERIAL_NO = "serialNumber"; private static final String ATTR_NEXT_SERIAL_NO = "nextSerialNumber"; private static final String ATTR_PARTIAL = "partial"; @@ -341,6 +340,9 @@ public class UserManagerService extends IUserManager.Stub { /** Elapsed realtime since boot when the user was unlocked. */ long unlockRealtime; + /** Wall clock time in millis when the user last entered the foreground. */ + long mLastEnteredForegroundTimeMillis; + private long mLastRequestQuietModeEnabledMillis; /** @@ -508,10 +510,6 @@ public class UserManagerService extends IUserManager.Stub { @GuardedBy("mUserLifecycleListeners") private final ArrayList<UserLifecycleListener> mUserLifecycleListeners = new ArrayList<>(); - // TODO(b/244333150): temporary array, should belong to UserVisibilityMediator - @GuardedBy("mUserVisibilityListeners") - private final ArrayList<UserVisibilityListener> mUserVisibilityListeners = new ArrayList<>(); - private final LockPatternUtils mLockPatternUtils; private final String ACTION_DISABLE_QUIET_MODE_AFTER_UNLOCK = @@ -680,6 +678,10 @@ public class UserManagerService extends IUserManager.Stub { final UserData user = mUms.getUserDataLU(targetUser.getUserIdentifier()); if (user != null) { user.startRealtime = SystemClock.elapsedRealtime(); + if (targetUser.getUserIdentifier() == UserHandle.USER_SYSTEM + && targetUser.isFull()) { + mUms.setLastEnteredForegroundTimeToNow(user); + } } } } @@ -695,6 +697,16 @@ public class UserManagerService extends IUserManager.Stub { } @Override + public void onUserSwitching(@NonNull TargetUser from, @NonNull TargetUser to) { + synchronized (mUms.mUsersLock) { + final UserData user = mUms.getUserDataLU(to.getUserIdentifier()); + if (user != null) { + mUms.setLastEnteredForegroundTimeToNow(user); + } + } + } + + @Override public void onUserStopping(@NonNull TargetUser targetUser) { synchronized (mUms.mUsersLock) { final UserData user = mUms.getUserDataLU(targetUser.getUserIdentifier()); @@ -920,6 +932,30 @@ public class UserManagerService extends IUserManager.Stub { return UserHandle.USER_NULL; } + @Override + public int getPreviousFullUserToEnterForeground() { + checkQueryOrCreateUsersPermission("get previous user"); + int previousUser = UserHandle.USER_NULL; + long latestEnteredTime = 0; + final int currentUser = getCurrentUserId(); + synchronized (mUsersLock) { + final int userSize = mUsers.size(); + for (int i = 0; i < userSize; i++) { + final UserData userData = mUsers.valueAt(i); + final int userId = userData.info.id; + if (userId != currentUser && userData.info.isFull() && !userData.info.partial + && !mRemovingUserIds.get(userId)) { + final long userEnteredTime = userData.mLastEnteredForegroundTimeMillis; + if (userEnteredTime > latestEnteredTime) { + latestEnteredTime = userEnteredTime; + previousUser = userId; + } + } + } + } + return previousUser; + } + public @NonNull List<UserInfo> getUsers(boolean excludeDying) { return getUsers(/*excludePartial= */ true, excludeDying, /* excludePreCreated= */ true); @@ -3973,6 +4009,8 @@ public class UserManagerService extends IUserManager.Stub { serializer.attribute(null, ATTR_LAST_LOGGED_IN_FINGERPRINT, userInfo.lastLoggedInFingerprint); } + serializer.attributeLong( + null, ATTR_LAST_ENTERED_FOREGROUND_TIME, userData.mLastEnteredForegroundTimeMillis); if (userInfo.iconPath != null) { serializer.attribute(null, ATTR_ICON_PATH, userInfo.iconPath); } @@ -4146,6 +4184,7 @@ public class UserManagerService extends IUserManager.Stub { long lastLoggedInTime = 0L; long lastRequestQuietModeEnabledTimestamp = 0L; String lastLoggedInFingerprint = null; + long lastEnteredForegroundTime = 0L; int profileGroupId = UserInfo.NO_PROFILE_GROUP_ID; int profileBadge = 0; int restrictedProfileParentId = UserInfo.NO_PROFILE_GROUP_ID; @@ -4191,6 +4230,8 @@ public class UserManagerService extends IUserManager.Stub { lastLoggedInTime = parser.getAttributeLong(null, ATTR_LAST_LOGGED_IN_TIME, 0); lastLoggedInFingerprint = parser.getAttributeValue(null, ATTR_LAST_LOGGED_IN_FINGERPRINT); + lastEnteredForegroundTime = + parser.getAttributeLong(null, ATTR_LAST_ENTERED_FOREGROUND_TIME, 0L); profileGroupId = parser.getAttributeInt(null, ATTR_PROFILE_GROUP_ID, UserInfo.NO_PROFILE_GROUP_ID); profileBadge = parser.getAttributeInt(null, ATTR_PROFILE_BADGE, 0); @@ -4285,6 +4326,7 @@ public class UserManagerService extends IUserManager.Stub { userData.seedAccountOptions = seedAccountOptions; userData.userProperties = userProperties; userData.setLastRequestQuietModeEnabledMillis(lastRequestQuietModeEnabledTimestamp); + userData.mLastEnteredForegroundTimeMillis = lastEnteredForegroundTime; if (ignorePrepareStorageErrors) { userData.setIgnorePrepareStorageErrors(); } @@ -6204,6 +6246,11 @@ public class UserManagerService extends IUserManager.Stub { || someUserHasSeedAccountNoChecks(accountName, accountType)); } + private void setLastEnteredForegroundTimeToNow(@NonNull UserData userData) { + userData.mLastEnteredForegroundTimeMillis = System.currentTimeMillis(); + scheduleWriteUser(userData); + } + @Override public void onShellCommand(FileDescriptor in, FileDescriptor out, FileDescriptor err, String[] args, ShellCallback callback, @@ -6330,9 +6377,6 @@ public class UserManagerService extends IUserManager.Stub { synchronized (mUserLifecycleListeners) { pw.println(" user lifecycle events: " + mUserLifecycleListeners.size()); } - synchronized (mUserVisibilityListeners) { - pw.println(" user visibility events: " + mUserVisibilityListeners.size()); - } // Dump UserTypes pw.println(); @@ -6428,6 +6472,9 @@ public class UserManagerService extends IUserManager.Stub { pw.print(" Unlock time: "); dumpTimeAgo(pw, tempStringBuilder, nowRealtime, userData.unlockRealtime); + pw.print(" Last entered foreground: "); + dumpTimeAgo(pw, tempStringBuilder, now, userData.mLastEnteredForegroundTimeMillis); + pw.print(" Has profile owner: "); pw.println(mIsUserManaged.get(userId)); pw.println(" Restrictions:"); @@ -6905,31 +6952,17 @@ public class UserManagerService extends IUserManager.Stub { @Override public void addUserVisibilityListener(UserVisibilityListener listener) { - synchronized (mUserVisibilityListeners) { - mUserVisibilityListeners.add(listener); - } + mUserVisibilityMediator.addListener(listener); } @Override public void removeUserVisibilityListener(UserVisibilityListener listener) { - synchronized (mUserVisibilityListeners) { - mUserVisibilityListeners.remove(listener); - } + mUserVisibilityMediator.removeListener(listener); } @Override - public void onUserVisibilityChanged(@UserIdInt int userId, boolean visible) { - EventLog.writeEvent(EventLogTags.UM_USER_VISIBILITY_CHANGED, userId, visible ? 1 : 0); - mHandler.post(() -> { - UserVisibilityListener[] listeners; - synchronized (mUserVisibilityListeners) { - listeners = new UserVisibilityListener[mUserVisibilityListeners.size()]; - mUserVisibilityListeners.toArray(listeners); - } - for (UserVisibilityListener listener : listeners) { - listener.onUserVisibilityChanged(userId, visible); - } - }); + public void onSystemUserVisibilityChanged(boolean visible) { + mUserVisibilityMediator.onSystemUserVisibilityChanged(visible); } @Override diff --git a/services/core/java/com/android/server/pm/UserVisibilityMediator.java b/services/core/java/com/android/server/pm/UserVisibilityMediator.java index 878855a89fb1..9b9ca1088064 100644 --- a/services/core/java/com/android/server/pm/UserVisibilityMediator.java +++ b/services/core/java/com/android/server/pm/UserVisibilityMediator.java @@ -32,6 +32,7 @@ import android.os.Handler; import android.os.UserHandle; import android.os.UserManager; import android.util.Dumpable; +import android.util.EventLog; import android.util.IndentingPrintWriter; import android.util.IntArray; import android.util.SparseIntArray; @@ -40,6 +41,7 @@ import android.view.Display; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.Preconditions; +import com.android.server.am.EventLogTags; import com.android.server.pm.UserManagerInternal.UserAssignmentResult; import com.android.server.pm.UserManagerInternal.UserVisibilityListener; import com.android.server.utils.Slogf; @@ -68,6 +70,7 @@ import java.util.concurrent.CopyOnWriteArrayList; public final class UserVisibilityMediator implements Dumpable { private static final boolean DBG = false; // DO NOT SUBMIT WITH TRUE + private static final boolean VERBOSE = false; // DO NOT SUBMIT WITH TRUE private static final String TAG = UserVisibilityMediator.class.getSimpleName(); @@ -381,8 +384,8 @@ public final class UserVisibilityMediator implements Dumpable { public boolean isUserVisible(@UserIdInt int userId) { // First check current foreground user and their profiles (on main display) if (isCurrentUserOrRunningProfileOfCurrentUser(userId)) { - if (DBG) { - Slogf.d(TAG, "isUserVisible(%d): true to current user or profile", userId); + if (VERBOSE) { + Slogf.v(TAG, "isUserVisible(%d): true to current user or profile", userId); } return true; } @@ -517,6 +520,14 @@ public final class UserVisibilityMediator implements Dumpable { } } + // TODO(b/242195409): remove this method if not needed anymore + /** + * Nofify all listeners that the system user visibility changed. + */ + void onSystemUserVisibilityChanged(boolean visible) { + dispatchVisibilityChanged(mListeners, USER_SYSTEM, visible); + } + /** * Nofify all listeners about the visibility changes from before / after a change of state. */ @@ -534,7 +545,7 @@ public final class UserVisibilityMediator implements Dumpable { Slogf.d(TAG, "dispatchVisibilityChanged(): visibleUsersBefore=%s, visibleUsersAfter=%s, " + "%d listeners (%s)", visibleUsersBefore, visibleUsersAfter, listeners.size(), - mListeners); + listeners); } for (int i = 0; i < visibleUsersBefore.size(); i++) { int userId = visibleUsersBefore.get(i); @@ -552,13 +563,14 @@ public final class UserVisibilityMediator implements Dumpable { private void dispatchVisibilityChanged(CopyOnWriteArrayList<UserVisibilityListener> listeners, @UserIdInt int userId, boolean visible) { + EventLog.writeEvent(EventLogTags.UM_USER_VISIBILITY_CHANGED, userId, visible ? 1 : 0); if (DBG) { Slogf.d(TAG, "dispatchVisibilityChanged(%d -> %b): sending to %d listeners", userId, visible, listeners.size()); } for (int i = 0; i < mListeners.size(); i++) { UserVisibilityListener listener = mListeners.get(i); - if (DBG) { + if (VERBOSE) { Slogf.v(TAG, "dispatchVisibilityChanged(%d -> %b): sending to %s", userId, visible, listener); } diff --git a/services/core/java/com/android/server/wm/ActivityStartInterceptor.java b/services/core/java/com/android/server/wm/ActivityStartInterceptor.java index d7c5e9373ad3..719f72c8ee52 100644 --- a/services/core/java/com/android/server/wm/ActivityStartInterceptor.java +++ b/services/core/java/com/android/server/wm/ActivityStartInterceptor.java @@ -399,8 +399,11 @@ class ActivityStartInterceptor { * @return The intercepting intent if needed. */ private Intent interceptWithConfirmCredentialsIfNeeded(ActivityInfo aInfo, int userId) { + if (!mService.mAmInternal.shouldConfirmCredentials(userId)) { + return null; + } if ((aInfo.flags & ActivityInfo.FLAG_SHOW_WHEN_LOCKED) != 0 - || !mService.mAmInternal.shouldConfirmCredentials(userId)) { + && (mUserManager.isUserUnlocked(userId) || aInfo.directBootAware)) { return null; } final IntentSender target = createIntentSenderForOriginalIntent(mCallingUid, diff --git a/services/credentials/java/com/android/server/credentials/ProviderCreateSession.java b/services/credentials/java/com/android/server/credentials/ProviderCreateSession.java index 332a75ea566b..8854453a61cd 100644 --- a/services/credentials/java/com/android/server/credentials/ProviderCreateSession.java +++ b/services/credentials/java/com/android/server/credentials/ProviderCreateSession.java @@ -25,7 +25,6 @@ import android.content.Intent; import android.credentials.ui.CreateCredentialProviderData; import android.credentials.ui.Entry; import android.credentials.ui.ProviderPendingIntentResponse; -import android.os.Bundle; import android.service.credentials.BeginCreateCredentialRequest; import android.service.credentials.BeginCreateCredentialResponse; import android.service.credentials.CreateCredentialRequest; @@ -68,12 +67,11 @@ public final class ProviderCreateSession extends ProviderSession< createRequestSession.mClientRequest, createRequestSession.mClientCallingPackage); if (providerCreateRequest != null) { - // TODO : Replace with proper splitting of request BeginCreateCredentialRequest providerBeginCreateRequest = new BeginCreateCredentialRequest( providerCreateRequest.getCallingPackage(), providerCreateRequest.getType(), - new Bundle()); + createRequestSession.mClientRequest.getCandidateQueryData()); return new ProviderCreateSession(context, providerInfo, createRequestSession, userId, remoteCredentialService, providerBeginCreateRequest, providerCreateRequest); } @@ -88,7 +86,7 @@ public final class ProviderCreateSession extends ProviderSession< String capability = clientRequest.getType(); if (providerCapabilities.contains(capability)) { return new CreateCredentialRequest(clientCallingPackage, capability, - clientRequest.getData()); + clientRequest.getCredentialData()); } Log.i(TAG, "Unable to create provider request - capabilities do not match"); return null; diff --git a/services/tests/mockingservicestests/src/com/android/server/am/AsyncProcessStartTest.java b/services/tests/mockingservicestests/src/com/android/server/am/AsyncProcessStartTest.java new file mode 100644 index 000000000000..ea14ffbe3572 --- /dev/null +++ b/services/tests/mockingservicestests/src/com/android/server/am/AsyncProcessStartTest.java @@ -0,0 +1,282 @@ +/* + * 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.am; + +import static android.os.Process.myPid; +import static android.os.Process.myUid; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; + +import android.app.ActivityManagerInternal; +import android.app.IApplicationThread; +import android.app.usage.UsageStatsManagerInternal; +import android.content.ComponentName; +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManagerInternal; +import android.os.Binder; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.IBinder; +import android.os.SystemClock; +import android.util.Log; + +import androidx.test.filters.MediumTest; +import androidx.test.platform.app.InstrumentationRegistry; + +import com.android.server.DropBoxManagerInternal; +import com.android.server.LocalServices; +import com.android.server.am.ActivityManagerService.Injector; +import com.android.server.appop.AppOpsService; +import com.android.server.wm.ActivityTaskManagerInternal; +import com.android.server.wm.ActivityTaskManagerService; + +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.io.File; +import java.util.Arrays; + + +/** + * Tests to verify process starts are completed or timeout correctly + */ +@MediumTest +@SuppressWarnings("GuardedBy") +public class AsyncProcessStartTest { + private static final String TAG = "AsyncProcessStartTest"; + + private static final String PACKAGE = "com.foo"; + + @Rule + public final ApplicationExitInfoTest.ServiceThreadRule + mServiceThreadRule = new ApplicationExitInfoTest.ServiceThreadRule(); + + private Context mContext; + private HandlerThread mHandlerThread; + + @Mock + private AppOpsService mAppOpsService; + @Mock + private DropBoxManagerInternal mDropBoxManagerInt; + @Mock + private PackageManagerInternal mPackageManagerInt; + @Mock + private UsageStatsManagerInternal mUsageStatsManagerInt; + @Mock + private ActivityManagerInternal mActivityManagerInt; + @Mock + private ActivityTaskManagerInternal mActivityTaskManagerInt; + @Mock + private BatteryStatsService mBatteryStatsService; + + private ActivityManagerService mRealAms; + private ActivityManagerService mAms; + + private ProcessList mRealProcessList = new ProcessList(); + private ProcessList mProcessList; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + + mContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); + + mHandlerThread = new HandlerThread(TAG); + mHandlerThread.start(); + + LocalServices.removeServiceForTest(DropBoxManagerInternal.class); + LocalServices.addService(DropBoxManagerInternal.class, mDropBoxManagerInt); + + LocalServices.removeServiceForTest(PackageManagerInternal.class); + LocalServices.addService(PackageManagerInternal.class, mPackageManagerInt); + + LocalServices.removeServiceForTest(ActivityManagerInternal.class); + LocalServices.addService(ActivityManagerInternal.class, mActivityManagerInt); + + LocalServices.removeServiceForTest(ActivityTaskManagerInternal.class); + LocalServices.addService(ActivityTaskManagerInternal.class, mActivityTaskManagerInt); + + doReturn(new ComponentName("", "")).when(mPackageManagerInt).getSystemUiServiceComponent(); + doReturn(true).when(mActivityTaskManagerInt).attachApplication(any()); + doNothing().when(mActivityTaskManagerInt).onProcessMapped(anyInt(), any()); + + mRealAms = new ActivityManagerService( + new TestInjector(mContext), mServiceThreadRule.getThread()); + mRealAms.mActivityTaskManager = new ActivityTaskManagerService(mContext); + mRealAms.mActivityTaskManager.initialize(null, null, mContext.getMainLooper()); + mRealAms.mAtmInternal = mActivityTaskManagerInt; + mRealAms.mPackageManagerInt = mPackageManagerInt; + mRealAms.mUsageStatsService = mUsageStatsManagerInt; + mRealAms.mProcessesReady = true; + mAms = spy(mRealAms); + mRealProcessList.mService = mAms; + mProcessList = spy(mRealProcessList); + + doAnswer((invocation) -> { + Log.v(TAG, "Intercepting isProcStartValidLocked() for " + + Arrays.toString(invocation.getArguments())); + return null; + }).when(mProcessList).isProcStartValidLocked(any(), anyLong()); + } + + @After + public void tearDown() throws Exception { + mHandlerThread.quit(); + } + + private class TestInjector extends Injector { + TestInjector(Context context) { + super(context); + } + + @Override + public AppOpsService getAppOpsService(File file, Handler handler) { + return mAppOpsService; + } + + @Override + public Handler getUiHandler(ActivityManagerService service) { + return mHandlerThread.getThreadHandler(); + } + + @Override + public ProcessList getProcessList(ActivityManagerService service) { + return mRealProcessList; + } + + @Override + public BatteryStatsService getBatteryStatsService() { + return mBatteryStatsService; + } + } + + private ProcessRecord makeActiveProcessRecord(String packageName, boolean wedge) + throws Exception { + final ApplicationInfo ai = makeApplicationInfo(packageName); + return makeActiveProcessRecord(ai, wedge); + } + + private ProcessRecord makeActiveProcessRecord(ApplicationInfo ai, boolean wedge) + throws Exception { + final IApplicationThread thread = mock(IApplicationThread.class); + final IBinder threadBinder = new Binder(); + doReturn(threadBinder).when(thread).asBinder(); + doAnswer((invocation) -> { + Log.v(TAG, "Intercepting bindApplication() for " + + Arrays.toString(invocation.getArguments())); + if (!wedge) { + mRealAms.finishAttachApplication(0); + } + return null; + }).when(thread).bindApplication( + any(), any(), + any(), any(), + any(), any(), + any(), any(), + any(), + any(), anyInt(), + anyBoolean(), anyBoolean(), + anyBoolean(), anyBoolean(), any(), + any(), any(), any(), + any(), any(), + any(), any(), + any(), + anyLong(), anyLong()); + + final ProcessRecord r = spy(new ProcessRecord(mAms, ai, ai.processName, ai.uid)); + r.setPid(myPid()); + r.setStartUid(myUid()); + r.setHostingRecord(new HostingRecord(HostingRecord.HOSTING_TYPE_BROADCAST)); + r.makeActive(thread, mAms.mProcessStats); + doNothing().when(r).killLocked(any(), any(), anyInt(), anyInt(), anyBoolean(), + anyBoolean()); + + return r; + } + + static ApplicationInfo makeApplicationInfo(String packageName) { + final ApplicationInfo ai = new ApplicationInfo(); + ai.packageName = packageName; + ai.processName = packageName; + ai.uid = myUid(); + return ai; + } + + /** + * Verify that we don't kill a normal process + */ + @Test + public void testNormal() throws Exception { + ProcessRecord app = startProcessAndWait(false); + + verify(app, never()).killLocked(any(), anyInt(), anyBoolean()); + } + + /** + * Verify that we kill a wedged process after the process start timeout + */ + @Test + public void testWedged() throws Exception { + ProcessRecord app = startProcessAndWait(true); + + verify(app).killLocked(any(), anyInt(), anyBoolean()); + } + + private ProcessRecord startProcessAndWait(boolean wedge) throws Exception { + final ProcessRecord app = makeActiveProcessRecord(PACKAGE, wedge); + final ApplicationInfo appInfo = makeApplicationInfo(PACKAGE); + + mProcessList.handleProcessStartedLocked(app, app.getPid(), /* usingWrapper */ false, + /* expectedStartSeq */ 0, /* procAttached */ false); + + app.getThread().bindApplication(PACKAGE, appInfo, + null, null, + null, + null, + null, null, + null, + null, 0, + false, false, + true, false, + null, + null, null, + null, + null, null, null, + null, null, + 0, 0); + + // Sleep until timeout should have triggered + SystemClock.sleep(ActivityManagerService.PROC_START_TIMEOUT + 1000); + + return app; + } +} diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/AsyncUserVisibilityListener.java b/services/tests/mockingservicestests/src/com/android/server/pm/AsyncUserVisibilityListener.java index a97491daa96e..ddfbf16f4833 100644 --- a/services/tests/mockingservicestests/src/com/android/server/pm/AsyncUserVisibilityListener.java +++ b/services/tests/mockingservicestests/src/com/android/server/pm/AsyncUserVisibilityListener.java @@ -30,7 +30,6 @@ import java.util.Collections; import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; - /** * {@link UserVisibilityListener} implementation that expects callback events to be asynchronously * received. diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorTestCase.java b/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorTestCase.java index c2038311e740..4487d136b708 100644 --- a/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorTestCase.java +++ b/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorTestCase.java @@ -329,6 +329,15 @@ abstract class UserVisibilityMediatorTestCase extends ExtendedMockitoTestCase { listener.verify(); } + @Test + public final void testOnSystemUserVisibilityChanged() throws Exception { + AsyncUserVisibilityListener listener = addListenerForEvents(onVisible(USER_SYSTEM)); + + mMediator.onSystemUserVisibilityChanged(/* visible= */ true); + + listener.verify(); + } + /** * Starts a user in foreground on the default display, asserting it was properly started. * diff --git a/services/tests/servicestests/src/com/android/server/DockObserverTest.java b/services/tests/servicestests/src/com/android/server/DockObserverTest.java index c325778a5683..ee09074f7625 100644 --- a/services/tests/servicestests/src/com/android/server/DockObserverTest.java +++ b/services/tests/servicestests/src/com/android/server/DockObserverTest.java @@ -20,6 +20,7 @@ import static com.google.common.truth.Truth.assertThat; import android.content.Intent; import android.os.Looper; +import android.provider.Settings; import android.testing.AndroidTestingRunner; import android.testing.TestableContext; import android.testing.TestableLooper; @@ -74,6 +75,11 @@ public class DockObserverTest { .isEqualTo(Intent.EXTRA_DOCK_STATE_UNDOCKED); } + void setDeviceProvisioned(boolean provisioned) { + Settings.Global.putInt(mContext.getContentResolver(), Settings.Global.DEVICE_PROVISIONED, + provisioned ? 1 : 0); + } + @Before public void setUp() { if (Looper.myLooper() == null) { @@ -131,4 +137,25 @@ public class DockObserverTest { assertDockEventIntentWithExtraThenUndock(observer, "DOCK=1\nKEY5=5", Intent.EXTRA_DOCK_STATE_HE_DESK); } + + @Test + public void testDockIntentBroadcast_deviceNotProvisioned() + throws ExecutionException, InterruptedException { + DockObserver observer = new DockObserver(mInterceptingContext); + // Set the device as not provisioned. + setDeviceProvisioned(false); + observer.onBootPhase(SystemService.PHASE_ACTIVITY_MANAGER_READY); + + BroadcastInterceptingContext.FutureIntent futureIntent = + updateExtconDockState(observer, "DOCK=1"); + TestableLooper.get(this).processAllMessages(); + // Verify no broadcast was sent as device was not provisioned. + futureIntent.assertNotReceived(); + + // Ensure we send the broadcast when the device is provisioned. + setDeviceProvisioned(true); + TestableLooper.get(this).processAllMessages(); + assertThat(futureIntent.get().getIntExtra(Intent.EXTRA_DOCK_STATE, -1)) + .isEqualTo(Intent.EXTRA_DOCK_STATE_DESK); + } } diff --git a/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java b/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java index a49214f9b4f5..e8b8253f0611 100644 --- a/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java +++ b/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java @@ -38,9 +38,6 @@ import static com.android.server.am.UserController.USER_COMPLETED_EVENT_MSG; import static com.android.server.am.UserController.USER_CURRENT_MSG; import static com.android.server.am.UserController.USER_START_MSG; import static com.android.server.am.UserController.USER_SWITCH_TIMEOUT_MSG; -import static com.android.server.am.UserController.USER_VISIBILITY_CHANGED_MSG; -import static com.android.server.pm.UserManagerInternal.USER_ASSIGNMENT_RESULT_SUCCESS_INVISIBLE; -import static com.android.server.pm.UserManagerInternal.USER_ASSIGNMENT_RESULT_SUCCESS_VISIBLE; import static com.google.android.collect.Lists.newArrayList; import static com.google.android.collect.Sets.newHashSet; @@ -102,7 +99,6 @@ import com.android.server.FgThread; import com.android.server.SystemService; import com.android.server.am.UserState.KeyEvictedCallback; import com.android.server.pm.UserManagerInternal; -import com.android.server.pm.UserManagerInternal.UserAssignmentResult; import com.android.server.pm.UserManagerService; import com.android.server.wm.WindowManagerService; @@ -162,18 +158,12 @@ public class UserControllerTest { REPORT_USER_SWITCH_MSG, USER_SWITCH_TIMEOUT_MSG, USER_START_MSG, - USER_VISIBILITY_CHANGED_MSG, USER_CURRENT_MSG); - private static final Set<Integer> START_INVISIBLE_BACKGROUND_USER_MESSAGE_CODES = newHashSet( + private static final Set<Integer> START_BACKGROUND_USER_MESSAGE_CODES = newHashSet( USER_START_MSG, REPORT_LOCKED_BOOT_COMPLETE_MSG); - private static final Set<Integer> START_VISIBLE_BACKGROUND_USER_MESSAGE_CODES = newHashSet( - USER_START_MSG, - USER_VISIBILITY_CHANGED_MSG, - REPORT_LOCKED_BOOT_COMPLETE_MSG); - @Before public void setUp() throws Exception { runWithDexmakerShareClassLoader(() -> { @@ -225,14 +215,12 @@ public class UserControllerTest { @Test public void testStartUser_background() { - mockAssignUserToMainDisplay(TEST_USER_ID, /* foreground= */ false, - USER_ASSIGNMENT_RESULT_SUCCESS_INVISIBLE); boolean started = mUserController.startUser(TEST_USER_ID, /* foreground= */ false); assertWithMessage("startUser(%s, foreground=false)", TEST_USER_ID).that(started).isTrue(); verify(mInjector.getWindowManager(), never()).startFreezingScreen(anyInt(), anyInt()); verify(mInjector.getWindowManager(), never()).setSwitchingUser(anyBoolean()); verify(mInjector, never()).clearAllLockedTasks(anyString()); - startBackgroundUserAssertions(/*visible= */ false); + startBackgroundUserAssertions(); verifyUserAssignedToDisplay(TEST_USER_ID, Display.DEFAULT_DISPLAY); } @@ -267,7 +255,7 @@ public class UserControllerTest { verify(mInjector.getWindowManager(), never()).startFreezingScreen(anyInt(), anyInt()); verify(mInjector.getWindowManager(), never()).setSwitchingUser(anyBoolean()); verify(mInjector, never()).clearAllLockedTasks(anyString()); - startBackgroundUserAssertions(/*visible= */ true); + startBackgroundUserAssertions(); } @Test @@ -293,8 +281,6 @@ public class UserControllerTest { @Test public void testStartPreCreatedUser_background() throws Exception { - mockAssignUserToMainDisplay(TEST_PRE_CREATED_USER_ID, /* foreground= */ false, - USER_ASSIGNMENT_RESULT_SUCCESS_INVISIBLE); assertTrue(mUserController.startUser(TEST_PRE_CREATED_USER_ID, /* foreground= */ false)); // Make sure no intents have been fired for pre-created users. assertTrue(mInjector.mSentIntents.isEmpty()); @@ -322,10 +308,8 @@ public class UserControllerTest { assertEquals("Unexpected message sent", expectedMessageCodes, actualCodes); } - private void startBackgroundUserAssertions(boolean visible) { - startUserAssertions(START_BACKGROUND_USER_ACTIONS, - visible ? START_VISIBLE_BACKGROUND_USER_MESSAGE_CODES - : START_INVISIBLE_BACKGROUND_USER_MESSAGE_CODES); + private void startBackgroundUserAssertions() { + startUserAssertions(START_BACKGROUND_USER_ACTIONS, START_BACKGROUND_USER_MESSAGE_CODES); } private void startForegroundUserAssertions() { @@ -433,7 +417,7 @@ public class UserControllerTest { verify(mInjector, times(0)).dismissKeyguard(any(), anyString()); verify(mInjector.getWindowManager(), times(1)).stopFreezingScreen(); continueUserSwitchAssertions(TEST_USER_ID, false); - verifySystemUserVisibilityChangedNotified(/* visible= */ false); + verifySystemUserVisibilityChangesNeverNotified(); } @Test @@ -454,7 +438,7 @@ public class UserControllerTest { verify(mInjector, times(1)).dismissKeyguard(any(), anyString()); verify(mInjector.getWindowManager(), times(1)).stopFreezingScreen(); continueUserSwitchAssertions(TEST_USER_ID, false); - verifySystemUserVisibilityChangedNotified(/* visible= */ false); + verifySystemUserVisibilityChangesNeverNotified(); } @Test @@ -561,7 +545,7 @@ public class UserControllerTest { assertFalse(mUserController.canStartMoreUsers()); assertEquals(Arrays.asList(new Integer[] {0, TEST_USER_ID1, TEST_USER_ID2}), mUserController.getRunningUsersLU()); - verifySystemUserVisibilityChangedNotified(/* visible= */ false); + verifySystemUserVisibilityChangesNeverNotified(); } /** @@ -709,24 +693,19 @@ public class UserControllerTest { @Test public void testStartProfile() throws Exception { - mockAssignUserToMainDisplay(TEST_PRE_CREATED_USER_ID, /* foreground= */ false, - USER_ASSIGNMENT_RESULT_SUCCESS_INVISIBLE); setUpAndStartProfileInBackground(TEST_USER_ID1); - startBackgroundUserAssertions(/*visible= */ true); + startBackgroundUserAssertions(); verifyUserAssignedToDisplay(TEST_USER_ID1, Display.DEFAULT_DISPLAY); } @Test public void testStartProfile_whenUsersOnSecondaryDisplaysIsEnabled() throws Exception { - mockAssignUserToMainDisplay(TEST_USER_ID1, /* foreground= */ false, - USER_ASSIGNMENT_RESULT_SUCCESS_VISIBLE); - mockIsUsersOnSecondaryDisplaysEnabled(true); setUpAndStartProfileInBackground(TEST_USER_ID1); - startBackgroundUserAssertions(/*visible= */ true); + startBackgroundUserAssertions(); verifyUserAssignedToDisplay(TEST_USER_ID1, Display.DEFAULT_DISPLAY); } @@ -983,13 +962,6 @@ public class UserControllerTest { when(mInjector.isUsersOnSecondaryDisplaysEnabled()).thenReturn(value); } - private void mockAssignUserToMainDisplay(@UserIdInt int userId, boolean foreground, - @UserAssignmentResult int result) { - when(mInjector.mUserManagerInternalMock.assignUserToDisplayOnStart(eq(userId), - /* profileGroupId= */ anyInt(), eq(foreground), eq(Display.DEFAULT_DISPLAY))) - .thenReturn(result); - } - private void verifyUserAssignedToDisplay(@UserIdInt int userId, int displayId) { verify(mInjector.getUserManagerInternal()).assignUserToDisplayOnStart(eq(userId), anyInt(), anyBoolean(), eq(displayId)); @@ -1008,8 +980,8 @@ public class UserControllerTest { verify(mInjector.getUserManagerInternal(), never()).unassignUserFromDisplayOnStop(userId); } - private void verifySystemUserVisibilityChangedNotified(boolean visible) { - verify(mInjector).onUserVisibilityChanged(UserHandle.USER_SYSTEM, visible); + private void verifySystemUserVisibilityChangesNeverNotified() { + verify(mInjector, never()).onSystemUserVisibilityChanged(anyBoolean()); } // Should be public to allow mocking @@ -1154,8 +1126,8 @@ public class UserControllerTest { } @Override - void onUserVisibilityChanged(@UserIdInt int userId, boolean visible) { - Log.i(TAG, "onUserVisibilityChanged(" + userId + ", " + visible + ")"); + void onSystemUserVisibilityChanged(boolean visible) { + Log.i(TAG, "onSystemUserVisibilityChanged(" + visible + ")"); } } diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java index afaee04d7cec..3e8a07021d6b 100644 --- a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java @@ -317,7 +317,7 @@ public class VirtualDeviceManagerServiceTest { VirtualDeviceParams params = new VirtualDeviceParams .Builder() .setBlockedActivities(getBlockedActivities()) - .addDevicePolicy(POLICY_TYPE_SENSORS, DEVICE_POLICY_CUSTOM) + .setDevicePolicy(POLICY_TYPE_SENSORS, DEVICE_POLICY_CUSTOM) .build(); mDeviceImpl = new VirtualDeviceImpl(mContext, mAssociationInfo, new Binder(), /* ownerUid */ 0, VIRTUAL_DEVICE_ID, diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceParamsTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceParamsTest.java index a226ebcdc8f2..aefe4b6f4c0a 100644 --- a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceParamsTest.java +++ b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceParamsTest.java @@ -46,7 +46,7 @@ public class VirtualDeviceParamsTest { VirtualDeviceParams originalParams = new VirtualDeviceParams.Builder() .setLockState(VirtualDeviceParams.LOCK_STATE_ALWAYS_UNLOCKED) .setUsersWithMatchingAccounts(Set.of(UserHandle.of(123), UserHandle.of(456))) - .addDevicePolicy(POLICY_TYPE_SENSORS, DEVICE_POLICY_CUSTOM) + .setDevicePolicy(POLICY_TYPE_SENSORS, DEVICE_POLICY_CUSTOM) .addVirtualSensorConfig( new VirtualSensorConfig.Builder(TYPE_ACCELEROMETER, SENSOR_NAME) .setVendor(SENSOR_VENDOR) diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationHistoryJobServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationHistoryJobServiceTest.java index af10b9dba63f..d758e71c62a2 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationHistoryJobServiceTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationHistoryJobServiceTest.java @@ -52,9 +52,9 @@ import java.lang.reflect.Field; @RunWith(AndroidTestingRunner.class) public class NotificationHistoryJobServiceTest extends UiServiceTestCase { private NotificationHistoryJobService mJobService; - private JobParameters mJobParams = new JobParameters(null, - NotificationHistoryJobService.BASE_JOB_ID, null, null, null, - 0, false, false, null, null, null); + + @Mock + private JobParameters mJobParams; @Captor ArgumentCaptor<JobInfo> mJobInfoCaptor; diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ReviewNotificationPermissionsJobServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ReviewNotificationPermissionsJobServiceTest.java index 3a6c0eb8fc2a..a83eb00dfa1f 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/ReviewNotificationPermissionsJobServiceTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/ReviewNotificationPermissionsJobServiceTest.java @@ -44,9 +44,9 @@ import org.mockito.Mock; @RunWith(AndroidTestingRunner.class) public class ReviewNotificationPermissionsJobServiceTest extends UiServiceTestCase { private ReviewNotificationPermissionsJobService mJobService; - private JobParameters mJobParams = new JobParameters(null, - ReviewNotificationPermissionsJobService.JOB_ID, null, null, null, - 0, false, false, null, null, null); + + @Mock + private JobParameters mJobParams; @Captor ArgumentCaptor<JobInfo> mJobInfoCaptor; diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityStartInterceptorTest.java b/services/tests/wmtests/src/com/android/server/wm/ActivityStartInterceptorTest.java index 1575336600b4..8a15c30afbb1 100644 --- a/services/tests/wmtests/src/com/android/server/wm/ActivityStartInterceptorTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/ActivityStartInterceptorTest.java @@ -28,6 +28,7 @@ import static com.android.server.pm.PackageManagerService.PLATFORM_PACKAGE_NAME; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.nullable; @@ -275,11 +276,60 @@ public class ActivityStartInterceptorTest { // THEN calling intercept returns true mInterceptor.intercept(null, null, mAInfo, null, null, null, 0, 0, null); - // THEN the returned intent is the quiet mode intent + // THEN the returned intent is the confirm credentials intent + assertTrue(CONFIRM_CREDENTIALS_INTENT.filterEquals(mInterceptor.mIntent)); + } + + @Test + public void testLockedManagedProfileShowWhenLocked() { + Intent originalIntent = new Intent(); + // GIVEN that the user is locked but its storage is unlocked and the activity has + // showWhenLocked flag + when(mAmInternal.shouldConfirmCredentials(TEST_USER_ID)).thenReturn(true); + when(mUserManager.isUserUnlocked(eq(TEST_USER_ID))).thenReturn(true); + mAInfo.flags |= ActivityInfo.FLAG_SHOW_WHEN_LOCKED; + + // THEN calling intercept returns true + mInterceptor.intercept(originalIntent, null, mAInfo, null, null, null, 0, 0, null); + + // THEN the returned intent is original intent + assertSame(originalIntent, mInterceptor.mIntent); + } + + @Test + public void testLockedManagedProfileShowWhenLockedEncryptedStorage() { + // GIVEN that the user storage is locked, activity has showWhenLocked flag but no + // directBootAware flag + when(mAmInternal.shouldConfirmCredentials(TEST_USER_ID)).thenReturn(true); + when(mUserManager.isUserUnlocked(eq(TEST_USER_ID))).thenReturn(false); + mAInfo.flags |= ActivityInfo.FLAG_SHOW_WHEN_LOCKED; + mAInfo.directBootAware = false; + + // THEN calling intercept returns true + mInterceptor.intercept(null, null, mAInfo, null, null, null, 0, 0, null); + + // THEN the returned intent is the confirm credentials intent assertTrue(CONFIRM_CREDENTIALS_INTENT.filterEquals(mInterceptor.mIntent)); } @Test + public void testLockedManagedProfileShowWhenLockedEncryptedStorageDirectBootAware() { + Intent originalIntent = new Intent(); + // GIVEN that the user storage is locked, activity has showWhenLocked flag and + // directBootAware flag + when(mAmInternal.shouldConfirmCredentials(TEST_USER_ID)).thenReturn(true); + when(mUserManager.isUserUnlocked(eq(TEST_USER_ID))).thenReturn(false); + mAInfo.flags |= ActivityInfo.FLAG_SHOW_WHEN_LOCKED; + mAInfo.directBootAware = true; + + // THEN calling intercept returns true + mInterceptor.intercept(originalIntent, null, mAInfo, null, null, null, 0, 0, null); + + // THEN the returned intent is original intent + assertSame(originalIntent, mInterceptor.mIntent); + } + + @Test public void testHarmfulAppWarning() throws RemoteException { // GIVEN the package we're about to launch has a harmful app warning set when(mPackageManager.getHarmfulAppWarning(TEST_PACKAGE_NAME, TEST_USER_ID)) diff --git a/tools/aapt2/cmd/Optimize.cpp b/tools/aapt2/cmd/Optimize.cpp index 9c1a2f6854ff..d7a39bfb8c0b 100644 --- a/tools/aapt2/cmd/Optimize.cpp +++ b/tools/aapt2/cmd/Optimize.cpp @@ -154,13 +154,22 @@ class Optimizer { return 1; } - if (options_.shorten_resource_paths) { - Obfuscator obfuscator(options_.table_flattener_options.shortened_path_map); + Obfuscator obfuscator(options_); + if (obfuscator.IsEnabled()) { if (!obfuscator.Consume(context_, apk->GetResourceTable())) { context_->GetDiagnostics()->Error(android::DiagMessage() << "failed shortening resource paths"); return 1; } + + if (options_.obfuscation_map_path && + !obfuscator.WriteObfuscationMap(options_.obfuscation_map_path.value())) { + context_->GetDiagnostics()->Error(android::DiagMessage() + << "failed to write the obfuscation map to file"); + return 1; + } + + // TODO(b/246489170): keep the old option and format until transform to the new one if (options_.shortened_paths_map_path && !WriteShortenedPathsMap(options_.table_flattener_options.shortened_path_map, options_.shortened_paths_map_path.value())) { @@ -292,6 +301,7 @@ class Optimizer { ArchiveEntry::kAlign, writer); } + // TODO(b/246489170): keep the old option and format until transform to the new one bool WriteShortenedPathsMap(const std::map<std::string, std::string> &path_map, const std::string &file_path) { std::stringstream ss; diff --git a/tools/aapt2/cmd/Optimize.h b/tools/aapt2/cmd/Optimize.h index 794a87b0faa5..1879f25bc1b0 100644 --- a/tools/aapt2/cmd/Optimize.h +++ b/tools/aapt2/cmd/Optimize.h @@ -58,6 +58,7 @@ struct OptimizeOptions { bool shorten_resource_paths = false; // Path to the output map of original resource paths to shortened paths. + // TODO(b/246489170): keep the old option and format until transform to the new one std::optional<std::string> shortened_paths_map_path; // Whether sparse encoding should be used for O+ resources. @@ -65,6 +66,9 @@ struct OptimizeOptions { // Whether sparse encoding should be used for all resources. bool force_sparse_encoding = false; + + // Path to the output map of original resource paths/names to obfuscated paths/names. + std::optional<std::string> obfuscation_map_path; }; class OptimizeCommand : public Command { @@ -120,9 +124,13 @@ class OptimizeCommand : public Command { AddOptionalSwitch("--shorten-resource-paths", "Shortens the paths of resources inside the APK.", &options_.shorten_resource_paths); + // TODO(b/246489170): keep the old option and format until transform to the new one AddOptionalFlag("--resource-path-shortening-map", - "Path to output the map of old resource paths to shortened paths.", - &options_.shortened_paths_map_path); + "[Deprecated]Path to output the map of old resource paths to shortened paths.", + &options_.shortened_paths_map_path); + AddOptionalFlag("--save-obfuscation-map", + "Path to output the map of original paths/names to obfuscated paths/names.", + &options_.obfuscation_map_path); AddOptionalSwitch( "--deduplicate-entry-values", "Whether to deduplicate pairs of resource entry and value for simple resources.\n" diff --git a/tools/aapt2/format/binary/TableFlattener.cpp b/tools/aapt2/format/binary/TableFlattener.cpp index f19223411232..8c594ba553a0 100644 --- a/tools/aapt2/format/binary/TableFlattener.cpp +++ b/tools/aapt2/format/binary/TableFlattener.cpp @@ -32,6 +32,7 @@ #include "format/binary/ChunkWriter.h" #include "format/binary/ResEntryWriter.h" #include "format/binary/ResourceTypeExtensions.h" +#include "optimize/Obfuscator.h" #include "trace/TraceBuffer.h" using namespace android; @@ -466,9 +467,6 @@ class PackageFlattener { // table. std::map<ConfigDescription, std::vector<FlatEntry>> config_to_entry_list_map; - // hardcoded string uses characters which make it an invalid resource name - const std::string obfuscated_resource_name = "0_resource_name_obfuscated"; - for (const ResourceTableEntryView& entry : type.entries) { if (entry.staged_id) { aliases_.insert(std::make_pair( @@ -477,30 +475,31 @@ class PackageFlattener { } uint32_t local_key_index; - ResourceName resource_name({}, type.named_type, entry.name); - if (!collapse_key_stringpool_ || - name_collapse_exemptions_.find(resource_name) != name_collapse_exemptions_.end()) { - local_key_index = (uint32_t)key_pool_.MakeRef(entry.name).index(); - } else { - // resource isn't exempt from collapse, add it as obfuscated value - if (entry.overlayable_item) { + auto onObfuscate = [this, &local_key_index, &entry](Obfuscator::Result obfuscatedResult, + const ResourceName& resource_name) { + if (obfuscatedResult == Obfuscator::Result::Keep_ExemptionList) { + local_key_index = (uint32_t)key_pool_.MakeRef(entry.name).index(); + } else if (obfuscatedResult == Obfuscator::Result::Keep_Overlayable) { // if the resource name of the specific entry is obfuscated and this // entry is in the overlayable list, the overlay can't work on this // overlayable at runtime because the name has been obfuscated in // resources.arsc during flatten operation. const OverlayableItem& item = entry.overlayable_item.value(); context_->GetDiagnostics()->Warn(android::DiagMessage(item.overlayable->source) - << "The resource name of overlayable entry " - << resource_name.to_string() << "'" - << " shouldn't be obfuscated in resources.arsc"); + << "The resource name of overlayable entry '" + << resource_name.to_string() + << "' shouldn't be obfuscated in resources.arsc"); local_key_index = (uint32_t)key_pool_.MakeRef(entry.name).index(); } else { - // TODO(b/228192695): output the entry.name and Resource id to make - // de-obfuscated possible. - local_key_index = (uint32_t)key_pool_.MakeRef(obfuscated_resource_name).index(); + local_key_index = + (uint32_t)key_pool_.MakeRef(Obfuscator::kObfuscatedResourceName).index(); } - } + }; + + Obfuscator::ObfuscateResourceName(collapse_key_stringpool_, name_collapse_exemptions_, + type.named_type, entry, onObfuscate); + // Group values by configuration. for (auto& config_value : entry.values) { config_to_entry_list_map[config_value->config].push_back( diff --git a/tools/aapt2/format/binary/TableFlattener.h b/tools/aapt2/format/binary/TableFlattener.h index 35254ba24e53..60605d2fb7fd 100644 --- a/tools/aapt2/format/binary/TableFlattener.h +++ b/tools/aapt2/format/binary/TableFlattener.h @@ -14,8 +14,13 @@ * limitations under the License. */ -#ifndef AAPT_FORMAT_BINARY_TABLEFLATTENER_H -#define AAPT_FORMAT_BINARY_TABLEFLATTENER_H +#ifndef TOOLS_AAPT2_FORMAT_BINARY_TABLEFLATTENER_H_ +#define TOOLS_AAPT2_FORMAT_BINARY_TABLEFLATTENER_H_ + +#include <map> +#include <set> +#include <string> +#include <unordered_map> #include "Resource.h" #include "ResourceTable.h" @@ -71,6 +76,9 @@ struct TableFlattenerOptions { // // This applies only to simple entries (entry->flags & ResTable_entry::FLAG_COMPLEX == 0). bool deduplicate_entry_values = false; + + // Map from original resource ids to obfuscated names. + std::unordered_map<uint32_t, std::string> id_resource_map; }; class TableFlattener : public IResourceTableConsumer { @@ -82,12 +90,12 @@ class TableFlattener : public IResourceTableConsumer { bool Consume(IAaptContext* context, ResourceTable* table) override; private: - DISALLOW_COPY_AND_ASSIGN(TableFlattener); - TableFlattenerOptions options_; android::BigBuffer* buffer_; + + DISALLOW_COPY_AND_ASSIGN(TableFlattener); }; } // namespace aapt -#endif /* AAPT_FORMAT_BINARY_TABLEFLATTENER_H */ +#endif // TOOLS_AAPT2_FORMAT_BINARY_TABLEFLATTENER_H_ diff --git a/tools/aapt2/format/proto/ProtoSerialize.cpp b/tools/aapt2/format/proto/ProtoSerialize.cpp index a6d58fd38f09..0e40124aa79e 100644 --- a/tools/aapt2/format/proto/ProtoSerialize.cpp +++ b/tools/aapt2/format/proto/ProtoSerialize.cpp @@ -18,6 +18,7 @@ #include "ValueVisitor.h" #include "androidfw/BigBuffer.h" +#include "optimize/Obfuscator.h" using android::ConfigDescription; @@ -366,21 +367,21 @@ void SerializeTableToPb(const ResourceTable& table, pb::ResourceTable* out_table } pb_type->set_name(type.named_type.to_string()); - // hardcoded string uses characters which make it an invalid resource name - static const char* obfuscated_resource_name = "0_resource_name_obfuscated"; for (const auto& entry : type.entries) { pb::Entry* pb_entry = pb_type->add_entry(); if (entry.id) { pb_entry->mutable_entry_id()->set_id(entry.id.value()); } - ResourceName resource_name({}, type.named_type, entry.name); - if (options.collapse_key_stringpool && - options.name_collapse_exemptions.find(resource_name) == - options.name_collapse_exemptions.end()) { - pb_entry->set_name(obfuscated_resource_name); - } else { - pb_entry->set_name(entry.name); - } + auto onObfuscate = [pb_entry, &entry](Obfuscator::Result obfuscatedResult, + const ResourceName& resource_name) { + pb_entry->set_name(obfuscatedResult == Obfuscator::Result::Obfuscated + ? Obfuscator::kObfuscatedResourceName + : entry.name); + }; + + Obfuscator::ObfuscateResourceName(options.collapse_key_stringpool, + options.name_collapse_exemptions, type.named_type, entry, + onObfuscate); // Write the Visibility struct. pb::Visibility* pb_visibility = pb_entry->mutable_visibility(); diff --git a/tools/aapt2/optimize/Obfuscator.cpp b/tools/aapt2/optimize/Obfuscator.cpp index 1fdd728928df..cc21093084b5 100644 --- a/tools/aapt2/optimize/Obfuscator.cpp +++ b/tools/aapt2/optimize/Obfuscator.cpp @@ -16,6 +16,8 @@ #include "optimize/Obfuscator.h" +#include <fstream> +#include <map> #include <set> #include <string> #include <unordered_set> @@ -32,7 +34,10 @@ static const char base64_chars[] = namespace aapt { -Obfuscator::Obfuscator(std::map<std::string, std::string>& path_map_out) : path_map_(path_map_out) { +Obfuscator::Obfuscator(OptimizeOptions& optimizeOptions) + : options_(optimizeOptions.table_flattener_options), + shorten_resource_paths_(optimizeOptions.shorten_resource_paths), + collapse_key_stringpool_(optimizeOptions.table_flattener_options.collapse_key_stringpool) { } std::string ShortenFileName(android::StringPiece file_path, int output_length) { @@ -77,7 +82,8 @@ struct PathComparator { } }; -bool Obfuscator::Consume(IAaptContext* context, ResourceTable* table) { +static bool HandleShortenFilePaths(ResourceTable* table, + std::map<std::string, std::string>& shortened_path_map) { // used to detect collisions std::unordered_set<std::string> shortened_paths; std::set<FileReference*, PathComparator> file_refs; @@ -109,10 +115,117 @@ bool Obfuscator::Consume(IAaptContext* context, ResourceTable* table) { shortened_path = GetShortenedPath(shortened_filename, extension, collision_count); } shortened_paths.insert(shortened_path); - path_map_.insert({*file_ref->path, shortened_path}); + shortened_path_map.insert({*file_ref->path, shortened_path}); file_ref->path = table->string_pool.MakeRef(shortened_path, file_ref->path.GetContext()); } return true; } +void Obfuscator::ObfuscateResourceName( + const bool collapse_key_stringpool, const std::set<ResourceName>& name_collapse_exemptions, + const ResourceNamedType& type_name, const ResourceTableEntryView& entry, + const android::base::function_ref<void(Result obfuscatedResult, const ResourceName&)> + onObfuscate) { + ResourceName resource_name({}, type_name, entry.name); + if (!collapse_key_stringpool || + name_collapse_exemptions.find(resource_name) != name_collapse_exemptions.end()) { + onObfuscate(Result::Keep_ExemptionList, resource_name); + } else { + // resource isn't exempt from collapse, add it as obfuscated value + if (entry.overlayable_item) { + // if the resource name of the specific entry is obfuscated and this + // entry is in the overlayable list, the overlay can't work on this + // overlayable at runtime because the name has been obfuscated in + // resources.arsc during flatten operation. + onObfuscate(Result::Keep_Overlayable, resource_name); + } else { + onObfuscate(Result::Obfuscated, resource_name); + } + } +} + +static bool HandleCollapseKeyStringPool( + const ResourceTable* table, const bool collapse_key_string_pool, + const std::set<ResourceName>& name_collapse_exemptions, + std::unordered_map<uint32_t, std::string>& id_resource_map) { + if (!collapse_key_string_pool) { + return true; + } + + int entryResId = 0; + auto onObfuscate = [&entryResId, &id_resource_map](const Obfuscator::Result obfuscatedResult, + const ResourceName& resource_name) { + if (obfuscatedResult == Obfuscator::Result::Obfuscated) { + id_resource_map.insert({entryResId, resource_name.entry}); + } + }; + + for (auto& package : table->packages) { + for (auto& type : package->types) { + for (auto& entry : type->entries) { + if (!entry->id.has_value() || entry->name.empty()) { + continue; + } + entryResId = entry->id->id; + ResourceTableEntryView entry_view{ + .name = entry->name, + .id = entry->id ? entry->id.value().entry_id() : (std::optional<uint16_t>)std::nullopt, + .visibility = entry->visibility, + .allow_new = entry->allow_new, + .overlayable_item = entry->overlayable_item, + .staged_id = entry->staged_id}; + + Obfuscator::ObfuscateResourceName(collapse_key_string_pool, name_collapse_exemptions, + type->named_type, entry_view, onObfuscate); + } + } + } + + return true; +} + +bool Obfuscator::Consume(IAaptContext* context, ResourceTable* table) { + HandleCollapseKeyStringPool(table, options_.collapse_key_stringpool, + options_.name_collapse_exemptions, options_.id_resource_map); + if (shorten_resource_paths_) { + return HandleShortenFilePaths(table, options_.shortened_path_map); + } + return true; +} + +bool Obfuscator::WriteObfuscationMap(const std::string& file_path) const { + pb::ResourceMappings resourceMappings; + for (const auto& [id, name] : options_.id_resource_map) { + auto* collapsedNameMapping = resourceMappings.mutable_collapsed_names()->add_resource_names(); + collapsedNameMapping->set_id(id); + collapsedNameMapping->set_name(name); + } + + for (const auto& [original_path, shortened_path] : options_.shortened_path_map) { + auto* resource_path = resourceMappings.mutable_shortened_paths()->add_resource_paths(); + resource_path->set_original_path(original_path); + resource_path->set_shortened_path(shortened_path); + } + + { // RAII style, output the pb content to file and close fout in destructor + std::ofstream fout(file_path, std::ios::out | std::ios::trunc | std::ios::binary); + if (!fout.is_open()) { + return false; + } + return resourceMappings.SerializeToOstream(&fout); + } +} + +/** + * Tell the optimizer whether it's needed to dump information for de-obfuscating. + * + * There are two conditions need to dump the information for de-obfuscating. + * * the option of shortening file paths is enabled. + * * the option of collapsing resource names is enabled. + * @return true if the information needed for de-obfuscating, otherwise false + */ +bool Obfuscator::IsEnabled() const { + return shorten_resource_paths_ || collapse_key_stringpool_; +} + } // namespace aapt diff --git a/tools/aapt2/optimize/Obfuscator.h b/tools/aapt2/optimize/Obfuscator.h index 1ea32db12815..5ccf54383aae 100644 --- a/tools/aapt2/optimize/Obfuscator.h +++ b/tools/aapt2/optimize/Obfuscator.h @@ -17,10 +17,15 @@ #ifndef TOOLS_AAPT2_OPTIMIZE_OBFUSCATOR_H_ #define TOOLS_AAPT2_OPTIMIZE_OBFUSCATOR_H_ -#include <map> +#include <set> #include <string> +#include "ResourceMetadata.pb.h" +#include "ResourceTable.h" +#include "android-base/function_ref.h" #include "android-base/macros.h" +#include "cmd/Optimize.h" +#include "format/binary/TableFlattener.h" #include "process/IResourceTableConsumer.h" namespace aapt { @@ -30,12 +35,28 @@ class ResourceTable; // Maps resources in the apk to shortened paths. class Obfuscator : public IResourceTableConsumer { public: - explicit Obfuscator(std::map<std::string, std::string>& path_map_out); + explicit Obfuscator(OptimizeOptions& optimizeOptions); bool Consume(IAaptContext* context, ResourceTable* table) override; + bool WriteObfuscationMap(const std::string& file_path) const; + + bool IsEnabled() const; + + enum class Result { Obfuscated, Keep_ExemptionList, Keep_Overlayable }; + + // hardcoded string uses characters which make it an invalid resource name + static constexpr char kObfuscatedResourceName[] = "0_resource_name_obfuscated"; + + static void ObfuscateResourceName( + const bool collapse_key_stringpool, const std::set<ResourceName>& name_collapse_exemptions, + const ResourceNamedType& type_name, const ResourceTableEntryView& entry, + const android::base::function_ref<void(Result, const ResourceName&)> onObfuscate); + private: - std::map<std::string, std::string>& path_map_; + TableFlattenerOptions& options_; + const bool shorten_resource_paths_; + const bool collapse_key_stringpool_; DISALLOW_COPY_AND_ASSIGN(Obfuscator); }; diff --git a/tools/aapt2/optimize/Obfuscator_test.cpp b/tools/aapt2/optimize/Obfuscator_test.cpp index a3339d486d4a..7f57b71ddf76 100644 --- a/tools/aapt2/optimize/Obfuscator_test.cpp +++ b/tools/aapt2/optimize/Obfuscator_test.cpp @@ -16,14 +16,19 @@ #include "optimize/Obfuscator.h" +#include <map> #include <memory> #include <string> #include "ResourceTable.h" +#include "android-base/file.h" #include "test/Test.h" using ::aapt::test::GetValue; +using ::testing::AnyOf; using ::testing::Eq; +using ::testing::HasSubstr; +using ::testing::IsTrue; using ::testing::Not; using ::testing::NotNull; @@ -51,8 +56,9 @@ TEST(ObfuscatorTest, FileRefPathsChangedInResourceTable) { .AddString("android:string/string", "res/should/still/be/the/same.png") .Build(); - std::map<std::string, std::string> path_map; - ASSERT_TRUE(Obfuscator(path_map).Consume(context.get(), table.get())); + OptimizeOptions options{.shorten_resource_paths = true}; + std::map<std::string, std::string>& path_map = options.table_flattener_options.shortened_path_map; + ASSERT_TRUE(Obfuscator(options).Consume(context.get(), table.get())); // Expect that the path map is populated ASSERT_THAT(path_map.find("res/drawables/xmlfile.xml"), Not(Eq(path_map.end()))); @@ -87,8 +93,9 @@ TEST(ObfuscatorTest, SkipColorFileRefPaths) { test::ParseConfigOrDie("mdp-v21")) .Build(); - std::map<std::string, std::string> path_map; - ASSERT_TRUE(Obfuscator(path_map).Consume(context.get(), table.get())); + OptimizeOptions options{.shorten_resource_paths = true}; + std::map<std::string, std::string>& path_map = options.table_flattener_options.shortened_path_map; + ASSERT_TRUE(Obfuscator(options).Consume(context.get(), table.get())); // Expect that the path map to not contain the ColorStateList ASSERT_THAT(path_map.find("res/color/colorlist.xml"), Eq(path_map.end())); @@ -107,8 +114,9 @@ TEST(ObfuscatorTest, KeepExtensions) { .AddFileReference("android:color/pngfile", original_png_path) .Build(); - std::map<std::string, std::string> path_map; - ASSERT_TRUE(Obfuscator(path_map).Consume(context.get(), table.get())); + OptimizeOptions options{.shorten_resource_paths = true}; + std::map<std::string, std::string>& path_map = options.table_flattener_options.shortened_path_map; + ASSERT_TRUE(Obfuscator(options).Consume(context.get(), table.get())); // Expect that the path map is populated ASSERT_THAT(path_map.find("res/drawable/xmlfile.xml"), Not(Eq(path_map.end()))); @@ -133,8 +141,10 @@ TEST(ObfuscatorTest, DeterministicallyHandleCollisions) { test::ResourceTableBuilder builder1; FillTable(builder1, 0, kNumResources); std::unique_ptr<ResourceTable> table1 = builder1.Build(); - std::map<std::string, std::string> expected_mapping; - ASSERT_TRUE(Obfuscator(expected_mapping).Consume(context.get(), table1.get())); + OptimizeOptions options{.shorten_resource_paths = true}; + std::map<std::string, std::string>& expected_mapping = + options.table_flattener_options.shortened_path_map; + ASSERT_TRUE(Obfuscator(options).Consume(context.get(), table1.get())); // We are trying to ensure lack of non-determinism, it is not simple to prove // a negative, thus we must try the test a few times so that the test itself @@ -153,8 +163,10 @@ TEST(ObfuscatorTest, DeterministicallyHandleCollisions) { FillTable(builder2, 0, start_index); std::unique_ptr<ResourceTable> table2 = builder2.Build(); - std::map<std::string, std::string> actual_mapping; - ASSERT_TRUE(Obfuscator(actual_mapping).Consume(context.get(), table2.get())); + OptimizeOptions actualOptimizerOptions{.shorten_resource_paths = true}; + TableFlattenerOptions& actual_options = actualOptimizerOptions.table_flattener_options; + std::map<std::string, std::string>& actual_mapping = actual_options.shortened_path_map; + ASSERT_TRUE(Obfuscator(actualOptimizerOptions).Consume(context.get(), table2.get())); for (auto& item : actual_mapping) { ASSERT_THAT(expected_mapping[item.first], Eq(item.second)); @@ -162,4 +174,126 @@ TEST(ObfuscatorTest, DeterministicallyHandleCollisions) { } } +TEST(ObfuscatorTest, DumpIdResourceMap) { + std::unique_ptr<IAaptContext> context = test::ContextBuilder().Build(); + + OverlayableItem overlayable_item(std::make_shared<Overlayable>("TestName", "overlay://theme")); + overlayable_item.policies |= PolicyFlags::PRODUCT_PARTITION; + overlayable_item.policies |= PolicyFlags::SYSTEM_PARTITION; + overlayable_item.policies |= PolicyFlags::VENDOR_PARTITION; + + std::string original_xml_path = "res/drawable/xmlfile.xml"; + std::string original_png_path = "res/drawable/pngfile.png"; + + std::string name = "com.app.test:string/overlayable"; + std::unique_ptr<ResourceTable> table = + test::ResourceTableBuilder() + .AddFileReference("android:color/xmlfile", original_xml_path) + .AddFileReference("android:color/pngfile", original_png_path) + .AddValue("com.app.test:color/mycolor", aapt::ResourceId(0x7f020000), + aapt::util::make_unique<aapt::BinaryPrimitive>( + uint8_t(android::Res_value::TYPE_INT_COLOR_ARGB8), 0xffaabbcc)) + .AddString("com.app.test:string/mystring", ResourceId(0x7f030000), "hi") + .AddString("com.app.test:string/in_exemption", ResourceId(0x7f030001), "Hi") + .AddString(name, ResourceId(0x7f030002), "HI") + .SetOverlayable(name, overlayable_item) + .Build(); + + OptimizeOptions options{.shorten_resource_paths = true}; + TableFlattenerOptions& flattenerOptions = options.table_flattener_options; + flattenerOptions.collapse_key_stringpool = true; + flattenerOptions.name_collapse_exemptions.insert( + ResourceName({}, ResourceType::kString, "in_exemption")); + auto& id_resource_map = flattenerOptions.id_resource_map; + ASSERT_TRUE(Obfuscator(options).Consume(context.get(), table.get())); + + // Expect that the id resource name map is populated + EXPECT_THAT(id_resource_map.at(0x7f020000), Eq("mycolor")); + EXPECT_THAT(id_resource_map.at(0x7f030000), Eq("mystring")); + EXPECT_THAT(id_resource_map.find(0x7f030001), Eq(id_resource_map.end())); + EXPECT_THAT(id_resource_map.find(0x7f030002), Eq(id_resource_map.end())); +} + +TEST(ObfuscatorTest, IsEnabledWithDefaultOption) { + OptimizeOptions options; + Obfuscator obfuscatorWithDefaultOption(options); + ASSERT_THAT(obfuscatorWithDefaultOption.IsEnabled(), Eq(false)); +} + +TEST(ObfuscatorTest, IsEnabledWithShortenPathOption) { + OptimizeOptions options{.shorten_resource_paths = true}; + Obfuscator obfuscatorWithShortenPathOption(options); + ASSERT_THAT(obfuscatorWithShortenPathOption.IsEnabled(), Eq(true)); +} + +TEST(ObfuscatorTest, IsEnabledWithCollapseStringPoolOption) { + OptimizeOptions options; + options.table_flattener_options.collapse_key_stringpool = true; + Obfuscator obfuscatorWithCollapseStringPoolOption(options); + ASSERT_THAT(obfuscatorWithCollapseStringPoolOption.IsEnabled(), Eq(true)); +} + +TEST(ObfuscatorTest, IsEnabledWithShortenPathAndCollapseStringPoolOption) { + OptimizeOptions options{.shorten_resource_paths = true}; + options.table_flattener_options.collapse_key_stringpool = true; + Obfuscator obfuscatorWithCollapseStringPoolOption(options); + ASSERT_THAT(obfuscatorWithCollapseStringPoolOption.IsEnabled(), Eq(true)); +} + +static std::unique_ptr<ResourceTable> getProtocolBufferTableUnderTest() { + std::string original_xml_path = "res/drawable/xmlfile.xml"; + std::string original_png_path = "res/drawable/pngfile.png"; + + return test::ResourceTableBuilder() + .AddFileReference("com.app.test:drawable/xmlfile", original_xml_path) + .AddFileReference("com.app.test:drawable/pngfile", original_png_path) + .AddValue("com.app.test:color/mycolor", aapt::ResourceId(0x7f020000), + aapt::util::make_unique<aapt::BinaryPrimitive>( + uint8_t(android::Res_value::TYPE_INT_COLOR_ARGB8), 0xffaabbcc)) + .AddString("com.app.test:string/mystring", ResourceId(0x7f030000), "hello world") + .Build(); +} + +TEST(ObfuscatorTest, WriteObfuscationMapInProtocolBufferFormat) { + OptimizeOptions options{.shorten_resource_paths = true}; + options.table_flattener_options.collapse_key_stringpool = true; + Obfuscator obfuscator(options); + ASSERT_TRUE(obfuscator.Consume(test::ContextBuilder().Build().get(), + getProtocolBufferTableUnderTest().get())); + + obfuscator.WriteObfuscationMap("obfuscated_map.pb"); + + std::string pbOut; + android::base::ReadFileToString("obfuscated_map.pb", &pbOut, false /* follow_symlinks */); + EXPECT_THAT(pbOut, HasSubstr("drawable/xmlfile.xml")); + EXPECT_THAT(pbOut, HasSubstr("drawable/pngfile.png")); + EXPECT_THAT(pbOut, HasSubstr("mycolor")); + EXPECT_THAT(pbOut, HasSubstr("mystring")); + pb::ResourceMappings resourceMappings; + EXPECT_THAT(resourceMappings.ParseFromString(pbOut), IsTrue()); + EXPECT_THAT(resourceMappings.collapsed_names().resource_names_size(), Eq(2)); + auto& resource_names = resourceMappings.collapsed_names().resource_names(); + EXPECT_THAT(resource_names.at(0).name(), AnyOf(Eq("mycolor"), Eq("mystring"))); + EXPECT_THAT(resource_names.at(1).name(), AnyOf(Eq("mycolor"), Eq("mystring"))); + auto& shortened_paths = resourceMappings.shortened_paths(); + EXPECT_THAT(shortened_paths.resource_paths_size(), Eq(2)); + EXPECT_THAT(shortened_paths.resource_paths(0).original_path(), + AnyOf(Eq("res/drawable/pngfile.png"), Eq("res/drawable/xmlfile.xml"))); + EXPECT_THAT(shortened_paths.resource_paths(1).original_path(), + AnyOf(Eq("res/drawable/pngfile.png"), Eq("res/drawable/xmlfile.xml"))); +} + +TEST(ObfuscatorTest, WriteObfuscatingMapWithNonEnabledOption) { + OptimizeOptions options; + Obfuscator obfuscator(options); + ASSERT_TRUE(obfuscator.Consume(test::ContextBuilder().Build().get(), + getProtocolBufferTableUnderTest().get())); + + obfuscator.WriteObfuscationMap("obfuscated_map.pb"); + + std::string pbOut; + android::base::ReadFileToString("obfuscated_map.pb", &pbOut, false /* follow_symlinks */); + ASSERT_THAT(pbOut, Eq("")); +} + } // namespace aapt |